provisioning-catalog/components/cert_manager/cluster/templates/tls-backup.yaml.j2

225 lines
6.8 KiB
Django/Jinja

{% if tls_backup_enabled %}---
apiVersion: v1
kind: ServiceAccount
metadata:
name: tls-backup
namespace: {{ taskserv.namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tls-backup-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
- apiGroups: ["cert-manager.io"]
resources: ["certificates", "clusterissuers"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tls-backup-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tls-backup-reader
subjects:
- kind: ServiceAccount
name: tls-backup
namespace: {{ taskserv.namespace }}
---
apiVersion: v1
kind: Secret
metadata:
name: restic-tls
namespace: {{ taskserv.namespace }}
type: Opaque
stringData:
RESTIC_REPOSITORY: "{{ restic_repository }}"
RESTIC_PASSWORD: "{{ restic_password }}"
AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}"
AWS_DEFAULT_REGION: "{{ aws_default_region }}"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tls-backup-watcher
namespace: {{ taskserv.namespace }}
data:
watch.nu: |
#!/usr/bin/env nu
$env.PATH = ($env.PATH | append "/kubectl-bin")
const CERT_NS = "cert-manager"
def dump_all [] {
print "[tls-backup] dumping TLS secrets"
let r = (do { ^kubectl get secrets -A -l cert-manager.io/certificate-name -o yaml } | complete)
if $r.exit_code == 0 {
$r.stdout | save --force /dump/tls-secrets.yaml
} else {
print $"[tls-backup] warn: secrets dump failed: ($r.stderr | str trim)"
}
print "[tls-backup] dumping ACME keys"
let ri = (do { ^kubectl get clusterissuers -o json } | complete)
if $ri.exit_code == 0 {
$ri.stdout | from json | get -o items | default []
| each {|i| $i | get -o spec.acme.privateKeySecretRef.name | default ""}
| where {|k| $k | is-not-empty}
| each {|key|
let rk = (do { ^kubectl get secret -n $CERT_NS $key -o yaml } | complete)
if $rk.exit_code == 0 { $rk.stdout | save --force $"/dump/acme-key--($key).yaml" }
}
| ignore
}
"" | save --force /trigger/ready
print "[tls-backup] backup trigger written"
}
def process_event [line: string] {
let parts = ($line | split row ' ')
if ($parts | length) < 3 { return }
let evtype = ($parts | get 0? | default "")
let ns = ($parts | get 1? | default "")
let name = ($parts | get 2? | default "")
if $evtype != "MODIFIED" { return }
let fmt = 'jsonpath={.status.conditions[?(@.type=="Ready")].status}'
let r = (do { ^kubectl get certificate $name -n $ns -o $fmt } | complete)
if ($r.stdout | str trim) == "True" {
print $"[tls-backup] ($ns)/($name) Ready"
dump_all
}
}
print "[tls-backup] watcher started"
loop {
print "[tls-backup] watching certificates"
let watch_fmt = 'jsonpath={.type} {.object.metadata.namespace} {.object.metadata.name}{"\n"}'
try {
^kubectl get certificates -A -w --output-watch-events=true -o $watch_fmt
| lines
| where {|l| $l | str trim | is-not-empty}
| each {|line| process_event $line; null}
| ignore
} catch {|e|
print $"[tls-backup] watch ended: ($e.msg)"
}
sleep 10sec
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tls-backup-watcher
namespace: {{ taskserv.namespace }}
spec:
replicas: 1
selector:
matchLabels:
app: tls-backup-watcher
template:
metadata:
labels:
app: tls-backup-watcher
spec:
serviceAccountName: tls-backup
initContainers:
- name: install-kubectl
image: alpine:3.21
command: ["/bin/sh", "-c"]
args:
- |
set -euo pipefail
apk add --no-cache curl
ARCH=$(uname -m)
case "$ARCH" in x86_64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; esac
curl -fsSL https://dl.k8s.io/release/{{ tls_backup_kubectl_version }}/bin/linux/${ARCH}/kubectl \
-o /kubectl-bin/kubectl
chmod +x /kubectl-bin/kubectl
echo "[tls-backup] kubectl installed (arch=${ARCH})"
volumeMounts:
- name: kubectl-bin
mountPath: /kubectl-bin
volumes:
- name: kubectl-bin
emptyDir: {}
- name: dump
emptyDir: {}
- name: trigger
emptyDir: {}
- name: scripts
configMap:
name: tls-backup-watcher
defaultMode: 0755
containers:
- name: watcher
image: {{ tls_backup_nushell_image }}
command: ["nu", "/scripts/watch.nu"]
volumeMounts:
- name: kubectl-bin
mountPath: /kubectl-bin
- name: dump
mountPath: /dump
- name: trigger
mountPath: /trigger
- name: scripts
mountPath: /scripts
- name: backup
image: restic/restic:0.17.3
env:
- name: RESTIC_REPOSITORY
valueFrom:
secretKeyRef:
name: restic-tls
key: RESTIC_REPOSITORY
- name: RESTIC_PASSWORD
valueFrom:
secretKeyRef:
name: restic-tls
key: RESTIC_PASSWORD
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: restic-tls
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: restic-tls
key: AWS_SECRET_ACCESS_KEY
- name: AWS_DEFAULT_REGION
valueFrom:
secretKeyRef:
name: restic-tls
key: AWS_DEFAULT_REGION
volumeMounts:
- name: dump
mountPath: /dump
readOnly: true
- name: trigger
mountPath: /trigger
command: ["/bin/sh", "-c"]
args:
- |
set -euo pipefail
restic snapshots >/dev/null 2>&1 || restic init
echo "[tls-backup] backup container ready, watching for trigger"
while true; do
if [ -f /trigger/ready ]; then
echo "[tls-backup] trigger detected, running backup"
restic backup /dump \
--tag tls --tag workspace=libre-wuji \
--host libre-wuji-cluster
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune
rm -f /trigger/ready
echo "[tls-backup] backup complete"
fi
sleep 5
done
{% else %}# tls-backup: not enabled (set tls_backup.enabled = true in component config){% endif %}