Merge "app/registry: integrate into cluster/kube"
diff --git a/.gitignore b/.gitignore
index 34b85ba..6e45050 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
 *swp
-bazel-*
+/bazel-*
 .kubectl
diff --git a/WORKSPACE b/WORKSPACE
index 5bf90e8..5d0d981 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,14 +1,14 @@
-
 load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
 # Skylib
 
 skylib_version = "0.8.0"
+
 http_archive(
     name = "bazel_skylib",
     type = "tar.gz",
-    url = "https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz".format (skylib_version, skylib_version),
+    url = "https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz".format(skylib_version, skylib_version),
     sha256 = "2ef429f5d7ce7111263289644d233707dba35e39696377ebab8b0bc701f7818e",
 )
 
@@ -21,12 +21,22 @@
     urls = ["https://github.com/bazelbuild/rules_docker/archive/v0.8.1.tar.gz"],
 )
 
+load("@io_bazel_rules_docker//toolchains/docker:toolchain.bzl", docker_toolchain_configure = "toolchain_configure")
+
+# This forces the use of Docker $HOME/.docker configuration.
+docker_toolchain_configure(
+    name = "docker_config",
+    client_config = "",
+    docker_path = "/usr/bin/docker",
+)
+
 load(
     "@io_bazel_rules_docker//repositories:repositories.bzl",
     container_repositories = "repositories",
 )
 
 container_repositories()
