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