diff --git a/kubernetes/apps/home-assistant/deployment.yaml b/kubernetes/apps/home-assistant/deployment.yaml index 785af4b4..326f09c1 100644 --- a/kubernetes/apps/home-assistant/deployment.yaml +++ b/kubernetes/apps/home-assistant/deployment.yaml @@ -234,6 +234,24 @@ spec: name: mosquitto-credentials key: homeassistant-password optional: true + # Write-only token for the in-cluster InfluxDB suedstr28 + # bucket. Created by the influxdb-ha-token Job in the + # influxdb namespace (kubernetes/seed/influxdb/ + # ha-token-job.yaml) on each fresh deploy and dropped into + # this Secret cross-namespace via RBAC. + # + # optional: HA must come up even if the Job hasn't + # populated the Secret yet (cluster InfluxDB might still + # be reseeding). The InfluxDB reconcile step below skips + # itself when the env is empty; once the Secret appears, + # the next loop iteration patches configuration.yaml and + # self-deletes the pod for a clean rollover. + - name: INFLUX_TOKEN + valueFrom: + secretKeyRef: + name: ha-influxdb-token + key: INFLUX_TOKEN + optional: true # Single switch covering every integration that has to stay # off until cutover (legacy HA still running on VM 103 + IoT # plumbing / USB devices not yet wired). When true, the @@ -412,6 +430,46 @@ spec: fi fi + # InfluxDB recorder reconcile. Top-level `influxdb:` + # block in configuration.yaml gets repointed at the + # in-cluster InfluxDB. Skipped when INFLUX_TOKEN is + # empty (Job in influxdb namespace hasn't populated + # the cross-namespace Secret yet). Once the token + # appears, the next loop iteration patches and + # self-deletes the pod for a clean rollover. Only the + # connection+auth keys are touched; HA-managed + # include/exclude/tags filters are left intact. + if [ -n "${INFLUX_TOKEN:-}" ]; then + desired_influx=$(jq -ncS \ + --arg token "$INFLUX_TOKEN" \ + '{api_version: 2, + host: "influxdb-influxdb2.influxdb.svc.cluster.local", + port: 8086, + ssl: false, + verify_ssl: false, + organization: "renner", + bucket: "suedstr28", + token: $token}') + current_influx=$(yq -o=json '.influxdb // {}' "$CONFIG" 2>/dev/null \ + | jq -cS --argjson d "$desired_influx" \ + 'with_entries(select(.key as $k | $d | has($k)))' \ + || echo '{}') + if [ "$desired_influx" != "$current_influx" ]; then + echo "influxdb config drift; patching" + yq -i ' + .influxdb.api_version = 2 | + .influxdb.host = "influxdb-influxdb2.influxdb.svc.cluster.local" | + .influxdb.port = 8086 | + .influxdb.ssl = false | + .influxdb.verify_ssl = false | + .influxdb.organization = "renner" | + .influxdb.bucket = "suedstr28" | + .influxdb.token = strenv(INFLUX_TOKEN) + ' "$CONFIG" + drift=1 + fi + fi + # Sibling-app integrations: HAOS supervisor addon names # (core-whisper / core-matter-server / …) don't resolve in # the cluster. Repoint each existing entry at the matching diff --git a/kubernetes/seed/influxdb/ha-token-job.yaml b/kubernetes/seed/influxdb/ha-token-job.yaml new file mode 100644 index 00000000..3528ecbb --- /dev/null +++ b/kubernetes/seed/influxdb/ha-token-job.yaml @@ -0,0 +1,162 @@ +--- +# Create a write-only InfluxDB auth scoped to the suedstr28 bucket and +# stash the token as a Secret in the home-assistant namespace, so HA's +# `influxdb:` integration can write to the in-cluster InfluxDB. +# +# Why a Job (not a static SOPS Secret): tokens are persisted in the +# server's bolt store, which is fresh on every rebuild. A SOPS-stored +# token wouldn't actually exist as an auth on the new server; it would +# just be a string. Each rebuild needs to create the auth fresh and +# capture the resulting token. +# +# Why in seed/influxdb/: the seed Flux KS already runs in the right +# phase (after CNPG/Ceph/cnpg-barman, before per-app KSes) and at +# cutover the whole directory moves to apps/influxdb/ along with the +# chart. The Job is part of InfluxDB's initial-state setup. +# +# Cross-namespace Secret write: ServiceAccount lives in influxdb; +# Role+RoleBinding in home-assistant grant just `get/create/update/ +# patch` on the single named Secret. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: influxdb-ha-token + namespace: influxdb +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ha-influxdb-token-writer + namespace: home-assistant +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["ha-influxdb-token"] + verbs: ["get", "create", "update", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ha-influxdb-token-writer + namespace: home-assistant +subjects: + - kind: ServiceAccount + name: influxdb-ha-token + namespace: influxdb +roleRef: + kind: Role + name: ha-influxdb-token-writer + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: influxdb-ha-token + namespace: influxdb + annotations: + # Job specs are immutable; force-recreate so spec changes actually + # apply on Flux re-reconcile. + kustomize.toolkit.fluxcd.io/force: enabled +spec: + backoffLimit: 6 + ttlSecondsAfterFinished: 86400 + template: + spec: + restartPolicy: OnFailure + serviceAccountName: influxdb-ha-token + securityContext: + runAsUser: 0 + runAsNonRoot: false + initContainers: + # Same wait pattern as the seed Job's wait-for-influxd: ensure + # the API is reachable AND the bootstrap admin token is the + # live token (not still falling back to a stale onboarding + # state). Two-stage so we don't try to create an auth before + # the chart's `influx setup` has finished. + - name: wait-for-influxd + image: influxdb:2.7.4 + env: + - { name: INFLUX_HOST, value: http://influxdb-influxdb2.influxdb.svc.cluster.local:8086 } + - { name: INFLUX_ORG, value: renner } + - name: INFLUX_TOKEN + valueFrom: + secretKeyRef: { name: influxdb-bootstrap, key: admin-token } + command: + - sh + - -c + - | + set -eu + until influx ping >/dev/null 2>&1; do + echo "[$(date -u +%H:%M:%S)] waiting for $INFLUX_HOST..." + sleep 5 + done + until influx bucket list >/dev/null 2>&1; do + echo "[$(date -u +%H:%M:%S)] API up; waiting for bootstrap admin token..." + sleep 5 + done + echo "[$(date -u +%H:%M:%S)] $INFLUX_HOST ready and authenticated" + containers: + - name: create + image: influxdb:2.7.4 + env: + - { name: INFLUX_HOST, value: http://influxdb-influxdb2.influxdb.svc.cluster.local:8086 } + - { name: INFLUX_ORG, value: renner } + - { name: INFLUX_BUCKET, value: suedstr28 } + - { name: TOKEN_DESC, value: home-assistant write } + - { name: TARGET_NS, value: home-assistant } + - { name: TARGET_SECRET, value: ha-influxdb-token } + - name: INFLUX_TOKEN + valueFrom: + secretKeyRef: { name: influxdb-bootstrap, key: admin-token } + command: + - bash + - -c + - | + set -euo pipefail + log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"; } + + # Need jq + kubectl. influxdb:2.7.4 has neither baked in. + apt-get update -qq + apt-get install -y --no-install-recommends curl jq ca-certificates >/dev/null + KV=$(curl -fsSL https://dl.k8s.io/release/stable.txt) + curl -fsSL "https://dl.k8s.io/release/$KV/bin/linux/amd64/kubectl" -o /usr/local/bin/kubectl + chmod +x /usr/local/bin/kubectl + + # Resolve bucket id (write-bucket auth needs the id, not + # the name). + BUCKET_ID=$(influx bucket list --name "$INFLUX_BUCKET" --json | jq -r '.[0].id') + [ -n "$BUCKET_ID" ] && [ "$BUCKET_ID" != "null" ] || { + log "ERROR: bucket $INFLUX_BUCKET not found" + exit 1 + } + log "bucket $INFLUX_BUCKET id=$BUCKET_ID" + + # Idempotency: reuse an existing matching auth if one is + # already on the server (e.g. left over from a previous + # Job run within the same cluster lifetime). + EXISTING=$(influx auth list --json \ + | jq -r --arg d "$TOKEN_DESC" '.[] | select(.description == $d) | .token' \ + | head -1) + if [ -n "$EXISTING" ]; then + TOKEN="$EXISTING" + log "reusing existing auth ($TOKEN_DESC)" + else + TOKEN=$(influx auth create \ + --org "$INFLUX_ORG" \ + --description "$TOKEN_DESC" \ + --write-bucket "$BUCKET_ID" \ + --json | jq -r '.token') + [ -n "$TOKEN" ] && [ "$TOKEN" != "null" ] || { + log "ERROR: influx auth create produced empty token" + exit 1 + } + log "created new auth ($TOKEN_DESC)" + fi + + log "writing $TARGET_NS/$TARGET_SECRET via apply" + kubectl -n "$TARGET_NS" create secret generic "$TARGET_SECRET" \ + --from-literal=INFLUX_TOKEN="$TOKEN" \ + --dry-run=client -o yaml \ + | kubectl apply -f - + + log "Done." diff --git a/kubernetes/seed/influxdb/kustomization.yaml b/kubernetes/seed/influxdb/kustomization.yaml index a3ac5bb2..5a0dc4d9 100644 --- a/kubernetes/seed/influxdb/kustomization.yaml +++ b/kubernetes/seed/influxdb/kustomization.yaml @@ -6,3 +6,4 @@ resources: - staging-pvc.yaml - legacy-token.sops.yaml - seed-job.yaml + - ha-token-job.yaml