this repo has no description

Add Tangled knot deployment with shared backup infrastructure

Deploy knot server at knot.sans-self.org with SSH ingress on port
2222, daily S3 backup cronjob, and TLS via cert-manager.

Refactor PDS backup to use a shared shell script driven by env vars,
eliminating duplication between PDS and knot backup jobs. Move S3
credentials to k8s/shared/ and generate per-namespace secrets from
the root kustomization.

+422 -66
+108
k8s/knot/backup-cronjob.yaml
··· 1 + apiVersion: batch/v1 2 + kind: CronJob 3 + metadata: 4 + name: knot-backup 5 + namespace: knot 6 + spec: 7 + schedule: "30 2 * * *" 8 + concurrencyPolicy: Forbid 9 + successfulJobsHistoryLimit: 3 10 + failedJobsHistoryLimit: 3 11 + jobTemplate: 12 + spec: 13 + template: 14 + spec: 15 + restartPolicy: OnFailure 16 + securityContext: 17 + fsGroup: 1000 18 + initContainers: 19 + - name: install-sqlite 20 + image: rclone/rclone:1.69 21 + command: 22 + - sh 23 + - -c 24 + - | 25 + apk add --no-cache sqlite > /dev/null 26 + cp /usr/bin/sqlite3 /tools/ 27 + for lib in $(ldd /usr/bin/sqlite3 | awk '/=>/ {print $3}'); do 28 + cp "$lib" /tools/ 29 + done 30 + volumeMounts: 31 + - name: tools 32 + mountPath: /tools 33 + securityContext: 34 + runAsUser: 0 35 + allowPrivilegeEscalation: false 36 + capabilities: 37 + drop: 38 + - ALL 39 + resources: 40 + requests: 41 + cpu: 50m 42 + memory: 64Mi 43 + limits: 44 + cpu: 100m 45 + memory: 128Mi 46 + containers: 47 + - name: backup 48 + image: rclone/rclone:1.69 49 + command: ["sh", "/scripts/backup.sh"] 50 + env: 51 + - name: BACKUP_DB_GLOB 52 + value: "/data/data/knotserver.db" 53 + - name: BACKUP_SYNC_DIRS 54 + value: | 55 + /data/repositories:repositories 56 + - name: BACKUP_BUCKET 57 + value: sans-self-net 58 + - name: BACKUP_PREFIX 59 + value: knot 60 + - name: S3_ACCESS_KEY 61 + valueFrom: 62 + secretKeyRef: 63 + name: knot-s3-credentials 64 + key: access-key 65 + - name: S3_SECRET_KEY 66 + valueFrom: 67 + secretKeyRef: 68 + name: knot-s3-credentials 69 + key: secret-key 70 + volumeMounts: 71 + - name: service-data 72 + mountPath: /data 73 + readOnly: true 74 + - name: tmp 75 + mountPath: /tmp 76 + - name: tools 77 + mountPath: /tools 78 + readOnly: true 79 + - name: scripts 80 + mountPath: /scripts 81 + readOnly: true 82 + securityContext: 83 + runAsUser: 1000 84 + runAsNonRoot: true 85 + allowPrivilegeEscalation: false 86 + capabilities: 87 + drop: 88 + - ALL 89 + resources: 90 + requests: 91 + cpu: 50m 92 + memory: 128Mi 93 + limits: 94 + cpu: 250m 95 + memory: 512Mi 96 + volumes: 97 + - name: service-data 98 + persistentVolumeClaim: 99 + claimName: knot-data 100 + - name: tmp 101 + emptyDir: 102 + sizeLimit: 1Gi 103 + - name: tools 104 + emptyDir: 105 + sizeLimit: 50Mi 106 + - name: scripts 107 + configMap: 108 + name: backup-script
+12
k8s/knot/cert.yaml
··· 1 + apiVersion: cert-manager.io/v1 2 + kind: Certificate 3 + metadata: 4 + name: knot-sans-self-org 5 + namespace: knot 6 + spec: 7 + secretName: knot-sans-self-org-tls 8 + issuerRef: 9 + name: letsencrypt-prod 10 + kind: ClusterIssuer 11 + dnsNames: 12 + - knot.sans-self.org
+14
k8s/knot/configmap.yaml
··· 1 + apiVersion: v1 2 + kind: ConfigMap 3 + metadata: 4 + name: knot-config 5 + namespace: knot 6 + data: 7 + KNOT_SERVER_HOSTNAME: knot.sans-self.org 8 + KNOT_SERVER_OWNER: did:plc:wydyrngmxbcsqdvhmd7whmye 9 + KNOT_SERVER_PORT: "443" 10 + KNOT_SERVER_LISTEN_ADDR: 0.0.0.0:5555 11 + KNOT_SERVER_INTERNAL_LISTEN_ADDR: 0.0.0.0:5444 12 + KNOT_REPO_SCAN_PATH: /home/git/repositories 13 + KNOT_SERVER_DB_PATH: /home/git/data/knotserver.db 14 + APPVIEW_ENDPOINT: https://tangled.org
+67
k8s/knot/deployment.yaml
··· 1 + apiVersion: apps/v1 2 + kind: Deployment 3 + metadata: 4 + name: knot 5 + namespace: knot 6 + spec: 7 + replicas: 1 8 + strategy: 9 + type: Recreate 10 + selector: 11 + matchLabels: 12 + app: knot 13 + template: 14 + metadata: 15 + labels: 16 + app: knot 17 + spec: 18 + terminationGracePeriodSeconds: 30 19 + initContainers: 20 + - name: fix-permissions 21 + image: busybox:1.37 22 + command: ["sh", "-c", "mkdir -p /home/git/repositories /home/git/data && chown -R 1000:1000 /home/git"] 23 + volumeMounts: 24 + - name: data 25 + mountPath: /home/git 26 + securityContext: 27 + runAsUser: 0 28 + runAsNonRoot: false 29 + containers: 30 + - name: knot 31 + image: tngl/knot:v1.10.0-alpha 32 + ports: 33 + - name: http 34 + containerPort: 5555 35 + - name: internal 36 + containerPort: 5444 37 + - name: ssh 38 + containerPort: 22 39 + envFrom: 40 + - configMapRef: 41 + name: knot-config 42 + volumeMounts: 43 + - name: data 44 + mountPath: /home/git 45 + livenessProbe: 46 + httpGet: 47 + path: / 48 + port: 5555 49 + initialDelaySeconds: 15 50 + periodSeconds: 60 51 + readinessProbe: 52 + httpGet: 53 + path: / 54 + port: 5555 55 + initialDelaySeconds: 5 56 + periodSeconds: 10 57 + resources: 58 + requests: 59 + cpu: 100m 60 + memory: 128Mi 61 + limits: 62 + cpu: 500m 63 + memory: 512Mi 64 + volumes: 65 + - name: data 66 + persistentVolumeClaim: 67 + claimName: knot-data
+28
k8s/knot/ingress.yaml
··· 1 + apiVersion: traefik.io/v1alpha1 2 + kind: Middleware 3 + metadata: 4 + name: strip-server-headers 5 + namespace: knot 6 + spec: 7 + headers: 8 + customResponseHeaders: 9 + X-Powered-By: "" 10 + --- 11 + apiVersion: traefik.io/v1alpha1 12 + kind: IngressRoute 13 + metadata: 14 + name: knot 15 + namespace: knot 16 + spec: 17 + entryPoints: 18 + - websecure 19 + tls: 20 + secretName: knot-sans-self-org-tls 21 + routes: 22 + - match: Host(`knot.sans-self.org`) 23 + kind: Rule 24 + middlewares: 25 + - name: strip-server-headers 26 + services: 27 + - name: knot 28 + port: 5555
+14
k8s/knot/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + 4 + resources: 5 + - namespace.yaml 6 + - configmap.yaml 7 + - pvc.yaml 8 + - deployment.yaml 9 + - service.yaml 10 + - ingress.yaml 11 + - cert.yaml 12 + - network-policy.yaml 13 + - ssh-ingress.yaml 14 + - backup-cronjob.yaml
+4
k8s/knot/namespace.yaml
··· 1 + apiVersion: v1 2 + kind: Namespace 3 + metadata: 4 + name: knot
+34
k8s/knot/network-policy.yaml
··· 1 + apiVersion: networking.k8s.io/v1 2 + kind: NetworkPolicy 3 + metadata: 4 + name: knot-ingress 5 + namespace: knot 6 + spec: 7 + podSelector: 8 + matchLabels: 9 + app: knot 10 + policyTypes: 11 + - Ingress 12 + ingress: 13 + # HTTP from Traefik 14 + - from: 15 + - namespaceSelector: 16 + matchLabels: 17 + kubernetes.io/metadata.name: traefik 18 + ports: 19 + - port: 5555 20 + # SSH from Traefik (TCP passthrough) 21 + - from: 22 + - namespaceSelector: 23 + matchLabels: 24 + kubernetes.io/metadata.name: traefik 25 + ports: 26 + - port: 22 27 + # Internal: allow Spindle (future) to reach knot events endpoint 28 + - from: 29 + - namespaceSelector: 30 + matchLabels: 31 + kubernetes.io/metadata.name: knot 32 + ports: 33 + - port: 5555 34 + - port: 5444
+12
k8s/knot/pvc.yaml
··· 1 + apiVersion: v1 2 + kind: PersistentVolumeClaim 3 + metadata: 4 + name: knot-data 5 + namespace: knot 6 + spec: 7 + accessModes: 8 + - ReadWriteOnce 9 + storageClassName: hcloud-volumes 10 + resources: 11 + requests: 12 + storage: 10Gi
+15
k8s/knot/service.yaml
··· 1 + apiVersion: v1 2 + kind: Service 3 + metadata: 4 + name: knot 5 + namespace: knot 6 + spec: 7 + selector: 8 + app: knot 9 + ports: 10 + - name: http 11 + port: 5555 12 + targetPort: 5555 13 + - name: ssh 14 + port: 22 15 + targetPort: 22
+13
k8s/knot/ssh-ingress.yaml
··· 1 + apiVersion: traefik.io/v1alpha1 2 + kind: IngressRouteTCP 3 + metadata: 4 + name: knot-ssh 5 + namespace: knot 6 + spec: 7 + entryPoints: 8 + - knot-ssh 9 + routes: 10 + - match: HostSNI(`*`) 11 + services: 12 + - name: knot 13 + port: 22
+28
k8s/kustomization.yaml
··· 3 3 4 4 resources: 5 5 - pds 6 + - knot 7 + 8 + generatorOptions: 9 + disableNameSuffixHash: true 10 + 11 + configMapGenerator: 12 + - name: backup-script 13 + namespace: pds 14 + files: 15 + - backup.sh=shared/backup.sh 16 + - name: backup-script 17 + namespace: knot 18 + files: 19 + - backup.sh=shared/backup.sh 20 + 21 + secretGenerator: 22 + - name: pds-s3-credentials 23 + namespace: pds 24 + type: Opaque 25 + files: 26 + - access-key=shared/s3-access-key.secret 27 + - secret-key=shared/s3-secret-key.secret 28 + - name: knot-s3-credentials 29 + namespace: knot 30 + type: Opaque 31 + files: 32 + - access-key=shared/s3-access-key.secret 33 + - secret-key=shared/s3-secret-key.secret
+20 -42
k8s/pds/backup-cronjob.yaml
··· 24 24 - | 25 25 apk add --no-cache sqlite > /dev/null 26 26 cp /usr/bin/sqlite3 /tools/ 27 - # Copy dynamically linked libraries sqlite3 needs 28 27 for lib in $(ldd /usr/bin/sqlite3 | awk '/=>/ {print $3}'); do 29 28 cp "$lib" /tools/ 30 29 done ··· 47 46 containers: 48 47 - name: backup 49 48 image: rclone/rclone:1.69 50 - command: 51 - - sh 52 - - -ec 53 - - | 54 - export LD_LIBRARY_PATH=/tools 55 - S3_OPTS="--s3-provider Other --s3-access-key-id ${S3_ACCESS_KEY} --s3-secret-access-key ${S3_SECRET_KEY} --s3-endpoint nbg1.your-objectstorage.com --s3-region nbg1 --s3-no-check-bucket --s3-acl private" 56 - TIMESTAMP=$(date +%Y%m%d-%H%M%S) 57 - 58 - # Copy databases to tmp first to avoid locking the live files 59 - for db in /pds/*.sqlite; do 60 - name=$(basename "$db" .sqlite) 61 - cp "$db" "/tmp/${name}-raw.sqlite" 62 - # Also copy WAL/SHM if present so the copy is consistent 63 - [ -f "${db}-wal" ] && cp "${db}-wal" "/tmp/${name}-raw.sqlite-wal" 64 - [ -f "${db}-shm" ] && cp "${db}-shm" "/tmp/${name}-raw.sqlite-shm" 65 - # Checkpoint the copy to fold WAL into main db 66 - /tools/sqlite3 "/tmp/${name}-raw.sqlite" "PRAGMA wal_checkpoint(TRUNCATE);" 67 - mv "/tmp/${name}-raw.sqlite" "/tmp/${name}-${TIMESTAMP}.sqlite" 68 - rm -f "/tmp/${name}-raw.sqlite-wal" "/tmp/${name}-raw.sqlite-shm" 69 - rclone copyto "/tmp/${name}-${TIMESTAMP}.sqlite" \ 70 - ":s3:sans-self-net/pds/db/${name}-${TIMESTAMP}.sqlite" \ 71 - ${S3_OPTS} 72 - echo "backed up: ${name}-${TIMESTAMP}.sqlite" 73 - done 74 - 75 - # Sync actor repos — only uploads new/changed files 76 - rclone sync /pds/actors \ 77 - :s3:sans-self-net/pds/actors \ 78 - ${S3_OPTS} 79 - echo "synced: actors" 80 - 81 - # Sync media blobs 82 - rclone sync /pds/blocks \ 83 - :s3:sans-self-net/pds/blocks \ 84 - ${S3_OPTS} 85 - echo "synced: blocks" 86 - 87 - echo "backup complete: ${TIMESTAMP}" 49 + command: ["sh", "/scripts/backup.sh"] 88 50 env: 51 + - name: BACKUP_DB_GLOB 52 + value: "/data/*.sqlite" 53 + - name: BACKUP_SYNC_DIRS 54 + value: | 55 + /data/actors:actors 56 + /data/blocks:blocks 57 + - name: BACKUP_BUCKET 58 + value: sans-self-net 59 + - name: BACKUP_PREFIX 60 + value: pds 89 61 - name: S3_ACCESS_KEY 90 62 valueFrom: 91 63 secretKeyRef: ··· 97 69 name: pds-s3-credentials 98 70 key: secret-key 99 71 volumeMounts: 100 - - name: pds-data 101 - mountPath: /pds 72 + - name: service-data 73 + mountPath: /data 102 74 readOnly: true 103 75 - name: tmp 104 76 mountPath: /tmp 105 77 - name: tools 106 78 mountPath: /tools 107 79 readOnly: true 80 + - name: scripts 81 + mountPath: /scripts 82 + readOnly: true 108 83 securityContext: 109 84 runAsUser: 1000 110 85 runAsNonRoot: true ··· 120 95 cpu: 250m 121 96 memory: 512Mi 122 97 volumes: 123 - - name: pds-data 98 + - name: service-data 124 99 persistentVolumeClaim: 125 100 claimName: pds-data 126 101 - name: tmp ··· 129 104 - name: tools 130 105 emptyDir: 131 106 sizeLimit: 50Mi 107 + - name: scripts 108 + configMap: 109 + name: backup-script
-6
k8s/pds/kustomization.yaml
··· 24 24 - PDS_ADMIN_PASSWORD=admin-password.secret 25 25 - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=plc-rotation-key.secret 26 26 - PDS_EMAIL_SMTP_URL=smtp-url.secret 27 - - name: pds-s3-credentials 28 - namespace: pds 29 - type: Opaque 30 - files: 31 - - access-key=s3-access-key.secret 32 - - secret-key=s3-secret-key.secret
k8s/pds/s3-access-key.secret k8s/shared/s3-access-key.secret
k8s/pds/s3-secret-key.secret k8s/shared/s3-secret-key.secret
+40
k8s/shared/backup.sh
··· 1 + #!/bin/sh 2 + set -eu 3 + 4 + export LD_LIBRARY_PATH=/tools 5 + S3_OPTS="--s3-provider Other --s3-access-key-id ${S3_ACCESS_KEY} --s3-secret-access-key ${S3_SECRET_KEY} --s3-endpoint nbg1.your-objectstorage.com --s3-region nbg1 --s3-no-check-bucket --s3-acl private" 6 + TIMESTAMP=$(date +%Y%m%d-%H%M%S) 7 + 8 + # Back up SQLite databases matching BACKUP_DB_GLOB (e.g. "/data/*.sqlite") 9 + # Uses copy-then-checkpoint to avoid write-locking the live database 10 + for db in ${BACKUP_DB_GLOB}; do 11 + [ -f "$db" ] || continue 12 + base=$(basename "$db") 13 + name="${base%.*}" 14 + ext="${base##*.}" 15 + 16 + cp "$db" "/tmp/${name}-raw.${ext}" 17 + [ -f "${db}-wal" ] && cp "${db}-wal" "/tmp/${name}-raw.${ext}-wal" 18 + [ -f "${db}-shm" ] && cp "${db}-shm" "/tmp/${name}-raw.${ext}-shm" 19 + 20 + /tools/sqlite3 "/tmp/${name}-raw.${ext}" "PRAGMA wal_checkpoint(TRUNCATE);" 21 + 22 + mv "/tmp/${name}-raw.${ext}" "/tmp/${name}-${TIMESTAMP}.${ext}" 23 + rm -f "/tmp/${name}-raw.${ext}-wal" "/tmp/${name}-raw.${ext}-shm" 24 + 25 + rclone copyto "/tmp/${name}-${TIMESTAMP}.${ext}" \ 26 + ":s3:${BACKUP_BUCKET}/${BACKUP_PREFIX}/db/${name}-${TIMESTAMP}.${ext}" \ 27 + ${S3_OPTS} 28 + echo "backed up: ${name}-${TIMESTAMP}.${ext}" 29 + done 30 + 31 + # Sync directories listed in BACKUP_SYNC_DIRS (newline-separated "local_path:remote_suffix") 32 + echo "${BACKUP_SYNC_DIRS}" | while IFS=: read -r src dest; do 33 + [ -z "$src" ] && continue 34 + rclone sync "$src" \ 35 + ":s3:${BACKUP_BUCKET}/${BACKUP_PREFIX}/${dest}" \ 36 + ${S3_OPTS} 37 + echo "synced: ${dest}" 38 + done 39 + 40 + echo "backup complete: ${TIMESTAMP}"
+13 -18
kube.tf
··· 427 427 # Example: 428 428 # traefik_additional_ports = [{name = "example", port = 1234, exposedPort = 1234}] 429 429 430 + # Tangled knot SSH access (git clone ssh://git@knot.sans-self.org:2222/...) 431 + traefik_additional_ports = [{ name = "knot-ssh", port = 2222, exposedPort = 2222 }] 432 + 430 433 # If you want to configure additional trusted IPs for traefik, enter them here as a list of IPs (strings). 431 434 # Example for Cloudflare: 432 435 # traefik_additional_trusted_ips = [ ··· 652 655 653 656 # Adding extra firewall rules, like opening a port 654 657 # More info on the format here https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/firewall 655 - # extra_firewall_rules = [ 656 - # { 657 - # description = "For Postgres" 658 - # direction = "in" 659 - # protocol = "tcp" 660 - # port = "5432" 661 - # source_ips = ["0.0.0.0/0", "::/0"] 662 - # destination_ips = [] # Won't be used for this rule 663 - # }, 664 - # { 665 - # description = "To Allow ArgoCD access to resources via SSH" 666 - # direction = "out" 667 - # protocol = "tcp" 668 - # port = "22" 669 - # source_ips = [] # Won't be used for this rule 670 - # destination_ips = ["0.0.0.0/0", "::/0"] 671 - # } 672 - # ] 658 + extra_firewall_rules = [ 659 + { 660 + description = "Tangled knot Git SSH" 661 + direction = "in" 662 + protocol = "tcp" 663 + port = "2222" 664 + source_ips = ["0.0.0.0/0", "::/0"] 665 + destination_ips = [] 666 + } 667 + ] 673 668 674 669 # If you want to configure a different CNI for k3s, use this flag 675 670 # possible values: flannel (Default), calico, and cilium