Merge "bgpwtf: add static v6 routes via bird"
diff --git a/app/covid-formity/prod.jsonnet b/app/covid-formity/prod.jsonnet
index a6ca8ab..18fb845 100644
--- a/app/covid-formity/prod.jsonnet
+++ b/app/covid-formity/prod.jsonnet
@@ -3,6 +3,7 @@
# 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 redis = import "../../kube/redis.libsonnet";
local postgres = import "../../kube/postgres.libsonnet";
{
@@ -10,9 +11,9 @@
local cfg = app.cfg,
cfg:: {
namespace: "covid-formity",
- image: "registry.k0.hswaw.net/informatic/covid-formity@sha256:8295f5b6d71266fb758c103210f12380f15903ba2467ead0e48ae0df16b6d608",
+ image: "registry.k0.hswaw.net/informatic/covid-formity@sha256:53c5fb0dbc4a6660ab47e39869a516f1e3f833dee5a03867386771bd9ffaf7b8",
domain: "covid19.hackerspace.pl",
- altDomains: ["covid.hackerspace.pl"],
+ altDomains: ["covid.hackerspace.pl", "www.covid.hackerspace.pl"],
},
metadata(component):: {
@@ -36,6 +37,15 @@
},
},
+ redis: redis {
+ cfg+: {
+ namespace: cfg.namespace,
+ appName: "covid-formity",
+ password: { secretKeyRef: { name: "covid-formity", key: "redis_password" } },
+ storageClassName: app.postgres.cfg.storageClassName,
+ },
+ },
+
deployment: kube.Deployment("covid-formity") {
metadata+: app.metadata("covid-formity"),
spec+: {
@@ -52,10 +62,14 @@
DATABASE_HOSTNAME: "postgres",
DATABASE_USERNAME: app.postgres.cfg.username,
DATABASE_PASSWORD: app.postgres.cfg.password,
+ CACHE_REDIS_PASSWORD: app.redis.cfg.password,
+ CACHE_REDIS_URL: "redis://default:$(CACHE_REDIS_PASSWORD)@redis",
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" } },
+ SHIPPING_KURJERZY_EMAIL: "qrde@hackerspace.pl",
+ SHIPPING_KURJERZY_PASSWORD: { secretKeyRef: { name: "covid-formity-shipping", key: "kurjerzy_password" } },
},
},
},
@@ -81,6 +95,11 @@
"kubernetes.io/tls-acme": "true",
"certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
"nginx.ingress.kubernetes.io/proxy-body-size": "0",
+ "nginx.ingress.kubernetes.io/configuration-snippet": "
+ location /qr1 { rewrite ^/qr1(.*)$ https://covid.hackerspace.pl$1 redirect; }
+ location /video { return 302 https://youtu.be/eC19w2NFO0E; }
+ location /manual { return 302 https://wiki.hackerspace.pl/_media/projects:covid-19:przylbica-instrukcja-v1.0.pdf; }
+ ",
},
},
spec+: {
diff --git a/kube/kube.upstream.libsonnet b/kube/kube.upstream.libsonnet
index e20d872..d12a786 100644
--- a/kube/kube.upstream.libsonnet
+++ b/kube/kube.upstream.libsonnet
@@ -53,6 +53,13 @@
// (client-side, and gives better line information).
{
+ // resource contructors will use kinds/versions/fields compatible at least with version:
+ minKubeVersion: {
+ major: 1,
+ minor: 9,
+ version: "%s.%s" % [self.major, self.minor],
+ },
+
// Returns array of values from given object. Does not include hidden fields.
objectValues(o):: [o[field] for field in std.objectFields(o)],
@@ -111,6 +118,8 @@
std.join("", [remapChar(c, "a", "z", "A") for c in std.stringChars(s)])
),
+ boolXor(x, y):: ((if x then 1 else 0) + (if y then 1 else 0) == 1),
+
_Object(apiVersion, kind, name):: {
local this = self,
apiVersion: apiVersion,
@@ -164,6 +173,7 @@
ports: [
{
port: service.port,
+ name: service.target_pod.spec.containers[0].ports[0].name,
targetPort: service.target_pod.spec.containers[0].ports[0].containerPort,
},
],
@@ -203,6 +213,7 @@
},
},
accessModes: ["ReadWriteOnce"],
+ [if pvc.storageClass != null then "storageClassName"]: pvc.storageClass,
},
},
@@ -212,7 +223,15 @@
imagePullPolicy: if std.endsWith(self.image, ":latest") then "Always" else "IfNotPresent",
envList(map):: [
- if std.type(map[x]) == "object" then { name: x, valueFrom: map[x] } else { name: x, value: map[x] }
+ if std.type(map[x]) == "object"
+ then {
+ name: x,
+ valueFrom: map[x],
+ } else {
+ // Let `null` value stay as such (vs string-ified)
+ name: x,
+ value: if map[x] == null then null else std.toString(map[x]),
+ }
for x in std.objectFields(map)
],
@@ -237,7 +256,10 @@
local this = self,
target_pod:: error "target_pod required",
spec: {
- minAvailable: 1,
+ assert $.boolXor(
+ std.objectHas(self, "minAvailable"),
+ std.objectHas(self, "maxUnavailable")
+ ) : "PDB '%s': exactly one of minAvailable/maxUnavailable required" % name,
selector: {
matchLabels: this.target_pod.metadata.labels,
},
@@ -255,7 +277,10 @@
containers_:: {},
local container_names_ordered = [self.default_container] + [n for n in container_names if n != self.default_container],
- containers: [{ name: $.hyphenate(name) } + self.containers_[name] for name in container_names_ordered if self.containers_[name] != null],
+ containers: (
+ assert std.length(self.containers_) > 0 : "Pod must have at least one container (via containers_ map)";
+ [{ name: $.hyphenate(name) } + self.containers_[name] for name in container_names_ordered if self.containers_[name] != null]
+ ),
// Note initContainers are inherently ordered, and using this
// named object will lose that ordering. If order matters, then
@@ -272,7 +297,7 @@
terminationGracePeriodSeconds: 30,
- assert std.length(self.containers) > 0 : "must have at least one container",
+ assert std.length(self.containers) > 0 : "Pod must have at least one container (via containers array)",
// Return an array of pod's ports numbers
ports(proto):: [
@@ -330,7 +355,7 @@
// subtype of EnvVarSource
ConfigMapRef(configmap, key): {
- assert std.objectHas(configmap.data, key) : "%s not in configmap.data" % [key],
+ assert std.objectHas(configmap.data, key) : "ConfigMap '%s' doesn't have '%s' field in configmap.data" % [configmap.metadata.name, key],
configMapKeyRef: {
name: configmap.metadata.name,
key: key,
@@ -347,7 +372,7 @@
// subtype of EnvVarSource
SecretKeyRef(secret, key): {
- assert std.objectHas(secret.data, key) : "%s not in secret.data" % [key],
+ assert std.objectHas(secret.data, key) : "Secret '%s' doesn't have '%s' field in secret.data" % [secret.metadata.name, key],
secretKeyRef: {
name: secret.metadata.name,
key: key,
@@ -370,7 +395,7 @@
},
},
- Deployment(name): $._Object("apps/v1beta2", "Deployment", name) {
+ Deployment(name): $._Object("apps/v1", "Deployment", name) {
local deployment = self,
spec: {
@@ -412,8 +437,10 @@
// NB: Upstream default is 0
minReadySeconds: 30,
+ // NB: Regular k8s default is to keep all revisions
+ revisionHistoryLimit: 10,
+
replicas: 1,
- assert self.replicas >= 0,
},
},
@@ -438,7 +465,7 @@
},
},
- StatefulSet(name): $._Object("apps/v1beta2", "StatefulSet", name) {
+ StatefulSet(name): $._Object("apps/v1", "StatefulSet", name) {
local sset = self,
spec: {
@@ -490,7 +517,6 @@
},
},
- // NB: kubernetes >= 1.8.x has batch/v1beta1 (olders were batch/v2alpha1)
CronJob(name): $._Object("batch/v1beta1", "CronJob", name) {
local cronjob = self,
@@ -504,7 +530,6 @@
},
},
},
-
schedule: error "Need to provide spec.schedule",
successfulJobsHistoryLimit: 10,
failedJobsHistoryLimit: 20,
@@ -521,16 +546,11 @@
restartPolicy: "OnFailure",
},
},
-
- selector: {
- matchLabels: this.template.metadata.labels,
- },
-
completions: 1,
parallelism: 1,
},
- DaemonSet(name): $._Object("apps/v1beta2", "DaemonSet", name) {
+ DaemonSet(name): $._Object("apps/v1", "DaemonSet", name) {
local ds = self,
spec: {
updateStrategy: {
@@ -553,7 +573,7 @@
},
},
- Ingress(name): $._Object("extensions/v1beta1", "Ingress", name) {
+ Ingress(name): $._Object("networking.k8s.io/v1beta1", "Ingress", name) {
spec: {},
local rel_paths = [
@@ -635,18 +655,14 @@
kind: "ClusterRoleBinding",
},
- // NB: datalines_ can be used to reduce boilerplate importstr as:
- // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.data > mysec-ssdata.txt
- // datalines_: importstr "mysec-ssddata.txt"
+ // NB: encryptedData can be imported into a SealedSecret as follows:
+ // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.encryptedData > sealedsecret.json
+ // encryptedData: std.parseJson(importstr "sealedsecret.json")
SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) {
spec: {
- data:
- if self.datalines_ != ""
- then std.join("", std.split(self.datalines_, "\n"))
- else error "data or datalines_ required (output from: kubeseal | jq -r .spec.data)",
- datalines_:: "",
+ encryptedData: {},
},
- assert std.base64Decode(self.spec.data) != "",
+ assert std.length(std.objectFields(self.spec.encryptedData)) != 0 : "SealedSecret '%s' has empty encryptedData field" % name,
},
// NB: helper method to access several Kubernetes objects podRef,
@@ -697,6 +713,36 @@
ingress_:: {},
egress: $.objectValues(self.egress_),
egress_:: {},
+ podSelector: {},
+ },
+ },
+
+ VerticalPodAutoscaler(name):: $._Object("autoscaling.k8s.io/v1beta2", "VerticalPodAutoscaler", name) {
+ local vpa = self,
+
+ target:: error "target required",
+
+ spec: {
+ targetRef: $.CrossVersionObjectReference(vpa.target),
+
+ updatePolicy: {
+ updateMode: "Auto",
+ },
+ },
+ },
+ // Helper function to ease VPA creation as e.g.:
+ // foo_vpa:: kube.createVPAFor($.foo_deploy)
+ createVPAFor(target, mode="Auto"):: $.VerticalPodAutoscaler(target.metadata.name) {
+ target:: target,
+
+ metadata+: {
+ namespace: target.metadata.namespace,
+ labels+: target.metadata.labels,
+ },
+ spec+: {
+ updatePolicy+: {
+ updateMode: mode,
+ },
},
},
}
diff --git a/kube/redis.libsonnet b/kube/redis.libsonnet
index d272227..e596ac2 100644
--- a/kube/redis.libsonnet
+++ b/kube/redis.libsonnet
@@ -12,6 +12,7 @@
appName: error "app name must be set",
storageClassName: "waw-hdd-redundant-1",
prefix: "", # if set, should be 'foo-'
+ password: null,
image: "redis:5.0.4-alpine",
resources: {
@@ -65,13 +66,16 @@
args: [
"redis-server",
"--appendonly", "yes",
- ],
+ ] + (if cfg.password != null then ["--requirepass", "$(REDIS_PASSWORD)"] else []),
ports_: {
client: { containerPort: 6379 },
},
volumeMounts_: {
data: { mountPath: "/data" },
},
+ env_: {
+ [if cfg.password != null then "REDIS_PASSWORD"]: cfg.password,
+ },
resources: cfg.resources,
},
},