diff --git a/kubernetes/apps/home-assistant/deployment.yaml b/kubernetes/apps/home-assistant/deployment.yaml index 805397dc..8ea3a6a6 100644 --- a/kubernetes/apps/home-assistant/deployment.yaml +++ b/kubernetes/apps/home-assistant/deployment.yaml @@ -30,126 +30,6 @@ spec: operator: Equal value: pinned effect: NoSchedule - initContainers: - # Stage the HAOS backup tarball into /config/backups/ and write - # /config/.HA_RESTORE so HA Core's native backup_restore.py performs - # the restore on its own startup. We don't extract anything here — - # HA decrypts + restores during its boot, then writes - # /config/.HA_RESTORE_RESULT (the trusted-proxies sidecar waits - # for that signal). - # - # Lives in the app pod (not a separate Job) so the RWO Ceph-RBD - # PVC is shared via the same kubelet mount — Multi-Attach is - # impossible by construction. Necessary now that production may - # have several edge nodes; the prior seed Job + manual `zone:edge` - # mirror only worked while there was a single edge node guaranteeing - # co-location with the app pod. - # - # Idempotency sentinel under /config/backups/ (the only dir HA's - # restore preserves), with a name HA will never write itself — so - # we can't be fooled by a partial-restore-then-default-config - # write of /config/configuration.yaml. - - name: seed - image: alpine:3.21 - command: - - sh - - -c - - | - set -eu - SENTINEL=/config/backups/.k8s-seed-applied - if [ -f "$SENTINEL" ]; then - echo "Sentinel $SENTINEL present — seed already applied." - exit 0 - fi - apk add --no-cache aws-cli jq ca-certificates >/dev/null - export HOME=/tmp AWS_ENDPOINT_URL="$S3_ENDPOINT" AWS_DEFAULT_REGION=garage - mkdir -p "$HOME/.aws" - printf '[default]\ns3 =\n addressing_style = path\n' > "$HOME/.aws/config" - - # NB: list with $S3_PREFIX (no trailing slash). The HACS - # `s3_compatible` uploader on legacy HA writes newer backups - # as `home-assistantAutomatic_backup_…tar` — concatenated - # prefix+filename without a `/` separator (older uploads - # had the slash, e.g. `home-assistant/Automatic_backup_…`). - # A `home-assistant/` filter would only see the older - # uploads; a `home-assistant` filter sees both. No other - # objects in this bucket start with `home-assistant`, so - # the looser prefix doesn't introduce false matches. - echo "Waiting for HAOS backup under s3://$S3_BUCKET/$S3_PREFIX* ..." - BACKUP_KEY="" - for i in $(seq 1 1080); do - BACKUP_KEY=$(aws s3api list-objects-v2 \ - --bucket "$S3_BUCKET" --prefix "$S3_PREFIX" --output json 2>/dev/null \ - | jq -r ' - (.Contents // []) - | map(select(.Key | endswith(".tar"))) - | sort_by(.LastModified) - | (last // {}).Key // "" - ') - if [ -n "$BACKUP_KEY" ]; then - echo "Newest backup: $BACKUP_KEY" - break - fi - echo " none yet ($i/1080) — sleeping 10s"; sleep 10 - if [ "$i" = "1080" ]; then - echo "ERROR: no backup appeared after 3 hours"; exit 1 - fi - done - - mkdir -p /config/backups - BACKUP_FILE="/config/backups/$(basename "$BACKUP_KEY")" - - if [ -f "$BACKUP_FILE" ]; then - echo "Backup already staged at $BACKUP_FILE (re-run); skipping download." - else - echo "Streaming backup → $BACKUP_FILE ..." - aws s3 cp "s3://$S3_BUCKET/$BACKUP_KEY" "$BACKUP_FILE" - fi - - if [ -z "${HA_BACKUP_PASSWORD:-}" ] || [ "${HA_BACKUP_PASSWORD}" = "REPLACE_ME_WITH_HA_BACKUP_PASSWORD" ]; then - echo "ERROR: HA_BACKUP_PASSWORD not set or still the placeholder." - exit 1 - fi - - echo "Writing /config/.HA_RESTORE manifest ..." - # Schema per homeassistant/backup_restore.py: - # path, password, remove_after_restore, restore_database, restore_homeassistant - jq -n \ - --arg path "$BACKUP_FILE" \ - --arg pw "$HA_BACKUP_PASSWORD" \ - '{path: $path, password: $pw, remove_after_restore: false, restore_database: true, restore_homeassistant: true}' \ - > /config/.HA_RESTORE - - chmod 600 /config/.HA_RESTORE - - touch "$SENTINEL" - echo "Done. HA will perform the restore on next startup." - ls -la /config - env: - - name: S3_ENDPOINT - value: "https://s3.nas-1.renner.uno" - - name: S3_BUCKET - value: "k8s-migration" - - name: S3_PREFIX - value: "home-assistant" - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: garage-migration-creds - key: AWS_ACCESS_KEY_ID - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: garage-migration-creds - key: AWS_SECRET_ACCESS_KEY - - name: HA_BACKUP_PASSWORD - valueFrom: - secretKeyRef: - name: home-assistant-backup-password - key: backup-password - volumeMounts: - - name: config - mountPath: /config containers: - name: home-assistant image: ghcr.io/home-assistant/home-assistant:2026.6.4 # {"$imagepolicy": "flux-system:home-assistant"} diff --git a/kubernetes/apps/home-assistant/garage-migration-creds.sops.yaml b/kubernetes/apps/home-assistant/garage-migration-creds.sops.yaml deleted file mode 100644 index 986744b7..00000000 --- a/kubernetes/apps/home-assistant/garage-migration-creds.sops.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: ENC[AES256_GCM,data:UCE=,iv:Sb1RKkCPMn9/JGwweTfU+K7vTuPyiHIwRkpFeM3Nmic=,tag:qWr9HyEsVS/Oncm+sppEGw==,type:str] -kind: ENC[AES256_GCM,data:h4LllnCZ,iv:sjf5XV2OD6wlvnlPLGYrfPMllXE6ORB4t/Gw9s/jDGI=,tag:n0ieyCsjp/F4zRU2IuH2dQ==,type:str] -metadata: - name: ENC[AES256_GCM,data:I1tg5Yg8ei78x5T3kzcMlTnyxCNkuw==,iv:IgTYTERWRBmMIHOGGOvwLrf0abSmbnAfKTZQFaLOxeI=,tag:okjjWlCTGIx4f7gibToZkA==,type:str] - namespace: ENC[AES256_GCM,data:6yrtXH8FLSEJhoFQ05M=,iv:XldOjworGBXl6tAN3feodVEDNyf5Cq7dvHWmqlbO4jE=,tag:Mmn+7YJ1czynDAk0rA3G/Q==,type:str] -type: ENC[AES256_GCM,data:FcD8FKHj,iv:k8y8EspY59lA3dfc2EaP0RKwFoSn48rxUqcKuRnQkNU=,tag:5o/7y5OpOeH6aSN/qyXt2w==,type:str] -stringData: - AWS_ACCESS_KEY_ID: ENC[AES256_GCM,data:AXrSeWOVcf8Ejejq9TlQQF/SIUcWyRG1lkw=,iv:Ts1ACdmZdU6lmSHwo+o50gI50Nr5FzF3Qav2PH99rBQ=,tag:/Mgfme9+qlSLQTFohSasFQ==,type:str] - AWS_SECRET_ACCESS_KEY: ENC[AES256_GCM,data:kVpzs+fJCNap6BX2xeYCb09Tz5BpOwl+FJ+rdQobqmOXr9WKcXTqVoxqTmEztl70VwnCCprMFYLonemrPc+HKw==,iv:4Zs+OfjcWVJbebgXk4C9HNa0bsw5ouSWgQRhxjA9WaM=,tag:MNivqJSBeaIv7GFxmXsbkA==,type:str] -sops: - age: - - recipient: age1yqjvqkwwwxdj7mr546zfs5ckzuwndp69ya2n2drd4vypwns8w4ys04qfy3 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMGQ5K2h3ZnlScmVsR1Bo - ODA4UVNhYnkrNzk0K1NwMHZMQWoyUkcyZVJJCkRnTDNLb1hZWVE0R2Fza1pTZFZ6 - VGRtTXdYc1VOQnFVSUJnamlDTitrZEkKLS0tIGtvbXU4ekpjZ1RWaW1FamxXOG9a - emdyVmtwZ2Y3aUsvcHg1R0trbTQ4SGMKSbNFTTUX13cepErkmF2J+VqbM6cKFFjj - vWLJeShGziD9TSuzCvFoiGKhLv15FF2bDaI5YakFDdvB1jC6n5rCxA== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-24T16:13:25Z" - mac: ENC[AES256_GCM,data:1vH6n6wzkoEbGGJlmB9kFi5XaViQHhNlNCx3srkql7DsF3PCrajd/eO5Y+vBkO8LBoTgh4cH53lThhQqpPEkOX1Ek/nz17aTg0YN4ocDOn/K224Ck9bisl6Uz3GzyEuOrPidVzNoKwmWR4Xc33glUN4DH0TRN4HKSIvAh/v/pgo=,iv:R+bu2j/SWaks0n0sFjtzX6DLSy9Sf8o5CtBXfxMBdtk=,tag:TeOBTzjB+5uS29i0SeNzhw==,type:str] - unencrypted_suffix: _unencrypted - version: 3.10.2 diff --git a/kubernetes/apps/home-assistant/kustomization.yaml b/kubernetes/apps/home-assistant/kustomization.yaml index dc7bc0de..9f2f2e1c 100644 --- a/kubernetes/apps/home-assistant/kustomization.yaml +++ b/kubernetes/apps/home-assistant/kustomization.yaml @@ -1,7 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - garage-migration-creds.sops.yaml - backup-password.sops.yaml - pvcs.yaml - deployment.yaml diff --git a/kubernetes/apps/matter-server/deployment.yaml b/kubernetes/apps/matter-server/deployment.yaml index f36a3ca7..d30c8e01 100644 --- a/kubernetes/apps/matter-server/deployment.yaml +++ b/kubernetes/apps/matter-server/deployment.yaml @@ -36,119 +36,6 @@ spec: operator: Equal value: pinned effect: NoSchedule - initContainers: - # One-shot import of matter-server controller state from the HAOS - # supervisor backup that's already in s3://k8s-migration/home- - # assistant/ for HA Core's restore. Same shape as HA's `seed` - # init: sentinel-guarded idempotency, stream-from-S3, alpine + - # apk for tooling. The addon archive (`core_matter_server.tar.gz`) - # is HA-securetar-encrypted; haos-import-script ConfigMap carries - # the small Python helper that the apk-installed alpine doesn't - # have a dedicated CLI for. - - name: seed - image: alpine:3.21 - command: - - sh - - -c - - | - set -eu - SENTINEL=/data/.haos-import-applied - if [ -f "$SENTINEL" ]; then - echo "Sentinel $SENTINEL present — already imported." - exit 0 - fi - - apk add --no-cache aws-cli jq ca-certificates py3-pip tar coreutils >/dev/null - pip install --break-system-packages --quiet securetar - - export HOME=/tmp AWS_ENDPOINT_URL="$S3_ENDPOINT" AWS_DEFAULT_REGION=garage - mkdir -p "$HOME/.aws" - printf '[default]\ns3 =\n addressing_style = path\n' > "$HOME/.aws/config" - - echo "Looking for HAOS backup under s3://$S3_BUCKET/$S3_PREFIX/ ..." - BACKUP_KEY=$(aws s3api list-objects-v2 \ - --bucket "$S3_BUCKET" --prefix "$S3_PREFIX/" --output json 2>/dev/null \ - | jq -r ' - (.Contents // []) - | map(select(.Key | endswith(".tar"))) - | sort_by(.LastModified) - | (last // {}).Key // "" - ') - if [ -z "$BACKUP_KEY" ]; then - echo "ERROR: no backup found under s3://$S3_BUCKET/$S3_PREFIX/" - exit 1 - fi - echo "Newest backup: $BACKUP_KEY" - - # The outer backup tar isn't encrypted — pull just the - # 4.5 MB matter addon archive out of the S3 stream rather - # than staging the full 1 GB tar to disk. - mkdir -p /tmp/work - echo "Streaming core_matter_server.tar.gz out of the backup ..." - aws s3 cp "s3://$S3_BUCKET/$BACKUP_KEY" - \ - | tar -xO core_matter_server.tar.gz > /tmp/work/core_matter_server.tar.gz - ls -la /tmp/work/ - - # The addon archive is securetar-encrypted; decrypt via the - # mounted helper. - mkdir -p /tmp/work/extracted - python3 /scripts/import.py \ - /tmp/work/core_matter_server.tar.gz \ - /tmp/work/extracted \ - "$HA_BACKUP_PASSWORD" - echo "Extracted addon archive contents:" - ls -la /tmp/work/extracted - - # HAOS addon backups put the addon's persistent /data - # contents under a `data/` subdirectory; locate it via the - # canonical filename rather than assuming a fixed layout. - SRC=$(find /tmp/work/extracted -name chip.json -type f -print -quit) - if [ -z "$SRC" ]; then - echo "ERROR: chip.json not found in extracted addon archive" - find /tmp/work/extracted -maxdepth 3 -type f - exit 1 - fi - SRC_DIR=$(dirname "$SRC") - echo "Found addon /data at: $SRC_DIR" - - # Replace the freshly-initialised /data with the imported - # state. The new pod's first boot wrote a baseline - # chip.json + empty fabric file; they must be overwritten - # so the imported fabric and security counters take effect. - cp -aT "$SRC_DIR" /data - echo "Files in /data after import:" - ls -la /data - - touch "$SENTINEL" - echo "Done. Sentinel $SENTINEL written." - env: - - name: S3_ENDPOINT - value: "https://s3.nas-1.renner.uno" - - name: S3_BUCKET - value: "k8s-migration" - - name: S3_PREFIX - value: "home-assistant" - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: garage-migration-creds - key: AWS_ACCESS_KEY_ID - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: garage-migration-creds - key: AWS_SECRET_ACCESS_KEY - - name: HA_BACKUP_PASSWORD - valueFrom: - secretKeyRef: - name: home-assistant-backup-password - key: backup-password - volumeMounts: - - name: data - mountPath: /data - - name: import-script - mountPath: /scripts - readOnly: true containers: - name: matter-server # Use the HAOS-published image from `ghcr.io/matter-js/...`, @@ -193,6 +80,3 @@ spec: - name: data persistentVolumeClaim: claimName: matter-server-data - - name: import-script - configMap: - name: matter-server-haos-import-script diff --git a/kubernetes/apps/matter-server/haos-import-script.yaml b/kubernetes/apps/matter-server/haos-import-script.yaml deleted file mode 100644 index 8665fd7e..00000000 --- a/kubernetes/apps/matter-server/haos-import-script.yaml +++ /dev/null @@ -1,28 +0,0 @@ ---- -# Decryption helper for the HAOS supervisor-backup matter-server addon -# archive. The outer backup tar is plain — `tar -xO` can pull -# `core_matter_server.tar.gz` straight out of an S3 stream — but each -# inner addon archive is HA's securetar format (magic bytes -# `SecureTar\x03`), which only the `securetar` Python lib can decrypt. -# Mounted into the matter-server seed init container and invoked once, -# guarded by the sentinel at /data/.haos-import-applied. -apiVersion: v1 -kind: ConfigMap -metadata: - name: matter-server-haos-import-script - namespace: home-assistant -data: - import.py: | - """Decrypt a securetar-encrypted addon archive and extract it.""" - import sys - from pathlib import Path - - from securetar import SecureTarFile - - src = Path(sys.argv[1]) - dst = Path(sys.argv[2]) - password = sys.argv[3] - - dst.mkdir(parents=True, exist_ok=True) - with SecureTarFile(src, password=password, gzip=True) as tar: - tar.extractall(dst) diff --git a/kubernetes/apps/matter-server/kustomization.yaml b/kubernetes/apps/matter-server/kustomization.yaml index 29fd390d..05b00050 100644 --- a/kubernetes/apps/matter-server/kustomization.yaml +++ b/kubernetes/apps/matter-server/kustomization.yaml @@ -6,4 +6,3 @@ resources: - deployment.yaml - service.yaml - network-attachment.yaml - - haos-import-script.yaml diff --git a/kubernetes/clusters/homelab/home-assistant.yaml b/kubernetes/clusters/homelab/home-assistant.yaml index 903a90b2..39044671 100644 --- a/kubernetes/clusters/homelab/home-assistant.yaml +++ b/kubernetes/clusters/homelab/home-assistant.yaml @@ -76,10 +76,6 @@ spec: - name: infrastructure - name: infrastructure-crds - name: infrastructure-ceph-cluster - # The seed initContainer mounts `garage-migration-creds`, which is owned - # by the home-assistant Kustomization. Without this dependency the init - # container CreateContainerConfigError-loops until HA's KS lands first. - - name: home-assistant --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization