kube: move cert-manager resources to kube.local.libsonnet

This way kubernetes consumers don't have to import anything from
cluster/, hopefully.

We also create a small abstraction for local additions for
kube.libsonnet without having to modify upstream.

Change-Id: I209095781f91c8867250a647fe944370cddd67d0
diff --git a/cluster/kube/cluster.jsonnet b/cluster/kube/cluster.jsonnet
index a0b1aed..e9e8932 100644
--- a/cluster/kube/cluster.jsonnet
+++ b/cluster/kube/cluster.jsonnet
@@ -176,7 +176,7 @@
     // Main nginx Ingress Controller
     nginx: nginx.Environment {},
     certmanager: certmanager.Environment {},
-    issuer: certmanager.ClusterIssuer("letsencrypt-prod") {
+    issuer: kube.ClusterIssuer("letsencrypt-prod") {
         spec: {
             acme: {
                 server: "https://acme-v02.api.letsencrypt.org/directory",
diff --git a/cluster/kube/lib/cert-manager.libsonnet b/cluster/kube/lib/cert-manager.libsonnet
index e9e4a4b..93b9357 100644
--- a/cluster/kube/lib/cert-manager.libsonnet
+++ b/cluster/kube/lib/cert-manager.libsonnet
@@ -501,13 +501,13 @@
         },
 
         issuers: {
-            webhookSelfsign: cm.Issuer("cert-manager-webhook-selfsign") {
+            webhookSelfsign: kube.Issuer("cert-manager-webhook-selfsign") {
                 metadata+: env.metadata,
                 spec: {
                     selfSigned: {},
                 },
             },
-            webhookCA: cm.Issuer("cert-manager-webhook-ca") {
+            webhookCA: kube.Issuer("cert-manager-webhook-ca") {
                 metadata+: env.metadata,
                 spec: {
                     ca: {
@@ -517,7 +517,7 @@
             },
         },
         certificates: {
-            webhookCA: cm.Certificate("cert-manager-webhook-ca") {
+            webhookCA: kube.Certificate("cert-manager-webhook-ca") {
                 metadata+: env.metadata,
                 spec: {
                     secretName: "cert-manager-webhook-ca",
@@ -529,7 +529,7 @@
                     isCA: true,
                 },
             },
-            webhookTLS: cm.Certificate("cert-manager-webhook-webhook-tls") {
+            webhookTLS: kube.Certificate("cert-manager-webhook-webhook-tls") {
                 metadata+: env.metadata,
                 spec: {
                     secretName: "cert-manager-webhook-webhook-tls",
@@ -696,16 +696,4 @@
             ],
         },
     },
-
-    Issuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "Issuer", name) {
-        spec: error "spec must be specified",
-    },
-
-    ClusterIssuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "ClusterIssuer", name) {
-        spec: error "spec must be specified",
-    },
-
-    Certificate(name): kube._Object("certmanager.k8s.io/v1alpha1", "Certificate", name) {
-        spec: error "spec must be specified",
-    },
 }
diff --git a/cluster/kube/lib/cockroachdb.libsonnet b/cluster/kube/lib/cockroachdb.libsonnet
index 212104d..0b58180 100644
--- a/cluster/kube/lib/cockroachdb.libsonnet
+++ b/cluster/kube/lib/cockroachdb.libsonnet
@@ -35,7 +35,6 @@
 
 
 local kube = import "../../../kube/kube.libsonnet";
-local cm = import "cert-manager.libsonnet";
 local policies = import "../../../kube/policies.libsonnet";
 
 {
@@ -76,14 +75,14 @@
         name(suffix):: if cluster.cfg.ownNamespace then suffix else name + "-" + suffix,
 
         pki: {
-            selfSignedIssuer: cm.Issuer(cluster.name("selfsigned")) {
+            selfSignedIssuer: kube.Issuer(cluster.name("selfsigned")) {
                 metadata+: cluster.metadata,
                 spec: {
                     selfSigned: {},
                 },
             },
 
-            selfSignedKeypair: cm.Certificate(cluster.name("cluster-ca")) {
+            selfSignedKeypair: kube.Certificate(cluster.name("cluster-ca")) {
                 metadata+: cluster.metadata,
                 spec: {
                     secretName: cluster.name("cluster-ca"),
@@ -96,7 +95,7 @@
                 },
             },
 
-            clusterIssuer: cm.Issuer(cluster.name("cluster-ca")) {
+            clusterIssuer: kube.Issuer(cluster.name("cluster-ca")) {
                 metadata+: cluster.metadata,
                 spec: {
                     ca: {
@@ -105,7 +104,7 @@
                 },
             },
 
-            nodeCertificate: cm.Certificate(cluster.name("node")) {
+            nodeCertificate: kube.Certificate(cluster.name("node")) {
                 metadata+: cluster.metadata,
                 spec: {
                     secretName: "cockroachdb-node-cert",
@@ -127,7 +126,7 @@
                 },
             },
 
-            clientCertificate: cm.Certificate(cluster.name("client")) {
+            clientCertificate: kube.Certificate(cluster.name("client")) {
                 metadata+: cluster.metadata,
                 spec: {
                     secretName: cluster.name("client-certificate"),
@@ -371,7 +370,7 @@
         },
 
         Client(name):: {
-            certificate: cm.Certificate(cluster.name("client-%s" % name)) {
+            certificate: kube.Certificate(cluster.name("client-%s" % name)) {
                 metadata+: cluster.metadata,
                 spec: {
                     secretName: cluster.name("client-%s-certificate" % name),
diff --git a/cluster/kube/lib/registry.libsonnet b/cluster/kube/lib/registry.libsonnet
index 5272b2d..d457830 100644
--- a/cluster/kube/lib/registry.libsonnet
+++ b/cluster/kube/lib/registry.libsonnet
@@ -5,7 +5,6 @@
 #    kubectl get secrets rook-ceph-object-user-<ceph-pool>-object-registry -n <ceph-namespace> -o yaml --export | kubectl replace -f - -n registry
 
 local kube = import "../../../kube/kube.libsonnet";
-local cm = import "cert-manager.libsonnet";
 
 {
     Environment: {
@@ -29,13 +28,13 @@
 
         namespace: kube.Namespace(cfg.namespace),
 
-        registryIssuer: cm.Issuer("registry-issuer") {
+        registryIssuer: kube.Issuer("registry-issuer") {
             metadata+: env.metadata("registry-issuer"),
             spec: {
                 selfSigned: {},
             },
         },
-        authCertificate: cm.Certificate("auth") {
+        authCertificate: kube.Certificate("auth") {
             metadata+: env.metadata("auth"),
             spec: {
                 secretName: "auth-internal",
@@ -46,7 +45,7 @@
                 commonName: "auth.registry",
             },
         },
-        registryCertificate: cm.Certificate("registry") {
+        registryCertificate: kube.Certificate("registry") {
             metadata+: env.metadata("registry"),
             spec: {
                 secretName: "registry-internal",
diff --git a/kube/kube.libsonnet b/kube/kube.libsonnet
index e20d872..c12e3db 100644
--- a/kube/kube.libsonnet
+++ b/kube/kube.libsonnet
@@ -1,702 +1,15 @@
-// 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).
+// Local extensions to kube.upstream.libsonnet.
 
-{
-  // Returns array of values from given object.  Does not include hidden fields.
-  objectValues(o):: [o[field] for field in std.objectFields(o)],
+local kube = import "kube.upstream.libsonnet";
 
-  // 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 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)])
-  ),
-
-  _Object(apiVersion, kind, name):: {
-    local this = self,
-    apiVersion: apiVersion,
-    kind: kind,
-    metadata: {
-      name: name,
-      labels: { name: std.join("-", std.split(this.metadata.name, ":")) },
-      annotations: {},
+kube {
+    ClusterIssuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "ClusterIssuer", name) {
+        spec: error "spec must be defined",
     },
-  },
-
-  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,
+    Issuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "Issuer", name) {
+        spec: error "spec must be defined",
     },
-
-    spec: {
-      selector: service.target_pod.metadata.labels,
-      ports: [
-        {
-          port: service.port,
-          targetPort: service.target_pod.spec.containers[0].ports[0].containerPort,
-        },
-      ],
-      type: "ClusterIP",
+    Certificate(name): kube._Object("certmanager.k8s.io/v1alpha1", "Certificate", name) {
+        spec: error "spec must be defined",
     },
-  },
-
-  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"],
-    },
-  },
-
-  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 { name: x, value: 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 !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: {
-      minAvailable: 1,
-      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: [{ 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 std.length(self.containers) > 0 : "must have at least one container",
-
-    // 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 std.length(nonstrings) == 0 : "data contains non-string values: %s" % [nonstrings],
-  },
-
-  // subtype of EnvVarSource
-  ConfigMapRef(configmap, key): {
-    assert std.objectHas(configmap.data, key) : "%s not in configmap.data" % [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 std.objectHas(secret.data, key) : "%s not in secret.data" % [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/v1beta2", "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,
-
-      replicas: 1,
-      assert self.replicas >= 0,
-    },
-  },
-
-  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 self.maxReplicas >= self.minReplicas,
-    },
-  },
-
-  StatefulSet(name): $._Object("apps/v1beta2", "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,
-      assert self.replicas >= 1,
-    },
-  },
-
-  Job(name): $._Object("batch/v1", "Job", name) {
-    local job = self,
-
-    spec: $.JobSpec {
-      template+: {
-        metadata+: {
-          labels: job.metadata.labels,
-        },
-      },
-    },
-  },
-
-  // NB: kubernetes >= 1.8.x has batch/v1beta1 (olders were batch/v2alpha1)
-  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",
-      },
-    },
-
-    selector: {
-      matchLabels: this.template.metadata.labels,
-    },
-
-    completions: 1,
-    parallelism: 1,
-  },
-
-  DaemonSet(name): $._Object("apps/v1beta2", "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("extensions/v1beta1", "Ingress", name) {
-    spec: {},
-
-    local rel_paths = [
-      p.path
-      for r in self.spec.rules
-      for p in r.http.paths
-      if !std.startsWith(p.path, "/")
-    ],
-    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/v1beta1", "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/v1beta1", "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: datalines_ can be used to reduce boilerplate importstr as:
-  // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.data > mysec-ssdata.txt
-  //   datalines_: importstr "mysec-ssddata.txt"
-  SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) {
-    spec: {
-      data:
-        if self.datalines_ != ""
-        then std.join("", std.split(self.datalines_, "\n"))
-        else error "data or datalines_ required (output from: kubeseal | jq -r .spec.data)",
-      datalines_:: "",
-    },
-    assert std.base64Decode(self.spec.data) != "",
-  },
-
-  // 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_:: {},
-    },
-  },
 }
diff --git a/kube/kube.upstream.libsonnet b/kube/kube.upstream.libsonnet
new file mode 100644
index 0000000..e20d872
--- /dev/null
+++ b/kube/kube.upstream.libsonnet
@@ -0,0 +1,702 @@
+// 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).
+
+{
+  // 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 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)])
+  ),
+
+  _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,
+          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"],
+    },
+  },
+
+  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 { name: x, value: 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 !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: {
+      minAvailable: 1,
+      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: [{ 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 std.length(self.containers) > 0 : "must have at least one container",
+
+    // 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 std.length(nonstrings) == 0 : "data contains non-string values: %s" % [nonstrings],
+  },
+
+  // subtype of EnvVarSource
+  ConfigMapRef(configmap, key): {
+    assert std.objectHas(configmap.data, key) : "%s not in configmap.data" % [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 std.objectHas(secret.data, key) : "%s not in secret.data" % [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/v1beta2", "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,
+
+      replicas: 1,
+      assert self.replicas >= 0,
+    },
+  },
+
+  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 self.maxReplicas >= self.minReplicas,
+    },
+  },
+
+  StatefulSet(name): $._Object("apps/v1beta2", "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,
+      assert self.replicas >= 1,
+    },
+  },
+
+  Job(name): $._Object("batch/v1", "Job", name) {
+    local job = self,
+
+    spec: $.JobSpec {
+      template+: {
+        metadata+: {
+          labels: job.metadata.labels,
+        },
+      },
+    },
+  },
+
+  // NB: kubernetes >= 1.8.x has batch/v1beta1 (olders were batch/v2alpha1)
+  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",
+      },
+    },
+
+    selector: {
+      matchLabels: this.template.metadata.labels,
+    },
+
+    completions: 1,
+    parallelism: 1,
+  },
+
+  DaemonSet(name): $._Object("apps/v1beta2", "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("extensions/v1beta1", "Ingress", name) {
+    spec: {},
+
+    local rel_paths = [
+      p.path
+      for r in self.spec.rules
+      for p in r.http.paths
+      if !std.startsWith(p.path, "/")
+    ],
+    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/v1beta1", "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/v1beta1", "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: datalines_ can be used to reduce boilerplate importstr as:
+  // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.data > mysec-ssdata.txt
+  //   datalines_: importstr "mysec-ssddata.txt"
+  SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) {
+    spec: {
+      data:
+        if self.datalines_ != ""
+        then std.join("", std.split(self.datalines_, "\n"))
+        else error "data or datalines_ required (output from: kubeseal | jq -r .spec.data)",
+      datalines_:: "",
+    },
+    assert std.base64Decode(self.spec.data) != "",
+  },
+
+  // 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_:: {},
+    },
+  },
+}