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