// Generic library of Kubernetes objects (https://github.com/bitnami-labs/kube-libsonnet) // // Objects in this file follow the regular Kubernetes API object // schema with two exceptions: // // ## Optional helpers // // A few objects have defaults or additional "helper" hidden // (double-colon) fields that will help with common situations. For // example, `Service.target_pod` generates suitable `selector` and // `ports` blocks for the common case of a single-pod/single-port // service. If for some reason you don't want the helper, just // provide explicit values for the regular Kubernetes fields that the // helper *would* have generated, and the helper logic will be // ignored. // // ## The Underscore Convention: // // Various constructs in the Kubernetes API use JSON arrays to // represent unordered sets or named key/value maps. This is // particularly annoying with jsonnet since we want to use jsonnet's // powerful object merge operation with these constructs. // // To combat this, this library attempts to provide more "jsonnet // native" variants of these arrays in alternative hidden fields that // end with an underscore. For example, the `env_` block in // `Container`: // ``` // kube.Container("foo") { // env_: { FOO: "bar" }, // } // ``` // ... produces the expected `container.env` JSON array: // ``` // { // "env": [ // { "name": "FOO", "value": "bar" } // ] // } // ``` // // If you are confused by the underscore versions, or don't want them // in your situation then just ignore them and set the regular // non-underscore field as usual. // // // ## TODO // // TODO: Expand this to include all API objects. // // Should probably fill out all the defaults here too, so jsonnet can // reference them. In addition, jsonnet validation is more useful // (client-side, and gives better line information). { // In case you may want/need to skip assertions for speed reasons (rather big configmaps/etc), // load the library with e.g. // local kube = (import "lib/kube.libsonnet") { _assert:: false }; _assert:: true, // resource contructors will use kinds/versions/fields compatible at least with version: minKubeVersion: { major: 1, minor: 14, version: "%s.%s" % [self.major, self.minor], }, // Returns array of values from given object. Does not include hidden fields. objectValues(o):: [o[field] for field in std.objectFields(o)], // Returns array of [key, value] pairs from given object. Does not include hidden fields. objectItems(o):: [[k, o[k]] for k in std.objectFields(o)], // Replace all occurrences of `_` with `-`. hyphenate(s):: std.join("-", std.split(s, "_")), // Convert an octal (as a string) to number, parseOctal(s):: ( local len = std.length(s); local leading = std.substr(s, 0, len - 1); local last = std.parseInt(std.substr(s, len - 1, 1)); assert (!$._assert) || last < 8 : "found '%s' digit >= 8" % [last]; last + (if len > 1 then 8 * $.parseOctal(leading) else 0) ), // Convert {foo: {a: b}} to [{name: foo, a: b}] mapToNamedList(o):: [{ name: $.hyphenate(n) } + o[n] for n in std.objectFields(o)], // Return object containing only these fields elements filterMapByFields(o, fields): { [field]: o[field] for field in std.setInter(std.objectFields(o), fields) }, // Convert from SI unit suffixes to regular number siToNum(n):: ( local convert = if std.endsWith(n, "m") then [1, 0.001] else if std.endsWith(n, "K") then [1, 1e3] else if std.endsWith(n, "M") then [1, 1e6] else if std.endsWith(n, "G") then [1, 1e9] else if std.endsWith(n, "T") then [1, 1e12] else if std.endsWith(n, "P") then [1, 1e15] else if std.endsWith(n, "E") then [1, 1e18] else if std.endsWith(n, "Ki") then [2, std.pow(2, 10)] else if std.endsWith(n, "Mi") then [2, std.pow(2, 20)] else if std.endsWith(n, "Gi") then [2, std.pow(2, 30)] else if std.endsWith(n, "Ti") then [2, std.pow(2, 40)] else if std.endsWith(n, "Pi") then [2, std.pow(2, 50)] else if std.endsWith(n, "Ei") then [2, std.pow(2, 60)] else error "Unknown numerical suffix in " + n; local n_len = std.length(n); std.parseInt(std.substr(n, 0, n_len - convert[0])) * convert[1] ), local remap(v, start, end, newstart) = if v >= start && v <= end then v - start + newstart else v, local remapChar(c, start, end, newstart) = std.char(remap( std.codepoint(c), std.codepoint(start), std.codepoint(end), std.codepoint(newstart) )), toLower(s):: ( std.join("", [remapChar(c, "A", "Z", "a") for c in std.stringChars(s)]) ), toUpper(s):: ( std.join("", [remapChar(c, "a", "z", "A") for c in std.stringChars(s)]) ), boolXor(x, y):: ((if x then 1 else 0) + (if y then 1 else 0) == 1), _Object(apiVersion, kind, name):: { local this = self, apiVersion: apiVersion, kind: kind, metadata: { name: name, labels: { name: std.join("-", std.split(this.metadata.name, ":")) }, annotations: {}, }, }, List(): { apiVersion: "v1", kind: "List", items_:: {}, items: $.objectValues(self.items_), }, Namespace(name): $._Object("v1", "Namespace", name) { }, Endpoints(name): $._Object("v1", "Endpoints", name) { Ip(addr):: { ip: addr }, Port(p):: { port: p }, subsets: [], }, Service(name): $._Object("v1", "Service", name) { local service = self, target_pod:: error "service target_pod required", port:: self.target_pod.spec.containers[0].ports[0].containerPort, // Helpers that format host:port in various ways host:: "%s.%s.svc" % [self.metadata.name, self.metadata.namespace], host_colon_port:: "%s:%s" % [self.host, self.spec.ports[0].port], http_url:: "http://%s/" % self.host_colon_port, proxy_urlpath:: "/api/v1/proxy/namespaces/%s/services/%s/" % [ self.metadata.namespace, self.metadata.name, ], // Useful in Ingress rules name_port:: { serviceName: service.metadata.name, servicePort: service.spec.ports[0].port, }, spec: { selector: service.target_pod.metadata.labels, ports: [ { port: service.port, name: service.target_pod.spec.containers[0].ports[0].name, targetPort: service.target_pod.spec.containers[0].ports[0].containerPort, }, ], type: "ClusterIP", }, }, PersistentVolume(name): $._Object("v1", "PersistentVolume", name) { spec: {}, }, // TODO: This is a terrible name PersistentVolumeClaimVolume(pvc): { persistentVolumeClaim: { claimName: pvc.metadata.name }, }, StorageClass(name): $._Object("storage.k8s.io/v1beta1", "StorageClass", name) { provisioner: error "provisioner required", }, PersistentVolumeClaim(name): $._Object("v1", "PersistentVolumeClaim", name) { local pvc = self, storageClass:: null, storage:: error "storage required", metadata+: if pvc.storageClass != null then { annotations+: { "volume.beta.kubernetes.io/storage-class": pvc.storageClass, }, } else {}, spec: { resources: { requests: { storage: pvc.storage, }, }, accessModes: ["ReadWriteOnce"], [if pvc.storageClass != null then "storageClassName"]: pvc.storageClass, }, }, Container(name): { name: name, image: error "container image value required", imagePullPolicy: if std.endsWith(self.image, ":latest") then "Always" else "IfNotPresent", envList(map):: [ if std.type(map[x]) == "object" then { name: x, valueFrom: map[x], } else { // Let `null` value stay as such (vs string-ified) name: x, value: if map[x] == null then null else std.toString(map[x]), } for x in std.objectFields(map) ], env_:: {}, env: self.envList(self.env_), args_:: {}, args: ["--%s=%s" % kv for kv in $.objectItems(self.args_)], ports_:: {}, ports: $.mapToNamedList(self.ports_), volumeMounts_:: {}, volumeMounts: $.mapToNamedList(self.volumeMounts_), stdin: false, tty: false, assert (!$._assert) || (!self.tty || self.stdin) : "tty=true requires stdin=true", }, PodDisruptionBudget(name): $._Object("policy/v1beta1", "PodDisruptionBudget", name) { local this = self, target_pod:: error "target_pod required", spec: { assert (!$._assert) || $.boolXor( std.objectHas(self, "minAvailable"), std.objectHas(self, "maxUnavailable") ) : "PDB '%s': exactly one of minAvailable/maxUnavailable required" % name, selector: { matchLabels: this.target_pod.metadata.labels, }, }, }, Pod(name): $._Object("v1", "Pod", name) { spec: $.PodSpec, }, PodSpec: { // The 'first' container is used in various defaults in k8s. local container_names = std.objectFields(self.containers_), default_container:: if std.length(container_names) > 1 then "default" else container_names[0], containers_:: {}, local container_names_ordered = [self.default_container] + [n for n in container_names if n != self.default_container], containers: ( assert (!$._assert) || std.length(self.containers_) > 0 : "Pod must have at least one container (via containers_ map)"; [{ name: $.hyphenate(name) } + self.containers_[name] for name in container_names_ordered if self.containers_[name] != null] ), // Note initContainers are inherently ordered, and using this // named object will lose that ordering. If order matters, then // manipulate `initContainers` directly (perhaps // appending/prepending to `super.initContainers` to mix+match // both approaches) initContainers_:: {}, initContainers: [{ name: $.hyphenate(name) } + self.initContainers_[name] for name in std.objectFields(self.initContainers_) if self.initContainers_[name] != null], volumes_:: {}, volumes: $.mapToNamedList(self.volumes_), imagePullSecrets: [], terminationGracePeriodSeconds: 30, assert (!$._assert) || std.length(self.containers) > 0 : "Pod must have at least one container (via containers array)", // Return an array of pod's ports numbers ports(proto):: [ p.containerPort for p in std.flattenArrays([ c.ports for c in self.containers ]) if ( (!(std.objectHas(p, "protocol")) && proto == "TCP") || ((std.objectHas(p, "protocol")) && p.protocol == proto) ) ], }, EmptyDirVolume(): { emptyDir: {}, }, HostPathVolume(path, type=""): { hostPath: { path: path, type: type }, }, GitRepoVolume(repository, revision): { gitRepo: { repository: repository, // "master" is possible, but should be avoided for production revision: revision, }, }, SecretVolume(secret): { secret: { secretName: secret.metadata.name }, }, ConfigMapVolume(configmap): { configMap: { name: configmap.metadata.name }, }, ConfigMap(name): $._Object("v1", "ConfigMap", name) { data: {}, // I keep thinking data values can be any JSON type. This check // will remind me that they must be strings :( local nonstrings = [ k for k in std.objectFields(self.data) if std.type(self.data[k]) != "string" ], assert (!$._assert) || std.length(nonstrings) == 0 : "data contains non-string values: %s" % [nonstrings], }, // subtype of EnvVarSource ConfigMapRef(configmap, key): { assert (!$._assert) || std.objectHas(configmap.data, key) : "ConfigMap '%s' doesn't have '%s' field in configmap.data" % [configmap.metadata.name, key], configMapKeyRef: { name: configmap.metadata.name, key: key, }, }, Secret(name): $._Object("v1", "Secret", name) { local secret = self, type: "Opaque", data_:: {}, data: { [k]: std.base64(secret.data_[k]) for k in std.objectFields(secret.data_) }, }, // subtype of EnvVarSource SecretKeyRef(secret, key): { assert (!$._assert) || std.objectHas(secret.data, key) : "Secret '%s' doesn't have '%s' field in secret.data" % [secret.metadata.name, key], secretKeyRef: { name: secret.metadata.name, key: key, }, }, // subtype of EnvVarSource FieldRef(key): { fieldRef: { apiVersion: "v1", fieldPath: key, }, }, // subtype of EnvVarSource ResourceFieldRef(key, divisor="1"): { resourceFieldRef: { resource: key, divisor: std.toString(divisor), }, }, Deployment(name): $._Object("apps/v1", "Deployment", name) { local deployment = self, spec: { template: { spec: $.PodSpec, metadata: { labels: deployment.metadata.labels, annotations: {}, }, }, selector: { matchLabels: deployment.spec.template.metadata.labels, }, strategy: { type: "RollingUpdate", local pvcs = [ v for v in deployment.spec.template.spec.volumes if std.objectHas(v, "persistentVolumeClaim") ], local is_stateless = std.length(pvcs) == 0, // Apps trying to maintain a majority quorum or similar will // want to tune these carefully. // NB: Upstream default is surge=1 unavail=1 rollingUpdate: if is_stateless then { maxSurge: "25%", // rounds up maxUnavailable: "25%", // rounds down } else { // Poor-man's StatelessSet. Useful mostly with replicas=1. maxSurge: 0, maxUnavailable: 1, }, }, // NB: Upstream default is 0 minReadySeconds: 30, // NB: Regular k8s default is to keep all revisions revisionHistoryLimit: 10, replicas: 1, }, }, CrossVersionObjectReference(target): { apiVersion: target.apiVersion, kind: target.kind, name: target.metadata.name, }, HorizontalPodAutoscaler(name): $._Object("autoscaling/v1", "HorizontalPodAutoscaler", name) { local hpa = self, target:: error "target required", spec: { scaleTargetRef: $.CrossVersionObjectReference(hpa.target), minReplicas: hpa.target.spec.replicas, maxReplicas: error "maxReplicas required", assert (!$._assert) || self.maxReplicas >= self.minReplicas, }, }, StatefulSet(name): $._Object("apps/v1", "StatefulSet", name) { local sset = self, spec: { serviceName: name, updateStrategy: { type: "RollingUpdate", rollingUpdate: { partition: 0, }, }, template: { spec: $.PodSpec, metadata: { labels: sset.metadata.labels, annotations: {}, }, }, selector: { matchLabels: sset.spec.template.metadata.labels, }, volumeClaimTemplates_:: {}, volumeClaimTemplates: [ // StatefulSet is overly fussy about "changes" (even when // they're no-ops). // In particular annotations={} is apparently a "change", // since the comparison is ignorant of defaults. std.prune($.PersistentVolumeClaim($.hyphenate(kv[0])) + { apiVersion:: null, kind:: null } + kv[1]) for kv in $.objectItems(self.volumeClaimTemplates_) ], replicas: 1, # TODO(github.com/bitnami-labs/kube-libsonnet/pull/66): update from # upstream when merged # assert (!$._assert) || self.replicas >= 1, }, }, Job(name): $._Object("batch/v1", "Job", name) { local job = self, spec: $.JobSpec { template+: { metadata+: { labels: job.metadata.labels, }, }, }, }, CronJob(name): $._Object("batch/v1beta1", "CronJob", name) { local cronjob = self, spec: { jobTemplate: { spec: $.JobSpec { template+: { metadata+: { labels: cronjob.metadata.labels, }, }, }, }, schedule: error "Need to provide spec.schedule", successfulJobsHistoryLimit: 10, failedJobsHistoryLimit: 20, // NB: upstream concurrencyPolicy default is "Allow" concurrencyPolicy: "Forbid", }, }, JobSpec: { local this = self, template: { spec: $.PodSpec { restartPolicy: "OnFailure", }, }, completions: 1, parallelism: 1, }, DaemonSet(name): $._Object("apps/v1", "DaemonSet", name) { local ds = self, spec: { updateStrategy: { type: "RollingUpdate", rollingUpdate: { maxUnavailable: 1, }, }, template: { metadata: { labels: ds.metadata.labels, annotations: {}, }, spec: $.PodSpec, }, selector: { matchLabels: ds.spec.template.metadata.labels, }, }, }, Ingress(name): $._Object("networking.k8s.io/v1beta1", "Ingress", name) { spec: {}, local rel_paths = [ p.path for r in self.spec.rules for p in r.http.paths if std.objectHas(p, "path") && !std.startsWith(p.path, "/") ], assert (!$._assert) || std.length(rel_paths) == 0 : "paths must be absolute: " + rel_paths, }, ThirdPartyResource(name): $._Object("extensions/v1beta1", "ThirdPartyResource", name) { versions_:: [], versions: [{ name: n } for n in self.versions_], }, CustomResourceDefinition(group, version, kind): { local this = self, apiVersion: "apiextensions.k8s.io/v1beta1", kind: "CustomResourceDefinition", metadata+: { name: this.spec.names.plural + "." + this.spec.group, }, spec: { scope: "Namespaced", group: group, version: version, names: { kind: kind, singular: $.toLower(self.kind), plural: self.singular + "s", listKind: self.kind + "List", }, }, }, ServiceAccount(name): $._Object("v1", "ServiceAccount", name) { }, Role(name): $._Object("rbac.authorization.k8s.io/v1", "Role", name) { rules: [], }, ClusterRole(name): $.Role(name) { kind: "ClusterRole", }, Group(name): { kind: "Group", name: name, apiGroup: "rbac.authorization.k8s.io", }, User(name): { kind: "User", name: name, apiGroup: "rbac.authorization.k8s.io", }, RoleBinding(name): $._Object("rbac.authorization.k8s.io/v1", "RoleBinding", name) { local rb = self, subjects_:: [], subjects: [{ kind: o.kind, namespace: o.metadata.namespace, name: o.metadata.name, } for o in self.subjects_], roleRef_:: error "roleRef is required", roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: rb.roleRef_.kind, name: rb.roleRef_.metadata.name, }, }, ClusterRoleBinding(name): $.RoleBinding(name) { kind: "ClusterRoleBinding", }, // NB: encryptedData can be imported into a SealedSecret as follows: // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.encryptedData > sealedsecret.json // encryptedData: std.parseJson(importstr "sealedsecret.json") SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) { spec: { encryptedData: {}, }, assert (!$._assert) || std.length(std.objectFields(self.spec.encryptedData)) != 0 : "SealedSecret '%s' has empty encryptedData field" % name, }, // NB: helper method to access several Kubernetes objects podRef, // used below to extract its labels podRef(obj):: ({ Pod: obj, Deployment: obj.spec.template, StatefulSet: obj.spec.template, DaemonSet: obj.spec.template, Job: obj.spec.template, CronJob: obj.spec.jobTemplate.spec.template, }[obj.kind]), // NB: return a { podSelector: ... } ready to use for e.g. NSPs (see below) // pod labels can be optionally filtered by their label name 2nd array arg podLabelsSelector(obj, filter=null):: { podSelector: std.prune({ matchLabels: if filter != null then $.filterMapByFields($.podRef(obj).metadata.labels, filter) else $.podRef(obj).metadata.labels, }), }, // NB: Returns an array as [{ port: num, protocol: "PROTO" }, {...}, ... ] // Need to split TCP, UDP logic to be able to dedup each set of protocol ports podsPorts(obj_list):: std.flattenArrays([ [ { port: port, protocol: protocol } for port in std.set( std.flattenArrays([$.podRef(obj).spec.ports(protocol) for obj in obj_list]) ) ] for protocol in ["TCP", "UDP"] ]), // NB: most of the "helper" stuff comes from above (podLabelsSelector, podsPorts), // NetworkPolicy returned object will have "Ingress", "Egress" policyTypes auto-set // based on populated spec.ingress or spec.egress // See tests/test-simple-validate.jsonnet for example(s). NetworkPolicy(name): $._Object("networking.k8s.io/v1", "NetworkPolicy", name) { local networkpolicy = self, spec: { policyTypes: std.prune([ if networkpolicy.spec.ingress != [] then "Ingress" else null, if networkpolicy.spec.egress != [] then "Egress" else null, ]), ingress: $.objectValues(self.ingress_), ingress_:: {}, egress: $.objectValues(self.egress_), egress_:: {}, podSelector: {}, }, }, VerticalPodAutoscaler(name):: $._Object("autoscaling.k8s.io/v1beta2", "VerticalPodAutoscaler", name) { local vpa = self, target:: error "target required", spec: { targetRef: $.CrossVersionObjectReference(vpa.target), updatePolicy: { updateMode: "Auto", }, }, }, // Helper function to ease VPA creation as e.g.: // foo_vpa:: kube.createVPAFor($.foo_deploy) createVPAFor(target, mode="Auto"):: $.VerticalPodAutoscaler(target.metadata.name) { target:: target, metadata+: { namespace: target.metadata.namespace, labels+: target.metadata.labels, }, spec+: { updatePolicy+: { updateMode: mode, }, }, }, }