forked from hswaw/hscloud
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
This commit is contained in:
parent
54490d385e
commit
e31d64f265
6 changed files with 725 additions and 724 deletions
|
@ -176,7 +176,7 @@ local Cluster(fqdn) = {
|
|||
// 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",
|
||||
|
|
|
@ -501,13 +501,13 @@ local kube = import "../../../kube/kube.libsonnet";
|
|||
},
|
||||
|
||||
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 @@ local kube = import "../../../kube/kube.libsonnet";
|
|||
},
|
||||
},
|
||||
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 @@ local kube = import "../../../kube/kube.libsonnet";
|
|||
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 @@ local kube = import "../../../kube/kube.libsonnet";
|
|||
],
|
||||
},
|
||||
},
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 @@ local policies = import "../../../kube/policies.libsonnet";
|
|||
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 @@ local policies = import "../../../kube/policies.libsonnet";
|
|||
},
|
||||
},
|
||||
|
||||
clusterIssuer: cm.Issuer(cluster.name("cluster-ca")) {
|
||||
clusterIssuer: kube.Issuer(cluster.name("cluster-ca")) {
|
||||
metadata+: cluster.metadata,
|
||||
spec: {
|
||||
ca: {
|
||||
|
@ -105,7 +104,7 @@ local policies = import "../../../kube/policies.libsonnet";
|
|||
},
|
||||
},
|
||||
|
||||
nodeCertificate: cm.Certificate(cluster.name("node")) {
|
||||
nodeCertificate: kube.Certificate(cluster.name("node")) {
|
||||
metadata+: cluster.metadata,
|
||||
spec: {
|
||||
secretName: "cockroachdb-node-cert",
|
||||
|
@ -127,7 +126,7 @@ local policies = import "../../../kube/policies.libsonnet";
|
|||
},
|
||||
},
|
||||
|
||||
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 @@ local policies = import "../../../kube/policies.libsonnet";
|
|||
},
|
||||
|
||||
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),
|
||||
|
|
|
@ -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 @@ local cm = import "cert-manager.libsonnet";
|
|||
|
||||
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 @@ local cm = import "cert-manager.libsonnet";
|
|||
commonName: "auth.registry",
|
||||
},
|
||||
},
|
||||
registryCertificate: cm.Certificate("registry") {
|
||||
registryCertificate: kube.Certificate("registry") {
|
||||
metadata+: env.metadata("registry"),
|
||||
spec: {
|
||||
secretName: "registry-internal",
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
kube {
|
||||
ClusterIssuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "ClusterIssuer", name) {
|
||||
spec: error "spec must be defined",
|
||||
},
|
||||
|
||||
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_:: {},
|
||||
Issuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "Issuer", name) {
|
||||
spec: error "spec must be defined",
|
||||
},
|
||||
Certificate(name): kube._Object("certmanager.k8s.io/v1alpha1", "Certificate", name) {
|
||||
spec: error "spec must be defined",
|
||||
},
|
||||
}
|
||||
|
|
702
kube/kube.upstream.libsonnet
Normal file
702
kube/kube.upstream.libsonnet
Normal file
|
@ -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_:: {},
|
||||
},
|
||||
},
|
||||
}
|
Loading…
Reference in a new issue