cluster/kube: initial cert-manager implementation
diff --git a/cluster/certs/ca.srl b/cluster/certs/ca.srl
index 2233eb9..a8f46e8 100644
--- a/cluster/certs/ca.srl
+++ b/cluster/certs/ca.srl
@@ -1 +1 @@
-80F13FCE5DBBF748
+80F13FCE5DBBF749
diff --git a/cluster/kube/cluster.jsonnet b/cluster/kube/cluster.jsonnet
index 48a8979..b49f01a 100644
--- a/cluster/kube/cluster.jsonnet
+++ b/cluster/kube/cluster.jsonnet
@@ -7,6 +7,7 @@
 local metallb = import "lib/metallb.libsonnet";
 local nginx = import "lib/nginx.libsonnet";
 local rook = import "lib/rook.libsonnet";
+local certmanager = import "lib/cert-manager.libsonnet";
 
 local Cluster(fqdn) = {
     local cluster = self,
@@ -61,6 +62,7 @@
     },
     // Main nginx Ingress Controller
     nginx: nginx.Environment {},
+    certmanager: certmanager.Environment {},
 
     // Rook Ceph storage
     rook: rook.Operator {},
diff --git a/cluster/kube/lib/cert-manager.libsonnet b/cluster/kube/lib/cert-manager.libsonnet
new file mode 100644
index 0000000..5809d73
--- /dev/null
+++ b/cluster/kube/lib/cert-manager.libsonnet
@@ -0,0 +1,720 @@
+# Deploy a per-cluster cert-manager
+
+local kube = import "../../../kube/kube.libsonnet";
+
+{
+    local cm = self,
+    Environment: {
+        local env = self,
+        local cfg = env.cfg,
+
+        cfg:: {
+            namespace: "cert-manager",
+        },
+
+        metadata:: {
+            namespace: cfg.namespace,
+        },
+
+        namespace: kube.Namespace(cfg.namespace) {
+            metadata+: {
+                labels: { "certmanager.k8s.io/disable-validation": "true" },
+            },
+        },
+
+        crds: {
+            certificates: kube.CustomResourceDefinition("certmanager.k8s.io", "v1alpha1", "Certificate") {
+                spec+: {
+                    additionalPrinterColumns: [
+                        { name: "Ready", type: "string", JSONPath: ".status.conditions[?(@.type==\"Ready\")].status" },
+                        { name: "Secret", type: "string", JSONPath: ".spec.secretName" },
+                        { name: "Issuer", type: "string", JSONPath: ".spec.issuerRef.name", priority: 1 },
+                        { name: "Status", type: "string", JSONPath: ".status.conditions[?(@.type==\"Ready\")].message", priority: 1 },
+                        { name: "Age", type: "date", JSONPath: ".metadata.creationTimestamp" },
+                    ],
+                    names+: {
+                        shortNames+: ["cert", "certs"],
+                    },
+                    scope: "Namespaced",
+                    validation: {
+                        # Converted from official YAML
+                        "openAPIV3Schema": {
+                            "properties": {
+                                "apiVersion": {
+                                    "description": "APIVersion defines the versioned schema of this representation\nof an object. Servers should convert recognized schemas to the latest\ninternal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
+                                    "type": "string"
+                                },
+                                "kind": {
+                                    "description": "Kind is a string value representing the REST resource this\nobject represents. Servers may infer this from the endpoint the client\nsubmits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
+                                    "type": "string"
+                                },
+                                "metadata": {
+                                    "type": "object"
+                                },
+                                "spec": {
+                                    "properties": {
+                                        "acme": {
+                                            "description": "ACME contains configuration specific to ACME Certificates.\nNotably, this contains details on how the domain names listed on this\nCertificate resource should be 'solved', i.e. mapping HTTP01 and DNS01\nproviders to DNS names.",
+                                            "properties": {
+                                                "config": {
+                                                    "items": {
+                                                        "properties": {
+                                                            "domains": {
+                                                                "description": "Domains is the list of domains that this SolverConfig\napplies to.",
+                                                                "items": {
+                                                                    "type": "string"
+                                                                },
+                                                                "type": "array"
+                                                            }
+                                                        },
+                                                        "required": [
+                                                            "domains"
+                                                        ],
+                                                        "type": "object"
+                                                    },
+                                                    "type": "array"
+                                                }
+                                            },
+                                            "required": [
+                                                "config"
+                                            ],
+                                            "type": "object"
+                                        },
+                                        "commonName": {
+                                            "description": "CommonName is a common name to be used on the Certificate",
+                                            "type": "string"
+                                        },
+                                        "dnsNames": {
+                                            "description": "DNSNames is a list of subject alt names to be used on the\nCertificate",
+                                            "items": {
+                                                "type": "string"
+                                            },
+                                            "type": "array"
+                                        },
+                                        "duration": {
+                                            "description": "Certificate default Duration",
+                                            "type": "string"
+                                        },
+                                        "ipAddresses": {
+                                            "description": "IPAddresses is a list of IP addresses to be used on the\nCertificate",
+                                            "items": {
+                                                "type": "string"
+                                            },
+                                            "type": "array"
+                                        },
+                                        "isCA": {
+                                            "description": "IsCA will mark this Certificate as valid for signing. This\nimplies that the 'signing' usage is set",
+                                            "type": "boolean"
+                                        },
+                                        "issuerRef": {
+                                            "description": "IssuerRef is a reference to the issuer for this certificate.\nIf the 'kind' field is not set, or set to 'Issuer', an Issuer resource\nwith the given name in the same namespace as the Certificate will\nbe used. If the 'kind' field is set to 'ClusterIssuer', a ClusterIssuer\nwith the provided name will be used. The 'name' field in this stanza\nis required at all times.",
+                                            "properties": {
+                                                "kind": {
+                                                    "type": "string"
+                                                },
+                                                "name": {
+                                                    "type": "string"
+                                                }
+                                            },
+                                            "required": [
+                                                "name"
+                                            ],
+                                            "type": "object"
+                                        },
+                                        "keyAlgorithm": {
+                                            "description": "KeyAlgorithm is the private key algorithm of the corresponding\nprivate key for this certificate. If provided, allowed values are\neither \"rsa\" or \"ecdsa\" If KeyAlgorithm is specified and KeySize is\nnot provided, key size of 256 will be used for \"ecdsa\" key algorithm\nand key size of 2048 will be used for \"rsa\" key algorithm.",
+                                            "enum": [
+                                                "rsa",
+                                            "ecdsa"
+                                            ],
+                                            "type": "string"
+                                        },
+                                        "keySize": {
+                                            "description": "KeySize is the key bit size of the corresponding private\nkey for this certificate. If provided, value must be between 2048\nand 8192 inclusive when KeyAlgorithm is empty or is set to \"rsa\",\nand value must be one of (256, 384, 521) when KeyAlgorithm is set\nto \"ecdsa\".",
+                                            "format": "int64",
+                                            "type": "integer"
+                                        },
+                                        "organization": {
+                                            "description": "Organization is the organization to be used on the Certificate",
+                                            "items": {
+                                                "type": "string"
+                                            },
+                                            "type": "array"
+                                        },
+                                        "renewBefore": {
+                                            "description": "Certificate renew before expiration duration",
+                                            "type": "string"
+                                        },
+                                        "secretName": {
+                                            "description": "SecretName is the name of the secret resource to store\nthis secret in",
+                                            "type": "string"
+                                        }
+                                    },
+                                    "required": [
+                                        "secretName",
+                                    "issuerRef"
+                                    ],
+                                    "type": "object"
+                                },
+                                "status": {
+                                    "properties": {
+                                        "conditions": {
+                                            "items": {
+                                                "properties": {
+                                                    "lastTransitionTime": {
+                                                        "description": "LastTransitionTime is the timestamp corresponding\nto the last status change of this condition.",
+                                                        "format": "date-time",
+                                                        "type": "string"
+                                                    },
+                                                    "message": {
+                                                        "description": "Message is a human readable description of the details\nof the last transition, complementing reason.",
+                                                        "type": "string"
+                                                    },
+                                                    "reason": {
+                                                        "description": "Reason is a brief machine readable explanation for\nthe condition's last transition.",
+                                                        "type": "string"
+                                                    },
+                                                    "status": {
+                                                        "description": "Status of the condition, one of ('True', 'False',\n'Unknown').",
+                                                        "enum": [
+                                                            "True",
+                                                        "False",
+                                                        "Unknown"
+                                                        ],
+                                                        "type": "string"
+                                                    },
+                                                    "type": {
+                                                        "description": "Type of the condition, currently ('Ready').",
+                                                        "type": "string"
+                                                    }
+                                                },
+                                                "required": [
+                                                    "type",
+                                                "status",
+                                                "lastTransitionTime",
+                                                "reason",
+                                                "message"
+                                                ],
+                                                "type": "object"
+                                            },
+                                            "type": "array"
+                                        },
+                                        "lastFailureTime": {
+                                            "format": "date-time",
+                                            "type": "string"
+                                        },
+                                        "notAfter": {
+                                            "description": "The expiration time of the certificate stored in the secret\nnamed by this resource in spec.secretName.",
+                                            "format": "date-time",
+                                            "type": "string"
+                                        }
+                                    },
+                                    "type": "object"
+                                }
+                            }
+                        }
+                    }
+                },
+            },
+            challenges: kube.CustomResourceDefinition("certmanager.k8s.io", "v1alpha1", "Challenge") {
+                spec+: {
+                    additionalPrinterColumns: [
+                        { name: "State", type: "string", JSONPath: ".status.state" },
+                        { name: "Domain", type: "string", JSONPath: ".spec.dnsName" },
+                        { name: "Reason", type: "string", JSONPath: ".status.reason", priority: 1 },
+                        { name: "Age", type: "date", JSONPath: ".metadata.creationTimestamp" },
+                    ],
+                    validation: {
+                        # ...
+                    },
+                },
+            },
+            clusterissuers: kube.CustomResourceDefinition("certmanager.k8s.io", "v1alpha1", "ClusterIssuer") {
+                spec+: {
+                    validation: {
+                        # ...
+                    },
+                    scope: "Cluster",
+                },
+            },
+            issuers: kube.CustomResourceDefinition("certmanager.k8s.io", "v1alpha1", "Issuer") {
+                spec+: {
+                    validation: {
+                        # ...
+                    },
+                    scope: "Namespaced",
+                },
+            },
+            orders: kube.CustomResourceDefinition("certmanager.k8s.io", "v1alpha1", "Order") {
+                spec+: {
+                    additionalPrinterColumns: [
+                        { name: "State", type: "string", JSONPath: ".status.state" },
+                        { name: "Issuer", type: "string", JSONPath: ".spec.issuerRef.name", priority: 1 },
+                        { name: "Reason", type: "string", JSONPath: ".status.reason", priority: 1 },
+                        { name: "Age", type: "date", JSONPath: ".metadata.creationTimestamp" },
+                    ],
+                    validation: {
+                        # ...
+                    },
+                    scope: "Namespaced",
+                },
+            },
+        },
+
+        sas: {
+            cainjector: kube.ServiceAccount("cert-manager-cainjector") {
+                metadata+: env.metadata,
+            },
+            webhook: kube.ServiceAccount("cert-manager-webhook") {
+                metadata+: env.metadata,
+            },
+            certmanager: kube.ServiceAccount("cert-manager") {
+                metadata+: env.metadata,
+            },
+        },
+
+        crs: {
+            cainjector: kube.ClusterRole("cert-manager-cainjector") {
+                rules: [
+                    {
+                        apiGroups: ["certmanager.k8s.io"],
+                        resources: ["certificates"],
+                        verbs: ["get", "list", "watch"],
+                    },
+                    {
+                        apiGroups: [""],
+                        resources: ["secrets"],
+                        verbs: ["get", "list", "watch"],
+                    },
+                    {
+                        apiGroups: [""],
+                        resources: ["configmaps", "events"],
+                        verbs: ["*"],
+                    },
+
+                    {
+                        apiGroups: ["admissionregistration.k8s.io"],
+                        resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"],
+                        verbs: ["*"],
+                    },
+                    {
+                        apiGroups: ["apiregistration.k8s.io"],
+                        resources: ["apiservices"],
+                        verbs: ["*"],
+                    },
+                ],
+            },
+            certmanager: kube.ClusterRole("cert-manager") {
+                rules: [
+                    {
+                        apiGroups: ["certmanager.k8s.io"],
+                        resources: ["certificates", "certificates/finalizers", "issuers", "clusterissuers", "orders", "orders/finalizers", "challenges"],
+                        verbs: ["*"],
+                    },
+                    {
+                        apiGroups: [""],
+                        resources: ["configmaps", "secrets", "events", "services", "pods"],
+                        verbs: ["*"],
+                    },
+                    {
+                        apiGroups: ["extensions"],
+                        resources: ["ingresses"],
+                        verbs: ["*"],
+                    },
+                ],
+            },
+            certmanagerView: kube.ClusterRole("cert-manager-view") {
+                rules: [
+                    {
+                        apiGroups: ["certmanager.k8s.io"],
+                        resources: ["certificates", "issuers"],
+                        verbs: ["get", "list", "watch"],
+                    },
+                ],
+            },
+            certmanagerEdit: kube.ClusterRole("cert-manager-edit") {
+                rules: [
+                    {
+                        apiGroups: ["certmanager.k8s.io"],
+                        resources: ["certificates", "issuers"],
+                        verbs: ["create", "delete", "deletecollection", "patch", "update"],
+                    },
+                ],
+            },
+            webhookRequester: kube.ClusterRole("cert-manager-webhook:webhook-requester") {
+                rules: [
+                    {
+                        apiGroups: ["admission.certmanager.k8s.io"],
+                        resources: ["certificates", "issuers", "clusterissuers"],
+                        verbs: ["create"],
+                    },
+                ],
+            },
+        },
+        rbs: {
+            cainjector: kube.ClusterRoleBinding("cert-manager-cainjector") {
+                roleRef_: env.crs.cainjector,
+                subjects_: [ env.sas.cainjector ],
+            },
+            certmanager: kube.ClusterRoleBinding("cert-manager") {
+                roleRef_: env.crs.certmanager,
+                subjects_: [ env.sas.certmanager ],
+            },
+            webhookAuthDelegator: kube.ClusterRoleBinding("cert-manager-webhook:auth-delegator") {
+                roleRef_: {
+                    kind: "ClusterRole",
+                    metadata: { name: "system:auth-delegator" },
+                },
+                subjects_: [ env.sas.webhook ],
+            },
+            webhookAuthReader: kube.RoleBinding("cert-manager-webhook:webhook-authentication-reader") {
+                metadata+: {
+                    namespace: "kube-system",
+                },
+                roleRef_: {
+                    kind: "Role",
+                    metadata: { name: "extension-apiserver-authentication-reader" },
+                },
+                subjects_: [ env.sas.webhook ],
+            },
+        },
+        deployments: {
+            cainjector: kube.Deployment("cert-manager-cainjector") {
+                metadata+: env.metadata,
+                spec+: {
+                    replicas: 1,
+                    template+: {
+                        spec+: {
+                            serviceAccountName: env.sas.cainjector.metadata.name,
+                            containers_: {
+                                cainjector: kube.Container("cainjector") {
+                                    image: "quay.io/jetstack/cert-manager-cainjector:v0.7.0",
+                                    args: [
+                                        "--leader-election-namespace=%s" % [cfg.namespace],
+                                    ],
+                                    env_: {
+                                        POD_NAMESPACE: kube.FieldRef("metadata.namespace"),
+                                    },
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+            webhook: kube.Deployment("cert-manager-webhook") {
+                metadata+: env.metadata {
+                    labels: {
+                        app: "webhook",
+                    },
+                },
+                spec+: {
+                    replicas: 1,
+                    template+: {
+                        spec+: {
+                            serviceAccountName: env.sas.webhook.metadata.name,
+                            containers_: {
+                                webhook: kube.Container("webhook") {
+                                    image: "quay.io/jetstack/cert-manager-webhook:v0.7.0",
+                                    args: [
+                                        "--v=12",
+                                        "--secure-port=6443",
+                                        "--tls-cert-file=/certs/tls.crt",
+                                        "--tls-private-key-file=/certs/tls.key",
+                                    ],
+                                    env_: {
+                                        POD_NAMESPACE: kube.FieldRef("metadata.namespace"),
+                                    },
+                                    ports_: { // changed
+                                        https: { containerPort: 6443 },
+                                    },
+                                    volumeMounts_: {
+                                        certs: { mountPath: "/certs" },
+                                    },
+                                },
+                            },
+                            volumes_: {
+                                certs: {
+                                    secret: { secretName: env.certificates.webhookTLS.spec.secretName },
+                                },
+                                // kube.SecretVolume(env.secrets.webhook_tls),
+                            },
+                        },
+                    },
+                },
+            },
+            certmanager: kube.Deployment("cert-manager") {
+                metadata+: env.metadata,
+                spec+: {
+                    replicas: 1,
+                    template+: {
+                        spec+: {
+                            serviceAccountName: env.sas.certmanager.metadata.name,
+                            containers_: {
+                                webhook: kube.Container("cert-manager") {
+                                    image: "quay.io/jetstack/cert-manager-controller:v0.7.0",
+                                    args: [
+                                        "--cluster-resource-namespace=%s" % [cfg.namespace],
+                                        "--leader-election-namespace=%s" % [cfg.namespace],
+                                    ],
+                                    env_: {
+                                        POD_NAMESPACE: kube.FieldRef("metadata.namespace"),
+                                    },
+                                    ports_: {
+                                        metrics: { containerPort: 9402 },
+                                    },
+                                    resources: {
+                                        requests: {
+                                            cpu: "10m",
+                                            memory: "32Mi",
+                                        },
+                                    },
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+        service: kube.Service("cert-manager-webhook") {
+            metadata+: env.metadata,
+            target_pod:: env.deployments.webhook.spec.template,
+            spec+: {
+                type: "ClusterIP",
+                ports: [
+                    { name: "https", port: 443, targetPort: 6443, protocol: "TCP" },
+                ],
+            },
+        },
+        apiservice: kube._Object("apiregistration.k8s.io/v1beta1", "APIService", "v1beta1.admission.certmanager.k8s.io") {
+            spec+: {
+                version: "v1beta1",
+                group: "admission.certmanager.k8s.io",
+                groupPriorityMinimum: 1000,
+                versionPriority: 15,
+                service: {
+                    name: env.service.metadata.name,
+                    namespace: cfg.namespace,
+                },
+            },
+        },
+        issuers: {
+            webhookSelfsign: cm.Issuer("cert-manager-webhook-selfsign") {
+                metadata+: env.metadata,
+                spec: {
+                    selfSigned: {},
+                },
+            },
+            webhookCA: cm.Issuer("cert-manager-webhook-ca") {
+                metadata+: env.metadata,
+                spec: {
+                    ca: {
+                        secretName: env.certificates.webhookCA.spec.secretName,
+                    },
+                },
+            },
+        },
+        certificates: {
+            webhookCA: cm.Certificate("cert-manager-webhook-ca") {
+                metadata+: env.metadata,
+                spec: {
+                    secretName: "cert-manager-webhook-ca",
+                    duration: "43800h", // 5 years
+                    issuerRef: {
+                        name: env.issuers.webhookSelfsign.metadata.name,
+                    },
+                    commonName: "ca.webhook.cert-manager",
+                    isCA: true,
+                },
+            },
+            webhookTLS: cm.Certificate("cert-manager-webhook-webhook-tls") {
+                metadata+: env.metadata,
+                spec: {
+                    secretName: "cert-manager-webhook-webhook-tls",
+                    duration: "8760h", // 1 year
+                    issuerRef: {
+                        name: env.issuers.webhookSelfsign.metadata.name,
+                    },
+                    dnsNames: [
+                        "cert-manager-webhook",
+                        "cert-manager-webhook.cert-manager",
+                        "cert-manager-webhook.cert-manager.svc",
+                    ],
+                },
+            },
+        },
+        admission: kube._Object("admissionregistration.k8s.io/v1beta1", "ValidatingWebhookConfiguration", "cert-manager-webhook") {
+            metadata+: {
+                annotations: {
+                    "certmanager.k8s.io/inject-apiserver-ca": "true",
+                },
+            },
+            webhooks: [
+                // Copied from official yaml
+                {
+                    "name": "certificates.admission.certmanager.k8s.io",
+                    "namespaceSelector": {
+                        "matchExpressions": [
+                            {
+                                "key": "certmanager.k8s.io/disable-validation",
+                                "operator": "NotIn",
+                                "values": [
+                                    "true"
+                                ]
+                            },
+                            {
+                                "key": "name",
+                                "operator": "NotIn",
+                                "values": [
+                                    "cert-manager"
+                                ]
+                            }
+                        ]
+                    },
+                    "rules": [
+                        {
+                            "apiGroups": [
+                                "certmanager.k8s.io"
+                            ],
+                            "apiVersions": [
+                                "v1alpha1"
+                            ],
+                            "operations": [
+                                "CREATE",
+                                "UPDATE"
+                            ],
+                            "resources": [
+                                "certificates"
+                            ]
+                        }
+                    ],
+                    "failurePolicy": "Fail",
+                    "clientConfig": {
+                        "service": {
+                            "name": "kubernetes",
+                            "namespace": "default",
+                            "path": "/apis/admission.certmanager.k8s.io/v1beta1/certificates"
+                        },
+                        "caBundle": "",
+                    }
+                },
+                {
+                    "name": "issuers.admission.certmanager.k8s.io",
+                    "namespaceSelector": {
+                        "matchExpressions": [
+                            {
+                                "key": "certmanager.k8s.io/disable-validation",
+                                "operator": "NotIn",
+                                "values": [
+                                    "true"
+                                ]
+                            },
+                            {
+                                "key": "name",
+                                "operator": "NotIn",
+                                "values": [
+                                    "cert-manager"
+                                ]
+                            }
+                        ]
+                    },
+                    "rules": [
+                        {
+                            "apiGroups": [
+                                "certmanager.k8s.io"
+                            ],
+                            "apiVersions": [
+                                "v1alpha1"
+                            ],
+                            "operations": [
+                                "CREATE",
+                                "UPDATE"
+                            ],
+                            "resources": [
+                                "issuers"
+                            ]
+                        }
+                    ],
+                    "failurePolicy": "Fail",
+                    "clientConfig": {
+                        "service": {
+                            "name": "kubernetes",
+                            "namespace": "default",
+                            "path": "/apis/admission.certmanager.k8s.io/v1beta1/issuers"
+                        },
+                        "caBundle": "",
+                    }
+                },
+                {
+                    "name": "clusterissuers.admission.certmanager.k8s.io",
+                    "namespaceSelector": {
+                        "matchExpressions": [
+                            {
+                                "key": "certmanager.k8s.io/disable-validation",
+                                "operator": "NotIn",
+                                "values": [
+                                    "true"
+                                ]
+                            },
+                            {
+                                "key": "name",
+                                "operator": "NotIn",
+                                "values": [
+                                    "cert-manager"
+                                ]
+                            }
+                        ]
+                    },
+                    "rules": [
+                        {
+                            "apiGroups": [
+                                "certmanager.k8s.io"
+                            ],
+                            "apiVersions": [
+                                "v1alpha1"
+                            ],
+                            "operations": [
+                                "CREATE",
+                                "UPDATE"
+                            ],
+                            "resources": [
+                                "clusterissuers"
+                            ]
+                        }
+                    ],
+                    "failurePolicy": "Fail",
+                    "clientConfig": {
+                        "service": {
+                            "name": "kubernetes",
+                            "namespace": "default",
+                            "path": "/apis/admission.certmanager.k8s.io/v1beta1/clusterissuers"
+                        },
+                        "caBundle": "",
+                    }
+                }
+            ],
+        },
+    },
+
+    /*
+    Issuer(name):: {
+        local cfg = self,
+        spec:: error "spec must be specified",
+        metadata:: {
+            namespace: "cert-manager",
+        },
+
+        issuer: kube._Object("certmanager.k8s.io/v1alpha1", "Issuer", name) {
+            metadata+: cfg.metadata,
+            spec: cfg.spec,
+        },
+    },
+    */
+
+    Issuer(name): kube._Object("certmanager.k8s.io/v1alpha1", "Issuer", name) {
+        spec: error "spec must be specified",
+    },
+
+    Certificate(name): kube._Object("certmanager.k8s.io/v1alpha1", "Certificate", name) {
+        spec: error "spec must be specified",
+    },
+}