ha/matter: remove completed HAOS migration-seed init containers #74
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -6,4 +6,3 @@ resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- network-attachment.yaml
|
||||
- haos-import-script.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
|
||||
|
||||
Reference in New Issue
Block a user