Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 1 | // Generic library of Kubernetes objects (https://github.com/bitnami-labs/kube-libsonnet) |
| 2 | // |
| 3 | // Objects in this file follow the regular Kubernetes API object |
| 4 | // schema with two exceptions: |
| 5 | // |
| 6 | // ## Optional helpers |
| 7 | // |
| 8 | // A few objects have defaults or additional "helper" hidden |
| 9 | // (double-colon) fields that will help with common situations. For |
| 10 | // example, `Service.target_pod` generates suitable `selector` and |
| 11 | // `ports` blocks for the common case of a single-pod/single-port |
| 12 | // service. If for some reason you don't want the helper, just |
| 13 | // provide explicit values for the regular Kubernetes fields that the |
| 14 | // helper *would* have generated, and the helper logic will be |
| 15 | // ignored. |
| 16 | // |
| 17 | // ## The Underscore Convention: |
| 18 | // |
| 19 | // Various constructs in the Kubernetes API use JSON arrays to |
| 20 | // represent unordered sets or named key/value maps. This is |
| 21 | // particularly annoying with jsonnet since we want to use jsonnet's |
| 22 | // powerful object merge operation with these constructs. |
| 23 | // |
| 24 | // To combat this, this library attempts to provide more "jsonnet |
| 25 | // native" variants of these arrays in alternative hidden fields that |
| 26 | // end with an underscore. For example, the `env_` block in |
| 27 | // `Container`: |
| 28 | // ``` |
| 29 | // kube.Container("foo") { |
| 30 | // env_: { FOO: "bar" }, |
| 31 | // } |
| 32 | // ``` |
| 33 | // ... produces the expected `container.env` JSON array: |
| 34 | // ``` |
| 35 | // { |
| 36 | // "env": [ |
| 37 | // { "name": "FOO", "value": "bar" } |
| 38 | // ] |
| 39 | // } |
| 40 | // ``` |
| 41 | // |
| 42 | // If you are confused by the underscore versions, or don't want them |
| 43 | // in your situation then just ignore them and set the regular |
| 44 | // non-underscore field as usual. |
| 45 | // |
| 46 | // |
| 47 | // ## TODO |
| 48 | // |
| 49 | // TODO: Expand this to include all API objects. |
| 50 | // |
| 51 | // Should probably fill out all the defaults here too, so jsonnet can |
| 52 | // reference them. In addition, jsonnet validation is more useful |
| 53 | // (client-side, and gives better line information). |
| 54 | |
| 55 | { |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 56 | // In case you may want/need to skip assertions for speed reasons (rather big configmaps/etc), |
| 57 | // load the library with e.g. |
| 58 | // local kube = (import "lib/kube.libsonnet") { _assert:: false }; |
| 59 | _assert:: true, |
| 60 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 61 | // resource contructors will use kinds/versions/fields compatible at least with version: |
| 62 | minKubeVersion: { |
| 63 | major: 1, |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 64 | minor: 14, |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 65 | version: "%s.%s" % [self.major, self.minor], |
| 66 | }, |
| 67 | |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 68 | // Returns array of values from given object. Does not include hidden fields. |
| 69 | objectValues(o):: [o[field] for field in std.objectFields(o)], |
| 70 | |
| 71 | // Returns array of [key, value] pairs from given object. Does not include hidden fields. |
| 72 | objectItems(o):: [[k, o[k]] for k in std.objectFields(o)], |
| 73 | |
| 74 | // Replace all occurrences of `_` with `-`. |
| 75 | hyphenate(s):: std.join("-", std.split(s, "_")), |
| 76 | |
| 77 | // Convert an octal (as a string) to number, |
| 78 | parseOctal(s):: ( |
| 79 | local len = std.length(s); |
| 80 | local leading = std.substr(s, 0, len - 1); |
| 81 | local last = std.parseInt(std.substr(s, len - 1, 1)); |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 82 | assert (!$._assert) || last < 8 : "found '%s' digit >= 8" % [last]; |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 83 | last + (if len > 1 then 8 * $.parseOctal(leading) else 0) |
| 84 | ), |
| 85 | |
| 86 | // Convert {foo: {a: b}} to [{name: foo, a: b}] |
| 87 | mapToNamedList(o):: [{ name: $.hyphenate(n) } + o[n] for n in std.objectFields(o)], |
| 88 | |
| 89 | // Return object containing only these fields elements |
| 90 | filterMapByFields(o, fields): { [field]: o[field] for field in std.setInter(std.objectFields(o), fields) }, |
| 91 | |
| 92 | // Convert from SI unit suffixes to regular number |
| 93 | siToNum(n):: ( |
| 94 | local convert = |
| 95 | if std.endsWith(n, "m") then [1, 0.001] |
| 96 | else if std.endsWith(n, "K") then [1, 1e3] |
| 97 | else if std.endsWith(n, "M") then [1, 1e6] |
| 98 | else if std.endsWith(n, "G") then [1, 1e9] |
| 99 | else if std.endsWith(n, "T") then [1, 1e12] |
| 100 | else if std.endsWith(n, "P") then [1, 1e15] |
| 101 | else if std.endsWith(n, "E") then [1, 1e18] |
| 102 | else if std.endsWith(n, "Ki") then [2, std.pow(2, 10)] |
| 103 | else if std.endsWith(n, "Mi") then [2, std.pow(2, 20)] |
| 104 | else if std.endsWith(n, "Gi") then [2, std.pow(2, 30)] |
| 105 | else if std.endsWith(n, "Ti") then [2, std.pow(2, 40)] |
| 106 | else if std.endsWith(n, "Pi") then [2, std.pow(2, 50)] |
| 107 | else if std.endsWith(n, "Ei") then [2, std.pow(2, 60)] |
| 108 | else error "Unknown numerical suffix in " + n; |
| 109 | local n_len = std.length(n); |
| 110 | std.parseInt(std.substr(n, 0, n_len - convert[0])) * convert[1] |
| 111 | ), |
| 112 | |
| 113 | local remap(v, start, end, newstart) = |
| 114 | if v >= start && v <= end then v - start + newstart else v, |
| 115 | local remapChar(c, start, end, newstart) = |
| 116 | std.char(remap( |
| 117 | std.codepoint(c), std.codepoint(start), std.codepoint(end), std.codepoint(newstart) |
| 118 | )), |
| 119 | toLower(s):: ( |
| 120 | std.join("", [remapChar(c, "A", "Z", "a") for c in std.stringChars(s)]) |
| 121 | ), |
| 122 | toUpper(s):: ( |
| 123 | std.join("", [remapChar(c, "a", "z", "A") for c in std.stringChars(s)]) |
| 124 | ), |
| 125 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 126 | boolXor(x, y):: ((if x then 1 else 0) + (if y then 1 else 0) == 1), |
| 127 | |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 128 | _Object(apiVersion, kind, name):: { |
| 129 | local this = self, |
| 130 | apiVersion: apiVersion, |
| 131 | kind: kind, |
| 132 | metadata: { |
| 133 | name: name, |
| 134 | labels: { name: std.join("-", std.split(this.metadata.name, ":")) }, |
| 135 | annotations: {}, |
| 136 | }, |
| 137 | }, |
| 138 | |
| 139 | List(): { |
| 140 | apiVersion: "v1", |
| 141 | kind: "List", |
| 142 | items_:: {}, |
| 143 | items: $.objectValues(self.items_), |
| 144 | }, |
| 145 | |
| 146 | Namespace(name): $._Object("v1", "Namespace", name) { |
| 147 | }, |
| 148 | |
| 149 | Endpoints(name): $._Object("v1", "Endpoints", name) { |
| 150 | Ip(addr):: { ip: addr }, |
| 151 | Port(p):: { port: p }, |
| 152 | |
| 153 | subsets: [], |
| 154 | }, |
| 155 | |
| 156 | Service(name): $._Object("v1", "Service", name) { |
| 157 | local service = self, |
| 158 | |
| 159 | target_pod:: error "service target_pod required", |
| 160 | port:: self.target_pod.spec.containers[0].ports[0].containerPort, |
| 161 | |
| 162 | // Helpers that format host:port in various ways |
| 163 | host:: "%s.%s.svc" % [self.metadata.name, self.metadata.namespace], |
| 164 | host_colon_port:: "%s:%s" % [self.host, self.spec.ports[0].port], |
| 165 | http_url:: "http://%s/" % self.host_colon_port, |
| 166 | proxy_urlpath:: "/api/v1/proxy/namespaces/%s/services/%s/" % [ |
| 167 | self.metadata.namespace, |
| 168 | self.metadata.name, |
| 169 | ], |
| 170 | // Useful in Ingress rules |
| 171 | name_port:: { |
| 172 | serviceName: service.metadata.name, |
| 173 | servicePort: service.spec.ports[0].port, |
| 174 | }, |
| 175 | |
| 176 | spec: { |
| 177 | selector: service.target_pod.metadata.labels, |
| 178 | ports: [ |
| 179 | { |
| 180 | port: service.port, |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 181 | name: service.target_pod.spec.containers[0].ports[0].name, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 182 | targetPort: service.target_pod.spec.containers[0].ports[0].containerPort, |
| 183 | }, |
| 184 | ], |
| 185 | type: "ClusterIP", |
| 186 | }, |
| 187 | }, |
| 188 | |
| 189 | PersistentVolume(name): $._Object("v1", "PersistentVolume", name) { |
| 190 | spec: {}, |
| 191 | }, |
| 192 | |
| 193 | // TODO: This is a terrible name |
| 194 | PersistentVolumeClaimVolume(pvc): { |
| 195 | persistentVolumeClaim: { claimName: pvc.metadata.name }, |
| 196 | }, |
| 197 | |
| 198 | StorageClass(name): $._Object("storage.k8s.io/v1beta1", "StorageClass", name) { |
| 199 | provisioner: error "provisioner required", |
| 200 | }, |
| 201 | |
| 202 | PersistentVolumeClaim(name): $._Object("v1", "PersistentVolumeClaim", name) { |
| 203 | local pvc = self, |
| 204 | |
| 205 | storageClass:: null, |
| 206 | storage:: error "storage required", |
| 207 | |
| 208 | metadata+: if pvc.storageClass != null then { |
| 209 | annotations+: { |
| 210 | "volume.beta.kubernetes.io/storage-class": pvc.storageClass, |
| 211 | }, |
| 212 | } else {}, |
| 213 | |
| 214 | spec: { |
| 215 | resources: { |
| 216 | requests: { |
| 217 | storage: pvc.storage, |
| 218 | }, |
| 219 | }, |
| 220 | accessModes: ["ReadWriteOnce"], |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 221 | [if pvc.storageClass != null then "storageClassName"]: pvc.storageClass, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 222 | }, |
| 223 | }, |
| 224 | |
| 225 | Container(name): { |
| 226 | name: name, |
| 227 | image: error "container image value required", |
| 228 | imagePullPolicy: if std.endsWith(self.image, ":latest") then "Always" else "IfNotPresent", |
| 229 | |
| 230 | envList(map):: [ |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 231 | if std.type(map[x]) == "object" |
| 232 | then { |
| 233 | name: x, |
| 234 | valueFrom: map[x], |
| 235 | } else { |
| 236 | // Let `null` value stay as such (vs string-ified) |
| 237 | name: x, |
| 238 | value: if map[x] == null then null else std.toString(map[x]), |
| 239 | } |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 240 | for x in std.objectFields(map) |
| 241 | ], |
| 242 | |
| 243 | env_:: {}, |
| 244 | env: self.envList(self.env_), |
| 245 | |
| 246 | args_:: {}, |
| 247 | args: ["--%s=%s" % kv for kv in $.objectItems(self.args_)], |
| 248 | |
| 249 | ports_:: {}, |
| 250 | ports: $.mapToNamedList(self.ports_), |
| 251 | |
| 252 | volumeMounts_:: {}, |
| 253 | volumeMounts: $.mapToNamedList(self.volumeMounts_), |
| 254 | |
| 255 | stdin: false, |
| 256 | tty: false, |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 257 | assert (!$._assert) || (!self.tty || self.stdin) : "tty=true requires stdin=true", |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 258 | }, |
| 259 | |
| 260 | PodDisruptionBudget(name): $._Object("policy/v1beta1", "PodDisruptionBudget", name) { |
| 261 | local this = self, |
| 262 | target_pod:: error "target_pod required", |
| 263 | spec: { |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 264 | assert (!$._assert) || $.boolXor( |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 265 | std.objectHas(self, "minAvailable"), |
| 266 | std.objectHas(self, "maxUnavailable") |
| 267 | ) : "PDB '%s': exactly one of minAvailable/maxUnavailable required" % name, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 268 | selector: { |
| 269 | matchLabels: this.target_pod.metadata.labels, |
| 270 | }, |
| 271 | }, |
| 272 | }, |
| 273 | |
| 274 | Pod(name): $._Object("v1", "Pod", name) { |
| 275 | spec: $.PodSpec, |
| 276 | }, |
| 277 | |
| 278 | PodSpec: { |
| 279 | // The 'first' container is used in various defaults in k8s. |
| 280 | local container_names = std.objectFields(self.containers_), |
| 281 | default_container:: if std.length(container_names) > 1 then "default" else container_names[0], |
| 282 | containers_:: {}, |
| 283 | |
| 284 | local container_names_ordered = [self.default_container] + [n for n in container_names if n != self.default_container], |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 285 | containers: ( |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 286 | assert (!$._assert) || std.length(self.containers_) > 0 : "Pod must have at least one container (via containers_ map)"; |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 287 | [{ name: $.hyphenate(name) } + self.containers_[name] for name in container_names_ordered if self.containers_[name] != null] |
| 288 | ), |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 289 | |
| 290 | // Note initContainers are inherently ordered, and using this |
| 291 | // named object will lose that ordering. If order matters, then |
| 292 | // manipulate `initContainers` directly (perhaps |
| 293 | // appending/prepending to `super.initContainers` to mix+match |
| 294 | // both approaches) |
| 295 | initContainers_:: {}, |
| 296 | initContainers: [{ name: $.hyphenate(name) } + self.initContainers_[name] for name in std.objectFields(self.initContainers_) if self.initContainers_[name] != null], |
| 297 | |
| 298 | volumes_:: {}, |
| 299 | volumes: $.mapToNamedList(self.volumes_), |
| 300 | |
| 301 | imagePullSecrets: [], |
| 302 | |
| 303 | terminationGracePeriodSeconds: 30, |
| 304 | |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 305 | assert (!$._assert) || std.length(self.containers) > 0 : "Pod must have at least one container (via containers array)", |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 306 | |
| 307 | // Return an array of pod's ports numbers |
| 308 | ports(proto):: [ |
| 309 | p.containerPort |
| 310 | for p in std.flattenArrays([ |
| 311 | c.ports |
| 312 | for c in self.containers |
| 313 | ]) |
| 314 | if ( |
| 315 | (!(std.objectHas(p, "protocol")) && proto == "TCP") |
| 316 | || |
| 317 | ((std.objectHas(p, "protocol")) && p.protocol == proto) |
| 318 | ) |
| 319 | ], |
| 320 | |
| 321 | }, |
| 322 | |
| 323 | EmptyDirVolume(): { |
| 324 | emptyDir: {}, |
| 325 | }, |
| 326 | |
| 327 | HostPathVolume(path, type=""): { |
| 328 | hostPath: { path: path, type: type }, |
| 329 | }, |
| 330 | |
| 331 | GitRepoVolume(repository, revision): { |
| 332 | gitRepo: { |
| 333 | repository: repository, |
| 334 | |
| 335 | // "master" is possible, but should be avoided for production |
| 336 | revision: revision, |
| 337 | }, |
| 338 | }, |
| 339 | |
| 340 | SecretVolume(secret): { |
| 341 | secret: { secretName: secret.metadata.name }, |
| 342 | }, |
| 343 | |
| 344 | ConfigMapVolume(configmap): { |
| 345 | configMap: { name: configmap.metadata.name }, |
| 346 | }, |
| 347 | |
| 348 | ConfigMap(name): $._Object("v1", "ConfigMap", name) { |
| 349 | data: {}, |
| 350 | |
| 351 | // I keep thinking data values can be any JSON type. This check |
| 352 | // will remind me that they must be strings :( |
| 353 | local nonstrings = [ |
| 354 | k |
| 355 | for k in std.objectFields(self.data) |
| 356 | if std.type(self.data[k]) != "string" |
| 357 | ], |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 358 | assert (!$._assert) || std.length(nonstrings) == 0 : "data contains non-string values: %s" % [nonstrings], |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 359 | }, |
| 360 | |
| 361 | // subtype of EnvVarSource |
| 362 | ConfigMapRef(configmap, key): { |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 363 | assert (!$._assert) || std.objectHas(configmap.data, key) : "ConfigMap '%s' doesn't have '%s' field in configmap.data" % [configmap.metadata.name, key], |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 364 | configMapKeyRef: { |
| 365 | name: configmap.metadata.name, |
| 366 | key: key, |
| 367 | }, |
| 368 | }, |
| 369 | |
| 370 | Secret(name): $._Object("v1", "Secret", name) { |
| 371 | local secret = self, |
| 372 | |
| 373 | type: "Opaque", |
| 374 | data_:: {}, |
| 375 | data: { [k]: std.base64(secret.data_[k]) for k in std.objectFields(secret.data_) }, |
| 376 | }, |
| 377 | |
| 378 | // subtype of EnvVarSource |
| 379 | SecretKeyRef(secret, key): { |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 380 | assert (!$._assert) || std.objectHas(secret.data, key) : "Secret '%s' doesn't have '%s' field in secret.data" % [secret.metadata.name, key], |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 381 | secretKeyRef: { |
| 382 | name: secret.metadata.name, |
| 383 | key: key, |
| 384 | }, |
| 385 | }, |
| 386 | |
| 387 | // subtype of EnvVarSource |
| 388 | FieldRef(key): { |
| 389 | fieldRef: { |
| 390 | apiVersion: "v1", |
| 391 | fieldPath: key, |
| 392 | }, |
| 393 | }, |
| 394 | |
| 395 | // subtype of EnvVarSource |
| 396 | ResourceFieldRef(key, divisor="1"): { |
| 397 | resourceFieldRef: { |
| 398 | resource: key, |
| 399 | divisor: std.toString(divisor), |
| 400 | }, |
| 401 | }, |
| 402 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 403 | Deployment(name): $._Object("apps/v1", "Deployment", name) { |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 404 | local deployment = self, |
| 405 | |
| 406 | spec: { |
| 407 | template: { |
| 408 | spec: $.PodSpec, |
| 409 | metadata: { |
| 410 | labels: deployment.metadata.labels, |
| 411 | annotations: {}, |
| 412 | }, |
| 413 | }, |
| 414 | |
| 415 | selector: { |
| 416 | matchLabels: deployment.spec.template.metadata.labels, |
| 417 | }, |
| 418 | |
| 419 | strategy: { |
| 420 | type: "RollingUpdate", |
| 421 | |
| 422 | local pvcs = [ |
| 423 | v |
| 424 | for v in deployment.spec.template.spec.volumes |
| 425 | if std.objectHas(v, "persistentVolumeClaim") |
| 426 | ], |
| 427 | local is_stateless = std.length(pvcs) == 0, |
| 428 | |
| 429 | // Apps trying to maintain a majority quorum or similar will |
| 430 | // want to tune these carefully. |
| 431 | // NB: Upstream default is surge=1 unavail=1 |
| 432 | rollingUpdate: if is_stateless then { |
| 433 | maxSurge: "25%", // rounds up |
| 434 | maxUnavailable: "25%", // rounds down |
| 435 | } else { |
| 436 | // Poor-man's StatelessSet. Useful mostly with replicas=1. |
| 437 | maxSurge: 0, |
| 438 | maxUnavailable: 1, |
| 439 | }, |
| 440 | }, |
| 441 | |
| 442 | // NB: Upstream default is 0 |
| 443 | minReadySeconds: 30, |
| 444 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 445 | // NB: Regular k8s default is to keep all revisions |
| 446 | revisionHistoryLimit: 10, |
| 447 | |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 448 | replicas: 1, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 449 | }, |
| 450 | }, |
| 451 | |
| 452 | CrossVersionObjectReference(target): { |
| 453 | apiVersion: target.apiVersion, |
| 454 | kind: target.kind, |
| 455 | name: target.metadata.name, |
| 456 | }, |
| 457 | |
| 458 | HorizontalPodAutoscaler(name): $._Object("autoscaling/v1", "HorizontalPodAutoscaler", name) { |
| 459 | local hpa = self, |
| 460 | |
| 461 | target:: error "target required", |
| 462 | |
| 463 | spec: { |
| 464 | scaleTargetRef: $.CrossVersionObjectReference(hpa.target), |
| 465 | |
| 466 | minReplicas: hpa.target.spec.replicas, |
| 467 | maxReplicas: error "maxReplicas required", |
| 468 | |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 469 | assert (!$._assert) || self.maxReplicas >= self.minReplicas, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 470 | }, |
| 471 | }, |
| 472 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 473 | StatefulSet(name): $._Object("apps/v1", "StatefulSet", name) { |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 474 | local sset = self, |
| 475 | |
| 476 | spec: { |
| 477 | serviceName: name, |
| 478 | |
| 479 | updateStrategy: { |
| 480 | type: "RollingUpdate", |
| 481 | rollingUpdate: { |
| 482 | partition: 0, |
| 483 | }, |
| 484 | }, |
| 485 | |
| 486 | template: { |
| 487 | spec: $.PodSpec, |
| 488 | metadata: { |
| 489 | labels: sset.metadata.labels, |
| 490 | annotations: {}, |
| 491 | }, |
| 492 | }, |
| 493 | |
| 494 | selector: { |
| 495 | matchLabels: sset.spec.template.metadata.labels, |
| 496 | }, |
| 497 | |
| 498 | volumeClaimTemplates_:: {}, |
| 499 | volumeClaimTemplates: [ |
| 500 | // StatefulSet is overly fussy about "changes" (even when |
| 501 | // they're no-ops). |
| 502 | // In particular annotations={} is apparently a "change", |
| 503 | // since the comparison is ignorant of defaults. |
| 504 | std.prune($.PersistentVolumeClaim($.hyphenate(kv[0])) + { apiVersion:: null, kind:: null } + kv[1]) |
| 505 | for kv in $.objectItems(self.volumeClaimTemplates_) |
| 506 | ], |
| 507 | |
| 508 | replicas: 1, |
Piotr Dobrowolski | 21c8cd6 | 2021-09-16 13:07:54 +0200 | [diff] [blame] | 509 | # TODO(github.com/bitnami-labs/kube-libsonnet/pull/66): update from |
| 510 | # upstream when merged |
| 511 | # assert (!$._assert) || self.replicas >= 1, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 512 | }, |
| 513 | }, |
| 514 | |
| 515 | Job(name): $._Object("batch/v1", "Job", name) { |
| 516 | local job = self, |
| 517 | |
| 518 | spec: $.JobSpec { |
| 519 | template+: { |
| 520 | metadata+: { |
| 521 | labels: job.metadata.labels, |
| 522 | }, |
| 523 | }, |
| 524 | }, |
| 525 | }, |
| 526 | |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 527 | CronJob(name): $._Object("batch/v1beta1", "CronJob", name) { |
| 528 | local cronjob = self, |
| 529 | |
| 530 | spec: { |
| 531 | jobTemplate: { |
| 532 | spec: $.JobSpec { |
| 533 | template+: { |
| 534 | metadata+: { |
| 535 | labels: cronjob.metadata.labels, |
| 536 | }, |
| 537 | }, |
| 538 | }, |
| 539 | }, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 540 | schedule: error "Need to provide spec.schedule", |
| 541 | successfulJobsHistoryLimit: 10, |
| 542 | failedJobsHistoryLimit: 20, |
| 543 | // NB: upstream concurrencyPolicy default is "Allow" |
| 544 | concurrencyPolicy: "Forbid", |
| 545 | }, |
| 546 | }, |
| 547 | |
| 548 | JobSpec: { |
| 549 | local this = self, |
| 550 | |
| 551 | template: { |
| 552 | spec: $.PodSpec { |
| 553 | restartPolicy: "OnFailure", |
| 554 | }, |
| 555 | }, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 556 | completions: 1, |
| 557 | parallelism: 1, |
| 558 | }, |
| 559 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 560 | DaemonSet(name): $._Object("apps/v1", "DaemonSet", name) { |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 561 | local ds = self, |
| 562 | spec: { |
| 563 | updateStrategy: { |
| 564 | type: "RollingUpdate", |
| 565 | rollingUpdate: { |
| 566 | maxUnavailable: 1, |
| 567 | }, |
| 568 | }, |
| 569 | template: { |
| 570 | metadata: { |
| 571 | labels: ds.metadata.labels, |
| 572 | annotations: {}, |
| 573 | }, |
| 574 | spec: $.PodSpec, |
| 575 | }, |
| 576 | |
| 577 | selector: { |
| 578 | matchLabels: ds.spec.template.metadata.labels, |
| 579 | }, |
| 580 | }, |
| 581 | }, |
| 582 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 583 | Ingress(name): $._Object("networking.k8s.io/v1beta1", "Ingress", name) { |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 584 | spec: {}, |
| 585 | |
| 586 | local rel_paths = [ |
| 587 | p.path |
| 588 | for r in self.spec.rules |
| 589 | for p in r.http.paths |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 590 | if std.objectHas(p, "path") && !std.startsWith(p.path, "/") |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 591 | ], |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 592 | assert (!$._assert) || std.length(rel_paths) == 0 : "paths must be absolute: " + rel_paths, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 593 | }, |
| 594 | |
| 595 | ThirdPartyResource(name): $._Object("extensions/v1beta1", "ThirdPartyResource", name) { |
| 596 | versions_:: [], |
| 597 | versions: [{ name: n } for n in self.versions_], |
| 598 | }, |
| 599 | |
| 600 | CustomResourceDefinition(group, version, kind): { |
| 601 | local this = self, |
| 602 | apiVersion: "apiextensions.k8s.io/v1beta1", |
| 603 | kind: "CustomResourceDefinition", |
| 604 | metadata+: { |
| 605 | name: this.spec.names.plural + "." + this.spec.group, |
| 606 | }, |
| 607 | spec: { |
| 608 | scope: "Namespaced", |
| 609 | group: group, |
| 610 | version: version, |
| 611 | names: { |
| 612 | kind: kind, |
| 613 | singular: $.toLower(self.kind), |
| 614 | plural: self.singular + "s", |
| 615 | listKind: self.kind + "List", |
| 616 | }, |
| 617 | }, |
| 618 | }, |
| 619 | |
| 620 | ServiceAccount(name): $._Object("v1", "ServiceAccount", name) { |
| 621 | }, |
| 622 | |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 623 | Role(name): $._Object("rbac.authorization.k8s.io/v1", "Role", name) { |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 624 | rules: [], |
| 625 | }, |
| 626 | |
| 627 | ClusterRole(name): $.Role(name) { |
| 628 | kind: "ClusterRole", |
| 629 | }, |
| 630 | |
| 631 | Group(name): { |
| 632 | kind: "Group", |
| 633 | name: name, |
| 634 | apiGroup: "rbac.authorization.k8s.io", |
| 635 | }, |
| 636 | |
| 637 | User(name): { |
| 638 | kind: "User", |
| 639 | name: name, |
| 640 | apiGroup: "rbac.authorization.k8s.io", |
| 641 | }, |
| 642 | |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 643 | RoleBinding(name): $._Object("rbac.authorization.k8s.io/v1", "RoleBinding", name) { |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 644 | local rb = self, |
| 645 | |
| 646 | subjects_:: [], |
| 647 | subjects: [{ |
| 648 | kind: o.kind, |
| 649 | namespace: o.metadata.namespace, |
| 650 | name: o.metadata.name, |
| 651 | } for o in self.subjects_], |
| 652 | |
| 653 | roleRef_:: error "roleRef is required", |
| 654 | roleRef: { |
| 655 | apiGroup: "rbac.authorization.k8s.io", |
| 656 | kind: rb.roleRef_.kind, |
| 657 | name: rb.roleRef_.metadata.name, |
| 658 | }, |
| 659 | }, |
| 660 | |
| 661 | ClusterRoleBinding(name): $.RoleBinding(name) { |
| 662 | kind: "ClusterRoleBinding", |
| 663 | }, |
| 664 | |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 665 | // NB: encryptedData can be imported into a SealedSecret as follows: |
| 666 | // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.encryptedData > sealedsecret.json |
| 667 | // encryptedData: std.parseJson(importstr "sealedsecret.json") |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 668 | SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) { |
| 669 | spec: { |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 670 | encryptedData: {}, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 671 | }, |
Bartosz Stebel | 3a15b83 | 2021-06-16 19:14:50 +0200 | [diff] [blame] | 672 | assert (!$._assert) || std.length(std.objectFields(self.spec.encryptedData)) != 0 : "SealedSecret '%s' has empty encryptedData field" % name, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 673 | }, |
| 674 | |
| 675 | // NB: helper method to access several Kubernetes objects podRef, |
| 676 | // used below to extract its labels |
| 677 | podRef(obj):: ({ |
| 678 | Pod: obj, |
| 679 | Deployment: obj.spec.template, |
| 680 | StatefulSet: obj.spec.template, |
| 681 | DaemonSet: obj.spec.template, |
| 682 | Job: obj.spec.template, |
| 683 | CronJob: obj.spec.jobTemplate.spec.template, |
| 684 | }[obj.kind]), |
| 685 | |
| 686 | // NB: return a { podSelector: ... } ready to use for e.g. NSPs (see below) |
| 687 | // pod labels can be optionally filtered by their label name 2nd array arg |
| 688 | podLabelsSelector(obj, filter=null):: { |
| 689 | podSelector: std.prune({ |
| 690 | matchLabels: |
| 691 | if filter != null then $.filterMapByFields($.podRef(obj).metadata.labels, filter) |
| 692 | else $.podRef(obj).metadata.labels, |
| 693 | }), |
| 694 | }, |
| 695 | |
| 696 | // NB: Returns an array as [{ port: num, protocol: "PROTO" }, {...}, ... ] |
| 697 | // Need to split TCP, UDP logic to be able to dedup each set of protocol ports |
| 698 | podsPorts(obj_list):: std.flattenArrays([ |
| 699 | [ |
| 700 | { port: port, protocol: protocol } |
| 701 | for port in std.set( |
| 702 | std.flattenArrays([$.podRef(obj).spec.ports(protocol) for obj in obj_list]) |
| 703 | ) |
| 704 | ] |
| 705 | for protocol in ["TCP", "UDP"] |
| 706 | ]), |
| 707 | |
| 708 | // NB: most of the "helper" stuff comes from above (podLabelsSelector, podsPorts), |
| 709 | // NetworkPolicy returned object will have "Ingress", "Egress" policyTypes auto-set |
| 710 | // based on populated spec.ingress or spec.egress |
| 711 | // See tests/test-simple-validate.jsonnet for example(s). |
| 712 | NetworkPolicy(name): $._Object("networking.k8s.io/v1", "NetworkPolicy", name) { |
| 713 | local networkpolicy = self, |
| 714 | spec: { |
| 715 | policyTypes: std.prune([ |
| 716 | if networkpolicy.spec.ingress != [] then "Ingress" else null, |
| 717 | if networkpolicy.spec.egress != [] then "Egress" else null, |
| 718 | ]), |
| 719 | ingress: $.objectValues(self.ingress_), |
| 720 | ingress_:: {}, |
| 721 | egress: $.objectValues(self.egress_), |
| 722 | egress_:: {}, |
Rafał Hirsz | ccda333 | 2020-05-16 21:01:03 +0200 | [diff] [blame] | 723 | podSelector: {}, |
| 724 | }, |
| 725 | }, |
| 726 | |
| 727 | VerticalPodAutoscaler(name):: $._Object("autoscaling.k8s.io/v1beta2", "VerticalPodAutoscaler", name) { |
| 728 | local vpa = self, |
| 729 | |
| 730 | target:: error "target required", |
| 731 | |
| 732 | spec: { |
| 733 | targetRef: $.CrossVersionObjectReference(vpa.target), |
| 734 | |
| 735 | updatePolicy: { |
| 736 | updateMode: "Auto", |
| 737 | }, |
| 738 | }, |
| 739 | }, |
| 740 | // Helper function to ease VPA creation as e.g.: |
| 741 | // foo_vpa:: kube.createVPAFor($.foo_deploy) |
| 742 | createVPAFor(target, mode="Auto"):: $.VerticalPodAutoscaler(target.metadata.name) { |
| 743 | target:: target, |
| 744 | |
| 745 | metadata+: { |
| 746 | namespace: target.metadata.namespace, |
| 747 | labels+: target.metadata.labels, |
| 748 | }, |
| 749 | spec+: { |
| 750 | updatePolicy+: { |
| 751 | updateMode: mode, |
| 752 | }, |
Sergiusz Bazanski | e31d64f | 2019-10-02 20:59:26 +0200 | [diff] [blame] | 753 | }, |
| 754 | }, |
| 755 | } |