smsgw: productionize, implement kube/mirko

This productionizes smsgw.

We also add some jsonnet machinery to provide a unified service for Go
micro/mirkoservices.

This machinery provides all the nice stuff:
 - a deployment
 - a service for all your types of pots
 - TLS certificates for HSPKI

We also update and test hspki for a new name scheme.

Change-Id: I292d00f858144903cbc8fe0c1c26eb1180d636bc
diff --git a/.bazelrc b/.bazelrc
index 27915cc..03cd77d 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,3 +4,4 @@
 run --host_force_python=PY2
 build --workspace_status_command=./bzl/workspace-status.sh
 test --build_tests_only
+test --test_output=errors
diff --git a/cluster/kube/cluster.jsonnet b/cluster/kube/cluster.jsonnet
index 60cbaaa..e89a801 100644
--- a/cluster/kube/cluster.jsonnet
+++ b/cluster/kube/cluster.jsonnet
@@ -13,11 +13,16 @@
 local prodvider = import "lib/prodvider.libsonnet";
 local registry = import "lib/registry.libsonnet";
 local rook = import "lib/rook.libsonnet";
+local pki = import "lib/pki.libsonnet";
 
-local Cluster(fqdn) = {
+local Cluster(short, realm) = {
     local cluster = self,
     local cfg = cluster.cfg,
 
+    short:: short,
+    realm:: realm,
+    fqdn:: "%s.%s" % [cluster.short, cluster.realm],
+
     cfg:: {
         // Storage class used for internal services (like registry). This must
         // be set to a valid storage class. This can either be a cloud provider class
@@ -54,7 +59,7 @@
                 apiGroup: "rbac.authorization.k8s.io",
                 kind: "User",
                 # A cluster API Server authenticates with a certificate whose CN is == to the FQDN of the cluster.
-                name: fqdn,
+                name: cluster.fqdn,
             },
         ],
     },
@@ -159,7 +164,7 @@
         cfg+: {
             cluster_domains: [
                 "cluster.local",
-                fqdn,
+                cluster.fqdn,
             ],
         },
     },
@@ -203,12 +208,15 @@
     // Docker registry
     registry: registry.Environment {
         cfg+: {
-            domain: "registry.%s" % [fqdn],
+            domain: "registry.%s" % [cluster.fqdn],
             storageClassName: cfg.storageClassNameParanoid,
             objectStorageName: "waw-hdd-redundant-2-object",
         },
     },
 
