Files
homelab/scripts/flux-graph.sh
claude-botandClaude Opus 5.5 ac44925baf
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
review-bot reviewed: comment
fix(flux): services that need OpenBao secrets apply after the secret store
crowdsec, external-dns-unifi, netbird-ingress and velero were in the
infrastructure Kustomization, while the ExternalSecrets that create their
Secrets were in infrastructure-crds, which depends on infrastructure. On a
cold start each was therefore installed before its Secret could exist: the
pods sat in CreateContainerConfigError until OpenBao was unsealed and ESO
synced. For the HelmReleases (crowdsec, external-dns-unifi, velero) Helm's
wait timed out meanwhile; with remediation.retries: 3 they give up after
four attempts and stay failed even once the Secret appears. Velero failing
that way stops every restore.

| workload | Secrets |
|---|---|
| crowdsec | crowdsec-bouncer-key (crowdsec, ingress), crowdsec-edge-bouncer-key |
| external-dns-unifi | external-dns-unifi-secret |
| netbird-ingress | netbird-ingress-setup-key |
| velero (+ backup-verify CronJob) | cloud-credentials, cloud-credentials-hetzner, hcloud-backup-verify |

Each now has a Kustomization of its own (clusters/homelab/
infrastructure-services.yaml), depending on infrastructure and
infrastructure-crds, with its ExternalSecrets beside it. The openbao
ClusterSecretStore in infrastructure-crds turns Ready only once OpenBao is
unsealed, so the Secret exists before the consumer installs. One
Kustomization each so that a broken service holds back only itself.

Velero's Schedules need the velero.io CRDs from its HelmRelease, so they move
from post-crds to infrastructure/velero-schedules with a Kustomization after
velero.

post-crds keeps cloudflare-api-token (consumed by its own ClusterIssuer) and
flux-git. The directories themselves stay where they are, so paths such as
the CI test infrastructure/velero/backup-verify/tests/run.sh are unchanged.

netbird-ingress gets 10m to roll its DaemonSet over three nodes
(maxUnavailable 1, minReadySeconds 45). The crowdsec comments name the
Kustomization it now runs in, and flux-graph.sh colours the new nodes.

Production rollout: infrastructure and infrastructure-crds must run with
prune: false (the stack's first PR) when this merges. Pruned, the velero
HelmRelease would be uninstalled, and the moved ExternalSecrets would take
their Secrets with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:14:41 +02:00

85 lines
3.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Draws the Flux Kustomization dependency graph from the dependsOn fields in
# kubernetes/clusters/homelab. Edges point from a dependency to its dependent.
#
# flux-graph.sh DOT on stdout, every edge
# flux-graph.sh out.dot the same, written to out.dot
# flux-graph.sh out.svg rendered; out.png likewise
#
# Rendered output is the transitive reduction (tred): an edge implied by a
# longer path is left out, the order is the same.
#
# Green: infrastructure. Blue: the Velero restore chain. Dashed: nothing is
# health-checked. Grey fill: suspended.
#
# Needs jq, either yq (mikefarah's or the Python wrapper) and, to render,
# graphviz.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLUSTER_DIR="${REPO_ROOT}/kubernetes/clusters/homelab"
OUT="${1:-}"
command -v jq >/dev/null || { echo "jq not found" >&2; exit 1; }
command -v yq >/dev/null || { echo "yq not found" >&2; exit 1; }
# One JSON document per YAML document.
yaml_to_json() {
if [[ "$(yq --version)" == *mikefarah* ]]; then
yq eval-all -o=json -I=0 '.' "$@"
else
yq -c '.' "$@"
fi
}
to_dot() {
yaml_to_json "${CLUSTER_DIR}"/*.yaml | jq -r -s '
[ .[]
| select(.kind == "Kustomization"
and .apiVersion == "kustomize.toolkit.fluxcd.io/v1") ] as $ks
| "digraph flux {",
" rankdir=LR;",
" node [shape=box, style=\"rounded\", fontname=\"Helvetica\", fontsize=10];",
" edge [color=\"#888888\"];",
( $ks[]
| .metadata.name as $n
# healthChecks are ignored under wait: true; without either, nothing
# is checked, healthCheckExprs included.
| (if .spec.wait == true then "wait"
elif ((.spec.healthChecks // []) | length) > 0 then "health checks"
else "wait: false" end) as $wait
| (if .spec.suspend == true then ", style=\"rounded,filled\", fillcolor=\"#dddddd\""
elif $wait == "wait: false" then ", style=\"rounded,dashed\""
else "" end) as $dash
| (if .spec.suspend == true then "\\nsuspended" else "" end) as $suspended
| (if ($n | startswith("restore-")) or ($n | IN("velero", "velero-backups-synced"))
then ", color=\"#1f77b4\", fontcolor=\"#1f77b4\""
elif ($n | startswith("infrastructure")) or ($n | IN("crowdsec", "external-dns-unifi", "netbird-ingress", "velero-schedules"))
then ", color=\"#2ca02c\", fontcolor=\"#2ca02c\""
else "" end) as $colour
| " \"\($n)\" [label=\"\($n)\\n\($wait) · \(.spec.timeout // "-")\($suspended)\"\($dash)\($colour)];" ),
( $ks[]
| .metadata.name as $n
| (.spec.dependsOn // [])[]
| " \"\(.name)\" -> \"\($n)\";" ),
"}"'
}
case "${OUT}" in
"")
to_dot
;;
*.dot)
to_dot > "${OUT}"
;;
*.svg|*.png)
command -v dot >/dev/null || { echo "graphviz (dot) not found" >&2; exit 1; }
to_dot | tred | dot -T"${OUT##*.}" -Gdpi=110 -o "${OUT}"
;;
*)
echo "usage: $0 [out.dot|out.svg|out.png]" >&2
exit 1
;;
esac