Merge "app/matrix: synapse upgrade"
diff --git a/README b/README
deleted file mode 100644
index dc389bd..0000000
--- a/README
+++ /dev/null
@@ -1,24 +0,0 @@
-HSCloud
-=======
-
-This is a monorepo. You'll need bash and Bazel 1.0.0+ to use it.
-
-If you have Nix installed you will also be able to manage bare metal nodes. If you don't want that, you can skip it.
-
-
-Getting started
----------------
-
-    cd hscloud
-    . env.sh # setup PATH and hscloud_root
-    tools/install.sh # build tools
-
-
-Then, to get Kubernetes access to k0.hswaw.net (current nearly-production cluster):
-
-    prodaccess
-    kubectl version
-
-You will automatically get a `personal-$USERNAME` namespace created in which you have full admin rights.
-
-For mor information about the cluster, see [cluster/README].
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7d2684d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+![](doc/img/hscloud-smol.png)
+
+`hscloud` is the main monorepo of the Warsaw Hackerspace infrastructure code.
+
+Any time you see a `//path/like/this`, it refers to the root of hscloud, ie. the path `path/like/this` in this repository. Perforce and/or Bazel users should feel right at home.
+
+
+Viewing this documentation
+--------------------------
+
+For a pleaseant web viewing experience, [see this documentation in hackdoc](https://hackdoc.hackerspace.pl/). This will allow you to read this markdown file (and others) in a pretty, linkable view.
+
+Getting started
+---------------
+
+You will need Bash and Bazel (1.2.0+).
+
+First, clone the repository:
+
+    git clone https://gerrit.hackerspace.pl/hscloud
+    cd hscloud
+
+Then, set up everything:
+
+    . ./env.sh       # setup PATH and hscloud_root
+    tools/install.sh # build tools
+
+A bunch of common tools will appearify in your `$PATH`. You should now be ready to follow other documentation.
+
+This does not pollute your system, and you can work on multiple hscloud checkouts independently.
+
+What now?
+---------
+
+If you want to use our Kubernetes cluster to run some stuff, see [//cluster/doc/user.md](cluster/doc/user.md).
+
+If you're looking for administrative docs about cluster maintenance, see [//cluster/doc/admin.md](cluster/doc/admin.md).
+
+If you want to browse the source of `hscloud` in a web browser, use [gerrit's gitiles](https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/refs/heads/master/).
+
+If you want to learn how to contribute to this repository, see [//doc/codelab/gerrit](doc/codelab/gerrit).
+
+If you want help, talk to q3k, informatic or your therapist.
diff --git a/WORKSPACE b/WORKSPACE
index e1c4c56..75f3901 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -164,25 +164,31 @@
     downloaded_file_path = "factorio.tar.xz",
 )
 
+http_file(
+    name = "factorio-headless-0.18.12",
+    urls = ["https://factorio.com/get-download/0.18.12/headless/linux64"],
+    sha256 = "e0c6a46d66cfc02cba294a5fd34265e7e7a5168b8c8a7b16ad8dbac31470ed33",
+    downloaded_file_path = "factorio.tar.xz",
+)
+
+http_file(
+    name = "factorio-headless-0.18.17",
+    urls = ["https://factorio.com/get-download/0.18.17/headless/linux64"],
+    sha256 = "42adce9fddde393023afb0aae19dd030a32ca0810191c0e7b9b7c55556e9bbce",
+    downloaded_file_path = "factorio.tar.xz",
+)
+
 # Go rules
 
 http_archive(
     name = "io_bazel_rules_go",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.21.3/rules_go-v0.21.3.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.21.3/rules_go-v0.21.3.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.22.2/rules_go-v0.22.2.tar.gz",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.22.2/rules_go-v0.22.2.tar.gz",
     ],
-    sha256 = "af04c969321e8f428f63ceb73463d6ea817992698974abeff0161e069cd08bd6",
+    sha256 = "142dd33e38b563605f0d20e89d9ef9eda0fc3cb539a14be1bdb1350de2eda659",
 )
 
-# Invoke go_rules_dependencies depending on host platform.
-load("//tools:go_sdk.bzl", "gen_imports")
-gen_imports(name = "go_sdk_imports")
-load("@go_sdk_imports//:imports.bzl", "load_go_sdk")
-load_go_sdk()
-
-# Go Gazelle rules
-
 http_archive(
     name = "bazel_gazelle",
     urls = [
@@ -194,6 +200,21 @@
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
 
+go_repository(
+    name = "org_golang_x_net",
+    commit = "d3edc9973b7eb1fb302b0ff2c62357091cea9a30",
+    importpath = "golang.org/x/net",
+)
+
+# Invoke go_rules_dependencies depending on host platform.
+load("//tools:go_sdk.bzl", "gen_imports")
+
+gen_imports(name = "go_sdk_imports")
+
+load("@go_sdk_imports//:imports.bzl", "load_go_sdk")
+
+load_go_sdk()
+
 gazelle_dependencies()
 
 # For devtools/gerrit/gerrit-oauth-provider
@@ -464,12 +485,6 @@
 )
 
 go_repository(
-    name = "org_golang_x_net",
-    commit = "13f9640d40b9",
-    importpath = "golang.org/x/net",
-)
-
-go_repository(
     name = "com_github_stackexchange_wmi",
     commit = "cbe66965904dbe8a6cd589e2298e5d8b986bd7dd",
     importpath = "github.com/stackexchange/wmi",
@@ -1934,3 +1949,76 @@
     commit = "d07dcb9293789fdc99c797d3499a5799bc343b86",
     importpath = "gopkg.in/irc.v3",
 )
+
+go_repository(
+    name = "in_gopkg_russross_blackfriday_v2",
+    commit = "d3b5b032dc8e8927d31a5071b56e14c89f045135",
+    importpath = "gopkg.in/russross/blackfriday.v2",
+)
+
+go_repository(
+    name = "com_github_shurcool_sanitized_anchor_name",
+    commit = "7bfe4c7ecddb3666a94b053b422cdd8f5aaa3615",
+    importpath = "github.com/shurcooL/sanitized_anchor_name",
+)
+
+go_repository(
+    name = "com_github_go_git_go_billy_v5",
+    commit = "d7a8afccaed297c30f8dff5724dbe422b491dd0d",
+    importpath = "github.com/go-git/go-billy/v5",
+    remote = "https://github.com/go-git/go-billy",
+    vcs = "git",
+)
+
+go_repository(
+    name = "com_github_go_git_go_git_v5",
+    commit = "3127ad9a44a2ee935502816065dfe39f494f583d",
+    importpath = "github.com/go-git/go-git/v5",
+    remote = "https://github.com/go-git/go-git",
+    vcs = "git",
+    build_extra_args = [
+        "-known_import=github.com/go-git/go-billy/v5",
+    ],
+)
+
+go_repository(
+    name = "com_github_go_git_gcfg",
+    commit = "22f18f9a74d34e3b1a7d59cfa33043bc50ebe376",
+    importpath = "github.com/go-git/gcfg",
+)
+
+go_repository(
+    name = "in_gopkg_warnings_v0",
+    commit = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b",
+    importpath = "gopkg.in/warnings.v0",
+)
+
+go_repository(
+    name = "com_github_emirpasic_gods",
+    commit = "80e934ed68b9084f386ae25f74f839aaecfb54d8",
+    importpath = "github.com/emirpasic/gods",
+)
+
+go_repository(
+    name = "com_github_jbenet_go_context",
+    commit = "d14ea06fba99483203c19d92cfcd13ebe73135f4",
+    importpath = "github.com/jbenet/go-context",
+)
+
+go_repository(
+    name = "com_github_kevinburke_ssh_config",
+    commit = "01f96b0aa0cdcaa93f9495f89bbc6cb5a992ce6e",
+    importpath = "github.com/kevinburke/ssh_config",
+)
+
+go_repository(
+    name = "com_github_xanzy_ssh_agent",
+    commit = "6a3e2ff9e7c564f36873c2e36413f634534f1c44",
+    importpath = "github.com/xanzy/ssh-agent",
+)
+
+go_repository(
+    name = "com_github_gabriel_vasile_mimetype",
+    commit = "06500030e7d26826f68caa5ca7d98c315c4caa28",
+    importpath = "github.com/gabriel-vasile/mimetype",
+)
diff --git a/app/covid-formity/prod.jsonnet b/app/covid-formity/prod.jsonnet
new file mode 100644
index 0000000..a6ca8ab
--- /dev/null
+++ b/app/covid-formity/prod.jsonnet
@@ -0,0 +1,106 @@
+# covid19.hackerspace.pl, a covid-formity instance.
+# This needs a secret provisioned, create with:
+#    kubectl -n covid-formity create secret generic covid-formity --from-literal=postgres_password=$(pwgen 24 1) --from-literal=secret_key=$(pwgen 24 1) --from-literal=oauth2_secret=...
+
+local kube = import "../../kube/kube.libsonnet";
+local postgres = import "../../kube/postgres.libsonnet";
+
+{
+    local app = self,
+    local cfg = app.cfg,
+    cfg:: {
+        namespace: "covid-formity",
+        image: "registry.k0.hswaw.net/informatic/covid-formity@sha256:8295f5b6d71266fb758c103210f12380f15903ba2467ead0e48ae0df16b6d608",
+        domain: "covid19.hackerspace.pl",
+        altDomains: ["covid.hackerspace.pl"],
+    },
+
+    metadata(component):: {
+        namespace: app.cfg.namespace,
+        labels: {
+            "app.kubernetes.io/name": "covid-formity",
+            "app.kubernetes.io/managed-by": "kubecfg",
+            "app.kubernetes.io/component": component,
+        },
+    },
+
+    namespace: kube.Namespace(app.cfg.namespace),
+
+    postgres: postgres {
+        cfg+: {
+            namespace: cfg.namespace,
+            appName: "covid-formity",
+            database: "covid-formity",
+            username: "covid-formity",
+            password: { secretKeyRef: { name: "covid-formity", key: "postgres_password" } },
+        },
+    },
+
+    deployment: kube.Deployment("covid-formity") {
+        metadata+: app.metadata("covid-formity"),
+        spec+: {
+            replicas: 1,
+            template+: {
+                spec+: {
+                    containers_: {
+                        web: kube.Container("covid-formity") {
+                            image: cfg.image,
+                            ports_: {
+                                http: { containerPort: 5000 },
+                            },
+                            env_: {
+                                DATABASE_HOSTNAME: "postgres",
+                                DATABASE_USERNAME: app.postgres.cfg.username,
+                                DATABASE_PASSWORD: app.postgres.cfg.password,
+                                DATABASE_NAME: app.postgres.cfg.appName,
+                                SPACEAUTH_CONSUMER_KEY: "covid-formity",
+                                SPACEAUTH_CONSUMER_SECRET: { secretKeyRef: { name: "covid-formity", key: "oauth2_secret" } },
+                                SECRET_KEY: { secretKeyRef: { name: "covid-formity", key: "secret_key" } },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+
+    svc: kube.Service("covid-formity") {
+        metadata+: app.metadata("covid-formity"),
+        target_pod:: app.deployment.spec.template,
+        spec+: {
+            ports: [
+                { name: "http", port: 5000, targetPort: 5000, protocol: "TCP" },
+            ],
+            type: "ClusterIP",
+        },
+    },
+
+    ingress: kube.Ingress("covid-formity") {
+        metadata+: app.metadata("covid-formity") {
+            annotations+: {
+                "kubernetes.io/tls-acme": "true",
+                "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+                "nginx.ingress.kubernetes.io/proxy-body-size": "0",
+            },
+        },
+        spec+: {
+            tls: [
+                {
+                    hosts: [cfg.domain] + cfg.altDomains,
+                    secretName: "covid-formity-tls",
+                },
+            ],
+            rules: [
+                {
+                    host: dom,
+                    http: {
+                        paths: [
+                            { path: "/", backend: app.svc.name_port },
+                        ]
+                    },
+                }
+                for dom in [cfg.domain] + cfg.altDomains
+            ],
+        },
+    },
+}
diff --git a/cluster/certs/etcd-bc01n01.hswaw.net.cert b/cluster/certs/etcd-bc01n01.hswaw.net.cert
index 26d4913..917ad22 100644
--- a/cluster/certs/etcd-bc01n01.hswaw.net.cert
+++ b/cluster/certs/etcd-bc01n01.hswaw.net.cert
@@ -1,29 +1,30 @@
 -----BEGIN CERTIFICATE-----
-MIIFADCCA+igAwIBAgIUeR6j2mArcp+yYCD1clxcYbN+5I0wDQYJKoZIhvcNAQEL
+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
-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
+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
-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
+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
++lUKqLwE5y4V7f/FWIKfhg==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcd-bc01n02.hswaw.net.cert b/cluster/certs/etcd-bc01n02.hswaw.net.cert
index fbf7f60..5fb1c6d 100644
--- a/cluster/certs/etcd-bc01n02.hswaw.net.cert
+++ b/cluster/certs/etcd-bc01n02.hswaw.net.cert
@@ -1,29 +1,30 @@
 -----BEGIN CERTIFICATE-----
-MIIFADCCA+igAwIBAgIUEOvIsxRTRhb/gGkSusEaIvihjmYwDQYJKoZIhvcNAQEL
+MIIFHDCCBASgAwIBAgIUfKVgcr+CKsr3u9FsVXFRZRXv8B0wDQYJKoZIhvcNAQEL
 BQAweDELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0xOTA0MDYxODA0MDBaFw0y
-MDA0MDUxODA0MDBaMFsxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
+Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0yMDAzMjgxNjQ1MDBaFw0y
+MTAzMjgxNjQ1MDBaMHcxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
 ZTEPMA0GA1UEBxMGV2Fyc2F3MSUwIwYDVQQLExxub2RlIGV0Y2Qgc2VydmVyIGNl
-cnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwrMOjn2b
-ESHDHzHeO73kqaNs0P3AKBr1gZ9Tfpg62zkah5wvW3oQ44/WubwEeEZGi7im3Srq
-wmqCFLmKHGkpVx34qdlsTgKa8sCxIfrz8QuOXW8fr68l/hIkUXXE3cIwxOBWvRkz
-kH97bqWnrbSOvMuFquTQNppA7ynlWnSP1L2KquwaTn8hyguSJwCSiFAp8d7ShKHC
-Kb+msuFXYeNhPRnTflvLuNmg8IesTMTF9D0Q2k6aZ/jPGjUoJAQsftrnDIz8Wlzg
-QaraSEYW5FrVTWZtl0ZbRZmOk25wU0mZR7L6hpFwAePsgGiU9Si/fPVvANk8h0Ah
-E+m8k2mu79Gsw6TmvFU5KT8jNNZXagHLUjuqAwrFEz16ai/Z0UQfnn+NGwp4BioN
-iZujfndh3D7qPdCdxCOJS3qwHZ2G+z6JmF61N1hFwR5wrKxjsgZmQXQTm+L6mpT0
-JBss/uEcTiZe1KEHPUzEWv9gvhdzeXPAMKj15P4GQCMyE+3ozWM2WpZdLaGlaQ0f
-OJm4G+3hs3V6DXr44LpSDJWo8dVDUGBgjOYwJXgyaxyWFKTwlHBYZeAtd4jpKzqw
-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
+cnRpZmljYXRlMRowGAYDVQQDExFiYzAxbjAyLmhzd2F3Lm5ldDCCAiIwDQYJKoZI
+hvcNAQEBBQADggIPADCCAgoCggIBAMKzDo59mxEhwx8x3ju95KmjbND9wCga9YGf
+U36YOts5GoecL1t6EOOP1rm8BHhGRou4pt0q6sJqghS5ihxpKVcd+KnZbE4CmvLA
+sSH68/ELjl1vH6+vJf4SJFF1xN3CMMTgVr0ZM5B/e26lp620jrzLhark0DaaQO8p
+5Vp0j9S9iqrsGk5/IcoLkicAkohQKfHe0oShwim/prLhV2HjYT0Z035by7jZoPCH
+rEzExfQ9ENpOmmf4zxo1KCQELH7a5wyM/Fpc4EGq2khGFuRa1U1mbZdGW0WZjpNu
+cFNJmUey+oaRcAHj7IBolPUov3z1bwDZPIdAIRPpvJNpru/RrMOk5rxVOSk/IzTW
+V2oBy1I7qgMKxRM9emov2dFEH55/jRsKeAYqDYmbo353Ydw+6j3QncQjiUt6sB2d
+hvs+iZhetTdYRcEecKysY7IGZkF0E5vi+pqU9CQbLP7hHE4mXtShBz1MxFr/YL4X
+c3lzwDCo9eT+BkAjMhPt6M1jNlqWXS2hpWkNHziZuBvt4bN1eg16+OC6UgyVqPHV
+Q1BgYIzmMCV4MmsclhSk8JRwWGXgLXeI6Ss6sMOnfhtdhFExw3OoF2lbuwbK1LJU
+Yx3bj0EjDLUoEudiuySYedcVian/i40vKYxOqjt58ewJyYQPrA2lKcbV0Hy3xXp5
+KreXmIubAgMBAAGjgZ4wgZswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG
+AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSv6oLJS4Ov
+F3EnzsT1IOfrUVwQszAfBgNVHSMEGDAWgBTxWbhmTT7D50LugaPURn85U3X5DzAc
+BgNVHREEFTATghFiYzAxbjAyLmhzd2F3Lm5ldDANBgkqhkiG9w0BAQsFAAOCAQEA
+Dd6WnaNXELgxr/xJVCSFSMiD8e3FjSpH/k/4sCUzonhFS/vIm9b8xEAT0p+bkL/4
+XCgRE/mjQQgdSFEXmZ75AEe+DqYDSjfoIJHAVxJzi/3uexd4+EauVQ4XZh8RMk05
+1HO3gP3wO8RFqUsTKGOTriUVF1zaIz4UxJEzT2BWJkgp5G60HUqXUyaKwNhTDNZL
+p9yzVpsuPHuRlyRZjAHdDafaW6sTFZWAQXxao2NKLMhSi3JLArlJuBuh/4QjhZzw
+UW0U/4yNot/H/kX6Nh41OoBY/mdGGTN7CAndFnXMEEVAuM3gja8LiG3VwDG2bpX7
+C4FJ50A5mcfOEOvnKLA6Zw==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcd-bc01n03.hswaw.net.cert b/cluster/certs/etcd-bc01n03.hswaw.net.cert
index 254f443..e2575f9 100644
--- a/cluster/certs/etcd-bc01n03.hswaw.net.cert
+++ b/cluster/certs/etcd-bc01n03.hswaw.net.cert
@@ -1,29 +1,30 @@
 -----BEGIN CERTIFICATE-----
-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
+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
-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
+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
-cnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvbhdEPDQ
-R3HD3o73LZd6qVxxqdWNlCwlHGa2+EiY+mzpT2shL3b/oggUmfVaLQ20TVbpUPun
-hDeAr5WZeUJ0WbIlGNp4P3MnwbPQDhtAO0v2dFAzQyGQRIpkHEliRE8xRUOwEoOG
-r1jfVdO+yooJgrMSs9wFu6r2jySwugWKNRXUQ81m2qesYHrq5D6eylSZAcBb5pgX
-EnhqTR11KKKVl1sKdaz42kSLvV10h67joZPPfVyqFPAtl+8BEL2U/vEJcWsZuqOv
-3BK18njqxncTzGCWFhK4p1+kIrVN4kZwehrwftwaiuWrDW6hyzoDOivMITU/kjh4
-NU34zpMHom/xPzcbcmpAEqZyzlDLYRFUM3H1nbveUc7jZFeSFNIOOzSuLy29ivZP
-h49O0jo/wTvzMLdjhV0n8oqI55yqAGB4tIWI0WEA8dH7e46MVlhoCmVZzCj1N0wA
-RfoChcaELGMQOdinh6OBZ5/cEXK3UUvhzQk6haOiCTYUhLm5BqxhK9gEV0ErZwCe
-vET7DlL9LHVMH8YLuI+JM+VIjbucevPUwZdlj5ZWAVCzGwSWy664MkW2thFk2QAB
-2y7IYj8XiXcAQfQ0lpc2uscHECRyVi4jPu1YhKdwl0bdHbRiXWnyETjVjwIOx71T
-cAwynJPX0w/Cqy4f9o4ElxKsiUS/bYobhIECAwEAAaOBnjCBmzAOBgNVHQ8BAf8E
-BAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQC
-MAAwHQYDVR0OBBYEFNMBPCre9auEsLZ/4t8/WILoaGcFMB8GA1UdIwQYMBaAFPFZ
-uGZNPsPnQu6Bo9RGfzlTdfkPMBwGA1UdEQQVMBOCEWJjMDFuMDMuaHN3YXcubmV0
-MA0GCSqGSIb3DQEBCwUAA4IBAQB/izfKue7fj5rBqPnYPH0l4kLxQ+M5KfZ1XGaN
-Xpm8LbofCBfqrHbKYgebnd2ccZwfDQqsq56CtuzA8yRYzL34lEaQyUTVxshPQxQu
-3MIuD2FQ6wbsrYygQ8Nr4cER/atExYlIf6DvperS9kQ7k30N3Mfo43EA1ddIXRM/
-9y6dI1brdU85zc2nDxCqPczsLVmbbGOBfKk3nTcZvz2QYZ+rnrA4r6ZlXKqLl1MH
-MOw5fCOrnS5zJtZ5BsAsY4Pf2PQoNL1N3eEdegF6Rw771gH1EFoDKX5XSzjHCeSD
-hJGWiUmjFNgI9GPCZPt/NjK+RCCk1Td+QjrnwRwPOp0n+6vX
+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
+mvS9hZ09yn7stK+5RxYgQA==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcd-calico.cert b/cluster/certs/etcd-calico.cert
index 62d8234..bbcd91d 100644
--- a/cluster/certs/etcd-calico.cert
+++ b/cluster/certs/etcd-calico.cert
@@ -1,29 +1,29 @@
 -----BEGIN CERTIFICATE-----
-MIIE9TCCA92gAwIBAgIUe09DNSzrB1J5weALRB+K2BeyvzMwDQYJKoZIhvcNAQEL
+MIIFBjCCA+6gAwIBAgIUZmjAlqCosP9W/6X/iIMLRrNb/bkwDQYJKoZIhvcNAQEL
 BQAweDELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0xOTA0MDYxNzU5MDBaFw0y
-MDA0MDUxNzU5MDBaMFsxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
+Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0yMDAzMjgxNTE1MDBaFw0y
+MTAzMjgxNTE1MDBaMGwxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
 ZTEPMA0GA1UEBxMGV2Fyc2F3MSUwIwYDVQQLExxyb290IGV0Y2QgY2xpZW50IGNl
-cnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvVUZSPAJ
-72SjzIbQ3n/Zq2MnXyDZSP0EvPghPGi5BX4UECR8o6qGxk6vvGVhVlmrf7ecPK8d
-AuMES0gcTwzei0cZkaFD2r5itpx+bE88emafTaJhKgnhYOoZ2gtT/bKISDocjnFz
-oZiLPHu108LJF4x2zIgnmnDvETll0zX3prVTkHQ7SPWpKDr/Pb5YYGPeyKjDWqJs
-9i5B8qcA4BEjZ1OGvfssa9gxaqfCYmLZQ2o4TLzJ/O21mGkP1+9vHwvA7tlJuwsp
-WTH5eYgW35qgDpAHjzQ67fPsj6E69n8eNC/9n5E2DS4GhBYzoiVuNYLl8UWUXeDU
-q3C+P85Z4gWtgarECOsgQw/4ClGQMlt0QqIwb+6XC5UestfoRiDXk6JaGr3l92k0
-g3nRz7IJU0aL+3YChnDxQec/LTVO25hLDUM0he0r5XcpjP/IXiuBwCWlxS6hJhxh
-A2QGkdJlPkeeQhggxoU5ZipM8YPE7tqyTK91+vSkYuZX6+a61u4ks7gNRB0hbwlp
-eEUTQvbDr34FuHaXK4z3z7PXfs1NZENH0BYbpADIDtJgYKvGmUjY8EIXH5gU05u7
-KItUbWlYalxt1pMPFfHWsXjLeglfJ+76D1zFTjsK62gXN2VtU7dqYe/Ix1aCRYFP
-qhyTt4iF/wK6WYn+Cm8fShC5TyA/48DmA/kCAwEAAaOBkzCBkDAOBgNVHQ8BAf8E
-BAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQC
-MAAwHQYDVR0OBBYEFGI6wkiqEwIBAnSmU57VJt/+3dj1MB8GA1UdIwQYMBaAFPFZ
-uGZNPsPnQu6Bo9RGfzlTdfkPMBEGA1UdEQQKMAiCBmNhbGljbzANBgkqhkiG9w0B
-AQsFAAOCAQEADnpq/+89AFr8NGJjphkiDW/pvef3c4+k//6S79NFkEnQaMEKwdte
-Xd/nNnyNYDJHU5AvX823Sv4KNqNSOraIwSSyWMeTrvI1plKkBRJ8+dv26VkHDd5P
-+yW75btsdp7TSleisfybGSttre/0AzSRgzTmnM3VkzAIHBOgXXmXi7BV6PeVd6jH
-aEIB/81S2u+j9mmFFMQ1Ur1sDtaLlOzEMH7RURXoXOgHAMYHUNe6tkBhy6/sXXLw
-lnZA6+1Qc/AIj/TcFcjApDL5zJ0ZIbmZscEU42D6hLcAGSHEPcCceXw1TtvmaPKp
-5mOZ0oukyNhXj+5llWNRGIdlnS+sBoGInA==
+cnRpZmljYXRlMQ8wDQYDVQQDEwZjYWxpY28wggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQC9VRlI8AnvZKPMhtDef9mrYydfINlI/QS8+CE8aLkFfhQQJHyj
+qobGTq+8ZWFWWat/t5w8rx0C4wRLSBxPDN6LRxmRoUPavmK2nH5sTzx6Zp9NomEq
+CeFg6hnaC1P9sohIOhyOcXOhmIs8e7XTwskXjHbMiCeacO8ROWXTNfemtVOQdDtI
+9akoOv89vlhgY97IqMNaomz2LkHypwDgESNnU4a9+yxr2DFqp8JiYtlDajhMvMn8
+7bWYaQ/X728fC8Du2Um7CylZMfl5iBbfmqAOkAePNDrt8+yPoTr2fx40L/2fkTYN
+LgaEFjOiJW41guXxRZRd4NSrcL4/zlniBa2BqsQI6yBDD/gKUZAyW3RCojBv7pcL
+lR6y1+hGINeToloaveX3aTSDedHPsglTRov7dgKGcPFB5z8tNU7bmEsNQzSF7Svl
+dymM/8heK4HAJaXFLqEmHGEDZAaR0mU+R55CGCDGhTlmKkzxg8Tu2rJMr3X69KRi
+5lfr5rrW7iSzuA1EHSFvCWl4RRNC9sOvfgW4dpcrjPfPs9d+zU1kQ0fQFhukAMgO
+0mBgq8aZSNjwQhcfmBTTm7soi1RtaVhqXG3Wkw8V8daxeMt6CV8n7voPXMVOOwrr
+aBc3ZW1Tt2ph78jHVoJFgU+qHJO3iIX/ArpZif4Kbx9KELlPID/jwOYD+QIDAQAB
+o4GTMIGQMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
+BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUYjrCSKoTAgECdKZTntUm3/7d
+2PUwHwYDVR0jBBgwFoAU8Vm4Zk0+w+dC7oGj1EZ/OVN1+Q8wEQYDVR0RBAowCIIG
+Y2FsaWNvMA0GCSqGSIb3DQEBCwUAA4IBAQCQV/w/TOPjnCJszWqLd5GeoEkPPE8o
+qevGTmTpm+/l/vlFERDsUB2kKnxb4mHBbVo2c2ux0aF53wKeIcp5/fdfH1LFjS67
+YY9hLce3zFmZMCEbkFGgpjQKpNy4zB72f4ksGRzbPienFLhghhY1dIv5Rdrhyz1O
+xhrUP9fgJHjYd33pFVfhyl8mIOon8yn+4AvGLrPATgp4dmkF+HM3EYtqd2LfeHVy
+Dc9PbjpmIGHz6IKpMKC4S6rlnnfzbk2PRULVcXHPtfX9ihXz+B892IORDK8jgSe8
+PdVe3ZGMo1EbSwQ9WwGZMrIqeiX1vfaEOhZgaw8GMuFM75OBK9yRopZ/
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcd-kube.cert b/cluster/certs/etcd-kube.cert
index ae19bd5..dd0b91f 100644
--- a/cluster/certs/etcd-kube.cert
+++ b/cluster/certs/etcd-kube.cert
@@ -1,29 +1,29 @@
 -----BEGIN CERTIFICATE-----
-MIIE8zCCA9ugAwIBAgIUfFMQAZYna+HSa0hSJnqmmJyfSw8wDQYJKoZIhvcNAQEL
+MIIFAjCCA+qgAwIBAgIUSc+yoWMgxteBtdswAaa+RZmh6hwwDQYJKoZIhvcNAQEL
 BQAweDELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0xOTA0MDYxNzU5MDBaFw0y
-MDA0MDUxNzU5MDBaMFsxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
+Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0yMDAzMjgxNTE1MDBaFw0y
+MTAzMjgxNTE1MDBaMGoxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
 ZTEPMA0GA1UEBxMGV2Fyc2F3MSUwIwYDVQQLExxrdWJlIGV0Y2QgY2xpZW50IGNl
-cnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw5jdxQxt
-ELgpg+M1jib4VklzIJ4ULrKH59xKxs6qK0iGJClc8EFvVEBiuNpqXQ6Vygb8vbGz
-lplo02ivmhPVIFZ4ymk46kxG+w0scNcm/wMAzL1RiJT2nD71eMzYhVzomF5cQRGo
-JvqNpQg88jbTFluqHNFYTkv1HAnS+OMu7sbIm9iGgNfIoBrn/JpV97Rf1x4CaQ3t
-NdlrDxihQcMI4xoG2deIJdxFI6z9Rh7s4nXjBaAz9i8cRJ9v4uaMRq6kv3mBEfki
-Ve2Ql7jHTHwMqS/CsemkSl6IAG7IOQLB9U8xKsFuFPiYX11Xghcoi0MbjH4Qwocw
-vzDxPuttxNfWOr8uy70tCHWJWOajZWKtjj6+z9J4baTpjUu24y786qNkR8OVhjVw
-W+ETlf1I4/dUJN8iP4Zv4ibseJz9EhJZ41jY6+73bZwRao0lKig9Z69538r1wFs/
-1zOJP9YSJnuGA+rIYgdsu1fsq3eUWqJdlEAwpyx1TxfUJvFgx9ni0YfCUhmSb0B8
-b5Jt1TU0Lk1arZJ3NE1qC8gdbY4V+8MEKKfyq5uIlzaLrOQokUo5panfRGxxAFfe
-Y5QZb4jpXmx1W31hb2V1NY73fy2o/4JhEZcpfmjdhoDCgBwlKP2Xf6AFo/wtLdZF
-QhAVmDg5Vy8vcU381QSS08DRysptAZAhIYcCAwEAAaOBkTCBjjAOBgNVHQ8BAf8E
-BAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQC
-MAAwHQYDVR0OBBYEFB3FpZrvkz7oq25DKwzCMiKCCsgMMB8GA1UdIwQYMBaAFPFZ
-uGZNPsPnQu6Bo9RGfzlTdfkPMA8GA1UdEQQIMAaCBGt1YmUwDQYJKoZIhvcNAQEL
-BQADggEBABNwEAoXdjn7fmFTopWrOPK1fw9fHNsLbD5MJt14Gj2XZAirHHj8sPDQ
-Y3SdUhCnI1CUS4TccDGZBVgCIQ/grr6fvXe2rZPnh8n5rfxbUvWhqey4OKekzjEV
-kPPtDZOGqa70jFdlYHjqMPfB1oR6yWJCt7CD5yeWkMlsfnOOI1xU4+2sbOrlkCDa
-FYmywe+m6nEJFh/AJ6luElkOw4XrkV778JzXg6O4qyCWCEqSkgOXLOQqhOfc/qGk
-1YlsSf5AT7Ual2/tYaAW29zPunaZ/jZP8dOBN1r93QYinnnh1E68sOYEsAig3Aff
-nr7TT4YwEp62SkNxAyDVkA1WXC2kTTY=
+cnRpZmljYXRlMQ0wCwYDVQQDEwRrdWJlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAw5jdxQxtELgpg+M1jib4VklzIJ4ULrKH59xKxs6qK0iGJClc8EFv
+VEBiuNpqXQ6Vygb8vbGzlplo02ivmhPVIFZ4ymk46kxG+w0scNcm/wMAzL1RiJT2
+nD71eMzYhVzomF5cQRGoJvqNpQg88jbTFluqHNFYTkv1HAnS+OMu7sbIm9iGgNfI
+oBrn/JpV97Rf1x4CaQ3tNdlrDxihQcMI4xoG2deIJdxFI6z9Rh7s4nXjBaAz9i8c
+RJ9v4uaMRq6kv3mBEfkiVe2Ql7jHTHwMqS/CsemkSl6IAG7IOQLB9U8xKsFuFPiY
+X11Xghcoi0MbjH4QwocwvzDxPuttxNfWOr8uy70tCHWJWOajZWKtjj6+z9J4baTp
+jUu24y786qNkR8OVhjVwW+ETlf1I4/dUJN8iP4Zv4ibseJz9EhJZ41jY6+73bZwR
+ao0lKig9Z69538r1wFs/1zOJP9YSJnuGA+rIYgdsu1fsq3eUWqJdlEAwpyx1TxfU
+JvFgx9ni0YfCUhmSb0B8b5Jt1TU0Lk1arZJ3NE1qC8gdbY4V+8MEKKfyq5uIlzaL
+rOQokUo5panfRGxxAFfeY5QZb4jpXmx1W31hb2V1NY73fy2o/4JhEZcpfmjdhoDC
+gBwlKP2Xf6AFo/wtLdZFQhAVmDg5Vy8vcU381QSS08DRysptAZAhIYcCAwEAAaOB
+kTCBjjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
+BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFB3FpZrvkz7oq25DKwzCMiKCCsgM
+MB8GA1UdIwQYMBaAFPFZuGZNPsPnQu6Bo9RGfzlTdfkPMA8GA1UdEQQIMAaCBGt1
+YmUwDQYJKoZIhvcNAQELBQADggEBAHGsNqvBFSQC8qWqTTCtghbmF6nQqyhwEgsn
+L/29QMgmNx234r41JPymEN8R2bMFHuMDrOgEliXcbqcirpOuCvx3nln8gFmimtN5
+q7MEjAllJHqa1sjx+O83TNToFkmc8gxUsDqdngrsS1IoEbs8tIP7P/Y80gshCUfz
+2Fnqv1/m8h04RJ7E8Vgxq9txR3JmZPWbFtiHTb7Izlv6y9c+mtCDwzFb8ZiXCfSe
+TFBOptEjx5S82kj7LgYvtngz99ZojoCeQnqczbnHagw7yLH+NqF1SpTr6xZKzdQ+
+hh5Zna0whQCAy2wF5paW3asPNb7Sh90xXARynYxuvCKAIeurAI0=
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcd-root.cert b/cluster/certs/etcd-root.cert
index 14b4897..604ff83 100644
--- a/cluster/certs/etcd-root.cert
+++ b/cluster/certs/etcd-root.cert
@@ -1,29 +1,29 @@
 -----BEGIN CERTIFICATE-----
-MIIE8zCCA9ugAwIBAgIUDCr9SS9iUS+70qRrwt2yhJ0kJjUwDQYJKoZIhvcNAQEL
+MIIFAjCCA+qgAwIBAgIULK8H0d1v3xxIrRUgoGwW+5x6GIIwDQYJKoZIhvcNAQEL
 BQAweDELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0xOTA0MDYxNzU5MDBaFw0y
-MDA0MDUxNzU5MDBaMFsxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
+Y2x1c3RlcmNmZzEQMA4GA1UEAxMHZXRjZCBjYTAeFw0yMDAzMjgxNTE1MDBaFw0y
+MTAzMjgxNTE1MDBaMGoxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tp
 ZTEPMA0GA1UEBxMGV2Fyc2F3MSUwIwYDVQQLExxyb290IGV0Y2QgY2xpZW50IGNl
-cnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAudCBVJv6
-6aRyizfh20/PeIt7iSP9HpTorxxtG0Ti7ybjhu7gpkPYAK4Zf6YyYPgIjmv2hagp
-1iCO/PZYlqUL73x3QyfRFrAighRcEh/v7zMGbgIrZPO2ida7A7q4V0VcPaK/zPNt
-0gAlcoZSXuD5WDKCdFA9elDRLpdsgtDe7BjTgBp0MTzj2NBUoTRso36Depxuh2Og
-nvQa/nGhAOV5GCovMQ6PA19mhnZnK+JUZXwYWSnOfzayR7Rjy3eRsnoQq7x7Dur+
-RAnX9BDXUObULJgA6cHpEfxU/fnpCqWXZOWYgougeWdR9C8VyB0G7neVQDZGDYSV
-x+VH7D22DPLmmqRAgsFs7aF6E1Yp15no3P2kTxHEYPcMus8aNOZgs4pgeiFwPmu+
-aT+vW2mf82IHjPPAFfhkrzBodS/LjHDpAFklb+/wp/ypMYtCuXucm9kbV0oXxx4l
-a0qAYSAfL13wHTU6WXs9WAMEeqqQqLYRmvmssVtanYJUaRNZjCOYzTTi5gILUTLz
-BBPxgTpLsbCO97EueVrAer4s9C0dBcYksRXs024+N//g4pYyJAIuI/ZRJIbtHItk
-mw8mdwizynXtkRtvWl2lV1qHilo+p3xhSrh0vFBGO+wuT8aragUdbC/XGDoecHBr
-7KDZIYx7cZw2vI9xMqujP4hLhG9d6Zko810CAwEAAaOBkTCBjjAOBgNVHQ8BAf8E
-BAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQC
-MAAwHQYDVR0OBBYEFDDZWVeg57MP21XCkTRqzFR2OmttMB8GA1UdIwQYMBaAFPFZ
-uGZNPsPnQu6Bo9RGfzlTdfkPMA8GA1UdEQQIMAaCBHJvb3QwDQYJKoZIhvcNAQEL
-BQADggEBACzrx2h6qwfEcqJl06Epd6BEbQnZZlVhte3/lX0MSh86hxw+qxAPwfrG
-2XdcjXee6OCpAZv3qP/dRWnjKoKbIPNIeEK72n5pwwYbaQkhsHCm4XGgRyj9MHOo
-Ua0t0tXHESoD7uP57E/Q4CtcHOpR3pUUhKwqFB3NUvo3hZw5F3fKGoRSLSK3Mdwq
-sxxvTcYXxp3kjXm2cjTaZMoQ6eLBpEL+gCGezdts6+ExXCCRMWSdeQhhciv0Ez65
-6mzCq1uLGfQs1qd3eycPBi2Tt0vZ+Iitei+deewzwfpZ3oPCbI39kY1bxnIUNU0P
-Jr4JXnoB8k8ZTXsi15yom38pUy0xJhc=
+cnRpZmljYXRlMQ0wCwYDVQQDEwRyb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAudCBVJv66aRyizfh20/PeIt7iSP9HpTorxxtG0Ti7ybjhu7gpkPY
+AK4Zf6YyYPgIjmv2hagp1iCO/PZYlqUL73x3QyfRFrAighRcEh/v7zMGbgIrZPO2
+ida7A7q4V0VcPaK/zPNt0gAlcoZSXuD5WDKCdFA9elDRLpdsgtDe7BjTgBp0MTzj
+2NBUoTRso36Depxuh2OgnvQa/nGhAOV5GCovMQ6PA19mhnZnK+JUZXwYWSnOfzay
+R7Rjy3eRsnoQq7x7Dur+RAnX9BDXUObULJgA6cHpEfxU/fnpCqWXZOWYgougeWdR
+9C8VyB0G7neVQDZGDYSVx+VH7D22DPLmmqRAgsFs7aF6E1Yp15no3P2kTxHEYPcM
+us8aNOZgs4pgeiFwPmu+aT+vW2mf82IHjPPAFfhkrzBodS/LjHDpAFklb+/wp/yp
+MYtCuXucm9kbV0oXxx4la0qAYSAfL13wHTU6WXs9WAMEeqqQqLYRmvmssVtanYJU
+aRNZjCOYzTTi5gILUTLzBBPxgTpLsbCO97EueVrAer4s9C0dBcYksRXs024+N//g
+4pYyJAIuI/ZRJIbtHItkmw8mdwizynXtkRtvWl2lV1qHilo+p3xhSrh0vFBGO+wu
+T8aragUdbC/XGDoecHBr7KDZIYx7cZw2vI9xMqujP4hLhG9d6Zko810CAwEAAaOB
+kTCBjjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
+BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFDDZWVeg57MP21XCkTRqzFR2Omtt
+MB8GA1UdIwQYMBaAFPFZuGZNPsPnQu6Bo9RGfzlTdfkPMA8GA1UdEQQIMAaCBHJv
+b3QwDQYJKoZIhvcNAQELBQADggEBAD+Hf/kVNyXPbWL7asmMsqcldGocnzVFDK8J
+91pWLPSdbAexSUwP6sq2yHUPYMH4uVwjQOg3nKR9GImRJpHkudwZ8M876VdqmCBS
+/KgwlCWQtoN6cw9fQXGnPlJA+LN4q/YBQv0KRN1/eL/jKMPZZL3f0Hy+/4uOvK40
+L2RgNcoXhvRWsJRN+xf00ZvATiHxyhq/uC2dfTgpdCFynl1X700Z6Mk600J6vbo/
+FtGdj6F7nKwi00g2236tb3BEaL1vl2xtdm36xIDmX6F23p3dtRdIl1ULPFg1qAoa
+g8QjcUxILkhwadgKmiUDxVPA+1/afYRklPcHGziB0VJfOTgz1Rw=
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcdpeer-bc01n01.hswaw.net.cert b/cluster/certs/etcdpeer-bc01n01.hswaw.net.cert
index f1e987d..0c3d762 100644
--- a/cluster/certs/etcdpeer-bc01n01.hswaw.net.cert
+++ b/cluster/certs/etcdpeer-bc01n01.hswaw.net.cert
@@ -1,29 +1,30 @@
 -----BEGIN CERTIFICATE-----
-MIIFAzCCA+ugAwIBAgIULQDI1WmHgFvhWsA2mhX9rdtXO8IwDQYJKoZIhvcNAQEL
+MIIFHzCCBAegAwIBAgIUbtFJibIQ+L3FalsNIadl2cqxzs4wDQYJKoZIhvcNAQEL
 BQAwfTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEVMBMGA1UEAxMMZXRjZCBwZWVyIGNhMB4XDTE5MDQwNjE3NTkw
-MFoXDTIwMDQwNTE3NTkwMFowWTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93
+Y2x1c3RlcmNmZzEVMBMGA1UEAxMMZXRjZCBwZWVyIGNhMB4XDTIwMDMyODE1NTMw
+MFoXDTIxMDMyODE1NTMwMFowdTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93
 aWVja2llMQ8wDQYDVQQHEwZXYXJzYXcxIzAhBgNVBAsTGm5vZGUgZXRjZCBwZWVy
-IGNlcnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1DgM
-53WoGjOczAv1tyaNnJ2bQLjlE9iEWxS23xHr54VTEw6c/lG+L3E/XB0jf4CJlBR9
-pg34PhTEbcXOm9H2Mh5U5W+y6Q+mCu2FdKK5raj8bxbYGNMhDzg98bd2PqDxZFfk
-TsVqzzLegRDus+Q1AMMWkHxFLxuQMXTqUd053Gmd6a6RkwjqewmRy3+id9uzRbyn
-Ey9ML6KGlPTWvMgqEYvvgwGz05wtSZEMp0pheFkpPetegC25GQ5KQCe2a2zJ6eFW
-3IpiIwmXyC7sMINGDFjgTbjwmVLSEaVOgYX9qdYLpXNlzTgdxA61AlV5LSF0M4VJ
-LpsrK6ec1pcddyy4LwzAPAJY3B33xvulaSD+BxpdPO2S2B2/tLmhhhC7DGv+UewL
-QVZ+yPACMSUtmADhPdIBOwMUwnxaw4WKpCRpU/5OhSfV3OKZD5ejJS34H0YpXAPn
-gq/Td0kL3suIGUBKFCUMIQ3qkErucGoGWF5HBDdOA7vTAo5Q4yWXgumLU6kMXG5o
-4Lxc0+jszX8MprlQ1Oj7c2qfM/M0tbarEpaiSFcNyacJNl/uiuNP8jUGhdu+R5ey
-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
+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
+yw/s8FL0AczmICjx6yjlu4RQ5w==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcdpeer-bc01n02.hswaw.net.cert b/cluster/certs/etcdpeer-bc01n02.hswaw.net.cert
index 25d9d57..8776183 100644
--- a/cluster/certs/etcdpeer-bc01n02.hswaw.net.cert
+++ b/cluster/certs/etcdpeer-bc01n02.hswaw.net.cert
@@ -1,29 +1,30 @@
 -----BEGIN CERTIFICATE-----
-MIIFAzCCA+ugAwIBAgIUOrI3Jbcvd5z7rjuvd/ZCJa/k6C4wDQYJKoZIhvcNAQEL
+MIIFHzCCBAegAwIBAgIUHPJfFvMMibUlrbwagmA8Zy9KtS4wDQYJKoZIhvcNAQEL
 BQAwfTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEVMBMGA1UEAxMMZXRjZCBwZWVyIGNhMB4XDTE5MDQwNjE4MDQw
-MFoXDTIwMDQwNTE4MDQwMFowWTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93
+Y2x1c3RlcmNmZzEVMBMGA1UEAxMMZXRjZCBwZWVyIGNhMB4XDTIwMDMyODE2NDUw
+MFoXDTIxMDMyODE2NDUwMFowdTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93
 aWVja2llMQ8wDQYDVQQHEwZXYXJzYXcxIzAhBgNVBAsTGm5vZGUgZXRjZCBwZWVy
-IGNlcnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxZ9m
-liz7Af+qD3n4peAg8wNQ3l+7CalN6KJOnOlyTom371pcD59O1Uz47+/mWR5/dfQC
-2Pc42KxWOOh4YZ3zLdYNoNqS6XkyR40n3uc5an6xJYOaCQbvAA1ncm11AmGIoBl2
-BKOC/FiYQr+RsplOtN7NGOYNLLxit6hCm7Drwqu7Za2JR0YDffJZ6kNtaWK+5M40
-pR/coCUa4Cf94m4LhsLVbitDGrXtCmQOQVQP4Vf/buu8UO+e+rgGEOnS/kRpBiVI
-R+anD+uVYSYQPPhMWfDi6JDV561Kep+jvSkFX4NWz17PuTSiiAD+0iyvxPXSRDRx
-2lylu/IvOn9rkVL1kSfDW1ArLGlSbd3qyt4XKLCgIMhHLQS/KnmIOhOnhL9ZsJBk
-eq9BEgEIZa5lL8dTOwsKW+7NqXWr71aBSli/VaLlth+VvPxlhkROLW/6BleEsgkh
-/t4TZ2VcRS0+FWQgys4/btSJR+lKcMEkoBra9WXEX7fitrLWK+9jO6PXVf6X0Y8c
-bWLfuquaE2s4l7uwGnlVmsGBd1978L9cTGK554C+Fzjr7blkCpTUleGh0bXNP0jD
-S5+Iu4AHShP5JAsSAyLMjskrSHhltcWh0D7GSdz5HYyRKNEjxg8lFmcH/U3a05sk
-BbW1vVECWI9wyGw12g8tfrtgiE61E4+d8o3aJOkCAwEAAaOBnjCBmzAOBgNVHQ8B
-Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB
-/wQCMAAwHQYDVR0OBBYEFDOk2Cyznv+t3Gbzw/FmmcVS8iJPMB8GA1UdIwQYMBaA
-FC17mq9oqhmkzCqIvvh25L0npyQOMBwGA1UdEQQVMBOCEWJjMDFuMDIuaHN3YXcu
-bmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBFc02XSE2W7e8ymZYcTlJvzo6jJllAf7Hz
-7QsyiRoEeFfGE6YpI+sr/isRNjpT6QtZnhB03N3wazYjBpD2otrTaOabJLyIchhS
-pJjLPHQUXRukWQTv2regnNpGmZajBIlZhNafy3p98JG6xP8ZTKRfnqddFPPEeRPB
-YH7IyskbrjZAvmpmLWINO4+mPFLsK+gVfqOhZc7pMDt64fD4DxDKo/OyfF4YY4Qm
-ndhfi6iDeAp1I9b8dVq4o2nh4mx+Qd6URASbjPChO4I3rOXL+VUFKkY6dRjBeLBn
-3vir+WTOqWEGa0ubRGqppTGwiH0y1YSgZNEeLWi5Ujnmqt6ZfBcn
+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
+aKEgxEqoYvpE9ls1tSaOXS2iLw==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/etcdpeer-bc01n03.hswaw.net.cert b/cluster/certs/etcdpeer-bc01n03.hswaw.net.cert
index cf32ae4..a67d385 100644
--- a/cluster/certs/etcdpeer-bc01n03.hswaw.net.cert
+++ b/cluster/certs/etcdpeer-bc01n03.hswaw.net.cert
@@ -1,29 +1,30 @@
 -----BEGIN CERTIFICATE-----
-MIIFAzCCA+ugAwIBAgIURYvLd+Fg/5bnkj7FhFfSNAUFfu8wDQYJKoZIhvcNAQEL
+MIIFHzCCBAegAwIBAgIUY0PqPfxno72apj4xsBsQPC/QTbUwDQYJKoZIhvcNAQEL
 BQAwfTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93aWVja2llMQ8wDQYDVQQH
 EwZXYXJzYXcxGzAZBgNVBAoTEldhcnNhdyBIYWNrZXJzcGFjZTETMBEGA1UECxMK
-Y2x1c3RlcmNmZzEVMBMGA1UEAxMMZXRjZCBwZWVyIGNhMB4XDTE5MDQwNjE4MDUw
-MFoXDTIwMDQwNTE4MDUwMFowWTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93
+Y2x1c3RlcmNmZzEVMBMGA1UEAxMMZXRjZCBwZWVyIGNhMB4XDTIwMDMyODE1MTUw
+MFoXDTIxMDMyODE1MTUwMFowdTELMAkGA1UEBhMCUEwxFDASBgNVBAgTC01hem93
 aWVja2llMQ8wDQYDVQQHEwZXYXJzYXcxIzAhBgNVBAsTGm5vZGUgZXRjZCBwZWVy
-IGNlcnRpZmljYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvsau
-00vTLtwruq7TPh3xZ1Y5NZvOUFfF3H48wOeFZ5U421VRpD7APUmzt713WbIi6Nl+
-W1ac/MDg7KYm2MjcKYWKn/1LKVGCBoa0doXude177Jqgr5S9zsAP69GMKeZ2cmBT
-PfKgCiAScbF15bDLwv9CcYZg2/GRuh4fdbEuSoFb0FZiwNwMrW5XMSzN+lsVhXUJ
-oKrNDjnkazKMWq5FninCQ8mnnEwFi9+j/aN/UCJYMf7BndIHIvrLGrSyQUJwQD8M
-lPL6CBhW2aOfoLUwMeJBSdYVVH5/QIsNH1XoaZBtDYZs6BjaMtCcTN0p2/cFz5BV
-4yz4WP7CCMCTW+64rAD/M8JZO1s9HQfc0jVEqXfjXVSAEnUJQ+YlYGxGK2oyAWA8
-iF4raLArp8yFbW0SndX2WQ1T2hs2wJp6n5GXD1zTl0oGBq2PxOI8l3llPLDlKcL5
-RnrS713TC7KS7pTr76R5WmrSxtWzmo/jMICSg5ysm3UcjJL9rTM9cEc9OsDCTTtp
-nfZfwae61607m1QzTf7o641jrlA1be/itg/uNbQ2+0pmjwpe+ZWf/WyW0aFhWoZW
-qpkq+SFtAU3dEh9kxkpSf53zWkDZwwBhe0aqfV9w4Uh9cEQXapam86tXASDfik1F
-TR/C+qHrad8iTgvaIHMjiumP4+8IQjncMhCVUuMCAwEAAaOBnjCBmzAOBgNVHQ8B
-Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB
-/wQCMAAwHQYDVR0OBBYEFJCMTpAkSKDHOps8B+BIk1Af4xfqMB8GA1UdIwQYMBaA
-FC17mq9oqhmkzCqIvvh25L0npyQOMBwGA1UdEQQVMBOCEWJjMDFuMDMuaHN3YXcu
-bmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBnXInlyte2V8TJ4/YF4VEonyeQR2lNv/Xs
-ZH+8dpV1RLJ0bharklu5fYcGGbVyeFJdnLu/NkDuEhVtw1dSu2NcMtNAvGiCBi7C
-5V45zZ9OLBRExSXz+L1ZDmbORZ5vBNyNWEEish0mptu2cNf0krXcwfSQ8n7caBHU
-CterMKIGS973/125yduu+tELVOvUGvRNqplTwRUrdyj2dL4gkirbE3nhIYbidK4B
-tm845sDVzguosoAJPAEsgXDw8NV7SZxR7WsFKsA8NTWB8L2kJ1wNL44cygDuGsL0
-5xKbTb79ePq0tSiNJ7KZXArhOIKRem3/iHikiD6Msbt6SuDKYrZB
+IGNlcnRpZmljYXRlMRowGAYDVQQDExFiYzAxbjAzLmhzd2F3Lm5ldDCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL7GrtNL0y7cK7qu0z4d8WdWOTWbzlBX
+xdx+PMDnhWeVONtVUaQ+wD1Js7e9d1myIujZfltWnPzA4OymJtjI3CmFip/9SylR
+ggaGtHaF7nXte+yaoK+Uvc7AD+vRjCnmdnJgUz3yoAogEnGxdeWwy8L/QnGGYNvx
+kboeH3WxLkqBW9BWYsDcDK1uVzEszfpbFYV1CaCqzQ455GsyjFquRZ4pwkPJp5xM
+BYvfo/2jf1AiWDH+wZ3SByL6yxq0skFCcEA/DJTy+ggYVtmjn6C1MDHiQUnWFVR+
+f0CLDR9V6GmQbQ2GbOgY2jLQnEzdKdv3Bc+QVeMs+Fj+wgjAk1vuuKwA/zPCWTtb
+PR0H3NI1RKl3411UgBJ1CUPmJWBsRitqMgFgPIheK2iwK6fMhW1tEp3V9lkNU9ob
+NsCaep+Rlw9c05dKBgatj8TiPJd5ZTyw5SnC+UZ60u9d0wuyku6U6++keVpq0sbV
+s5qP4zCAkoOcrJt1HIyS/a0zPXBHPTrAwk07aZ32X8GnutetO5tUM03+6OuNY65Q
+NW3v4rYP7jW0NvtKZo8KXvmVn/1sltGhYVqGVqqZKvkhbQFN3RIfZMZKUn+d81pA
+2cMAYXtGqn1fcOFIfXBEF2qWpvOrVwEg34pNRU0fwvqh62nfIk4L2iBzI4rpj+Pv
+CEI53DIQlVLjAgMBAAGjgZ4wgZswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQG
+CCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSQjE6Q
+JEigxzqbPAfgSJNQH+MX6jAfBgNVHSMEGDAWgBQte5qvaKoZpMwqiL74duS9J6ck
+DjAcBgNVHREEFTATghFiYzAxbjAzLmhzd2F3Lm5ldDANBgkqhkiG9w0BAQsFAAOC
+AQEARkvDE+dGvSXWF8iHeXrasd+MoZz6+ifpFA27dsGLDfbEfmbks+nbJCAGhIqf
+YqKJ8H+lopIBDtxbcRfTjXDLsncmNTrWf/FVEx/WLbGQsYrqncW8ym2Xz1qBCMde
+/QBKKXSEEiBpVLYn0+41dvtxw8nAfIPin98pScnY04p+5BGaxFsKb2CsshBJlDOR
+g7BFYiWpQz2mAr6tyO/lG2KqN0B07P07jszej0c8Xl+tkbMtpghIi2GcJ75VXNJf
+XRtmeTw7YqcLgKTknBfJ9+GotmFUfAF2f+jA21URWY/6f4Yr+BniZj6Ikahx3eD+
+fw+y3c4eBLl5G3WugIFj5cM0UQ==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-controllermanager.cert b/cluster/certs/kube-controllermanager.cert
index 9e834f2..291c604 100644
--- a/cluster/certs/kube-controllermanager.cert
+++ b/cluster/certs/kube-controllermanager.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFdjCCBF6gAwIBAgIUcEMCdXAJRvlOgGKso6iXGykspCQwDQYJKoZIhvcNAQEL
+MIIFdjCCBF6gAwIBAgIUFcalvELfl2XL6uFIqLjD+5G2tD4wDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDQ4MDBaFw0yMDA0MDUyMDQ4MDBaMIG3MQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNTE1MDBaFw0yMTAzMjgxNTE1MDBaMIG3MQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEnMCUGA1UEChMec3lzdGVt
 Omt1YmUtY29udHJvbGxlci1tYW5hZ2VyMS8wLQYDVQQLEyZLdWJlcm5ldGVzIENv
 bXBvbmVudCBjb250cm9sbGVybWFuYWdlcjEnMCUGA1UEAxMec3lzdGVtOmt1YmUt
@@ -21,12 +21,12 @@
 pTfb5fo5ZuSyY1fSjq2f8LzCOeSmIyb0HKTVvWa1vo3yiXECAwEAAaOBqzCBqDAO
 BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG
 A1UdEwEB/wQCMAAwHQYDVR0OBBYEFAjGzAQ6wd1qf2er14tBEmwSV76JMB8GA1Ud
-IwQYMBaAFJgyXQ5PMx77CemJJBMWQmqA0ZnQMCkGA1UdEQQiMCCCHnN5c3RlbTpr
-dWJlLWNvbnRyb2xsZXItbWFuYWdlcjANBgkqhkiG9w0BAQsFAAOCAQEAQWgcjWP2
-nYF1HLYMZzkfbHaBcCCcPH1WBMBJXCgAHfdREwtj/Y2ouDB7XGcBnq7uwmHdfc72
-TLsduzlSfYIyvefUBa+xRBoZ9jtte7oesryb4VvrtiEgKxeOODjFb8fYgxOphmDK
-/FO7iGO4Fk6hB1P6Tk5+2OkEgJV8UEkIO2oncFCQa4fKnz2KiDjguZtAbXRcAou+
-MpVw0jRKXm0Ih/IFjOxF7tm/rXmk5FQ4aTaoLfBD5WtWVnX2BQmX7VMm8HY/vNfq
-EhYu4qqKxS6u92cRxXXeNHDLex+LMv7/3X/SB+gpd9foeAbPmPZKi/3Awi9u7X48
-jZNAXYMAEMg7sQ==
+IwQYMBaAFJgyXQ5PMx77CemJJBMWQmqA0ZnQMCkGA1UdEQQiMCCGHnN5c3RlbTpr
+dWJlLWNvbnRyb2xsZXItbWFuYWdlcjANBgkqhkiG9w0BAQsFAAOCAQEAjeeYwCxm
+8yfTfiWSKMMW9HTlK7zAl8PKngOvARihFgUfO1MbWQLTqYvkZ8/b8d3tXqxGHIY8
+TqLtK0N4a3ty+7IFwqnA29+apSPQOjK2f6RfSwUPFLqGDFXcM0pHHRbalYmUM///
+pEu3B223yimrtacAj3NFy1c5Jd7V36ZhwBAzzh3raWpqbuvm80MZccmb1gml1a/G
+KuyLzrtu8N6ObpsMFQSDbelgvgVvNh0akPANqrDc8BG5gDWkurIhsRWQC+POMKlE
+/c/EgyGXpZfvLtTprgCz+RRIGhV3GNRRbN/sdc3jO7M9QyJod3639VCf5gg5EU2l
+5sbxhHL+mvqJwA==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-kubelet-bc01n01.hswaw.net.cert b/cluster/certs/kube-kubelet-bc01n01.hswaw.net.cert
index 94b95df..d846aa9 100644
--- a/cluster/certs/kube-kubelet-bc01n01.hswaw.net.cert
+++ b/cluster/certs/kube-kubelet-bc01n01.hswaw.net.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFVjCCBD6gAwIBAgIUPmXvbmeRs74W9l5NLg6qG8QKJ90wDQYJKoZIhvcNAQEL
+MIIFVjCCBD6gAwIBAgIUfBwlWrk6L56SB1jAWUjjt4c5rOUwDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDMwMDBaFw0yMDA0MDUyMDMwMDBaMIGFMQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNTUzMDBaFw0yMTAzMjgxNTUzMDBaMIGFMQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEVMBMGA1UEChMMc3lzdGVt
 Om5vZGVzMRAwDgYDVQQLEwdLdWJlbGV0MSYwJAYDVQQDEx1zeXN0ZW06bm9kZTpi
 YzAxbjAxLmhzd2F3Lm5ldDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
@@ -20,12 +20,12 @@
 QaaisNvK2CqFoim0cjiBWWTlpUxv3XF0TCnlso18z5EVAgMBAAGjgb0wgbowDgYD
 VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
 HRMBAf8EAjAAMB0GA1UdDgQWBBRQZwM3WgGW+l8MKQBF2DZ5IWBh6jAfBgNVHSME
-GDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DA7BgNVHREENDAygh1zeXN0ZW06bm9k
-ZTpiYzAxbjAxLmhzd2F3Lm5ldIIRYmMwMW4wMS5oc3dhdy5uZXQwDQYJKoZIhvcN
-AQELBQADggEBALQ9uq5DCJYilyUQ1HYT1pP0PD8szSscTsQCVA5ExEuevlTn4ka/
-qtru+4Ht9eap12cmHqEQFpVZpBQyLmgRSVZPALNVYmaCrATyskz3uKDWUtRM1yAF
-+CfSR9Ibi6l9U4FOoA8U2xDrOAzJN2WYpmv/W363TJt0HuvpbrXEUsN6GFc0c/Sq
-7h/UWzyskoBup8eJrR6WX79pQSfoNCXJrEmGGZ2+hoU1/tF6siWMhwAu1UTpKQCw
-/rcnKwc04WRGl2zk84cffFnmpjJXf2BFdItVDBD5N/e4oRoaNocZjkHnZ/Xg1LoJ
-jC7BMy4ScK4S5Nxem8PVYnpLS7o5KLqqTnI=
+GDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DA7BgNVHREENDAyghFiYzAxbjAxLmhz
+d2F3Lm5ldIYdc3lzdGVtOm5vZGU6YmMwMW4wMS5oc3dhdy5uZXQwDQYJKoZIhvcN
+AQELBQADggEBAKfYV2qyVs9yvsOhjKo8/A/t8Juz6FCBbPuuTdUt0TVENro3/njr
+wvl+TrtdvRwOfgYbzl+UUKgmwOY9gBumWeEOHeOUSJS+Cz9Ad5YgrQzQ9T9stOAC
+/8DdGq4rMaU2tbxpNCAc38XJUmwOnSrVbreWc98LZSlV17FFoB45R7FFhz0VZHpM
+B0/I+fK/IkF3CbSHVViGqjPcTwASU4AQaw8mcEAvpT4yXF7nUY+jORZSH/48y6U4
+6hApMGjLrdnSyCPddvPxfW6BFw15TNh2PnCNIb5P0fwitjVcArtock7T/vHVe+8Y
++Xc7y0kqHBNKNDFRaA5I4HIF+Jc+kzZgeaA=
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-kubelet-bc01n02.hswaw.net.cert b/cluster/certs/kube-kubelet-bc01n02.hswaw.net.cert
index 835386d..919dcc2 100644
--- a/cluster/certs/kube-kubelet-bc01n02.hswaw.net.cert
+++ b/cluster/certs/kube-kubelet-bc01n02.hswaw.net.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFVjCCBD6gAwIBAgIUd3WmIRGYLAcILkm11fqlgkalJEowDQYJKoZIhvcNAQEL
+MIIFVjCCBD6gAwIBAgIUXvb9hqtoTXFM458nQblwXTSNeLAwDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDMyMDBaFw0yMDA0MDUyMDMyMDBaMIGFMQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNjQ1MDBaFw0yMTAzMjgxNjQ1MDBaMIGFMQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEVMBMGA1UEChMMc3lzdGVt
 Om5vZGVzMRAwDgYDVQQLEwdLdWJlbGV0MSYwJAYDVQQDEx1zeXN0ZW06bm9kZTpi
 YzAxbjAyLmhzd2F3Lm5ldDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
@@ -20,12 +20,12 @@
 GJzRDqOv3q3hGo7+a6gVMU60DHsgTFbTKJ1TUh0V+D1nAgMBAAGjgb0wgbowDgYD
 VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
 HRMBAf8EAjAAMB0GA1UdDgQWBBSnIgfLJiK7R+k9wfSCeKuqjpkYNjAfBgNVHSME
-GDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DA7BgNVHREENDAygh1zeXN0ZW06bm9k
-ZTpiYzAxbjAyLmhzd2F3Lm5ldIIRYmMwMW4wMi5oc3dhdy5uZXQwDQYJKoZIhvcN
-AQELBQADggEBAEV3RzyxUHspOi5ZX3p2y66dJaRpF2ja8EUgXZHZ9ls+IsuKkxBe
-2pSfo9rWAJu10h05UztN8ruL1+OuVitUYWPvhr3XpdmxfklGgU6yfGjfb9HeBAC8
-qfoeLZ9T59qiYfGTmm/KO8C2BGynd/VeWpRNrCcREdCyxP8v97oSqS88qY6GHAn/
-ijnbnTSEVc65P/YS7CayAoXzBFgtmcvwE0E9JxuJ9RlD3TZQd4Vo77V++QKGPr34
-Z2inu7WSGou1tsuue/fyuuDHHm82ZdPYjYLM6/HHkx5v7t0e+EM1ghc8qVbrpBdi
-dzgdOByF7drN8eQurjOgSCh0BzNLMdXm8Rc=
+GDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DA7BgNVHREENDAyghFiYzAxbjAyLmhz
+d2F3Lm5ldIYdc3lzdGVtOm5vZGU6YmMwMW4wMi5oc3dhdy5uZXQwDQYJKoZIhvcN
+AQELBQADggEBAJ0HFPiLL+Opy04Zm3H5bRHOlUcPiSrRUi4QM8PnnrC0t9R1Wvlb
+PvuvAG2EI2rQsN9qi73riOW5KwUmvxe3ArpHH20uhUumBfyikK3nqnQW6XNBzirQ
+pv/2b0Pm9CCn71ETcCrUaenGaUjUhmY4Ojvbp4Ycc5LQ2E4PlsR11GnETM15CK7K
+0z1VUtiu1+XubS+1trYw5aUF3WQGitTDl4T8VCdQRUKeyygO1HMQwmJmRwuMMLrP
+MTbaNOQBD+c+QIzQDE/+yGPkItU2efBmNvsp6B54AHznEkUMrqJWzkt4SnJ2KbAu
+DtzSfnNpaGpluvOyh0NxHsCcUMlm9J8ajNk=
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-kubelet-bc01n03.hswaw.net.cert b/cluster/certs/kube-kubelet-bc01n03.hswaw.net.cert
index 3939527..52a01c0 100644
--- a/cluster/certs/kube-kubelet-bc01n03.hswaw.net.cert
+++ b/cluster/certs/kube-kubelet-bc01n03.hswaw.net.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFVjCCBD6gAwIBAgIUXfXM+dY4X8VJWHt/56FVD2BnK4gwDQYJKoZIhvcNAQEL
+MIIFVjCCBD6gAwIBAgIUbS3md+5hxDAJRfmGxv8lV5m21x0wDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDMyMDBaFw0yMDA0MDUyMDMyMDBaMIGFMQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNTE1MDBaFw0yMTAzMjgxNTE1MDBaMIGFMQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEVMBMGA1UEChMMc3lzdGVt
 Om5vZGVzMRAwDgYDVQQLEwdLdWJlbGV0MSYwJAYDVQQDEx1zeXN0ZW06bm9kZTpi
 YzAxbjAzLmhzd2F3Lm5ldDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
@@ -20,12 +20,12 @@
 H0ShXe8QV2yvt0ISPzJjHNxG+pv79moribfs1gX5KUYpAgMBAAGjgb0wgbowDgYD
 VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
 HRMBAf8EAjAAMB0GA1UdDgQWBBT1m3RgBiqGCDhqwxutcUg0f9nBzDAfBgNVHSME
-GDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DA7BgNVHREENDAygh1zeXN0ZW06bm9k
-ZTpiYzAxbjAzLmhzd2F3Lm5ldIIRYmMwMW4wMy5oc3dhdy5uZXQwDQYJKoZIhvcN
-AQELBQADggEBABW6nR/wWc5SRJH+AnkK+YaqL0baO6BtKzNHSLMIHDDDfSufeNUx
-uAMy9j7Or44++9b6jS6nE0cWWMk8BSL1YZ9fTSP6vl9FIDdA9Iezdj92lcC94y08
-pmkI8Zdlk9Jc5NklU4a1gnipWpYU82RfHvoFREotSkn/u3Fp7DmaoWhzDwcRIiiP
-+wDeMf9BWt/uVvMo8dCKImWannPI+LNdrqRovNF0sQPxsFAgWby4r/TYPmPbg1E4
-MaGVuvk/dyHw6ijs+Bgy3DQ2virMfEl4UiJ/lTQUra0uVWydMobFLaQXJleQpjC6
-NGAxyBrbIuXCFu9fMppOQrtj2BLBhc3VO/k=
+GDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DA7BgNVHREENDAyghFiYzAxbjAzLmhz
+d2F3Lm5ldIYdc3lzdGVtOm5vZGU6YmMwMW4wMy5oc3dhdy5uZXQwDQYJKoZIhvcN
+AQELBQADggEBAFmt/mBuQHW0mfWNgc91OhNRUAL4Y23zFy1hpL4t0VNtGwEv51K1
+hTV7GlQHAcjE0Ti8Ivb9b+gU0FV6E1xFDsXg3w/unmZBhFMnKkwR/f8AIadgO/JT
+MgV4XvQgxXwVRetetXbr2uQV4Nz5cji9E2Rcad6NkN67FNpKratKR0+sPWCz9DYJ
+5mPlfmGBBW6ptAMGnekg0ttvup1a2FbCCxKpMnL+X4hv0a05Pgviwemm+uwckl/k
+zTqB7VDYtlS5SloRpHP4D3VxXU6j2vwkV7D/pEWKY5kXTHmBN2VDL+9mU03LB+aa
+NAeu2cme5Cu80BfzG+Eit7AD6hapm1WLmuM=
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-proxy.cert b/cluster/certs/kube-proxy.cert
index fe99d01..c638345 100644
--- a/cluster/certs/kube-proxy.cert
+++ b/cluster/certs/kube-proxy.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFQzCCBCugAwIBAgIUZlYtttc6/gOrhyj8uQTG7hFz3powDQYJKoZIhvcNAQEL
+MIIFQzCCBCugAwIBAgIUNOcUBxeoeF2k8lsdrL+mqCe0O7swDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDMwMDBaFw0yMDA0MDUyMDMwMDBaMIGRMQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNTE1MDBaFw0yMTAzMjgxNTE1MDBaMIGRMQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEaMBgGA1UEChMRc3lzdGVt
 Omt1YmUtcHJveHkxIzAhBgNVBAsTGkt1YmVybmV0ZXMgQ29tcG9uZW50IHByb3h5
 MRowGAYDVQQDExFzeXN0ZW06a3ViZS1wcm94eTCCAiIwDQYJKoZIhvcNAQEBBQAD
@@ -21,11 +21,11 @@
 AAGjgZ4wgZswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr
 BgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTn0MVsvuVE2ZiSJNZfEp6Q
 pb8u9jAfBgNVHSMEGDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ0DAcBgNVHREEFTAT
-ghFzeXN0ZW06a3ViZS1wcm94eTANBgkqhkiG9w0BAQsFAAOCAQEAEsBVXPHfGVdL
-s2BDmbhArb98byrWGbSWyz008OaDt6LLrneUDIyiwJgBOxgyY5vPR9fz5qSJb3Ua
-/7NngHE4k3afKU8/OI/mrDHIwnHrKuKWNpYcpotzKbHhTBn0erptl+KJIGhiUgOW
-LTSvEG/0k5Kxrs737Eq9R0DsOe2vNiw+IerNUAyG0wwD+HbT6pEkE6gsD6k8Fkwc
-kO+JT2hs/e0bcaCb4PUMV8CMqe5sZKGOcr1foUP72GOpE7oZ4Madq2AuNZnm4RIo
-xJGAVfejo3JG5qWglk8Kl1qGl0Wn2yUqRp3ErMUY/7UFJSKazucfnZ3zic1Z4pB4
-3svHQJ+pdA==
+hhFzeXN0ZW06a3ViZS1wcm94eTANBgkqhkiG9w0BAQsFAAOCAQEAa+LkfAbWHUSs
+19veJ09P0uo6PYKqXsOrh9soWX8lusI3Zt3zhTdSXkzJwKi8bH3zFx8niWAIHNwZ
+mxesvJIH6fPA0/401MkjhSRo3cyMUnjKmjx5+DD2qIEKKBPsr/xNMpJGPKaDjGtk
+YyRHW8Bg7kX+Jc/uv7Gg6U+/xtdbELaxL/USufRN7obC7gNtenXkdOUgINrfllX/
+66+K7yqaAqaCR4gEGSLUnUvkbFZ/+XB7Z1tLKgWurJj5v82ZxnkBI+aU3tVwLtoT
+tnH7OLi5Tbo+RYuf3iMd1vGxVwEPcD9cBUz0lRsK9TTJxRytS8CnS3EIwcitYo86
++yl7LltiSA==
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-scheduler.cert b/cluster/certs/kube-scheduler.cert
index 1544599..abdf6a4 100644
--- a/cluster/certs/kube-scheduler.cert
+++ b/cluster/certs/kube-scheduler.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFUzCCBDugAwIBAgIUS4QEUvDV3mIJIIuyi1HJ/lmoINkwDQYJKoZIhvcNAQEL
+MIIFUzCCBDugAwIBAgIUSlMSojfxDUiCtKO/7Mr5kJbxBxQwDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDMwMDBaFw0yMDA0MDUyMDMwMDBaMIGdMQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNTE1MDBaFw0yMTAzMjgxNTE1MDBaMIGdMQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEeMBwGA1UEChMVc3lzdGVt
 Omt1YmUtc2NoZWR1bGVyMScwJQYDVQQLEx5LdWJlcm5ldGVzIENvbXBvbmVudCBz
 Y2hlZHVsZXIxHjAcBgNVBAMTFXN5c3RlbTprdWJlLXNjaGVkdWxlcjCCAiIwDQYJ
@@ -21,11 +21,11 @@
 pY0nOn+0jIjzAgMBAAGjgaIwgZ8wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQG
 CCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQw8PPX
 ExubjWf7o/eChtV5jnt1kTAfBgNVHSMEGDAWgBSYMl0OTzMe+wnpiSQTFkJqgNGZ
-0DAgBgNVHREEGTAXghVzeXN0ZW06a3ViZS1zY2hlZHVsZXIwDQYJKoZIhvcNAQEL
-BQADggEBALU1R9svNLyFNCOXcyvAg0T9u3mH5SD0F1MQgKrgZTuVKc4/Oa/LOBbD
-wgwp/1eJZ0xkMVTZl3lw6N6KkgtydbZskc2m2qQPBVdv/RFzecKROI2UKvLL+lTS
-HlNIxv6e5T1q2B52o++B4QoEfBhwclxtq0oHPpqu+7ZQ0lGDHeOcIphyMGONOWoT
-s7LzYMB+ud9XTzdB91eIIXcYZz0OlF5qI21URy/Mi6j1RENG8U+GtGmNnkZ3z3Yb
-SAL8hqDlwabD3V44xqKJMfaWzAbXO43t2FTmyi1uUVsfJc3J/fCSZLJNRfyUOMVb
-mJsokVUNYaaOLrHHZz77+k7ruR/KWkM=
+0DAgBgNVHREEGTAXhhVzeXN0ZW06a3ViZS1zY2hlZHVsZXIwDQYJKoZIhvcNAQEL
+BQADggEBABw3aqj8FQtaZKHHzGY+cpjvOT1VUKax1k0iQAbYS5/8d3kaToDed05M
+omXDcIxb3VHs6+sWxJYWAiRPiA5mrDdA7XQcfIv1xtP+DL3dbRqhz276XNM4/NIj
+vt9aQox/WSE0HCDTUSN/clYbB6tigLfSxXhnuz214N6NwkcTl8xQVvXxg3z6ryc7
+XUTEA0fvl/fe+KsO2l4kxBk9Ef5cud3j2e4F4l8tFHz1bRXfcEEcS5uLLgK3KIAu
+sf3Sf+t/jcTTrJ+3YVFBAY+F7AN4UjNdlAfyvTG7xB+pxD7RlEd6Ycozd0tYppg4
+VBbXtQ4TOxHLVvrlANi3MJAzYSUuB54=
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kube-serviceaccounts.cert b/cluster/certs/kube-serviceaccounts.cert
index 684cce9..7ba5a5f 100644
--- a/cluster/certs/kube-serviceaccounts.cert
+++ b/cluster/certs/kube-serviceaccounts.cert
@@ -1,30 +1,30 @@
 -----BEGIN CERTIFICATE-----
-MIIFKjCCBBKgAwIBAgIUGx40+NyFMYjk3UltyhqHHSfyEkkwDQYJKoZIhvcNAQEL
+MIIFKjCCBBKgAwIBAgIUC8G46cdD+fUIfepfl2RRtz7D5FgwDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0xOTA0
-MDYyMDMwMDBaFw0yMDA0MDUyMDMwMDBaMHsxCzAJBgNVBAYTAlBMMRQwEgYDVQQI
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDAz
+MjgxNTE1MDBaFw0yMTAzMjgxNTE1MDBaMHsxCzAJBgNVBAYTAlBMMRQwEgYDVQQI
 EwtNYXpvd2llY2tpZTEPMA0GA1UEBxMGV2Fyc2F3MSswKQYDVQQLEyJLdWJlcm5l
 dGVzIFNlcnZpY2UgQWNjb3VudHMgU2lnbmVyMRgwFgYDVQQDEw9zZXJ2aWNlYWNj
-b3VudHMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDS8y3HM9/PG1K1
-9usXmY3dw7x0NEBH2rHJZET8kwnjifU0uuhQwSAwyqp1RuWWUxrIudgPfZ+TrPFA
-fMNpRnEqzbfPYYboXSmK3Yrc+XXJeD+HvD4UKcR2fL0/muNi9qkLeNhZUuLxrXUQ
-JxZhLokqdLo92yy4sjVCNrqJmqyU78I0XCduFwoxQXW8o6v0kIWZj8SsBkr8SMl9
-1drTG86buI31xf+vl9UWfXhChXQ3tHM5LS2mygHZBW7fGTzxydJxkE9g3rw2Y/lD
-ZnHgHdChV1BL2guYudMmlpYcoAL9womhlvmoMTW2BJb0EoBeQRAy1RgohgelusZm
-wTJeXsmgd2vRIbju6u6Qup2r5p8WIpiDPZVC0XZAacO4/NRsC2cYjsXHgdaid73N
-J1wYcauc+ddha07QSwhwcHtKfv+x8QAVgJ38iJvVK+y+iOJctEbx4UBN5jvwxwwW
-rUnSkXpLL+w4+9q3e/FhBdN7HX9luuiZnCMx/nfUDz29461WSYlx2l5HNatHDugC
-vgBJDnQ/1Aj1BNL7dXGsGuO5jjyAfoKvxOVf03rVTLzLhuN5GhC6liCzJJ0Kfm1w
-EwQgpwFZteIvGvLUCg6WF9nwzZqQ9ZNZaOE3wwFkaPA4bJkOs/S+ZDkTFKl+/wVo
-hF36Rnp6zKI1NT2Rz5bo0ZGnc7KflQIDAQABo4GcMIGZMA4GA1UdDwEB/wQEAwIF
+b3VudHMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDFSCquXVjQANUN
+IIkGFgsgrCKxqF4gT1sxIcDnsoyncEXnsdqfYAn3yvi0iEZq0JwMkAhWI9k9oAV4
+DOW3hMBrqWZUjRnHPwUwoewvUwqCZyjOzhFSyy2E2iEq7yfrZkgxVVHIdUMoq179
+/jRRa8fk/oUmqiiQNWy//q6VX1ASX7elh4oKfwRMFwnf6vQO7WUm2wqlbYNHaGji
+XDMVuGUyx8XG/F0c1YrAQPIPx5vU6GVV+Qpdl38E/wDIUCS/RPml8M2Q5eBljo2P
+Xhr26tO2OQuOu5UBvzg4e7k1rEKsMlwQSATB2PIVyLNQrWN6zUuI2pV2OUE6Oreh
+ZI6qpZ3eo+QJi496QriZeZ6tLnzoPPaw9QIJG03Si25PjT7p1ULEx7EQ2OOcBBXj
+UoQF1KDkqoqJ5GEqA2ie/U9FhobFUaQpqiZsOWYG08u9oERzNnK+h057XjLsblod
+Bi4d2x+oLFi0q2V/zb6yts3jHicTEyAXCkOq3q6pFd7N8YUbSU5Og8Bgk6KzPoSb
+Klg6L8ttDwXXQRNl4/1CR6+17hFCECoKRVKvTeOX0O7Rl/raWpL5WmBZohpaDfQf
+VpRBONC4p9K73bnlK7P1E38DrrWO4kO7xrmGF0KuRXVCzBZngG+8dpHJMBg7CyH0
+Wv3ZmrcEgb0rRwWYj8LY71EQzO2D2QIDAQABo4GcMIGZMA4GA1UdDwEB/wQEAwIF
 oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd
-BgNVHQ4EFgQUbrez0M0eXWQD6wus9hshP8quFBQwHwYDVR0jBBgwFoAUmDJdDk8z
+BgNVHQ4EFgQUliCshdOww6BLgNw1Cu+0XiqCpg8wHwYDVR0jBBgwFoAUmDJdDk8z
 HvsJ6YkkExZCaoDRmdAwGgYDVR0RBBMwEYIPc2VydmljZWFjY291bnRzMA0GCSqG
-SIb3DQEBCwUAA4IBAQAYay9hJEEyj0wBE25FayKEKKT4YM6UiPE0GqOxdyC4J1iv
-GTeb2KpZ/TDnS9oz8+ihyU7IvrtqeDb84rR6Wbb1ae7CaqCPHgrE5CW+O8+jd9TH
-6ZEKcOUDfXWH+2SjQYdFM3NnxXrcdkkgZsy0lZpRu3YWTurzsZI0j8AXLq1W/vgT
-dDMN6jlfQC4HUPMoaFnZFuJel3lU/pB6E0j7ErQf+c8q2knu+jNUjJmet+l7I4VT
-0YJTpY6oVrK1CZ1XFOnhWUCYCBfKZ/05QzkIv4SsE5HfWEIN1Ne51vsnm8JBEpz/
-8elXu6B8P1V/2AXun9/R0v4zkuDm953885MGHrW2
+SIb3DQEBCwUAA4IBAQB7xM6vfvk3dw9cFP0F2YTAxLVot1E+KzHWz952uIm3CrtU
+Vq3WHBX3NRTVrzg3Ycx4tNniOHBqNrzgksz0XmFZw7VyiY+yEzueVCJ9HU9y8Kb2
+XdL5zqTtgVYspr0dI/34NbGnFVJAOJ57fAc4LxhPwAZMG6s4LwDiBDYIBw+KoJsD
+FOiHJ+AfW5taGONEGY+HuNnSo+RllCgFdjPW0hK4X8Jt4p5Qr+oICO4Nzp4jZwv0
+WqxHzmX4DYDQLztqrQelSDkaQaP/xAhq7nsaK91sMob1OqQcYSMckm2SFEmTwBCs
+VpJg24y/LRw1LPK4lE7GGGIkyko+aDCsIm+YDX7U
 -----END CERTIFICATE-----
diff --git a/cluster/certs/kubefront-apiserver.cert b/cluster/certs/kubefront-apiserver.cert
index c8fe77b..409beb3 100644
--- a/cluster/certs/kubefront-apiserver.cert
+++ b/cluster/certs/kubefront-apiserver.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFEzCCA/ugAwIBAgIUBrhuHQZXw8Fe+lov3RlTO4t7kBEwDQYJKoZIhvcNAQEL
+MIIFEzCCA/ugAwIBAgIURk8WW4qapypnrPH9a2aMG+oMUpgwDQYJKoZIhvcNAQEL
 BQAwgYcxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
 CmNsdXN0ZXJjZmcxHzAdBgNVBAMTFmt1YmVybmV0ZXMgZnJvbnRlbmQgQ0EwHhcN
-MTkwNDA2MjAzMDAwWhcNMjAwNDA1MjAzMDAwWjBmMQswCQYDVQQGEwJQTDEUMBIG
+MjAwMzI4MTUxNTAwWhcNMjEwMzI4MTUxNTAwWjBmMQswCQYDVQQGEwJQTDEUMBIG
 A1UECBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEcMBoGA1UECxMTS3Vi
 ZXJuZXRlcyBGcm9udGVuZDESMBAGA1UEAxMJYXBpc2VydmVyMIICIjANBgkqhkiG
 9w0BAQEFAAOCAg8AMIICCgKCAgEAuVXNUv1oJrLw9XxagSTyHegDeHT71JSdeYWx
@@ -20,11 +20,11 @@
 /xZzYekCAwEAAaOBljCBkzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
 BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKkQFBbQv3W0
 jUMCdTBkmb0YlS7DMB8GA1UdIwQYMBaAFI+XbXEIcmDLzHcqyvkE9KRqvCX0MBQG
-A1UdEQQNMAuCCWFwaXNlcnZlcjANBgkqhkiG9w0BAQsFAAOCAQEAW9DxplTEyKrA
-WM3yC/kkSz9iTSqSO5X3QWFPydvN3nUY2g8CD4f5VYPAZbqljNu2W/dA9LberQl3
-FUh5PM6E1+QekdosHfiQrjBi4mD8G7mVkJBCtrcflerpb6+VhYUlHvMMITbV5wrx
-Hz4/G2Ndym9IqwVBn8Srbjh3w1yYcVUGsQD7HHxJOnE9YknlQH73tMfbyPYsEgUo
-fB5unEBGRrEjqGa2lCWHwxE0MT5WowsKUhKr5ikH3l2AufOnVETfkq24yHi/qKt7
-Lq/303b/5y/2uwC5J0wpiqd71BuwXJoprAMHPjI8b58MKZtMBsYDTBkP8Ls150CP
-PpB6p9/L9w==
+A1UdEQQNMAuCCWFwaXNlcnZlcjANBgkqhkiG9w0BAQsFAAOCAQEAGI6MmkpsH4Ur
+mU20a8EmXkaGK9pD4hvbnsFsnzR0CjE8KHmDWIKlkg6RJZlnZnsd4UmnJTbnpM6A
+IkOycdwXsD8QrWwOMOqT3mIw4OYZqog4WqXcXlpeQwDmzC5mqEPWmy/Vr0CCIzFF
+Cx2ElcoIBQ46o9hm4Lx91uyWqDFRsBLleE+rgr9nCqesG4kYylT4Tb21l+YGQqtC
+v1mpXD7jaoyVVGpm28zUE2v/bGZsBfmd5cC9MVvyVrlhL4soI1UCO5aP/n/DxY0a
+iJ3UvlNnUbFuo0GUalbIwlTlVK/l6o6XRPINGbxTPEaueDLPKIqvvF+ZZBDMx7Hl
+k3IBIbEW8A==
 -----END CERTIFICATE-----
diff --git a/cluster/clustercfg/ca.py b/cluster/clustercfg/ca.py
index 9ed2053..0107080 100644
--- a/cluster/clustercfg/ca.py
+++ b/cluster/clustercfg/ca.py
@@ -1,4 +1,5 @@
 # encoding: utf-8
+from datetime import datetime, timezone
 import json
 import logging
 import os
@@ -171,6 +172,35 @@
 
         return key, csr
 
+    def gen_csr(self, key, hosts, o=_std_subj['O'], ou=_std_subj['OU']):
+        """
+        Generate a CSR while already having a private key - for renewals, etc.
+
+        TODO(q3k): this shouldn't be a CA method, but a cert method.
+        """
+        cfg = {
+            "CN": hosts[0],
+            "hosts": hosts,
+            "key": {
+                "algo": "rsa",
+                "size": 4096,
+            },
+            "names": [
+                {
+                    "C": _std_subj["C"],
+                    "ST": _std_subj["ST"],
+                    "L": _std_subj["L"],
+                    "O": o,
+                    "OU": ou,
+                },
+            ],
+        }
+        cfg.update(_ca_config)
+        logger.info("{}: Generating CSR for {}".format(self, hosts))
+        out = self._cfssl_call(['gencsr', '-key', key, '-'], obj=cfg)
+
+        return out['csr']
+
     def sign(self, csr, save=None, profile='client-server'):
         logging.info("{}: Signing CSR".format(self))
         ca = self._cert
@@ -246,12 +276,30 @@
         with open(self.cert_path) as f:
             return f.read()
 
+    @property
+    def cert_expires_soon(self):
+        if not self.cert_exists:
+            return False
+
+        out = self.ca._cfssl_call(['certinfo', '-cert', self.cert_path], stdin="")
+        not_after = datetime.strptime(out['not_after'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
+        until = not_after - datetime.now(timezone.utc)
+        if until.days < 30:
+            return True
+        return False
+
     def ensure(self):
-        if self.key_exists and self.cert_exists:
+        if self.key_exists and self.cert_exists and not self.cert_expires_soon:
             return
 
-        logger.info("{}: Generating...".format(self))
-        key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
+        key = None
+        if not self.key_exists:
+            logger.info("{}: Generating key...".format(self))
+            key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
+        else:
+            logger.info("{}: Renewing certificate...".format(self))
+            # Use already existing key
+            csr = self.ca.gen_csr(self.key_path, self.hosts, o=self.o, ou=self.ou)
         self.ca.sign(csr, save=self.cert, profile=self.profile)
 
     def upload(self, c, remote_cert, remote_key, concat_ca=False):
diff --git a/cluster/clustercfg/clustercfg.py b/cluster/clustercfg/clustercfg.py
index eb9f52d..c3e7bd6 100644
--- a/cluster/clustercfg/clustercfg.py
+++ b/cluster/clustercfg/clustercfg.py
@@ -175,7 +175,7 @@
         ca_kube = ca.CA(ss, certs_root, 'kube', 'kubernetes main CA')
 
         # Make prodvider intermediate CA.
-        ca_kube.make_cert('ca-kube-prodvider', o='Warsaw Hackerspace', ou='kubernetes prodvider intermediate', hosts=['kubernetes prodvider intermediate CA'], profile='intermediate')
+        ca_kube.make_cert('ca-kube-prodvider', o='Warsaw Hackerspace', ou='kubernetes prodvider intermediate', hosts=['kubernetes prodvider intermediate CA'], profile='intermediate').ensure()
 
         # Make kubelet certificate (per node).
         ca_kube.make_cert('kube-kubelet-'+fqdn, o='system:nodes', ou='Kubelet', hosts=['system:node:'+fqdn, fqdn])
diff --git a/cluster/README b/cluster/doc/admin.md
similarity index 77%
rename from cluster/README
rename to cluster/doc/admin.md
index 0120859..27b30ca 100644
--- a/cluster/README
+++ b/cluster/doc/admin.md
@@ -1,23 +1,10 @@
 HSCloud Clusters
 ================
 
+Admin documentation. For user documentation, see [//cluster/doc/user.md](/cluster/doc/user.md).
+
 Current cluster: `k0.hswaw.net`
 
-Accessing via kubectl
----------------------
-
-    prodaccess # get a short-lived certificate for your use via SSO
-               # if youre local username is not the same as your HSWAW SSO
-               # username, pass `-username foo`
-    kubectl version
-    kubectl top nodes
-
-Every user gets a `personal-$username` namespace. Feel free to use it for your own purposes, but watch out for resource usage!
-
-    kubectl run -n personal-$username run --image=alpine:latest -it foo
-
-To proceed further you should be somewhat familiar with Kubernetes. Otherwise the rest of terminology might not make sense. We recommend going through the original Kubernetes tutorials.
-
 Persistent Storage (waw2)
 -------------------------
 
@@ -64,9 +51,10 @@
 
 We run Ceph via Rook. The Rook operator is running in the `ceph-rook-system` namespace. To debug Ceph issues, start by looking at its logs.
 
-A dashboard is available at https://ceph-waw2.hswaw.net/, to get the admin password run:
+A dashboard is available at https://ceph-waw2.hswaw.net/ and https://ceph-waw3.hswaw.net, to get the admin password run:
 
     kubectl -n ceph-waw2 get secret rook-ceph-dashboard-password -o yaml | grep "password:" | awk '{print $2}' | base64 --decode ; echo
+    kubectl -n ceph-waw2 get secret rook-ceph-dashboard-password -o yaml | grep "password:" | awk '{print $2}' | base64 --decode ; echo
 
 
 Ceph - Backups
@@ -75,6 +63,7 @@
 Kubernetes PVs backed in Ceph RBDs get backed up using Benji. An hourly cronjob runs in every Ceph cluster. You can also manually trigger a run by doing:
 
     kubectl -n ceph-waw2 create job --from=cronjob/ceph-waw2-benji ceph-waw2-benji-manual-$(date +%s)
+    kubectl -n ceph-waw3 create job --from=cronjob/ceph-waw3-benji ceph-waw3-benji-manual-$(date +%s)
 
 Ceph ObjectStorage pools (RADOSGW) are _not_ backed up yet!
 
@@ -83,8 +72,7 @@
 
 To create an object store user consult rook.io manual (https://rook.io/docs/rook/v0.9/ceph-object-store-user-crd.html)
 User authentication secret is generated in ceph cluster namespace (`ceph-waw2`),
-thus may need to be manually copied into application namespace. (see
-`app/registry/prod.jsonnet` comment)
+thus may need to be manually copied into application namespace. (see `app/registry/prod.jsonnet` comment)
 
 `tools/rook-s3cmd-config` can be used to generate test configuration file for s3cmd.
 Remember to append `:default-placement` to your region name (ie. `waw-hdd-redundant-1-object:default-placement`)
diff --git a/cluster/doc/index.md b/cluster/doc/index.md
new file mode 100644
index 0000000..afd04e4
--- /dev/null
+++ b/cluster/doc/index.md
@@ -0,0 +1,6 @@
+Warsaw Hackerspace Kubernetes Cluster
+=====================================
+
+**User documentation**: [user.md](user.md).
+
+**Admin documentation**: [admin.md](admin.md).
diff --git a/cluster/doc/user.md b/cluster/doc/user.md
new file mode 100644
index 0000000..6dd6938
--- /dev/null
+++ b/cluster/doc/user.md
@@ -0,0 +1,58 @@
+Warsaw Hackerspace Kubernetes Clusters
+======================================
+
+End-user^Whacker documentation.
+
+Intro
+-----
+
+We run Kubernetes, a cluster system on our production machines. This allows you to schedule software to run without having to worry about traditional deployment, or where your particular piece of code is actually running. This document will not teach you how to use Kubernetes, but will give you a short hands-on example on how to access it, and then point you in the right direction for general documentation to follow.
+
+Accessing Kubernetes
+--------------------
+
+Kubernetes is accessed fully via an API, for which there exists a standard command line tool: `kubectl`. If you've check out hscloud and followed the instructions in [//README.md]("/README.md"), you should have that tool built and available for you to use.
+
+Before you can use `kubectl`, however, you will need to authenticate yourself. To do that, run `prodaccess`. This will issue you short-term (~hours) credentials that `kubectl` can then pass on to Kubernetes to authenticate itself.
+
+    $ prodaccess
+    Enter SSO/LDAP password for q3k@hackerspace.pl: 
+    Good evening professor. I see you have driven here in your Ferrari.
+
+If `prodaccess` is not on your $PATH, ensure you have sourced `env.sh` from the root of hscloud and ran `tools/install.sh`.
+
+By default, `prodaccess` will use your local user name to authenticate as `<user>@hackerspce.pl`. If your Hackerspace SSO name is different, specify it using the `-u` flag to prodaccess, eg. `prodaccess -u informatic`.
+
+You can now check that you indeed have access to Kubernetes:
+
+    $ kubectl version   # show version of Kubernetes
+    $ kubectl top nodes # show node (machine/server) statistics
+
+You are now fully set up to schedule your own jobs on `k0.hswaw.net`, our currently only Kubernetes cluster.
+
+Running Stuff
+-------------
+
+We have a fairly extensive role-based access control system set up to provide a level of multi-tenancy of our Kubernetes cluster. What this means is that you will not be able to modify other people's stuff. Indeed, by default, you barely have any access. So as you can experiment with Kubernetes, we automatically provision you a personal namespace (`personal-$USER`) in Kubernetes. This acts as your own playground, where you can run anything you want, as long as it doesn't eat into our resources too much.
+
+For example, to run an Alpine Linux Docker image in your own namespace:
+
+    kubectl -n personal-$USER run --image=alpine:latest -it foo
+
+This will create a Kubernetes deployment named foo, running the `alpine:latest` Docker image, and drop you in an interactive shell in it. Naturally, replace `$USER` with your SSO username if it's different from your system username.
+
+Once you're done, delete the Deployment:
+
+    kubectl -n personal-$USER delete deployment foo
+
+Pod Security
+------------
+
+Apart from the RBAC (role based access control) that prevents you from poling at things that you shouldn't over the API, we have one more security measure in place. Throught a Kubernetes mechanism called 'PodSecurityPolicy' we limit what pods (ie. containers) can do. Notably, pods will by default not be able to access any host data, run in privileged mode, or even setuid to a different uid. The most notable side effect of this is that some basic system tools within pods will not work: ie., apt on Ubuntu.
+
+More Kubernetes
+---------------
+
+We highly recommend following the [Kubernetes Basics](https://kubernetes.io/docs/tutorials/kubernetes-basics/) tutorial as a first step in using Kubernetes for real world applications.
+
+For defining production jobs, we use a language called `Jsonnet` via a tool called `kubecfg`. This is to replace some more popular tools that other Kubernetes systems use, eg. Helm. For more information about that, ping q3k so that he writes a codelab about it :).
diff --git a/cluster/kube/cluster.jsonnet b/cluster/kube/cluster.jsonnet
index 3952f66..49e1c5a 100644
--- a/cluster/kube/cluster.jsonnet
+++ b/cluster/kube/cluster.jsonnet
@@ -270,6 +270,7 @@
             clients: {
                 cccampix: k0.cockroach.waw2.Client("cccampix"),
                 cccampixDev: k0.cockroach.waw2.Client("cccampix-dev"),
+                buglessDev: k0.cockroach.waw2.Client("bugless-dev"),
             },
         },
         ceph: {
@@ -372,7 +373,7 @@
             waw3: rook.Cluster(k0.cluster.rook, "ceph-waw3") {
                 spec: {
                     mon: {
-                        count: 1,
+                        count: 3,
                         allowMultiplePerNode: false,
                     },
                     storage: {
diff --git a/devtools/depotview/BUILD.bazel b/devtools/depotview/BUILD.bazel
new file mode 100644
index 0000000..908a629
--- /dev/null
+++ b/devtools/depotview/BUILD.bazel
@@ -0,0 +1,46 @@
+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_library(
+    name = "go_default_library",
+    srcs = ["main.go"],
+    importpath = "code.hackerspace.pl/hscloud/devtools/depotview",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//devtools/depotview/proto:go_default_library",
+        "//devtools/depotview/service:go_default_library",
+        "//go/mirko:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "depotview",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
+
+container_layer(
+    name = "layer_bin",
+    files = [
+        ":depotview",
+    ],
+    directory = "/devtools/",
+)
+
+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 = "devtools/depotview",
+    tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+)
diff --git a/devtools/depotview/README.md b/devtools/depotview/README.md
new file mode 100644
index 0000000..e1faca4
--- /dev/null
+++ b/devtools/depotview/README.md
@@ -0,0 +1,24 @@
+depotview
+=========
+
+Git-as-a-service over gRPC. Useful to get read-only access to hscloud.
+
+Production
+----------
+
+There's a prod instance running at depotview.devtools-prod.svc.cluster.local.
+
+Development
+-----------
+
+    $ bazel run //devtools/depotview -- -hspki_disable
+    $ grpcurl -plaintext -d '{"ref": "master"}' 127.0.0.1:4200 depotview.DepotView.Resolve
+    {
+      "hash": "154baf1cf6ed99ae5b2849f512ea4d58dbbf199e",
+      "lastChecked": 1586377071253733703
+    }
+    $ grpcurl -plaintext -d '{"hash": "154baf1cf6ed99ae5b2849f512ea4d58dbbf199e", "path": "//README"}' 127.0.0.1:4200 depotview.DepotView.Read
+    {
+      "data": "SFNDbG...."
+    }
+
diff --git a/devtools/depotview/main.go b/devtools/depotview/main.go
new file mode 100644
index 0000000..7b4aed4
--- /dev/null
+++ b/devtools/depotview/main.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"flag"
+
+	"code.hackerspace.pl/hscloud/go/mirko"
+	"github.com/golang/glog"
+
+	pb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
+	"code.hackerspace.pl/hscloud/devtools/depotview/service"
+)
+
+var (
+	flagRemote = "https://gerrit.hackerspace.pl/hscloud"
+)
+
+func main() {
+	flag.StringVar(&flagRemote, "git_remote", flagRemote, "Address of Git repository to serve")
+	flag.Parse()
+
+	m := mirko.New()
+	if err := m.Listen(); err != nil {
+		glog.Exitf("Listen(): %v", err)
+	}
+
+	s := service.New(flagRemote)
+	pb.RegisterDepotViewServer(m.GRPC(), s)
+
+	if err := m.Serve(); err != nil {
+		glog.Exitf("Serve(): %v", err)
+	}
+
+	<-m.Done()
+}
diff --git a/devtools/depotview/proto/BUILD.bazel b/devtools/depotview/proto/BUILD.bazel
new file mode 100644
index 0000000..47df920
--- /dev/null
+++ b/devtools/depotview/proto/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "proto_proto",
+    srcs = ["depotview.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "proto_go_proto",
+    compilers = ["@io_bazel_rules_go//proto:go_grpc"],
+    importpath = "code.hackerspace.pl/hscloud/devtools/depotview/proto",
+    proto = ":proto_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    embed = [":proto_go_proto"],
+    importpath = "code.hackerspace.pl/hscloud/devtools/depotview/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/devtools/depotview/proto/depotview.proto b/devtools/depotview/proto/depotview.proto
new file mode 100644
index 0000000..b948fae
--- /dev/null
+++ b/devtools/depotview/proto/depotview.proto
@@ -0,0 +1,58 @@
+syntax = "proto3";
+package depotview;
+option go_package = "code.hackerspace.pl/hscloud/devtools/depotview/proto";
+
+service DepotView {
+    // Resolve a git branch/tag/ref... into a commit hash.
+    rpc Resolve(ResolveRequest) returns (ResolveResponse);
+
+    // Resolve a gerrit change number into a git commit hash.
+    rpc ResolveGerritChange(ResolveGerritChangeRequest) returns (ResolveGerritChangeResponse);
+        
+    // Minimal file access API. It kinda stinks.
+    rpc Stat(StatRequest) returns (StatResponse);
+    rpc Read(ReadRequest) returns (stream ReadResponse);
+}
+
+message ResolveRequest {
+    string ref = 1;
+}
+
+message ResolveResponse {
+    string hash = 1;
+    int64 last_checked = 2;
+}
+
+message ResolveGerritChangeRequest {
+    int64 change = 1;
+}
+
+message ResolveGerritChangeResponse {
+    string hash = 1;
+    int64 last_checked = 2;
+}
+
+message StatRequest {
+    string hash = 1;
+    string path = 2;
+}
+
+message StatResponse {
+    enum Type {
+        TYPE_INVALID = 0;
+        TYPE_NOT_PRESENT = 1;
+        TYPE_FILE = 2;
+        TYPE_DIRECTORY = 3;
+    };
+    Type type = 1;
+}
+
+message ReadRequest {
+    string hash = 1;
+    string path = 2;
+}
+
+message ReadResponse {
+    // Chunk of data. Empty once everything has been sent over.
+    bytes data = 1;
+}
diff --git a/devtools/depotview/service/BUILD.bazel b/devtools/depotview/service/BUILD.bazel
new file mode 100644
index 0000000..056ec30
--- /dev/null
+++ b/devtools/depotview/service/BUILD.bazel
@@ -0,0 +1,31 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "gerrit.go",
+        "service.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/devtools/depotview/service",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//devtools/depotview/proto:go_default_library",
+        "@com_github_go_git_go_git_v5//:go_default_library",
+        "@com_github_go_git_go_git_v5//config:go_default_library",
+        "@com_github_go_git_go_git_v5//plumbing:go_default_library",
+        "@com_github_go_git_go_git_v5//plumbing/filemode:go_default_library",
+        "@com_github_go_git_go_git_v5//plumbing/object:go_default_library",
+        "@com_github_go_git_go_git_v5//storage:go_default_library",
+        "@com_github_go_git_go_git_v5//storage/memory:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+        "@org_golang_google_grpc//codes:go_default_library",
+        "@org_golang_google_grpc//status:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["service_test.go"],
+    embed = [":go_default_library"],
+    deps = ["//devtools/depotview/proto:go_default_library"],
+)
diff --git a/devtools/depotview/service/gerrit.go b/devtools/depotview/service/gerrit.go
new file mode 100644
index 0000000..7dab9e6
--- /dev/null
+++ b/devtools/depotview/service/gerrit.go
@@ -0,0 +1,56 @@
+package service
+
+import (
+	"strconv"
+	"strings"
+)
+
+type gerritMeta struct {
+	patchSet int64
+	changeId string
+	commit   string
+}
+
+// parseGerritMetadata takes a NoteDB metadata entry and extracts info from it.
+func parseGerritMetadata(messages []string) *gerritMeta {
+	meta := &gerritMeta{}
+
+	for _, message := range messages {
+		for _, line := range strings.Split(message, "\n") {
+			line = strings.TrimSpace(line)
+			if len(line) == 0 {
+				continue
+			}
+
+			parts := strings.SplitN(line, ":", 2)
+			if len(parts) < 2 {
+				continue
+			}
+			k, v := parts[0], strings.TrimSpace(parts[1])
+
+			switch k {
+			case "Patch-set":
+				n, err := strconv.ParseInt(v, 10, 64)
+				if err != nil {
+					continue
+				}
+				meta.patchSet = n
+			case "Change-id":
+				meta.changeId = v
+			case "Commit":
+				meta.commit = v
+			}
+		}
+	}
+
+	if meta.patchSet == 0 {
+		return nil
+	}
+	if meta.changeId == "" {
+		return nil
+	}
+	if meta.commit == "" {
+		return nil
+	}
+	return meta
+}
diff --git a/devtools/depotview/service/service.go b/devtools/depotview/service/service.go
new file mode 100644
index 0000000..fad2029
--- /dev/null
+++ b/devtools/depotview/service/service.go
@@ -0,0 +1,278 @@
+package service
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/golang/glog"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	git "github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/config"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/filemode"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/go-git/go-git/v5/storage"
+	"github.com/go-git/go-git/v5/storage/memory"
+
+	pb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
+)
+
+var (
+	reHash = regexp.MustCompile(`[a-f0-9]{40,64}`)
+)
+
+type Service struct {
+	remote string
+	storer storage.Storer
+
+	mu        sync.Mutex
+	repo      *git.Repository
+	lastFetch time.Time
+}
+
+func New(remote string) *Service {
+	return &Service{
+		remote: remote,
+		storer: memory.NewStorage(),
+	}
+}
+
+func (s *Service) ensureRepo(ctx context.Context) error {
+	// Clone repository if necessary.
+	if s.repo == nil {
+		repo, err := git.CloneContext(ctx, s.storer, nil, &git.CloneOptions{
+			URL: s.remote,
+		})
+		if err != nil {
+			glog.Errorf("Clone(%q): %v", s.remote, err)
+			return status.Error(codes.Unavailable, "could not clone repository")
+		}
+		s.repo = repo
+	}
+
+	// Fetch if necessary.
+	if time.Since(s.lastFetch) > 10*time.Second {
+		glog.Infof("Fetching...")
+		err := s.repo.FetchContext(ctx, &git.FetchOptions{
+			RefSpecs: []config.RefSpec{
+				config.RefSpec("+refs/heads/*:refs/remotes/origin/*"),
+				config.RefSpec("+refs/changes/*:refs/changes/*"),
+			},
+			Force: true,
+		})
+		if err != nil && err != git.NoErrAlreadyUpToDate {
+			glog.Errorf("Fetch(): %v", err)
+		} else {
+			s.lastFetch = time.Now()
+		}
+
+	}
+
+	return nil
+}
+
+func (s *Service) Resolve(ctx context.Context, req *pb.ResolveRequest) (*pb.ResolveResponse, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if req.Ref == "" {
+		return nil, status.Error(codes.InvalidArgument, "ref must be set")
+	}
+
+	if err := s.ensureRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	h, err := s.repo.ResolveRevision(plumbing.Revision(req.Ref))
+	switch {
+	case err == plumbing.ErrReferenceNotFound:
+		return &pb.ResolveResponse{Hash: "", LastChecked: s.lastFetch.UnixNano()}, nil
+	case err != nil:
+		return nil, status.Errorf(codes.Unavailable, "git resolve error: %v", err)
+	default:
+		return &pb.ResolveResponse{Hash: h.String(), LastChecked: s.lastFetch.UnixNano()}, nil
+	}
+}
+
+func (s *Service) ResolveGerritChange(ctx context.Context, req *pb.ResolveGerritChangeRequest) (*pb.ResolveGerritChangeResponse, error) {
+	if err := s.ensureRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	// I'm totally guessing this, from these examples:
+	//    refs/changes/03/3/meta
+	//    refs/changes/77/77/meta
+	//    refs/changes/47/247/meta
+	// etc...
+	shard := fmt.Sprintf("%02d", req.Change%100)
+	metaRef := fmt.Sprintf("refs/changes/%s/%d/meta", shard, req.Change)
+
+	h, err := s.repo.ResolveRevision(plumbing.Revision(metaRef))
+	switch {
+	case err == plumbing.ErrReferenceNotFound:
+		return &pb.ResolveGerritChangeResponse{Hash: "", LastChecked: s.lastFetch.UnixNano()}, nil
+	case err != nil:
+		return nil, status.Errorf(codes.Unavailable, "git metadata resolve error: %v", err)
+	}
+
+	c, err := s.repo.CommitObject(*h)
+	if err != nil {
+		return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
+	}
+
+	var messages []string
+	for {
+		messages = append([]string{c.Message}, messages...)
+
+		if len(c.ParentHashes) != 1 {
+			break
+		}
+
+		c, err = s.repo.CommitObject(c.ParentHashes[0])
+		if err != nil {
+			return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
+		}
+	}
+
+	meta := parseGerritMetadata(messages)
+	if meta == nil {
+		return nil, status.Errorf(codes.Internal, "could not parse gerrit metadata for ref %q", metaRef)
+	}
+	return &pb.ResolveGerritChangeResponse{Hash: meta.commit, LastChecked: s.lastFetch.UnixNano()}, nil
+}
+
+func (s *Service) getFile(ctx context.Context, hash, path string, notFoundOkay bool) (*object.File, error) {
+	if !reHash.MatchString(hash) {
+		return nil, status.Error(codes.InvalidArgument, "hash must be valid full git hash string")
+	}
+	if path == "" {
+		return nil, status.Error(codes.InvalidArgument, "path must be set")
+	}
+
+	path = pathNormalize(path)
+	if path == "" {
+		return nil, status.Error(codes.InvalidArgument, "path must be a valid unix or depot-style path")
+	}
+
+	if err := s.ensureRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	c, err := s.repo.CommitObject(plumbing.NewHash(hash))
+	switch {
+	case err == plumbing.ErrObjectNotFound:
+		return nil, status.Error(codes.NotFound, "hash not found")
+	case err != nil:
+		return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
+	}
+
+	file, err := c.File(path)
+	switch {
+	case err == object.ErrFileNotFound && !notFoundOkay:
+		return nil, status.Error(codes.NotFound, "file not found")
+	case err == object.ErrFileNotFound && notFoundOkay:
+		return nil, nil
+	case err != nil:
+		return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
+	}
+
+	return file, nil
+}
+
+func (s *Service) Stat(ctx context.Context, req *pb.StatRequest) (*pb.StatResponse, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	file, err := s.getFile(ctx, req.Hash, req.Path, true)
+	if err != nil {
+		return nil, err
+	}
+
+	if file == nil {
+		return &pb.StatResponse{Type: pb.StatResponse_TYPE_NOT_PRESENT}, nil
+	}
+
+	switch {
+	case file.Mode == filemode.Dir:
+		return &pb.StatResponse{Type: pb.StatResponse_TYPE_DIRECTORY}, nil
+	case file.Mode.IsFile():
+		return &pb.StatResponse{Type: pb.StatResponse_TYPE_FILE}, nil
+	default:
+		return nil, status.Errorf(codes.Unimplemented, "unknown file type %o", file.Mode)
+	}
+}
+
+func (s *Service) Read(req *pb.ReadRequest, srv pb.DepotView_ReadServer) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	ctx := srv.Context()
+
+	file, err := s.getFile(ctx, req.Hash, req.Path, false)
+	if err != nil {
+		return err
+	}
+
+	reader, err := file.Reader()
+	if err != nil {
+		return status.Errorf(codes.Unavailable, "file read error: %v", err)
+	}
+	defer reader.Close()
+
+	for {
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+
+		// 1 MB read
+		chunk := make([]byte, 16*1024)
+		n, err := reader.Read(chunk)
+		switch {
+		case err == io.EOF:
+			n = 0
+		case err != nil:
+			return status.Errorf(codes.Unavailable, "file read error: %v", err)
+		}
+
+		err = srv.Send(&pb.ReadResponse{Data: chunk[:n]})
+		if err != nil {
+			return err
+		}
+
+		if n == 0 {
+			break
+		}
+	}
+
+	return nil
+}
+
+func pathNormalize(path string) string {
+	leadingSlashes := 0
+	for _, c := range path {
+		if c != '/' {
+			break
+		}
+		leadingSlashes += 1
+	}
+
+	// Only foo/bar, /foo/bar, and //foo/bar paths allowed.
+	if leadingSlashes > 2 {
+		return ""
+	}
+	path = path[leadingSlashes:]
+
+	// No trailing slashes allowed.
+	if strings.HasSuffix(path, "/") {
+		return ""
+	}
+
+	return path
+}
diff --git a/devtools/depotview/service/service_test.go b/devtools/depotview/service/service_test.go
new file mode 100644
index 0000000..8ea0764
--- /dev/null
+++ b/devtools/depotview/service/service_test.go
@@ -0,0 +1,32 @@
+package service
+
+import (
+	"context"
+	"testing"
+
+	pb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
+)
+
+func TestIntegration(t *testing.T) {
+	// TODO(q3k); bring up fake git
+	s := New("https://gerrit.hackerspace.pl/hscloud")
+	ctx := context.Background()
+
+	res, err := s.Resolve(ctx, &pb.ResolveRequest{Ref: "master"})
+	if err != nil {
+		t.Fatalf("Resolve(master): %v", err)
+	}
+
+	if len(res.Hash) != 40 {
+		t.Fatalf("Resolve returned odd hash: %q", res.Hash)
+	}
+
+	res2, err := s.Stat(ctx, &pb.StatRequest{Hash: res.Hash, Path: "//WORKSPACE"})
+	if err != nil {
+		t.Fatalf("Stat(//WORKSPACE): %v", err)
+	}
+
+	if want, got := pb.StatResponse_TYPE_FILE, res2.Type; want != got {
+		t.Fatalf("Stat(//WORKSPACE): got %v, want %v", got, want)
+	}
+}
diff --git a/devtools/hackdoc/BUILD.bazel b/devtools/hackdoc/BUILD.bazel
new file mode 100644
index 0000000..5536760
--- /dev/null
+++ b/devtools/hackdoc/BUILD.bazel
@@ -0,0 +1,55 @@
+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_library(
+    name = "go_default_library",
+    srcs = [
+        "helpers.go",
+        "main.go",
+        "markdown.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/devtools/hackdoc",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//devtools/depotview/proto:go_default_library",
+        "//devtools/hackdoc/config:go_default_library",
+        "//devtools/hackdoc/source:go_default_library",
+        "//go/mirko:go_default_library",
+        "//go/pki:go_default_library",
+        "@com_github_gabriel_vasile_mimetype//:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+        "@in_gopkg_russross_blackfriday_v2//:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "hackdoc",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
+
+container_layer(
+    name = "layer_bin",
+    files = [
+        ":hackdoc",
+    ],
+    directory = "/devtools/",
+)
+
+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 = "devtools/hackdoc",
+    tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+)
diff --git a/devtools/hackdoc/README.md b/devtools/hackdoc/README.md
new file mode 100644
index 0000000..3226cf2
--- /dev/null
+++ b/devtools/hackdoc/README.md
@@ -0,0 +1,23 @@
+Hackdoc
+=======
+
+Hackdoc is a tool to automatically serve documentation based on a checkout of the [hscloud](/) source.
+
+Usage
+-----
+
+Any Markdown submitted to hscloud is visible via hackdoc. Simply go to https://hackdoc.hackerspace.pl/path/to/markdown.md to see it rendered.
+
+You can pass a `?ref=foo` URL parameter to a hackdoc URL to get it to render a particular vesrion of the hscloud monorepo. For example:
+
+- https://hackdoc.hackerspace.pl/?ref=master for the `master` branch
+- https://hackdoc.hackerspace.pl/?ref=change/249 for the the source code at change '249'
+
+Local Rendering
+---------------
+
+To run hackdoc locally on a filesystem checkout (ie. when working on docs, templates, or hackdoc itself), run:
+
+     bazel run //devtools/hackdoc  -- -hspki_disable -docroot /path/to/hscloud
+
+The output log should tell you where hackdoc just started listening at. Currently this is `127.0.0.1:8080` by default. You can change this by passing a `-listen` flag, eg. `-listen 127.0.0.1:4242`.
diff --git a/devtools/hackdoc/config/BUILD.bazel b/devtools/hackdoc/config/BUILD.bazel
new file mode 100644
index 0000000..c5052c7
--- /dev/null
+++ b/devtools/hackdoc/config/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["config.go"],
+    importpath = "code.hackerspace.pl/hscloud/devtools/hackdoc/config",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//devtools/hackdoc/source:go_default_library",
+        "@com_github_burntsushi_toml//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["config_test.go"],
+    embed = [":go_default_library"],
+    deps = ["@com_github_go_test_deep//:go_default_library"],
+)
diff --git a/devtools/hackdoc/config/config.go b/devtools/hackdoc/config/config.go
new file mode 100644
index 0000000..aba384b
--- /dev/null
+++ b/devtools/hackdoc/config/config.go
@@ -0,0 +1,139 @@
+package config
+
+import (
+	"context"
+	"fmt"
+	"html/template"
+	"strings"
+
+	"github.com/BurntSushi/toml"
+
+	"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
+)
+
+// Config is a configuration concerning a given path of the source. It is built
+// from files present in the source, and from global configuration.
+type Config struct {
+	// DefaultIndex is the filenames that should attempt to be rendered if no exact path is given
+	DefaultIndex []string
+	// Templates are the templates available to render markdown files, keyed by template name.
+	Templates map[string]*template.Template
+
+	// Errors that occured while building this config (due to config file errors, etc).
+	Errors map[string]error
+}
+
+type configToml struct {
+	DefaultIndex []string                       `toml:"default_index"`
+	Templates    map[string]*configTomlTemplate `toml:"template"`
+}
+
+type configTomlTemplate struct {
+	Sources []string `toml:"sources"`
+}
+
+func parseToml(data []byte) (*configToml, error) {
+	var c configToml
+	err := toml.Unmarshal(data, &c)
+	if err != nil {
+		return nil, err
+	}
+	if c.Templates == nil {
+		c.Templates = make(map[string]*configTomlTemplate)
+	}
+	return &c, nil
+}
+
+func configFileLocations(path string) []string {
+	// Support for unix-style filesystem prefix (/foo/bar/baz) and
+	// perforce-depot-style prefix (//foo/bar/baz).
+	// Also support relative paths.
+	pathTrimmed := strings.TrimLeft(path, "/")
+	prefixLen := len(path) - len(pathTrimmed)
+	prefix := path[:prefixLen]
+	path = pathTrimmed
+	if len(prefix) > 2 {
+		return nil
+	}
+
+	// Turn path into possible directory names, including root.
+	path = strings.Trim(path, "/")
+	parts := strings.Split(path, "/")
+	if parts[0] != "" {
+		parts = append([]string{""}, parts...)
+	}
+
+	locations := []string{}
+	for i, _ := range parts {
+		p := strings.Join(parts[:i+1], "/")
+		p += "/hackdoc.toml"
+		p = prefix + strings.Trim(p, "/")
+		locations = append(locations, p)
+	}
+	return locations
+}
+
+func ForPath(ctx context.Context, s source.Source, path string) (*Config, error) {
+	if path != "//" {
+		path = strings.TrimRight(path, "/")
+	}
+
+	cfg := &Config{
+		Templates: make(map[string]*template.Template),
+		Errors:    make(map[string]error),
+	}
+
+	tomlPaths := configFileLocations(path)
+	for _, p := range tomlPaths {
+		file, err := s.IsFile(ctx, p)
+		if err != nil {
+			return nil, fmt.Errorf("IsFile(%q): %w", path, err)
+		}
+		if !file {
+			continue
+		}
+		data, err := s.ReadFile(ctx, p)
+		if err != nil {
+			return nil, fmt.Errorf("ReadFile(%q): %w", path, err)
+		}
+
+		c, err := parseToml(data)
+		if err != nil {
+			cfg.Errors[p] = err
+			continue
+		}
+
+		err = cfg.updateFromToml(ctx, p, s, c)
+		if err != nil {
+			return nil, fmt.Errorf("updating from %q: %w", p, err)
+		}
+	}
+
+	return cfg, nil
+}
+
+func (c *Config) updateFromToml(ctx context.Context, p string, s source.Source, t *configToml) error {
+	if t.DefaultIndex != nil {
+		c.DefaultIndex = t.DefaultIndex
+	}
+
+	for k, v := range t.Templates {
+		tmpl := template.New(k)
+
+		for _, source := range v.Sources {
+			data, err := s.ReadFile(ctx, source)
+			if err != nil {
+				c.Errors[p] = fmt.Errorf("reading template file %q: %w", source, err)
+				return nil
+			}
+			tmpl, err = tmpl.Parse(string(data))
+			if err != nil {
+				c.Errors[p] = fmt.Errorf("parsing template file %q: %w", source, err)
+				return nil
+			}
+		}
+		c.Templates[k] = tmpl
+	}
+
+	return nil
+}
diff --git a/devtools/hackdoc/config/config_test.go b/devtools/hackdoc/config/config_test.go
new file mode 100644
index 0000000..ba542fe
--- /dev/null
+++ b/devtools/hackdoc/config/config_test.go
@@ -0,0 +1,99 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/go-test/deep"
+)
+
+func TestParse(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		data string
+		want *configToml
+	}{
+		{
+			name: "normal config",
+			data: `
+				default_index = ["foo.md", "bar.md"]
+				[template.default]
+				sources = ["hackdoc/bar.html", "hackdoc/baz.html"]
+				[template.foo]
+				sources = ["foo/bar.html", "foo/baz.html"]
+			`,
+			want: &configToml{
+				DefaultIndex: []string{"foo.md", "bar.md"},
+				Templates: map[string]*configTomlTemplate{
+					"default": &configTomlTemplate{
+						Sources: []string{"hackdoc/bar.html", "hackdoc/baz.html"},
+					},
+					"foo": &configTomlTemplate{
+						Sources: []string{"foo/bar.html", "foo/baz.html"},
+					},
+				},
+			},
+		}, {
+			name: "empty config",
+			data: "",
+			want: &configToml{
+				DefaultIndex: nil,
+				Templates:    map[string]*configTomlTemplate{},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			got, err := parseToml([]byte(test.data))
+			if err != nil {
+				t.Fatalf("could not parse config: %w", err)
+			}
+			if diff := deep.Equal(test.want, got); diff != nil {
+				t.Fatal(diff)
+			}
+		})
+	}
+}
+
+func TestLocations(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		path string
+		want []string
+	}{
+		{
+			name: "perforce-style path",
+			path: "//foo/bar/baz",
+			want: []string{"//hackdoc.toml", "//foo/hackdoc.toml", "//foo/bar/hackdoc.toml", "//foo/bar/baz/hackdoc.toml"},
+		}, {
+			name: "unix-style path",
+			path: "/foo/bar/baz",
+			want: []string{"/hackdoc.toml", "/foo/hackdoc.toml", "/foo/bar/hackdoc.toml", "/foo/bar/baz/hackdoc.toml"},
+		}, {
+			name: "relative-style path",
+			path: "foo/bar/baz",
+			want: []string{"hackdoc.toml", "foo/hackdoc.toml", "foo/bar/hackdoc.toml", "foo/bar/baz/hackdoc.toml"},
+		}, {
+			name: "root perforce-style path",
+			path: "//",
+			want: []string{"//hackdoc.toml"},
+		}, {
+			name: "root unix-style path",
+			path: "/",
+			want: []string{"/hackdoc.toml"},
+		}, {
+			name: "empty path",
+			path: "",
+			want: []string{"hackdoc.toml"},
+		}, {
+			name: "weird path",
+			path: "///what/is///this///",
+			want: nil,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			got := configFileLocations(test.path)
+			if diff := deep.Equal(test.want, got); diff != nil {
+				t.Fatal(diff)
+			}
+		})
+	}
+}
diff --git a/devtools/hackdoc/helpers.go b/devtools/hackdoc/helpers.go
new file mode 100644
index 0000000..a2bd93a
--- /dev/null
+++ b/devtools/hackdoc/helpers.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/golang/glog"
+)
+
+func handle404(w http.ResponseWriter, r *http.Request) {
+	logRequest(w, r, "404")
+	w.WriteHeader(http.StatusNotFound)
+	fmt.Fprintf(w, "404!\n")
+}
+
+func handle500(w http.ResponseWriter, r *http.Request) {
+	logRequest(w, r, "500")
+	w.WriteHeader(http.StatusNotFound)
+	fmt.Fprintf(w, "500 :(\n")
+}
+
+func logRequest(w http.ResponseWriter, r *http.Request, format string, args ...interface{}) {
+	result := fmt.Sprintf(format, args...)
+	glog.Infof("result: %s, remote: %q, ua: %q, referrer: %q, host: %q path: %q", result, r.RemoteAddr, r.Header.Get("User-Agent"), r.Header.Get("Referrer"), r.Host, r.URL.Path)
+}
diff --git a/devtools/hackdoc/main.go b/devtools/hackdoc/main.go
new file mode 100644
index 0000000..aae850f
--- /dev/null
+++ b/devtools/hackdoc/main.go
@@ -0,0 +1,261 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"net/http"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	"code.hackerspace.pl/hscloud/go/mirko"
+	"code.hackerspace.pl/hscloud/go/pki"
+	"github.com/golang/glog"
+	"google.golang.org/grpc"
+
+	dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
+	"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
+	"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
+)
+
+var (
+	flagListen              = "127.0.0.1:8080"
+	flagDocRoot             = ""
+	flagDepotViewAddress    = ""
+	flagHackdocURL          = ""
+	flagGitwebDefaultBranch = "master"
+
+	rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`)
+)
+
+func init() {
+	flag.Set("logtostderr", "true")
+}
+
+func main() {
+	flag.StringVar(&flagListen, "pub_listen", flagListen, "Address to listen on for HTTP traffic")
+	flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents. Either this or depotview must be set")
+	flag.StringVar(&flagDepotViewAddress, "depotview", flagDepotViewAddress, "gRPC endpoint of depotview to serve from Git. Either this or docroot must be set")
+	flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes")
+	flag.StringVar(&source.FlagGitwebURLPattern, "gitweb_url_pattern", source.FlagGitwebURLPattern, "Pattern to sprintf to for URL for viewing a file in Git. First string is ref/rev, second is bare file path (sans //)")
+	flag.StringVar(&flagGitwebDefaultBranch, "gitweb_default_rev", flagGitwebDefaultBranch, "Default Git rev to render/link to")
+	flag.Parse()
+
+	if flagHackdocURL == "" {
+		flagHackdocURL = fmt.Sprintf("http://%s", flagListen)
+	}
+
+	if flagDocRoot == "" && flagDepotViewAddress == "" {
+		glog.Errorf("Either -docroot or -depotview must be set")
+	}
+	if flagDocRoot != "" && flagDepotViewAddress != "" {
+		glog.Errorf("Only one of -docroot or -depotview must be set")
+	}
+
+	m := mirko.New()
+	if err := m.Listen(); err != nil {
+		glog.Exitf("Listen(): %v", err)
+	}
+
+	var s *service
+	if flagDocRoot != "" {
+		path, err := filepath.Abs(flagDocRoot)
+		if err != nil {
+			glog.Exitf("Could not dereference path %q: %w", path, err)
+		}
+		glog.Infof("Starting in docroot mode for %q -> %q", flagDocRoot, path)
+
+		s = &service{
+			source: source.NewSingleRefProvider(source.NewLocal(path)),
+		}
+	} else {
+		glog.Infof("Starting in depotview mode (server %q)", flagDepotViewAddress)
+		conn, err := grpc.Dial(flagDepotViewAddress, pki.WithClientHSPKI())
+		if err != nil {
+			glog.Exitf("grpc.Dial(%q): %v", flagDepotViewAddress, err)
+		}
+		stub := dvpb.NewDepotViewClient(conn)
+		s = &service{
+			source: source.NewDepotView(stub),
+		}
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", s.handler)
+	srv := &http.Server{Addr: flagListen, Handler: mux}
+
+	glog.Infof("Listening on %q...", flagListen)
+	go func() {
+		if err := srv.ListenAndServe(); err != nil {
+			glog.Error(err)
+		}
+	}()
+
+	<-m.Done()
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	srv.Shutdown(ctx)
+
+}
+
+type service struct {
+	source source.SourceProvider
+}
+
+func (s *service) handler(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "GET" && r.Method != "HEAD" {
+		w.WriteHeader(http.StatusMethodNotAllowed)
+		fmt.Fprintf(w, "method not allowed")
+		return
+	}
+
+	ref := r.URL.Query().Get("ref")
+	if ref == "" {
+		ref = flagGitwebDefaultBranch
+	}
+
+	ctx := r.Context()
+	source, err := s.source.Source(ctx, ref)
+	switch {
+	case err != nil:
+		glog.Errorf("Source(%q): %v", ref, err)
+		handle500(w, r)
+		return
+	case source == nil:
+		handle404(w, r)
+		return
+	}
+
+	path := r.URL.Path
+
+	if match := rePagePath.FindStringSubmatch(path); match != nil {
+		req := &request{
+			w:      w,
+			r:      r,
+			ctx:    r.Context(),
+			ref:    ref,
+			source: source,
+		}
+		req.handlePage(match[1])
+		return
+	}
+	handle404(w, r)
+}
+
+type request struct {
+	w   http.ResponseWriter
+	r   *http.Request
+	ctx context.Context
+
+	ref    string
+	source source.Source
+	// rpath is the path requested by the client
+	rpath string
+}
+
+func (r *request) handle500() {
+	handle500(r.w, r.r)
+}
+
+func (r *request) handle404() {
+	handle404(r.w, r.r)
+}
+
+func (r *request) logRequest(format string, args ...interface{}) {
+	logRequest(r.w, r.r, format, args...)
+}
+
+func urlPathToDepotPath(url string) string {
+	// Sanitize request.
+	parts := strings.Split(url, "/")
+	for i, p := range parts {
+		// Allow last part to be "", ie, for a path to end in /
+		if p == "" {
+			if i != len(parts)-1 {
+				return ""
+			}
+		}
+
+		// net/http sanitizes this anyway, but we better be sure.
+		if p == "." || p == ".." {
+			return ""
+		}
+	}
+	path := "//" + strings.Join(parts, "/")
+
+	return path
+}
+
+func (r *request) handlePageAuto(dirpath string) {
+	cfg, err := config.ForPath(r.ctx, r.source, dirpath)
+	if err != nil {
+		glog.Errorf("could not get config for path %q: %w", dirpath, err)
+		r.handle500()
+		return
+	}
+	for _, f := range cfg.DefaultIndex {
+		fpath := dirpath + f
+		file, err := r.source.IsFile(r.ctx, fpath)
+		if err != nil {
+			glog.Errorf("IsFile(%q): %w", fpath, err)
+			r.handle500()
+			return
+		}
+
+		if file {
+			http.Redirect(r.w, r.r, "/"+fpath, 302)
+			return
+		}
+	}
+	r.handle404()
+}
+
+func (r *request) handlePage(page string) {
+	r.rpath = urlPathToDepotPath(page)
+
+	if strings.HasSuffix(r.rpath, "/") {
+		// Directory path given, autoresolve.
+		dirpath := r.rpath
+		if r.rpath != "//" {
+			dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
+		}
+		r.handlePageAuto(dirpath)
+		return
+	}
+
+	// Otherwise, try loading the file.
+	file, err := r.source.IsFile(r.ctx, r.rpath)
+	if err != nil {
+		glog.Errorf("IsFile(%q): %w", r.rpath, err)
+		r.handle500()
+		return
+	}
+
+	// File exists, render that.
+	if file {
+		parts := strings.Split(r.rpath, "/")
+		dirpath := strings.Join(parts[:(len(parts)-1)], "/")
+		// TODO(q3k): figure out this hack, hopefully by implementing a real path type
+		if dirpath == "/" {
+			dirpath = "//"
+		}
+
+		cfg, err := config.ForPath(r.ctx, r.source, dirpath)
+		if err != nil {
+			glog.Errorf("could not get config for path %q: %w", dirpath, err)
+			r.handle500()
+			return
+		}
+		r.handleFile(r.rpath, cfg)
+		return
+	}
+
+	// Otherwise assume directory, try all posibilities.
+	dirpath := r.rpath
+	if r.rpath != "//" {
+		dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
+	}
+	r.handlePageAuto(dirpath)
+}
diff --git a/devtools/hackdoc/markdown.go b/devtools/hackdoc/markdown.go
new file mode 100644
index 0000000..06e2c6e
--- /dev/null
+++ b/devtools/hackdoc/markdown.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+	"bytes"
+	"html/template"
+	"net/url"
+	"strings"
+
+	"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
+
+	"github.com/gabriel-vasile/mimetype"
+	"github.com/golang/glog"
+	"gopkg.in/russross/blackfriday.v2"
+)
+
+// renderMarkdown renders markdown to HTML, replacing all relative (intra-hackdoc) links with version that have ref set.
+func renderMarkdown(input []byte, ref string) []byte {
+	r := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
+		Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
+	})
+
+	parser := blackfriday.New(blackfriday.WithRenderer(r), blackfriday.WithExtensions(blackfriday.CommonExtensions))
+	ast := parser.Parse(input)
+
+	var buf bytes.Buffer
+	buf.Write([]byte(`<div class="toc"><h1>Page Contents</h1>`))
+	r.RenderHeader(&buf, ast)
+	buf.Write([]byte(`</div><div class="content">`))
+	ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
+		if ref != "" && entering && node.Type == blackfriday.Link {
+			dest := string(node.Destination)
+			u, err := url.Parse(dest)
+			if err == nil && !u.IsAbs() {
+				q := u.Query()
+				q["ref"] = []string{ref}
+				u.RawQuery = q.Encode()
+				node.Destination = []byte(u.String())
+				glog.Infof("link fix %q -> %q", dest, u.String())
+			}
+		}
+		return r.RenderNode(&buf, node, entering)
+	})
+	buf.Write([]byte(`</div>`))
+	r.RenderFooter(&buf, ast)
+	return buf.Bytes()
+}
+
+type pathPart struct {
+	Label string
+	Path  string
+}
+
+func (r *request) handleFile(path string, cfg *config.Config) {
+	data, err := r.source.ReadFile(r.ctx, path)
+	if err != nil {
+		glog.Errorf("ReadFile(%q): %w", err)
+		r.handle500()
+		return
+	}
+
+	// TODO(q3k): do MIME detection instead.
+	if strings.HasSuffix(path, ".md") {
+		rendered := renderMarkdown([]byte(data), r.ref)
+
+		r.logRequest("serving markdown at %s, cfg %+v", path, cfg)
+
+		// TODO(q3k): allow markdown files to override which template to load
+		tmpl, ok := cfg.Templates["default"]
+		if !ok {
+			glog.Errorf("No default template found for %s", path)
+			// TODO(q3k): implement fallback template
+			r.w.Write(rendered)
+			return
+		}
+
+		pathInDepot := strings.TrimPrefix(path, "//")
+		pathParts := []pathPart{
+			{Label: "//", Path: "/"},
+		}
+		parts := strings.Split(pathInDepot, "/")
+		fullPath := ""
+		for i, p := range parts {
+			label := p
+			if i != len(parts)-1 {
+				label = label + "/"
+			}
+			fullPath += "/" + p
+			pathParts = append(pathParts, pathPart{Label: label, Path: fullPath})
+		}
+
+		vars := map[string]interface{}{
+			"Rendered":    template.HTML(rendered),
+			"Title":       path,
+			"Path":        path,
+			"PathInDepot": pathInDepot,
+			"PathParts":   pathParts,
+			"HackdocURL":  flagHackdocURL,
+			"WebLinks":    r.source.WebLinks(pathInDepot),
+		}
+		err = tmpl.Execute(r.w, vars)
+		if err != nil {
+			glog.Errorf("Could not execute template for %s: %v", err)
+		}
+
+		return
+	}
+
+	// Just serve the file.
+	mime := mimetype.Detect(data)
+	r.w.Header().Set("Content-Type", mime.String())
+	r.w.Write(data)
+}
diff --git a/devtools/hackdoc/source/BUILD.bazel b/devtools/hackdoc/source/BUILD.bazel
new file mode 100644
index 0000000..f7f09c6
--- /dev/null
+++ b/devtools/hackdoc/source/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "source.go",
+        "source_depotview.go",
+        "source_local.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/devtools/hackdoc/source",
+    visibility = ["//visibility:public"],
+    deps = ["//devtools/depotview/proto:go_default_library"],
+)
diff --git a/devtools/hackdoc/source/source.go b/devtools/hackdoc/source/source.go
new file mode 100644
index 0000000..73d8990
--- /dev/null
+++ b/devtools/hackdoc/source/source.go
@@ -0,0 +1,36 @@
+package source
+
+import "context"
+
+var (
+	FlagGitwebURLPattern = "https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/%s/%s"
+)
+
+type Source interface {
+	IsFile(ctx context.Context, path string) (bool, error)
+	ReadFile(ctx context.Context, path string) ([]byte, error)
+	IsDirectory(ctx context.Context, path string) (bool, error)
+	WebLinks(fpath string) []WebLink
+}
+
+type WebLink struct {
+	Kind      string
+	LinkLabel string
+	LinkURL   string
+}
+
+type SourceProvider interface {
+	Source(ctx context.Context, rev string) (Source, error)
+}
+
+type singleRefProvider struct {
+	source Source
+}
+
+func (s *singleRefProvider) Source(ctx context.Context, rev string) (Source, error) {
+	return s.source, nil
+}
+
+func NewSingleRefProvider(s Source) SourceProvider {
+	return &singleRefProvider{s}
+}
diff --git a/devtools/hackdoc/source/source_depotview.go b/devtools/hackdoc/source/source_depotview.go
new file mode 100644
index 0000000..6a256be
--- /dev/null
+++ b/devtools/hackdoc/source/source_depotview.go
@@ -0,0 +1,126 @@
+package source
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
+)
+
+type DepotViewSourceProvider struct {
+	stub dvpb.DepotViewClient
+}
+
+func NewDepotView(stub dvpb.DepotViewClient) SourceProvider {
+	return &DepotViewSourceProvider{
+		stub: stub,
+	}
+}
+
+func changeRef(ref string) int64 {
+	ref = strings.ToLower(ref)
+	if !strings.HasPrefix(ref, "change/") && !strings.HasPrefix(ref, "cr/") {
+		return 0
+	}
+	n, err := strconv.ParseInt(strings.SplitN(ref, "/", 2)[1], 10, 64)
+	if err != nil {
+		return 0
+	}
+
+	return n
+}
+
+func (s *DepotViewSourceProvider) Source(ctx context.Context, ref string) (Source, error) {
+	var hash string
+	n := changeRef(ref)
+	if n != 0 {
+		res, err := s.stub.ResolveGerritChange(ctx, &dvpb.ResolveGerritChangeRequest{Change: n})
+		if err != nil {
+			return nil, err
+		}
+		hash = res.Hash
+	} else {
+		res, err := s.stub.Resolve(ctx, &dvpb.ResolveRequest{Ref: ref})
+		if err != nil {
+			return nil, err
+		}
+		hash = res.Hash
+	}
+
+	if hash == "" {
+		return nil, nil
+	}
+
+	return &depotViewSource{
+		stub:   s.stub,
+		hash:   hash,
+		change: n,
+	}, nil
+}
+
+type depotViewSource struct {
+	stub   dvpb.DepotViewClient
+	hash   string
+	change int64
+}
+
+func (s *depotViewSource) IsFile(ctx context.Context, path string) (bool, error) {
+	res, err := s.stub.Stat(ctx, &dvpb.StatRequest{
+		Hash: s.hash,
+		Path: path,
+	})
+	if err != nil {
+		return false, err
+	}
+	return res.Type == dvpb.StatResponse_TYPE_FILE, nil
+}
+
+func (s *depotViewSource) IsDirectory(ctx context.Context, path string) (bool, error) {
+	res, err := s.stub.Stat(ctx, &dvpb.StatRequest{
+		Hash: s.hash,
+		Path: path,
+	})
+	if err != nil {
+		return false, err
+	}
+	return res.Type == dvpb.StatResponse_TYPE_DIRECTORY, nil
+}
+
+func (s *depotViewSource) ReadFile(ctx context.Context, path string) ([]byte, error) {
+	var data []byte
+	srv, err := s.stub.Read(ctx, &dvpb.ReadRequest{
+		Hash: s.hash,
+		Path: path,
+	})
+	if err != nil {
+		return nil, err
+	}
+	for {
+		res, err := srv.Recv()
+		if err != nil {
+			return nil, err
+		}
+		if len(res.Data) == 0 {
+			break
+		}
+		data = append(data, res.Data...)
+	}
+	return data, nil
+}
+
+func (s *depotViewSource) WebLinks(fpath string) []WebLink {
+	gitURL := fmt.Sprintf(FlagGitwebURLPattern, s.hash, fpath)
+	links := []WebLink{
+		WebLink{Kind: "gitweb", LinkLabel: s.hash[:16], LinkURL: gitURL},
+	}
+
+	if s.change != 0 {
+		gerritLabel := fmt.Sprintf("change %d", s.change)
+		gerritLink := fmt.Sprintf("https://gerrit.hackerspace.pl/%d", s.change)
+		links = append(links, WebLink{Kind: "gerrit", LinkLabel: gerritLabel, LinkURL: gerritLink})
+	}
+
+	return links
+}
diff --git a/devtools/hackdoc/source/source_local.go b/devtools/hackdoc/source/source_local.go
new file mode 100644
index 0000000..feecd8b
--- /dev/null
+++ b/devtools/hackdoc/source/source_local.go
@@ -0,0 +1,77 @@
+package source
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+type LocalSource struct {
+	root string
+}
+
+func NewLocal(root string) Source {
+	return &LocalSource{
+		root: strings.TrimRight(root, "/"),
+	}
+}
+
+func (s *LocalSource) resolve(path string) (string, error) {
+	if !strings.HasPrefix(path, "//") {
+		return "", fmt.Errorf("invalid path %q, expected // prefix", path)
+	}
+	path = path[2:]
+	if strings.HasPrefix(path, "/") {
+		return "", fmt.Errorf("invalid path %q, expected // prefix", path)
+	}
+
+	return s.root + "/" + path, nil
+}
+
+func (s *LocalSource) IsFile(ctx context.Context, path string) (bool, error) {
+	path, err := s.resolve(path)
+	if err != nil {
+		return false, err
+	}
+	stat, err := os.Stat(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("os.Stat(%q): %w", path, err)
+	}
+	return !stat.IsDir(), nil
+}
+
+func (s *LocalSource) ReadFile(ctx context.Context, path string) ([]byte, error) {
+	path, err := s.resolve(path)
+	if err != nil {
+		return nil, err
+	}
+	// TODO(q3k): limit size
+	return ioutil.ReadFile(path)
+}
+
+func (s *LocalSource) IsDirectory(ctx context.Context, path string) (bool, error) {
+	path, err := s.resolve(path)
+	if err != nil {
+		return false, err
+	}
+	stat, err := os.Stat(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("os.Stat(%q): %w", path, err)
+	}
+	return stat.IsDir(), nil
+}
+
+func (s *LocalSource) WebLinks(fpath string) []WebLink {
+	gitURL := fmt.Sprintf(FlagGitwebURLPattern, "master", fpath)
+	return []WebLink{
+		WebLink{Kind: "gitweb", LinkLabel: "master", LinkURL: gitURL},
+	}
+}
diff --git a/devtools/hackdoc/tpl/base.html b/devtools/hackdoc/tpl/base.html
new file mode 100644
index 0000000..5fd861a
--- /dev/null
+++ b/devtools/hackdoc/tpl/base.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>hackdoc:{{ .Title }}</title>
+        {{ template "head" . }}
+    </head>
+    <body>
+        {{ template "body" . }}
+    </body>
+</html>
diff --git a/devtools/hackdoc/tpl/default.html b/devtools/hackdoc/tpl/default.html
new file mode 100644
index 0000000..3e3b268
--- /dev/null
+++ b/devtools/hackdoc/tpl/default.html
@@ -0,0 +1,239 @@
+{{ define "head" }}
+<style type="text/css">
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+
+body {
+    font-size: 14px;
+    line-height: 1.25em;
+    background-color: #f0f0f0;
+}
+
+.wrapper {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    width: 100%;
+}
+
+.column {
+    width: 80em;
+    padding: 1rem 0 1rem 0;
+}
+
+.page {
+    background-color: #fefefe;
+    padding: 0.5rem 2rem 3rem 2rem;
+}
+
+.header {
+    font-size: 1.2em;
+    font-family: Consolas, monospace;
+    margin-top: 1rem;
+    padding: 0.5em 0 0.5em 0;
+}
+
+.header a {
+    text-decoration: none;
+}
+.header a:hover {
+    text-decoration: underline;
+}
+
+.header span.red {
+    color: #b30014;
+}
+
+.header span.part {
+    color: #666;
+    padding-left: 0.2em;
+}
+
+.header span.part a {
+    color: rgb(27, 106, 203);
+}
+.header span.part a:visited {
+    color: rgb(27, 106, 203);
+}
+
+.footer {
+    font-size: 0.8em;
+    color: #ccc;
+    font-weight: 800;
+    font-family: helvetica, arial, sans-serif;
+    padding: 0.5em 1em 1em;
+    text-align: right;
+}
+
+.footer .left {
+    float: left;
+}
+
+.footer .right {
+    float: right;
+}
+
+.footer a {
+    color: #bbb;
+}
+
+h1,h2,h3,h4 {
+    font-family: helvetica, arial, sans-serif;
+}
+
+.content h1 {
+    font-size: 1.6em;
+    padding: 1em 0 0 0;
+    font-weight: 800;
+}
+
+.content h2 {
+    font-size: 1.3em;
+    padding: 0.8em 0 0 0;
+    color: #333;
+    font-weight: 800;
+}
+
+.content h3 {
+    font-size: 1.2em;
+    padding: 0.4em 0 0 0;
+    color: #444;
+}
+
+.content h4 {
+    font-size: 1.0em;
+    color: #555;
+}
+
+.content code {
+    font-family: Consolas, monospace;
+    background-color: #f8f8f8;
+}
+
+.content pre {
+    background-color: #f8f8f8;
+    border: 1px solid #d8d8d8;
+    margin: 1em;
+    padding: 0.5em;
+    overflow: auto;
+}
+
+.content p {
+    margin-top: 0.8em;
+    line-height: 1.5em;
+}
+
+.content ul {
+    padding-top: 0.5em;
+}
+
+.content ul li {
+    padding-left: 1em;
+}
+
+.content ul li::before {
+    content: "•";
+    color: #333;;
+    display: inline-block;
+    width: 1em;
+    margin-left: -1em;
+}
+
+.toc {
+    float: right;
+    padding: 1em 1em 1em 1em;
+    border: 1px solid #ddd;
+    background-color: #f8f8f8;
+    margin: 2em;
+    max-width: 30%;
+}
+
+.toc h1 {
+    font-size: 1.2em;
+    padding-bottom: 0.5em;
+}
+
+.toc a {
+    text-decoration: none;
+}
+
+.toc li {
+    padding-left: 0.5em;
+}
+
+.toc ul {
+    list-style-type: disc;
+    padding-left: 1em;
+}
+
+.toc ul ul {
+    list-style-type: circle;
+}
+
+
+</style>
+{{ end }}
+{{ define "body" }}
+<div class="wrapper">
+    <div class="column">
+        <div class="page">
+            <div class="header">
+                <span class="red">hackdoc:</span>
+                {{ range .PathParts }}<span class="part"><a href="{{ .Path }}">{{ .Label }}</a></span>{{ end }}
+                <span class="red" style="margin-left: 1em;">shortcuts:</span> <a href="/">root</a>, <a href="/cluster/doc">cluster docs</a>, <a href="/doc/codelabs">codelabs</a>
+            </div>
+            {{ .Rendered }}
+        </div>
+        <div class="footer">
+            <div class="left">
+                View in:
+                {{ range .WebLinks }}
+                <span class="muted">[{{ .Kind }} <a href="{{ .LinkURL }}">{{ .LinkLabel }}</a>]</span>
+                {{ end }}
+            </div>
+            <div class="right">Generated by <a href="{{ .HackdocURL }}/devtools/hackdoc">hackdoc</a>.</div>
+        </div>
+    </div>
+</div>
+{{ end }}
diff --git a/doc/codelabs/index.md b/doc/codelabs/index.md
new file mode 100644
index 0000000..9af259f
--- /dev/null
+++ b/doc/codelabs/index.md
@@ -0,0 +1,6 @@
+Codelabs
+========
+
+Short and sweet technical tutorials.
+
+Coming soon. Continue pinging q3k.
diff --git a/doc/img/hscloud-smol.png b/doc/img/hscloud-smol.png
new file mode 100644
index 0000000..3aa3126
--- /dev/null
+++ b/doc/img/hscloud-smol.png
Binary files differ
diff --git a/doc/img/hscloud.png b/doc/img/hscloud.png
new file mode 100644
index 0000000..9e3c1a1
--- /dev/null
+++ b/doc/img/hscloud.png
Binary files differ
diff --git a/env.sh b/env.sh
index 374df77..27cf34f 100644
--- a/env.sh
+++ b/env.sh
@@ -18,7 +18,7 @@
 
 # Detect NixOS
 if [ -d /nix ] && [ ! -f /lib/ld-linux.so.2 ]; then
-    hscloud_nixos=true
+    export hscloud_nixos=true
 fi
 
 gpg-unlock() {
diff --git a/hackdoc.toml b/hackdoc.toml
new file mode 100644
index 0000000..83eacea
--- /dev/null
+++ b/hackdoc.toml
@@ -0,0 +1,7 @@
+default_index = ["index.md", "readme.md", "README.md"]
+
+[template.default]
+sources = [
+    "//devtools/hackdoc/tpl/base.html",
+    "//devtools/hackdoc/tpl/default.html",
+]
diff --git a/personal/q3k/factorio/BUILD b/personal/q3k/factorio/BUILD
index 9c8bed8..7a0a49d 100644
--- a/personal/q3k/factorio/BUILD
+++ b/personal/q3k/factorio/BUILD
@@ -37,11 +37,29 @@
     entrypoint = ["/entrypoint.sh"],
 )
 
+container_image(
+    name="0.18.12-2",
+    base="@prodimage-bionic//image",
+    tars = ["@factorio-headless-0.18.12//file"],
+    files = [":entrypoint.sh"],
+    directory = "/",
+    entrypoint = ["/entrypoint.sh"],
+)
+
+container_image(
+    name="0.18.17-1",
+    base="@prodimage-bionic//image",
+    tars = ["@factorio-headless-0.18.17//file"],
+    files = [":entrypoint.sh"],
+    directory = "/",
+    entrypoint = ["/entrypoint.sh"],
+)
+
 container_push(
     name = "push_latest",
-    image = ":0.17.79-1",
+    image = ":0.18.17-1",
     format = "Docker",
     registry = "registry.k0.hswaw.net",
     repository = "app/factorio",
-    tag = "0.17.79-1",
+    tag = "0.18.17-1",
 )
diff --git a/personal/q3k/factorio/kube/factorio.libsonnet b/personal/q3k/factorio/kube/factorio.libsonnet
index be69054..6ee7b49 100644
--- a/personal/q3k/factorio/kube/factorio.libsonnet
+++ b/personal/q3k/factorio/kube/factorio.libsonnet
@@ -35,7 +35,7 @@
     metadata:: {
         namespace: cfg.namespace,
         labels: {
-            "app.kubernetes.io/name": cfg.appName,
+            "app.kubernetes.io/name": factorio.makeName("factorio"),
             "app.kubernetes.io/managed-by": "kubecfg",
             "app.kubernetes.io/component": "factorio",
         },
@@ -101,7 +101,12 @@
         },
     },
     svc: kube.Service(factorio.makeName("factorio")) {
-        metadata+: factorio.metadata,
+        metadata+: factorio.metadata {
+            // hack - have to keep existing naming scheme otherwise we'd lose addresses
+            labels: {
+                "app.kubernetes.io/name": cfg.appName,
+            },
+        },
         target_pod:: factorio.deployment.spec.template,
         spec+: {
             ports: [
diff --git a/personal/q3k/factorio/kube/prod.jsonnet b/personal/q3k/factorio/kube/prod.jsonnet
index 523ba8b..d30b652 100644
--- a/personal/q3k/factorio/kube/prod.jsonnet
+++ b/personal/q3k/factorio/kube/prod.jsonnet
@@ -7,6 +7,8 @@
 //  - 0.17.41-1
 //  - 0.17.52-1
 //  - 0.17.79-1
+//  - 0.18.12-2
+//  - 0.18.17-1
 
 {
     local prod = self,
@@ -20,5 +22,7 @@
         }
     },
 
-    q3k: prod.instance("q3k", "0.17.79-1"),
+    q3k: prod.instance("q3k", "0.18.17-1"),
+    ds: prod.instance("ds", "0.18.17-1"),
+    pymods: prod.instance("pymods", "0.18.17-1"),
 }
diff --git a/personal/q3k/test/test b/personal/q3k/test/test
new file mode 100644
index 0000000..357b8b6
--- /dev/null
+++ b/personal/q3k/test/test
@@ -0,0 +1,3 @@
+yo
+
+actually, no, yo, to you!