+    // TLS PKI machinery
+    pki: pki.Environment(cluster.short, cluster.realm),
+
     // Prodvider
     prodvider: prodvider.Environment {
         cfg+: {
@@ -221,7 +229,7 @@
 {
     k0: {
         local k0 = self,
-        cluster: Cluster("k0.hswaw.net") {
+        cluster: Cluster("k0", "hswaw.net") {
             cfg+: {
                 storageClassNameParanoid: k0.ceph.blockParanoid.name,
             },
diff --git a/cluster/kube/lib/pki.libsonnet b/cluster/kube/lib/pki.libsonnet
new file mode 100644
index 0000000..b9b9df3
--- /dev/null
+++ b/cluster/kube/lib/pki.libsonnet
@@ -0,0 +1,48 @@
+local kube = import "../../../kube/kube.libsonnet";
+
+{
+    Environment(clusterShort, realm): {
+        local env = self,
+
+        realm:: realm,
+        clusterShort:: clusterShort,
+        clusterFQDN:: "%s.%s" % [clusterShort, realm],
+
+        namespace:: "cert-manager", // https://github.com/jetstack/cert-manager/issues/2130
+
+        // An issuer that self-signs certificates, used for the CA certificate.
+        selfSignedIssuer: kube.Issuer("pki-selfsigned") {
+            metadata+: {
+                namespace: env.namespace,
+            },
+            spec: {
+                selfSigned: {},
+            },
+        },
+        
+        // CA keypair, self-signed by the above issuer.
+        selfSignedCert: kube.Certificate("pki-selfsigned") {
+            metadata+: {
+                namespace: env.namespace,
+            },
+            spec: {
+                secretName: "pki-selfsigned-cert",
+                duration: "43800h0m0s", // 5 years,
+                isCA: true,
+                issuerRef: {
+                    name: env.selfSignedIssuer.metadata.name,
+                },
+                commonName: "pki-ca",
+            },
+        },
+        
+        // CA issuer, used to issue certificates signed by the CA.
+        issuer: kube.ClusterIssuer("pki-ca") {
+            spec: {
+                ca: {
+                    secretName: env.selfSignedCert.spec.secretName,
+                },
+            },
+        },
+    },
+}
diff --git a/go/pki/BUILD.bazel b/go/pki/BUILD.bazel
index 0d3544f..5bc7522 100644
--- a/go/pki/BUILD.bazel
+++ b/go/pki/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 go_library(
     name = "go_default_library",
@@ -15,3 +15,10 @@
         "@org_golang_x_net//trace:go_default_library",
     ],
 )
+
+go_test(
+    name = "go_default_test",
+    srcs = ["grpc_test.go"],
+    embed = [":go_default_library"],
+    deps = ["@com_github_go_test_deep//:go_default_library"],
+)
diff --git a/go/pki/README.md b/go/pki/README.md
index b84c32d..f44a970 100644
--- a/go/pki/README.md
+++ b/go/pki/README.md
@@ -24,36 +24,49 @@
 
 All certs for mutual auth have the following CN/SAN format:
 
-    <job>.<principal>.<realm>
+    <job>.<principal>.svc.<cluster-short>.<realm>
+    or
+    <principal>.person.<realm>
+    or
+    <principal>.external.<realm>
 
-For example, if principal maps into a 'group' and job into a 'user':
+Where in adition we define `<cluster>` as being `<realm>` plus its next left-side member.
 
-    arista-proxy-dcr01u23.cluster-management-prod.c.example.com
+For example, for kubernetes jobs:
 
-    job = arista-proxy-dcr01u23
-    principal = cluster-management-prod
-    realm = c.example.com
+    foo.bar.svc.k0.hswaw.net
+
+    job = foo
+    principal = bar.svc
+    cluster = k0.hswaw.net
+    realm = hswaw.net
+
+Where foo is the name of a kubernets service, bar is the name of the namespace its in, and
+k0.hswaw.net is the cluster running them.
+
+For people and external services:
+
+    q3k.person.hswaw.net
+
+    job =
+    principal = q3k
+    cluster = person.hswaw.net
+    realm = hswaw.net
 
 The Realm is a DNS name that is global to all jobs that need mutual authentication.
 
-The Principal is any name that carries significance for logical grouping of jobs.
-It can, but doesn't need to, group jobs by similar permissions.
+The Principal is any name that carries significance for an authentication principal,
+ie. a unit that gives information about an identity of an element. In case of kubernetes
+it's a namespace (as we split authentication/authorization into namespaces). In the case of external
+services and people it's the name of the service or person.
 
-The Job is any name that identifies uniquely (within the principal) a security
-endpoint that describes a single security policy for a gRPC endpoint.
+The Job is a name that makes the Principal more specific, if possible. If set, the Principal
+can be treated as a group of Jobs.
 
 The entire CN should be DNS resolvable into an IP address that would respond to
 gRPC requests on port 42000 (with a server TLS certificate that represents this CN) if the
 job represents a service.
 
-This maps nicely to the Kubernetes Cluster DNS format if you set `realm` to `svc.cluster.local`.
-Then, `principal` maps to a Kubernetes namespace, and `job` maps into a Kubernetes service.
-
-    arista-proxy-dcr01u23.infrastructure-prod.svc.cluster.local
-
-    job/service = arista-proxy-dcr01u23
-    principal/namespace = infrastructure-prod
-    realm = svc.cluster.local
 
 ACL, or How do I restrict access to my service?
 -----------------------------------------------
@@ -84,8 +97,10 @@
 
 Once linked into your program, the following flags will be automatically present:
 
-    -hspki_realm string
+    -hspki_cluster string
         PKI realm (default "svc.cluster.local")
+    -hspki_realm string
+        PKI realm (default "cluster.local")
     -hspki_tls_ca_path string
         Path to PKI CA certificate (default "pki/ca.pem")
     -hspki_tls_certificate_path string
diff --git a/go/pki/grpc.go b/go/pki/grpc.go
index 6d8f173..1720ad8 100644
--- a/go/pki/grpc.go
+++ b/go/pki/grpc.go
@@ -36,6 +36,7 @@
 	flagCAPath          string
 	flagCertificatePath string
 	flagKeyPath         string
+	flagPKICluster      string
 	flagPKIRealm        string
 	flagPKIDisable      bool
 
@@ -53,7 +54,8 @@
 	flag.StringVar(&flagCAPath, "hspki_tls_ca_path", "pki/ca.pem", "Path to PKI CA certificate")
 	flag.StringVar(&flagCertificatePath, "hspki_tls_certificate_path", "pki/service.pem", "Path to PKI service certificate")
 	flag.StringVar(&flagKeyPath, "hspki_tls_key_path", "pki/service-key.pem", "Path to PKI service private key")
-	flag.StringVar(&flagPKIRealm, "hspki_realm", "svc.cluster.local", "PKI realm")
+	flag.StringVar(&flagPKICluster, "hspki_cluster", "local.hswaw.net", "FQDN of cluster on which this service runs")
+	flag.StringVar(&flagPKIRealm, "hspki_realm", "hswaw.net", "Cluster realm (top level from which we accept foreign cluster certs)")
 	flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)")
 }
 
@@ -81,14 +83,39 @@
 	if !strings.HasSuffix(name, "."+flagPKIRealm) {
 		return nil, fmt.Errorf("invalid realm")
 	}
-	service := strings.TrimSuffix(name, "."+flagPKIRealm)
-	parts := strings.Split(service, ".")
-	if len(parts) != 2 {
-		return nil, fmt.Errorf("invalid job/principal format")
+
+	inRealm := strings.TrimSuffix(name, "."+flagPKIRealm)
+
+	special := []string{"person", "external"}
+
+	for _, s := range special {
+		// Special case for people running jobs from workstations, or for non-cluster services.
+		if strings.HasSuffix(inRealm, "."+s) {
+			asPerson := strings.TrimSuffix(inRealm, "."+s)
+			parts := strings.Split(asPerson, ".")
+			if len(parts) != 1 {
+				return nil, fmt.Errorf("invalid person fqdn")
+			}
+			return &ClientInfo{
+				Cluster:   fmt.Sprintf("%s.%s", s, flagPKIRealm),
+				Principal: parts[0],
+				Job:       "",
+			}, nil
+		}
 	}
+
+	parts := strings.Split(inRealm, ".")
+	if len(parts) != 4 {
+		return nil, fmt.Errorf("invalid job/principal format for in-cluster")
+	}
+	if parts[2] != "svc" {
+		return nil, fmt.Errorf("can only refer to services within cluster")
+	}
+	clusterShort := parts[3]
+
 	return &ClientInfo{
-		Realm:     flagPKIRealm,
-		Principal: parts[1],
+		Cluster:   fmt.Sprintf("%s.%s", clusterShort, flagPKIRealm),
+		Principal: fmt.Sprintf("%s.svc", parts[1]),
 		Job:       parts[0],
 	}, nil
 }
@@ -137,15 +164,24 @@
 // ClientInfo contains information about the HSPKI authentication data of the
 // gRPC client that has made the request.
 type ClientInfo struct {
-	Realm     string
+	Cluster   string
 	Principal string
 	Job       string
 }
 
 // String returns a human-readable representation of the ClientInfo in the
-// form "job=foo, principal=bar, realm=baz".
+// form "job=foo, principal=bar.svc, cluster=baz.hswaw.net".
 func (c *ClientInfo) String() string {
-	return fmt.Sprintf("job=%q, principal=%q, realm=%q", c.Job, c.Principal, c.Realm)
+	return fmt.Sprintf("job=%q, principal=%q, cluster=%q", c.Job, c.Principal, c.Cluster)
+}
+
+// Person returns a reference to a person's ID if the ClientInfo describes a person.
+// Otherwise, it returns an empty string.
+func (c *ClientInfo) Person() string {
+	if c.Cluster != fmt.Sprintf("person.%s", flagPKIRealm) {
+		return ""
+	}
+	return c.Principal
 }
 
 // ClientInfoFromContext returns ClientInfo from a gRPC service context.
diff --git a/go/pki/grpc_test.go b/go/pki/grpc_test.go
new file mode 100644
index 0000000..9ce2a8c
--- /dev/null
+++ b/go/pki/grpc_test.go
@@ -0,0 +1,69 @@
+package pki
+
+import (
+	"testing"
+
+	"github.com/go-test/deep"
+)
+
+func TestParseClient(t *testing.T) {
+	flagPKIRealm = "hswaw.net"
+
+	tests := []struct {
+		name string
+		want *ClientInfo
+	}{
+		// Local cluster
+		{"foo.bar.svc.k0.hswaw.net", &ClientInfo{Cluster: "k0.hswaw.net", Principal: "bar.svc", Job: "foo"}},
+		{"foo.bar.k0.hswaw.net", nil},
+
+		// Foreign cluster
+		{"foo.bar.svc.k1.hswaw.net", &ClientInfo{Cluster: "k1.hswaw.net", Principal: "bar.svc", Job: "foo"}},
+		{"foo.bar.k1.hswaw.net", nil},
+
+		// Human admins (admins, as we know, don't have a real job)
+		{"q3k.person.hswaw.net", &ClientInfo{Cluster: "person.hswaw.net", Principal: "q3k", Job: ""}},
+
+		// External services
+		{"kasownik.external.hswaw.net", &ClientInfo{Cluster: "external.hswaw.net", Principal: "kasownik", Job: ""}},
+
+		// Broken.
+		{"foo.hswaw.net", nil},
+		{"ldap.hackerspace.pl", nil},
+		{"", nil},
+		{"..what..plz...don.t.hack.me.hswaw.net", nil},
+	}
+
+	for i, te := range tests {
+		res, err := parseClientName(te.name)
+		if err != nil {
+			if te.want != nil {
+				t.Errorf("#%d: wanted result, got err %v", i, err)
+			}
+			continue
+		}
+
+		if te.want == nil {
+			t.Errorf("#%d: wanted err, got %+v", i, res)
+			continue
+		}
+
+		if diff := deep.Equal(*te.want, *res); diff != nil {
+			t.Errorf("#%d: res diff: %v", i, diff)
+			continue
+		}
+	}
+}
+
+func TestCheckPerson(t *testing.T) {
+	flagPKIRealm = "hswaw.net"
+
+	res, err := parseClientName("q3k.person.hswaw.net")
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
+	if want, got := "q3k", res.Person(); want != got {
+		t.Fatalf("wanted %q, got %q", want, got)
+	}
+}
diff --git a/hswaw/kube/hswaw.jsonnet b/hswaw/kube/hswaw.jsonnet
new file mode 100644
index 0000000..d8a5131
--- /dev/null
+++ b/hswaw/kube/hswaw.jsonnet
@@ -0,0 +1,96 @@
+local mirko = import "../../kube/mirko.libsonnet";
+local kube = import "../../kube/kube.libsonnet";
+
+{
+    hswaw(name):: mirko.Environment(name) {
+        local env = self,
+        local cfg = self.cfg,
+
+        cfg+: {
+            smsgw: {
+                secret: {
+                    twilio_token: error "twilio_token must be set",
+                },
+                image: "registry.k0.hswaw.net/q3k/smsgs:1570049853-05c5b491c45de6d960979d4aee8635768f3178e9",
+                webhookFQDN: error "webhookFQDN must be set",
+            },
+        },
+
+        components: {
+            smsgw: mirko.Component(env, "smsgw") {
+                local smsgw = self,
+                cfg+: {
+                    image: cfg.smsgw.image,
+                    container: smsgw.GoContainer("main", "/smsgw/smsgw") {
+                        env_: {
+                            TWILIO_TOKEN: kube.SecretKeyRef(smsgw.secret, "twilio_token"),
+                        },
+                        command+: [
+                            "-twilio_friendly_phone", "48732168371",
+                            "-twilio_sid", "AC806ed4bf4b6c80c8f8ea686379b69518",
+                            "-twilio_token", "$(TWILIO_TOKEN)",
+                            "-webhook_listen", "0.0.0.0:5000",
+                            "-webhook_public", "https://%s/" % [ env.cfg.smsgw.webhookFQDN ],
+                        ],
+                    },
+                    ports+: {
+                        publicHTTP: {
+                            webhook: {
+                                port: 5000,
+                                dns: env.cfg.smsgw.webhookFQDN,
+                            }
+                        },
+                    },
+                },
+
+                secret: kube.Secret("smsgw") {
+                    metadata+: smsgw.metadata,
+                    data: env.cfg.smsgw.secret,
+                },
+
+                // Temporary machinery to access gRPC from outsite.
+                // In the future, this will be handled by a proxy/API gateway.
+                // For now, we need this running.
+                // TODO(q3k): remove this when we have an API GW or proxy.
+                stopgap: {
+                    rpcLB: kube.Service("smsgw-tcp-rpc") {
+                        metadata+: smsgw.metadata,
+                        target_pod: smsgw.deployment.spec.template,
+                        spec+: {
+                            type: "LoadBalancer",
+                            ports: [
+                                { name: "grpc-external", port: 443, targetPort: 4200 },
+                            ],
+                        },
+                    },
+
+                    rpcCertificate: kube.Certificate("smsgw-tcp-rpc-consumer") {
+                        metadata+: smsgw.metadata,
+                        spec: {
+                            secretName: "smsgw-tcp-rpc-consumer",
+                            duration: "35040h0m0s", // 4 years
+                            issuerRef: {
+                                // Contract with cluster/lib/pki.libsonnet.
+                                // Copied over.
+                                name: "pki-ca",
+                                kind: "ClusterIssuer",
+                            },
+                            commonName: "kasownik.external.hswaw.net",
+                        },
+                    },
+                }
+            },
+        },
+    },
+
+    prod: self.hswaw("hswaw-prod") {
+        cfg+: {
+            smsgw+: {
+                secret+: {
+                    twilio_token: std.base64(std.split(importstr "secrets/plain/prod-twilio-token", "\n")[0]),
+                },
+                webhookFQDN: "smsgw-webhook-prod.hswaw.net",
+            }
+        },
+    },
+}
diff --git a/hswaw/kube/secrets/.gitignore b/hswaw/kube/secrets/.gitignore
new file mode 100644
index 0000000..b9bca01
--- /dev/null
+++ b/hswaw/kube/secrets/.gitignore
@@ -0,0 +1 @@
+plain
diff --git a/hswaw/kube/secrets/cipher/prod-twilio-token b/hswaw/kube/secrets/cipher/prod-twilio-token
new file mode 100644
index 0000000..12b5749
--- /dev/null
+++ b/hswaw/kube/secrets/cipher/prod-twilio-token
@@ -0,0 +1,40 @@
+-----BEGIN PGP MESSAGE-----
+
+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
+jDM7tUYE8kNa/hBa1S7vX11NJLjRoE+P0d1p
+=WIU6
+-----END PGP MESSAGE-----
diff --git a/hswaw/smsgw/BUILD.bazel b/hswaw/smsgw/BUILD.bazel
index 35b36f1..0e91141 100644
--- a/hswaw/smsgw/BUILD.bazel
+++ b/hswaw/smsgw/BUILD.bazel
@@ -1,3 +1,4 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
 
 go_library(
@@ -29,3 +30,28 @@
     srcs = ["dispatcher_test.go"],
     embed = [":go_default_library"],
 )
+
+container_layer(
+    name = "layer_bin",
+    files = [
+        ":smsgw",
+    ],
+    directory = "/smsgw/",
+)
+
+container_image(
+    name = "runtime",
+    base = "@prodimage-bionic//image",
+    layers = [
+        ":layer_bin",
+    ],
+)
+
+container_push(
+    name = "push",
+    image = ":runtime",
+    format = "Docker",
+    registry = "registry.k0.hswaw.net",
+    repository = "q3k/smsgs",
+    tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+)
diff --git a/hswaw/smsgw/main.go b/hswaw/smsgw/main.go
index a0a6a07..3095c00 100644
--- a/hswaw/smsgw/main.go
+++ b/hswaw/smsgw/main.go
@@ -76,7 +76,7 @@
 			glog.Infof("Webhook not yet ready, currently %s %q", pn.SMSMethod, pn.SMSURL)
 			time.Sleep(5 * time.Second)
 		}
-		glog.Infof("Webhook verifier")
+		glog.Infof("Webhook verified")
 	} else {
 		glog.Infof("Webhook up to date")
 	}
diff --git a/kube/mirko.libsonnet b/kube/mirko.libsonnet
new file mode 100644
index 0000000..55ff90e
--- /dev/null
+++ b/kube/mirko.libsonnet
@@ -0,0 +1,216 @@
+# Mirko, an abstraction layer for hscloud kubernetes services.
+
+local kube = import "kube.libsonnet";
+
+{
+    Environment(name): {
+        local env = self,
+        local cfg = env.cfg,
+        cfg:: {
+            name: name,
+            namespace: cfg.name,
+        },
+
+        namespace: kube.Namespace(cfg.namespace),
+
+        components: {}, // type: mirko.Component
+
+        // Currently hardcoded!
+        // This might end up being something passed part of kubecfg evaluation,
+        // when we get to supporting multiple/federated clusters.
+        // For now, this is goog enough.
+        pkiRealm:: "hswaw.net",
+        pkiClusterFQDN:: "k0.hswaw.net",
+
+        // Generate an ingress if we have any public ports.
+        publicHTTPPorts:: std.flattenArrays([
+            [
+                {
+                    local component = env.components[c],
+
+                    service: component.svc,
+                    port: component.cfg.ports.publicHTTP[p].port,
+                    dns: component.cfg.ports.publicHTTP[p].dns,
+                }
+                for p in std.objectFields(env.components[c].cfg.ports.publicHTTP)
+            ]
+            for c in std.objectFields(env.components)
+        ]),
+
+        ingress: if std.length(env.publicHTTPPorts) > 0 then kube.Ingress("mirko-public") {
+            metadata+: {
+                namespace: env.cfg.namespace,
+                labels: {
+                    "app.kubernetes.io/name": cfg.name,
+                    "app.kubernetes.io/managed-by": "kubecfg-mirko",
+                    "app.kubernetes.io/component": cfg.name,
+                    "mirko.hscloud.hackerspace.pl/environment": env.cfg.name,
+                    "mirko.hscloud.hackerspace.pl/component": "mirko-public-ingress",
+                },
+                annotations+: {
+                    "kubernetes.io/tls-acme": "true",
+                    "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+                },
+            },
+            spec+: {
+                tls: [
+                    {
+                        hosts: [p.dns for p in env.publicHTTPPorts],
+                        secretName: "mirko-public-tls",
+                    },
+                ],
+                rules: [
+                    {
+                        host: p.dns,
+                        http: {
+                            paths: [
+                                { path: "/", backend: { serviceName: p.service.metadata.name, servicePort: p.port }},
+                            ],
+                        },
+                    }
+                    for p in env.publicHTTPPorts
+                ],
+            },
+        } else {}
+    },
+
+    Component(env, name): {
+        local component = self,
+        local cfg = component.cfg,
+
+        makeName(suffix):: "%s%s%s" % [cfg.prefix, cfg.name, suffix],
+
+        metadata:: {
+            namespace: env.cfg.namespace,
+            labels: {
+                "app.kubernetes.io/name": env.cfg.name,
+                "app.kubernetes.io/managed-by": "kubecfg-mirko",
+                "app.kubernetes.io/component": cfg.name,
+                "mirko.hscloud.hackerspace.pl/environment": env.cfg.name,
+                "mirko.hscloud.hackerspace.pl/component": cfg.name,
+            },
+        },
+
+
+        # Tunables for users.
+        cfg:: {
+            name: name,
+
+            prefix:: "",
+            image:: env.image,
+            volumes:: {},
+            containers:: {
+                main: cfg.container,
+            }, 
+            container:: error "container(s) must be set",
+            ports:: {
+                publicHTTP: {}, // name -> { port: no, dns: fqdn }
+                grpc: { main: 4200 }, // name -> port no
+            },
+
+        },
+
+        allPorts:: {
+            ['grpc-' + p]: cfg.ports.grpc[p]
+            for p in std.objectFields(cfg.ports.grpc)
+        } + {
+            ['pubhttp-' + p] : cfg.ports.publicHTTP[p].port
+            for p in std.objectFields(cfg.ports.publicHTTP)
+        },
+
+        Container(name):: kube.Container(component.makeName(name)) {
+            image: cfg.image,
+            volumeMounts_: {
+                pki: { mountPath: "/mnt/pki" },
+            },
+            ports_: {
+                [p]: { containerPort: component.allPorts[p] }
+                for p in std.objectFields(component.allPorts)
+            },
+            resources: {
+                requests: {
+                    cpu: "25m",
+                    memory: "64Mi",
+                },
+                limits: {
+                    cpu: "500m",
+                    memory: "128Mi",
+                },
+            },
+        },
+
+        GoContainer(name, binary):: component.Container(name) {
+            command: [
+                binary,
+                "-hspki_realm", env.pkiRealm,
+                "-hspki_cluster", env.pkiClusterFQDN,
+                "-hspki_tls_ca_path", "/mnt/pki/ca.crt",
+                "-hspki_tls_certificate_path", "/mnt/pki/tls.crt",
+                "-hspki_tls_key_path", "/mnt/pki/tls.key",
+                "-logtostderr",
+                "-listen_address", "0.0.0.0:4200",
+            ],
+        },
+
+        deployment: kube.Deployment(component.makeName("-main")) {
+            metadata+: component.metadata,
+            spec+: {
+                template+: {
+                    spec+: {
+                        volumes_: {
+                            pki: {
+                                secret: { secretName: component.pki.cert.spec.secretName },
+                            },
+                        } + cfg.volumes,
+                        containers_: cfg.containers,
+
+                        serviceAccountName: component.sa.metadata.name,
+                    },
+                },
+            },
+        },
+
+        svc: kube.Service(component.makeName("")) { // No suffix, name part of DNS entry.
+            metadata+: component.metadata,
+            target_pod:: component.deployment.spec.template,
+            spec+: {
+                ports: [
+                    {
+                        name: p,
+                        port: component.allPorts[p],
+                        targetPort: component.allPorts[p],
+                    }
+                    for p in std.objectFields(component.allPorts)
+                ],
+            },
+        },
+
+        sa: kube.ServiceAccount(component.makeName("-main")) {
+            metadata+: component.metadata,
+        },
+
+        pki: {
+            cert: kube.Certificate(component.makeName("-cert")) {
+                metadata+: component.metadata,
+        
+                spec: {
+                    secretName: component.makeName("-cert"),
+                    duration: "35040h0m0s", // 4 years
+                    issuerRef: {
+                        // Contract with cluster/lib/pki.libsonnet.
+                        name: "pki-ca",
+                        kind: "ClusterIssuer",
+                    },
+                    commonName: "%s.%s.svc.%s" % [component.svc.metadata.name, component.svc.metadata.namespace, env.pkiClusterFQDN ],
+                    dnsNames: [
+                        "%s" % [component.svc.metadata.name ],
+                        "%s.%s" % [component.svc.metadata.name, component.svc.metadata.namespace ],
+                        "%s.%s.svc" % [component.svc.metadata.name, component.svc.metadata.namespace ],
+                        "%s.%s.svc.cluster.local" % [component.svc.metadata.name, component.svc.metadata.namespace ],
+                        "%s.%s.svc.%s" % [component.svc.metadata.name, component.svc.metadata.namespace, env.pkiClusterFQDN ],
+                    ],
+                },
+            },
+        },
+    },
+}