+
 # Nix rules
 http_archive(
     name = "io_tweag_rules_nixpkgs",
@@ -76,10 +86,10 @@
 
 container_pull(
     name = "prodimage-bionic",
-    registry = "index.docker.io",
-    repository = "library/ubuntu",
-    tag = "bionic-20190515",
-    digest = "sha256:b36667c98cf8f68d4b7f1fb8e01f742c2ed26b5f0c965a788e98dfe589a4b3e4",
+    registry = "registry.k0.hswaw.net",
+    repository = "q3k/prodimage",
+    tag = "20190725-1806",
+    digest = "sha256:169ca347eb5daa140dd61a9af71db5efe2abd7ac618cfd162375481cd91d5db8",
 )
 
 container_pull(
@@ -172,6 +182,23 @@
     sha1 = "4216af16d38465bbab0f3dff8efa14204f7a399a",
 )
 
+# For devtools/bazel-cache
+
+git_repository(
+    name = "com_github_buchgr_bazel_remote",
+    remote = "https://github.com/buchgr/bazel-remote.git",
+    commit = "a9374e638411da72a2ef2a83f490e61e2d74a976",
+)
+
+# Go image repos for Docker
+
+load(
+    "@io_bazel_rules_docker//go:image.bzl",
+    go_image_repositories = "repositories",
+)
+
+go_image_repositories()
+
 # Go repositories
 
 go_repository(
@@ -360,3 +387,215 @@
     commit = "de5bf2ad457846296e2031421a34e2568e304e35",
     importpath = "github.com/PuerkitoBio/urlesc",
 )
+
+go_repository(
+    name = "com_github_abbot_go_http_auth",
+    commit = "860ed7f246ff5abfdbd5c7ce618fd37b49fd3d86",
+    importpath = "github.com/abbot/go-http-auth",
+)
+
+go_repository(
+    name = "com_github_urfave_cli",
+    commit = "693af58b4d51b8fcc7f9d89576da170765980581",
+    importpath = "github.com/urfave/cli",
+)
+
+go_repository(
+    name = "org_golang_x_crypto",
+    commit = "4def268fd1a49955bfb3dda92fe3db4f924f2285",
+    importpath = "golang.org/x/crypto",
+)
+
+go_repository(
+    name = "org_golang_x_oauth2",
+    commit = "0f29369cfe4552d0e4bcddc57cc75f4d7e672a33",
+    importpath = "golang.org/x/oauth2",
+)
+
+go_repository(
+    name = "com_github_djherbis_atime",
+    commit = "2d569978378562c466df74eda2d82900f435c5f4",
+    importpath = "github.com/djherbis/atime",
+)
+
+go_repository(
+    name = "com_google_cloud_go",
+    commit = "71971b35976fc2f904ed2772536790a5458d9996",
+    importpath = "cloud.google.com/go",
+)
+
+go_repository(
+    name = "org_golang_x_net",
+    commit = "da137c7871d730100384dbcf36e6f8fa493aef5b",
+    importpath = "golang.org/x/net",
+)
+
+go_repository(
+    name = "com_github_stackexchange_wmi",
+    commit = "cbe66965904dbe8a6cd589e2298e5d8b986bd7dd",
+    importpath = "github.com/stackexchange/wmi",
+)
+
+go_repository(
+    name = "com_github_go_ole_go_ole",
+    commit = "938323a72016e9cf84fa5fba7635089efb0ad87f",
+    importpath = "github.com/go-ole/go-ole",
+)
+
+go_repository(
+    name = "com_github_dustin_go_humanize",
+    commit = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e",
+    importpath = "github.com/dustin/go-humanize",
+)
+
+go_repository(
+    name = "io_k8s_client_go",
+    commit = "0c47f9da00011ea9a8717671127ac21625c7a6c0",
+    importpath = "k8s.io/client-go",
+)
+
+go_repository(
+    name = "io_k8s_apimachinery",
+    commit = "bfcf53abc9f82bad3e534fcb1c36599d3c989ebf",
+    importpath = "k8s.io/apimachinery",
+    build_file_proto_mode = "disable",
+)
+
+go_repository(
+    name = "io_k8s_klog",
+    commit = "6a023d6d0e0954feabd46dc2d3a6a2c3c991fe1a",
+    importpath = "k8s.io/klog",
+)
+
+go_repository(
+    name = "io_k8s_utils",
+    commit = "3dccf664f023863740c508fb4284e49742bedfa4",
+    importpath = "k8s.io/utils",
+)
+
+go_repository(
+    name = "com_github_googleapis_gnostic",
+    commit = "25d8b0b6698593f520d9d8dc5a88e6b16ca9ecc0",
+    importpath = "github.com/googleapis/gnostic",
+)
+
+go_repository(
+    name = "io_k8s_api",
+    commit = "3043179095b6baa0087e8735d796bd6dfa881f8e",
+    importpath = "k8s.io/api",
+    build_file_proto_mode = "disable",
+)
+
+go_repository(
+    name = "org_golang_x_time",
+    commit = "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef",
+    importpath = "golang.org/x/time",
+)
+
+go_repository(
+    name = "com_github_google_gofuzz",
+    commit = "f140a6486e521aad38f5917de355cbf147cc0496",
+    importpath = "github.com/google/gofuzz",
+)
+
+go_repository(
+    name = "io_k8s_sigs_yaml",
+    commit = "4cd0c284b15f1735b8cc247df097d262b8903f9f",
+    importpath = "sigs.k8s.io/yaml",
+)
+
+go_repository(
+    name = "com_github_modern_go_reflect2",
+    commit = "94122c33edd36123c84d5368cfb2b69df93a0ec8",
+    importpath = "github.com/modern-go/reflect2",
+)
+
+go_repository(
+    name = "com_github_davecgh_go_spew",
+    commit = "d8f796af33cc11cb798c1aaeb27a4ebc5099927d",
+    importpath = "github.com/davecgh/go-spew",
+)
+
+go_repository(
+    name = "com_github_json_iterator_go",
+    commit = "27518f6661eba504be5a7a9a9f6d9460d892ade3",
+    importpath = "github.com/json-iterator/go",
+)
+
+go_repository(
+    name = "com_github_modern_go_concurrent",
+    commit = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
+    importpath = "github.com/modern-go/concurrent",
+)
+
+go_repository(
+    name = "in_gopkg_inf_v0",
+    commit = "d2d2541c53f18d2a059457998ce2876cc8e67cbf",
+    importpath = "gopkg.in/inf.v0",
+)
+
+go_repository(
+    name = "com_github_cloudflare_cfrpki",
+    commit = "adece784464315db69299ba75e9287c60cd95c69",
+    importpath = "github.com/cloudflare/cfrpki",
+)
+
+go_repository(
+    name = "com_github_prometheus_client_golang",
+    commit = "bb9b00a86ebaaa691ba43af1f9ba9d16156cc545",
+    importpath = "github.com/prometheus/client_golang",
+)
+
+go_repository(
+    name = "com_github_rs_cors",
+    commit = "db0fe48135e83b5812a5a31be0eea66984b1b521",
+    importpath = "github.com/rs/cors",
+)
+
+go_repository(
+    name = "com_github_cloudflare_gortr",
+    commit = "95270606e8853d9b93f5be46d656d08ec0a4ef09",
+    importpath = "github.com/cloudflare/gortr",
+)
+
+go_repository(
+    name = "com_github_gorilla_mux",
+    commit = "e67b3c02c7195c052acff13261f0c9fd1ba53011",
+    importpath = "github.com/gorilla/mux",
+)
+
+go_repository(
+    name = "com_github_sirupsen_logrus",
+    commit = "07a84ee7412e7a28663d92930a1d46f81b124ee1",
+    importpath = "github.com/sirupsen/logrus",
+)
+
+go_repository(
+    name = "com_github_prometheus_common",
+    commit = "33bc620f956eb70fbb8355e87df6a97891657ed5",
+    importpath = "github.com/prometheus/common",
+)
+
+go_repository(
+    name = "com_github_beorn7_perks",
+    commit = "4b2b341e8d7715fae06375aa633dbb6e91b3fb46",
+    importpath = "github.com/beorn7/perks",
+)
+
+go_repository(
+    name = "com_github_prometheus_client_model",
+    commit = "fd36f4220a901265f90734c3183c5f0c91daa0b8",
+    importpath = "github.com/prometheus/client_model",
+)
+
+go_repository(
+    name = "com_github_prometheus_procfs",
+    commit = "8f55e607908ea781ad9d08521730d73e047d9ac4",
+    importpath = "github.com/prometheus/procfs",
+)
+
+go_repository(
+    name = "com_github_matttproud_golang_protobuf_extensions",
+    commit = "c182affec369e30f25d3eb8cd8a478dee585ae7d",
+    importpath = "github.com/matttproud/golang_protobuf_extensions",
+)
diff --git a/bgpwtf/cccampix/kube/camp.jsonnet b/bgpwtf/cccampix/kube/camp.jsonnet
new file mode 100644
index 0000000..c059401
--- /dev/null
+++ b/bgpwtf/cccampix/kube/camp.jsonnet
@@ -0,0 +1,9 @@
+local ix = import "ix.libsonnet";
+
+{
+    camp: ix.IX {
+        cfg+: {
+            namespace: "cccamp-ix",
+        },
+    },
+}
diff --git a/bgpwtf/cccampix/kube/ix.libsonnet b/bgpwtf/cccampix/kube/ix.libsonnet
new file mode 100644
index 0000000..def1d79
--- /dev/null
+++ b/bgpwtf/cccampix/kube/ix.libsonnet
@@ -0,0 +1,84 @@
+local kube = import "../../../kube/kube.libsonnet";
+
+{
+    IX: {
+        local ix = self,
+        local cfg = ix.cfg,
+        cfg:: {
+            octorpki: {
+                image: "registry.k0.hswaw.net/q3k/octorpki:1564072856-3bfb2ef7fd180e774f74bbc9eebf6d97b9d80003",
+                storageClassName: "waw-hdd-redundant-1",
+                resources: {
+                    requests: { cpu: "100m", memory: "500Mi" },
+                    limits: { cpu: "500m", memory: "1Gi" },
+                },
+            },
+
+            appName: "ix",
+            namespace: error "namespace must be defined",
+            prefix: "",
+        },
+
+        namespace: kube.Namespace(cfg.namespace),
+        name(component):: cfg.prefix + component,
+        metadata(component):: {
+            namespace: cfg.namespace,
+            labels: {
+                "app.kubernetes.io/name": cfg.appName,
+                "app.kubernetes.io/managed-by": "kubecfg",
+                "app.kubernetes.io/component": component,
+            },
+        },
+
+        octorpki: {
+            cache: kube.PersistentVolumeClaim(ix.name("octorpki")) {
+                metadata+: ix.metadata("octorpki"),
+                spec+: {
+                    storageClassName: cfg.octorpki.storageClassName,
+                    accessModes: [ "ReadWriteOnce" ],
+                    resources: {
+                        requests: {
+                            storage: "2Gi",
+                        },
+                    },
+                },
+            },
+            deployment: kube.Deployment(ix.name("octorpki")) {
+                metadata+: ix.metadata("octorpki"),
+                spec+: {
+                    template+: {
+                        spec+: {
+                            volumes_: {
+                                cache: kube.PersistentVolumeClaimVolume(ix.octorpki.cache),
+                            },
+                            containers_: {
+                                octorpki: kube.Container(ix.name("octorpki")){
+                                    image: cfg.octorpki.image,
+                                    args: [
+                                        "/octorpki/entrypoint.sh",
+                                    ],
+                                    ports_: {
+                                        client: { containerPort: 8080 },
+                                    },
+                                    volumeMounts_: {
+                                        cache: { mountPath: "/cache" },
+                                    },
+                                    resources: cfg.octorpki.resources,
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+            svc: kube.Service(ix.name("octorpki")) {
+                metadata+: ix.metadata("octorpki"),
+                target_pod:: ix.octorpki.deployment.spec.template,
+                spec+: {
+                    ports: [
+                        { name: "client", port: 8080, targetPort: 8080, protocol: "TCP" },
+                    ],
+                },
+            },
+        },
+    },
+}
diff --git a/bgpwtf/cccampix/octorpki/BUILD.bazel b/bgpwtf/cccampix/octorpki/BUILD.bazel
new file mode 100644
index 0000000..d9fc491
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/BUILD.bazel
@@ -0,0 +1,35 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
+
+container_layer(
+    name = "layer_bin",
+    files = [
+        "@com_github_cloudflare_cfrpki//cmd/octorpki:octorpki",
+        "entrypoint.sh",
+    ],
+    directory = "/octorpki/",
+)
+
+container_layer(
+    name = "layer_tals",
+    files = glob(["tals/*"]),
+    directory = "/octorpki/tals/",
+)
+
+container_image(
+    name = "octorpki",
+    base = "@prodimage-bionic//image",
+    entrypoint = "/octorpki/entrypoint.sh",
+    layers = [
+        ":layer_bin",
+        ":layer_tals",
+    ],
+)
+
+container_push(
+    name = "push",
+    image = ":octorpki",
+    format = "Docker",
+    registry = "registry.k0.hswaw.net",
+    repository = "q3k/octorpki",
+    tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+)
diff --git a/bgpwtf/cccampix/octorpki/entrypoint.sh b/bgpwtf/cccampix/octorpki/entrypoint.sh
new file mode 100644
index 0000000..62c59b1
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/entrypoint.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -e
+
+cd /octorpki
+
+./octorpki -cache /cache/ -output.sign=false "$@"
diff --git a/bgpwtf/cccampix/octorpki/tals/afrinic.tal b/bgpwtf/cccampix/octorpki/tals/afrinic.tal
new file mode 100644
index 0000000..fc7639f
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/tals/afrinic.tal
@@ -0,0 +1,9 @@
+rsync://rpki.afrinic.net/repository/AfriNIC.cer
+
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsAqAhWIO+ON2Ef9oRDM
+pKxv+AfmSLIdLWJtjrvUyDxJPBjgR+kVrOHUeTaujygFUp49tuN5H2C1rUuQavTH
+vve6xNF5fU3OkTcqEzMOZy+ctkbde2SRMVdvbO22+TH9gNhKDc9l7Vu01qU4LeJH
+k3X0f5uu5346YrGAOSv6AaYBXVgXxa0s9ZvgqFpim50pReQe/WI3QwFKNgpPzfQL
+6Y7fDPYdYaVOXPXSKtx7P4s4KLA/ZWmRL/bobw/i2fFviAGhDrjqqqum+/9w1hEl
+L/vqihVnV18saKTnLvkItA/Bf5i11Yhw2K7qv573YWxyuqCknO/iYLTR1DToBZcZ
+UQIDAQAB
diff --git a/bgpwtf/cccampix/octorpki/tals/apnic.tal b/bgpwtf/cccampix/octorpki/tals/apnic.tal
new file mode 100644
index 0000000..fc781ee
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/tals/apnic.tal
@@ -0,0 +1,9 @@
+rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
+
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
+qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
+7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
+LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
+i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
+cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
+0wIDAQAB
diff --git a/bgpwtf/cccampix/octorpki/tals/arin.tal b/bgpwtf/cccampix/octorpki/tals/arin.tal
new file mode 100644
index 0000000..92f84bf
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/tals/arin.tal
@@ -0,0 +1,7 @@
+rsync://rpki.arin.net/repository/arin-rpki-ta.cer

+

+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3lZPjbHvMRV5sDDqfLc/685th5FnreHMJjg8

+pEZUbG8Y8TQxSBsDebbsDpl3Ov3Cj1WtdrJ3CIfQODCPrrJdOBSrMATeUbPC+JlNf2SRP3UB+VJFgtTj

+0RN8cEYIuhBW5t6AxQbHhdNQH+A1F/OJdw0q9da2U29Lx85nfFxvnC1EpK9CbLJS4m37+RlpNbT1cba+

+b+loXpx0Qcb1C4UpJCGDy7uNf5w6/+l7RpATAHqqsX4qCtwwDYlbHzp2xk9owF3mkCxzl0HwncO+sEHH

+eaL3OjtwdIGrRGeHi2Mpt+mvWHhtQqVG+51MHTyg+nIjWFKKGx1Q9+KDx4wJStwveQIDAQAB

diff --git a/bgpwtf/cccampix/octorpki/tals/lacnic.tal b/bgpwtf/cccampix/octorpki/tals/lacnic.tal
new file mode 100644
index 0000000..55bbf31
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/tals/lacnic.tal
@@ -0,0 +1,9 @@
+rsync://repository.lacnic.net/rpki/lacnic/rta-lacnic-rpki.cer
+
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZEzhYK0+PtDOPfub/KR
+c3MeWx3neXx4/wbnJWGbNAtbYqXg3uU5J4HFzPgk/VIppgSKAhlO0H60DRP48by9
+gr5/yDHu2KXhOmnMg46sYsUIpfgtBS9+VtrqWziJfb+pkGtuOWeTnj6zBmBNZKK+
+5AlMCW1WPhrylIcB+XSZx8tk9GS/3SMQ+YfMVwwAyYjsex14Uzto4GjONALE5oh1
+M3+glRQduD6vzSwOD+WahMbc9vCOTED+2McLHRKgNaQf0YJ9a1jG9oJIvDkKXEqd
+fqDRktwyoD74cV57bW3tBAexB7GglITbInyQAsmdngtfg2LUMrcROHHP86QPZINj
+DQIDAQAB
diff --git a/bgpwtf/cccampix/octorpki/tals/ripe.tal b/bgpwtf/cccampix/octorpki/tals/ripe.tal
new file mode 100644
index 0000000..acdb173
--- /dev/null
+++ b/bgpwtf/cccampix/octorpki/tals/ripe.tal
@@ -0,0 +1,9 @@
+rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer
+
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0URYSGqUz2myBsOzeW1j
+Q6NsxNvlLMyhWknvnl8NiBCs/T/S2XuNKQNZ+wBZxIgPPV2pFBFeQAvoH/WK83Hw
+A26V2siwm/MY2nKZ+Olw+wlpzlZ1p3Ipj2eNcKrmit8BwBC8xImzuCGaV0jkRB0G
+Z0hoH6Ml03umLprRsn6v0xOP0+l6Qc1ZHMFVFb385IQ7FQQTcVIxrdeMsoyJq9eM
+kE6DoclHhF/NlSllXubASQ9KUWqJ0+Ot3QCXr4LXECMfkpkVR2TZT+v5v658bHVs
+6ZxRD1b6Uk1uQKAyHUbn/tXvP8lrjAibGzVsXDT2L0x4Edx+QdixPgOji3gBMyL2
+VwIDAQAB
diff --git a/app/internet/Dockerfile b/bgpwtf/internet/Dockerfile
similarity index 100%
rename from app/internet/Dockerfile
rename to bgpwtf/internet/Dockerfile
diff --git a/app/internet/kube/prod.jsonnet b/bgpwtf/internet/kube/prod.jsonnet
similarity index 96%
rename from app/internet/kube/prod.jsonnet
rename to bgpwtf/internet/kube/prod.jsonnet
index fa712e0..39aa4dd 100644
--- a/app/internet/kube/prod.jsonnet
+++ b/bgpwtf/internet/kube/prod.jsonnet
@@ -9,7 +9,7 @@
         domain: "internet.hackerspace.pl",
 
         tag: "201907091256",
-        image: "registry.k0.hswaw.net/app/internet:" + cfg.tag,
+        image: "registry.k0.hswaw.net/bgpwtf/internet:" + cfg.tag,
 
         resources: {
             requests: {
diff --git a/app/internet/static/index.html b/bgpwtf/internet/static/index.html
similarity index 100%
rename from app/internet/static/index.html
rename to bgpwtf/internet/static/index.html
diff --git a/app/internet/static/regulamin.pdf b/bgpwtf/internet/static/regulamin.pdf
similarity index 100%
rename from app/internet/static/regulamin.pdf
rename to bgpwtf/internet/static/regulamin.pdf
Binary files differ
diff --git a/bzl/workspace-status.sh b/bzl/workspace-status.sh
index dcc90c5..5450017 100755
--- a/bzl/workspace-status.sh
+++ b/bzl/workspace-status.sh
@@ -15,3 +15,6 @@
 }
 
 echo STABLE_BUILD_GERRIT-OAUTH-PROVIDER_LABEL $(rev .)
+echo STABLE_GIT_COMMIT $(git rev-parse HEAD)
+echo STABLE_GIT_VERSION $(rev .)
+echo STABLE_BUILDER $(id -un)@$(hostname -f):$(pwd)
diff --git a/cluster/tools/install.sh b/cluster/tools/install.sh
index d2084ae..08e3476 100755
--- a/cluster/tools/install.sh
+++ b/cluster/tools/install.sh
@@ -9,7 +9,11 @@
 
 cd "${hscloud_root}"
 
-bazel build //cluster/tools/...
+bazel build \
+        //cluster/tools:kubectl \
+        //cluster/tools:kubecfg \
+        //cluster/tools:calicoctl \
+        //cluster/tools:cfssl
 
 if [ ! -e /nix ] ; then
     echo "WARNING: No Nix installation detected. nix-dependent tools (nixops) will not be built or available." 
diff --git a/devtools/bazel-cache/BUILD.bazel b/devtools/bazel-cache/BUILD.bazel
new file mode 100644
index 0000000..2781a59
--- /dev/null
+++ b/devtools/bazel-cache/BUILD.bazel
@@ -0,0 +1,10 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_push")
+
+container_push(
+    name = "push",
+    image = "@com_github_buchgr_bazel_remote//:bazel-remote-image",
+    format = "Docker",
+    registry = "registry.k0.hswaw.net",
+    repository = "devtools/bazel-cache",
+    tag = "latest",
+)
diff --git a/devtools/bazel-cache/prod.jsonnet b/devtools/bazel-cache/prod.jsonnet
index 9b65e95..b4a9b6e 100644
--- a/devtools/bazel-cache/prod.jsonnet
+++ b/devtools/bazel-cache/prod.jsonnet
@@ -49,7 +49,7 @@
                     },
                     containers_: {
                         auth: kube.Container("bazel-remote") {
-                            image: "buchgr/bazel-remote-cache",
+                            image: "registry.k0.hswaw.net/devtools/bazel-cache:latest",
                             volumeMounts_: {
                                 data: { mountPath: "/data" },
                             },
diff --git a/devtools/prodimage/Dockerfile b/devtools/prodimage/Dockerfile
new file mode 100644
index 0000000..85a0e50
--- /dev/null
+++ b/devtools/prodimage/Dockerfile
@@ -0,0 +1,9 @@
+FROM ubuntu:bionic-20190515
+
+RUN set -e -x ;\
+    export DEBIAN_FRONTEND=noninteractive ;\
+    apt-get -y update ;\
+    apt-get -y install \
+        ca-certificates \
+        rsync ;\
+    rm -rf /var/lib/apt/lists
diff --git a/go/mirko/BUILD.bazel b/go/mirko/BUILD.bazel
index 59ed755..89b40ed 100644
--- a/go/mirko/BUILD.bazel
+++ b/go/mirko/BUILD.bazel
@@ -2,13 +2,18 @@
 
 go_library(
     name = "go_default_library",
-    srcs = ["mirko.go"],
+    srcs = [
+        "kubernetes.go",
+        "mirko.go",
+    ],
     importpath = "code.hackerspace.pl/hscloud/go/mirko",
     visibility = ["//visibility:public"],
     deps = [
         "//go/pki:go_default_library",
         "//go/statusz:go_default_library",
         "@com_github_golang_glog//:go_default_library",
+        "@io_k8s_client_go//kubernetes:go_default_library",
+        "@io_k8s_client_go//rest:go_default_library",
         "@org_golang_google_grpc//:go_default_library",
         "@org_golang_google_grpc//reflection:go_default_library",
         "@org_golang_x_net//trace:go_default_library",
diff --git a/go/mirko/kubernetes.go b/go/mirko/kubernetes.go
new file mode 100644
index 0000000..c3a0bb3
--- /dev/null
+++ b/go/mirko/kubernetes.go
@@ -0,0 +1,23 @@
+package mirko
+
+import (
+	"github.com/golang/glog"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+)
+
+func (m *Mirko) kubernetesConnect() {
+	config, err := rest.InClusterConfig()
+	if err != nil {
+		glog.Errorf("mirko.KubernetesClientSet: %v", err)
+		return
+	}
+
+	clientset, err := kubernetes.NewForConfig(config)
+	if err != nil {
+		glog.Errorf("kubernetes.NewForConfig: %v", err)
+		return
+	}
+
+	m.kubernetesCS = clientset
+}
diff --git a/go/mirko/mirko.go b/go/mirko/mirko.go
index 01766db..78dbda1 100644
--- a/go/mirko/mirko.go
+++ b/go/mirko/mirko.go
@@ -8,14 +8,18 @@
 	"net/http"
 	"os"
 	"os/signal"
+	"sort"
+	"strings"
 	"time"
 
-	"code.hackerspace.pl/hscloud/go/pki"
-	"code.hackerspace.pl/hscloud/go/statusz"
 	"github.com/golang/glog"
 	"golang.org/x/net/trace"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/reflection"
+	"k8s.io/client-go/kubernetes"
+
+	"code.hackerspace.pl/hscloud/go/pki"
+	"code.hackerspace.pl/hscloud/go/statusz"
 )
 
 var (
@@ -38,6 +42,8 @@
 	httpServer *http.Server
 	httpMux    *http.ServeMux
 
+	kubernetesCS *kubernetes.Clientset
+
 	ctx    context.Context
 	cancel context.CancelFunc
 }
@@ -113,6 +119,12 @@
 		http.Redirect(w, r, "/debug/status", http.StatusSeeOther)
 	})
 
+	m.kubernetesConnect()
+
+	debugParts := strings.Split(flagDebugAddress, ":")
+	debugPort := debugParts[len(debugParts)-1]
+	statusz.PublicAddress = fmt.Sprintf("http://%s:%s/", m.Address().String(), debugPort)
+
 	m.httpListen = httpLis
 	m.httpServer = &http.Server{
 		Addr:    flagDebugAddress,
@@ -196,3 +208,98 @@
 		return err
 	}
 }
+
+// Address returns a linkable address where this service is running, sans port.
+// If running within kubernetes, this will return the pod IP.
+// Otherwise, this will guess the main, 'external' IP address of the machine it's running on.
+// On failures, returns loopback address.
+func (m *Mirko) Address() net.IP {
+	// If we're not running in Kubernetes and binding to 127.0.0.1, return loopback.
+	if m.kubernetesCS == nil && strings.HasPrefix(flagListenAddress, "127.0.0.1:") {
+		return net.ParseIP("127.0.0.1")
+	}
+
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		glog.Errorf("net.Interface(): %v", err)
+		return net.ParseIP("127.0.0.1")
+	}
+
+	addrmap := make(map[string]net.IP)
+
+	for _, iface := range ifaces {
+		addrs, err := iface.Addrs()
+		if err != nil {
+			glog.Errorf("iface(%q).Addrs(): %v", iface.Name, err)
+			continue
+		}
+
+		for _, addr := range addrs {
+			var ip net.IP
+			switch v := addr.(type) {
+			case *net.IPNet:
+				ip = v.IP
+			case *net.IPAddr:
+				ip = v.IP
+			default:
+				continue
+			}
+
+			if strings.HasPrefix(ip.String(), "fe80:") {
+				continue
+			}
+			addrmap[iface.Name] = ip
+		}
+	}
+
+	if m.kubernetesCS != nil {
+		addr, ok := addrmap["eth0"]
+		if !ok {
+			glog.Errorf("Running on Kubernetes but no eth0! Available interfaces: %v", addrmap)
+			return net.ParseIP("127.0.0.1")
+		}
+
+		return addr
+	}
+
+	if len(addrmap) == 0 {
+		glog.Errorf("No interfaces found!")
+		return net.ParseIP("127.0.0.1")
+	}
+
+	// Heuristics ahoy!
+	prioritized := []*ifaceWithPriority{}
+	for iface, addr := range addrmap {
+		prio := &ifaceWithPriority{
+			iface: iface,
+			addr:  addr,
+		}
+		switch {
+		case strings.HasPrefix(iface, "lo"):
+			prio.priority = -10
+		case strings.HasPrefix(iface, "tap"):
+			prio.priority = -5
+		case strings.HasPrefix(iface, "tun"):
+			prio.priority = -5
+		case strings.HasPrefix(iface, "veth"):
+			prio.priority = 5
+		case strings.HasPrefix(iface, "wl"):
+			prio.priority = 5
+		case strings.HasPrefix(iface, "enp"):
+			prio.priority = 10
+		case strings.HasPrefix(iface, "eth"):
+			prio.priority = 10
+		}
+
+		prioritized = append(prioritized, prio)
+	}
+
+	sort.Slice(prioritized, func(i, j int) bool { return prioritized[i].priority > prioritized[j].priority })
+	return prioritized[0].addr
+}
+
+type ifaceWithPriority struct {
+	iface    string
+	addr     net.IP
+	priority int
+}
diff --git a/go/statusz/BUILD.bazel b/go/statusz/BUILD.bazel
index f051d72..4434b95 100644
--- a/go/statusz/BUILD.bazel
+++ b/go/statusz/BUILD.bazel
@@ -12,4 +12,10 @@
         "@com_github_golang_glog//:go_default_library",
         "@com_github_shirou_gopsutil//load:go_default_library",
     ],
+    x_defs = {
+        "code.hackerspace.pl/hscloud/go/statusz.GitCommit": "{STABLE_GIT_COMMIT}",
+        "code.hackerspace.pl/hscloud/go/statusz.GitVersion": "{STABLE_GIT_VERSION}",
+        "code.hackerspace.pl/hscloud/go/statusz.Builder": "{STABLE_BUILDER}",
+        "code.hackerspace.pl/hscloud/go/statusz.BuildTimestamp": "{BUILD_TIMESTAMP}",
+    },
 )
diff --git a/go/statusz/statusz.go b/go/statusz/statusz.go
index 2aa76e4..20d5151 100644
--- a/go/statusz/statusz.go
+++ b/go/statusz/statusz.go
@@ -42,6 +42,7 @@
 	"os"
 	"os/user"
 	"path/filepath"
+	"strconv"
 	"sync"
 	"time"
 
@@ -60,6 +61,13 @@
 	tmpl     = template.Must(reparse(nil))
 	funcs    = make(template.FuncMap)
 
+	GitCommit      = "unknown"
+	GitVersion     = "unknown"
+	Builder        = "unknown"
+	BuildTimestamp = "0"
+	// mirko populates this
+	PublicAddress = "#"
+
 	DefaultMux = true
 )
 
@@ -75,15 +83,18 @@
 <title>Status for {{.BinaryName}}</title>
 <style>
 body {
-font-family: sans-serif;
 background: #fff;
 }
 h1 {
+font-family: sans-serif;
 clear: both;
 width: 100%;
 text-align: center;
 font-size: 120%;
+padding-top: 0.3em;
+padding-bottom: 0.3em;
 background: #eeeeff;
+margin-top: 1em;
 }
 .lefthand {
 float: left;
@@ -97,16 +108,19 @@
 <h1>Status for {{.BinaryName}}</h1>
 <div>
 <div class=lefthand>
-Started at {{.StartTime}}<br>
-Current time {{.CurrentTime}}<br>
+Started: {{.StartTime}} -- up {{.Up}}<br>
+Built on {{.BuildTime}}<br>
+Built at {{.Builder}}<br>
+Built from git checkout <a href="https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/{{.GitCommit}}">{{.GitVersion}}</a><br>
 SHA256 {{.BinaryHash}}<br>
 </div>
 <div class=righthand>
-Running as {{.Username}} on {{.Hostname}}<br>
+Running as {{.Username}} on <a href="{{.PublicAddress}}">{{.Hostname}}</a><br>
 Load {{.LoadAvg}}<br>
 View <a href=/debug/status>status</a>,
 	<a href=/debug/requests>requests</a>
 </div>
+<div style="clear: both;"> </div>
 </div>`
 
 func reparse(sections []section) (*template.Template, error) {
@@ -133,24 +147,39 @@
 	lock.Lock()
 	defer lock.Unlock()
 
+	buildTime := time.Unix(0, 0)
+	if buildTimeNum, err := strconv.Atoi(BuildTimestamp); err == nil {
+		buildTime = time.Unix(int64(buildTimeNum), 0)
+	}
+
 	data := struct {
-		Sections    []section
-		BinaryName  string
-		BinaryHash  string
-		Hostname    string
-		Username    string
-		StartTime   string
-		CurrentTime string
-		LoadAvg     string
+		Sections      []section
+		BinaryName    string
+		BinaryHash    string
+		GitVersion    string
+		GitCommit     string
+		Builder       string
+		BuildTime     string
+		Hostname      string
+		Username      string
+		StartTime     string
+		Up            string
+		LoadAvg       string
+		PublicAddress string
 	}{
-		Sections:    sections,
-		BinaryName:  binaryName,
-		BinaryHash:  binaryHash,
-		Hostname:    hostname,
-		Username:    username,
-		StartTime:   serverStart.Format(time.RFC1123),
-		CurrentTime: time.Now().Format(time.RFC1123),
-		LoadAvg:     loadAverage(),
+		Sections:      sections,
+		BinaryName:    binaryName,
+		BinaryHash:    binaryHash,
+		GitVersion:    GitVersion,
+		GitCommit:     GitCommit,
+		Builder:       Builder,
+		BuildTime:     fmt.Sprintf("%s (%d)", buildTime.Format(time.RFC1123), buildTime.Unix()),
+		Hostname:      hostname,
+		Username:      username,
+		StartTime:     serverStart.Format(time.RFC1123),
+		Up:            time.Since(serverStart).String(),
+		LoadAvg:       loadAverage(),
+		PublicAddress: PublicAddress,
 	}
 
 	if err := tmpl.ExecuteTemplate(w, "status", data); err != nil {
diff --git a/personal/q3k/annoyatron/prod.jsonnet b/personal/q3k/annoyatron/prod.jsonnet
new file mode 100644
index 0000000..977b8e0
--- /dev/null
+++ b/personal/q3k/annoyatron/prod.jsonnet
@@ -0,0 +1,73 @@
+local kube = import '../../../kube/kube.libsonnet';
+{
+    local annoyatron = self,
+    local cfg = self.cfg,
+    cfg:: {
+        image: "registry.k0.hswaw.net/q3k/annoyatron:latest",
+        domain: "annoyatron-prod.q3k.org",
+    },
+
+    deploy: kube.Deployment("annoyatron") {
+        metadata+: {
+            namespace: "q3k",
+        },
+        spec+: {
+            template+: {
+                spec+: {
+                    containers_: {
+                        annoyatron: kube.Container("annoyatron") {
+                            image: cfg.image,
+                            env_: {
+                                TOKEN: {
+                                    secretKeyRef: { name: "annoyatron-token", key: "token" },
+                                },
+                            },
+                            command: [
+                                "/app/annoyatron",
+                                "-token=$(TOKEN)",
+                            ],
+                            ports_: {
+                                client: { containerPort: 8080 },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+    svc: kube.Service("annoyatron") {
+        metadata+: {
+            namespace: "q3k",
+        },
+        target_pod:: annoyatron.deploy.spec.template,
+        spec+: {
+            ports: [
+                { name: "client", port: 8080, targetPort: 8080, protocol: "TCP" },
+            ],
+        },
+    },
+    ingress: kube.Ingress("annoyatron") {
+        metadata+: {
+            namespace: "q3k",
+            annotations+: {
+                "kubernetes.io/tls-acme": "true",
+                "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+            },
+        },
+        spec+: {
+            tls: [
+                { hosts: [cfg.domain], secretName: "annoyatron-tls" },
+            ],
+            rules: [
+                {
+                    host: cfg.domain,
+                    http: {
+                        paths: [
+                            { path: "/", backend: annoyatron.svc.name_port },
+                        ],
+                    },
+                }
+            ],
+        },
+    },
+}
diff --git a/personal/q3k/djtest/BUILD b/personal/q3k/djtest/BUILD
new file mode 100644
index 0000000..155b171
--- /dev/null
+++ b/personal/q3k/djtest/BUILD
@@ -0,0 +1,27 @@
+py_library(
+    name = "app",
+    srcs = glob(["djtest/**/*.py"]),
+    deps = [
+        "@pip36//django",
+    ],
+)
+
+py_binary(
+    name = "manage",
+    srcs = ["manage.py"],
+    deps = [
+       ":app",
+    ],
+)
+
+py_binary(
+    name = "uwsgi-start",
+    srcs = ["uwsgi-start.py"],
+    deps = [
+       ":app",
+       "@bazel_tools//tools/python/runfiles",
+    ],
+    data = [
+        '@pip36//uwsgi/scripts:uwsgi',
+    ],
+)
diff --git a/personal/q3k/djtest/djtest/__init__.py b/personal/q3k/djtest/djtest/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/personal/q3k/djtest/djtest/__init__.py
diff --git a/personal/q3k/djtest/djtest/settings.py b/personal/q3k/djtest/djtest/settings.py
new file mode 100644
index 0000000..1942799
--- /dev/null
+++ b/personal/q3k/djtest/djtest/settings.py
@@ -0,0 +1,120 @@
+"""
+Django settings for djtest project.
+
+Generated by 'django-admin startproject' using Django 2.2.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.2/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'av&&kc(mhuhms+s+av-lz+3d3a*)%!f1$7u0^)91t3)()ix*j@'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'djtest.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'djtest.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.2/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/personal/q3k/djtest/djtest/urls.py b/personal/q3k/djtest/djtest/urls.py
new file mode 100644
index 0000000..633f699
--- /dev/null
+++ b/personal/q3k/djtest/djtest/urls.py
@@ -0,0 +1,21 @@
+"""djtest URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+]
diff --git a/personal/q3k/djtest/djtest/wsgi.py b/personal/q3k/djtest/djtest/wsgi.py
new file mode 100644
index 0000000..ba3abe3
--- /dev/null
+++ b/personal/q3k/djtest/djtest/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for djtest project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djtest.settings')
+
+application = get_wsgi_application()
diff --git a/personal/q3k/djtest/manage.py b/personal/q3k/djtest/manage.py
new file mode 100644
index 0000000..e7ff590
--- /dev/null
+++ b/personal/q3k/djtest/manage.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djtest.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/personal/q3k/djtest/uwsgi-start.py b/personal/q3k/djtest/uwsgi-start.py
new file mode 100644
index 0000000..7597a6f
--- /dev/null
+++ b/personal/q3k/djtest/uwsgi-start.py
@@ -0,0 +1,44 @@
+import configparser
+import os
+import subprocess
+import tempfile
+
+import bazel_tools
+import bazel_tools.tools.python
+
+from bazel_tools.tools.python.runfiles import runfiles
+r = runfiles.Create()
+
+uwsgi = r.Rlocation("pip36/uwsgi/scripts/uwsgi")
+settings = r.Rlocation("__main__/personal/q3k/djtest/djtest/settings.py")
+
+apppath = os.path.dirname(settings)
+sitepath = os.path.dirname(apppath)
+
+pythonpath = os.environ['PYTHONPATH']
+
+# Make UWSGI ini config file
+cfgf = tempfile.NamedTemporaryFile(mode='w', delete=False)
+
+config = configparser.ConfigParser()
+config['uwsgi'] = {}
+config['uwsgi']['master'] = '1'
+config['uwsgi']['chdir'] = sitepath
+config['uwsgi']['module'] = 'djtest.wsgi'
+config['uwsgi']['env'] = 'DJANGO_SETTINGS_MODULE=djtest.settings'
+config['uwsgi']['http'] = '127.0.0.1:8080'
+config['uwsgi']['pythonpath'] = pythonpath
+
+config.write(cfgf)
+cfgf.close()
+
+args = [
+    # uwsgi from runfiles is non-chmodded, run through interpreter
+    '/lib64/ld-linux-x86-64.so.2',
+    uwsgi,
+    '--ini', cfgf.name,
+]
+
+subprocess.call(args)
+
+os.unlink(cfgf.name)
diff --git a/app/factorio/BUILD b/personal/q3k/factorio/BUILD
similarity index 87%
rename from app/factorio/BUILD
rename to personal/q3k/factorio/BUILD
index f0c47f7..44be4dd 100644
--- a/app/factorio/BUILD
+++ b/personal/q3k/factorio/BUILD
@@ -45,8 +45,8 @@
     executable = True,
     cmd = """
         tag=0.17.52-1
-        docker tag bazel/app/factorio:$$tag registry.k0.hswaw.net/app/factorio:$$tag
-        docker push registry.k0.hswaw.net/app/factorio:$$tag
+        docker tag bazel/personal/q3k/factorio:$$tag registry.k0.hswaw.net/personal/q3k/factorio:$$tag
+        docker push registry.k0.hswaw.net/personal/q3k/factorio:$$tag
         echo -ne "#!/bin/sh\necho Pushed $$tag.\n" > $(OUTS)
     """,
 )
diff --git a/app/factorio/entrypoint.sh b/personal/q3k/factorio/entrypoint.sh
similarity index 100%
rename from app/factorio/entrypoint.sh
rename to personal/q3k/factorio/entrypoint.sh
diff --git a/app/factorio/kube/factorio.libsonnet b/personal/q3k/factorio/kube/factorio.libsonnet
similarity index 97%
rename from app/factorio/kube/factorio.libsonnet
rename to personal/q3k/factorio/kube/factorio.libsonnet
index b90419a..497c62a 100644
--- a/app/factorio/kube/factorio.libsonnet
+++ b/personal/q3k/factorio/kube/factorio.libsonnet
@@ -1,6 +1,6 @@
 # Factorio on Kubernetes.
 
-local kube = import "../../../kube/kube.libsonnet";
+local kube = import "../../../../kube/kube.libsonnet";
 
 {
     local factorio = self,
diff --git a/app/factorio/kube/prod.jsonnet b/personal/q3k/factorio/kube/prod.jsonnet
similarity index 87%
rename from app/factorio/kube/prod.jsonnet
rename to personal/q3k/factorio/kube/prod.jsonnet
index b58bea9..c3cc074 100644
--- a/app/factorio/kube/prod.jsonnet
+++ b/personal/q3k/factorio/kube/prod.jsonnet
@@ -1,5 +1,5 @@
 local factorio = import "factorio.libsonnet";
-local kube = import "../../../kube/kube.libsonnet";
+local kube = import "../../../../kube/kube.libsonnet";
 
 
 // Available versions: