* fix(bp-external-secrets): split ClusterSecretStore into bp-external-secrets-stores chart (resolves CRD ordering, closes #331) bp-external-secrets@1.0.0 deadlocked on first install on otech.omani.works: Helm install failed for release external-secrets-system/external-secrets with chart bp-external-secrets@1.0.0: failed post-install: unable to build kubernetes object for deleting hook bp-external-secrets/templates/clustersecretstore-vault-region1.yaml: resource mapping not found for name: "vault-region1" namespace: "" no matches for kind "ClusterSecretStore" in version "external-secrets.io/v1beta1" Root cause: Helm's `helm.sh/hook-delete-policy: before-hook-creation` ran a kubectl-style lookup of the existing ClusterSecretStore CR before the upstream `external-secrets` subchart's CRDs finished registration. The in-line ClusterSecretStore template (templates/clustersecretstore-vault- region1.yaml) and the upstream subchart's CRDs co-installed in the same release; admission ordering wasn't deterministic enough to make the post-install hook safe. Fix — same pattern as PR #247 (bp-crossplane@1.1.3 ↔ bp-crossplane-claims@1.0.0): split the chart into controller + stores. Flux dependsOn orders them. - bp-external-secrets@1.1.0 — controller-only (just upstream subchart + NetworkPolicy + ServiceMonitor toggle). CRDs register here. - bp-external-secrets-stores@1.0.0 (NEW) — the default ClusterSecretStore CR; depends on bp-external-secrets being Ready. No Helm hooks needed: by the time this chart's HelmRelease starts, Flux has already verified bp-external-secrets is Ready=True and therefore the CRDs are registered. Files: NEW: platform/external-secrets-stores/blueprint.yaml (1.0.0) NEW: platform/external-secrets-stores/chart/Chart.yaml (1.0.0; no upstream subchart, annotation `catalyst.openova.io/no-upstream: "true"`) NEW: platform/external-secrets-stores/chart/values.yaml (clusterSecretStore.* knobs moved from controller chart) MOVED: platform/external-secrets/chart/templates/clustersecretstore-vault-region1.yaml → platform/external-secrets-stores/chart/templates/clustersecretstore-vault-region1.yaml (Helm hook annotations removed — Flux dependsOn now handles ordering) TOUCHED: platform/external-secrets/chart/Chart.yaml (1.0.0 → 1.1.0; description note appended) TOUCHED: platform/external-secrets/blueprint.yaml (1.0.0 → 1.1.0) TOUCHED: platform/external-secrets/chart/values.yaml (clusterSecretStore block removed; pointer comment added) NEW: clusters/_template/bootstrap-kit/15a-external-secrets-stores.yaml (Flux HelmRelease, dependsOn: [bp-external-secrets, bp-openbao]) TOUCHED: clusters/_template/bootstrap-kit/15-external-secrets.yaml (chart version 1.0.0 → 1.1.0) TOUCHED: clusters/_template/bootstrap-kit/kustomization.yaml (slot 15a inserted after 15) Out of scope for this PR (separate tickets): - blueprint-release.yaml CI fan-out: verify the path-matrix picks up the new platform/external-secrets-stores/ directory automatically; if not, add the directory to the matrix in a follow-up. - Per-Sovereign cluster directory edits (#257 will delete those). - Phase 0 minimum trim (#310 will renumber slots; this PR uses 15a as a non-disruptive sub-slot insertion that works with both the current 35-slot kustomization and the eventual 15-slot canonical layout — when #310 renumbers, 15 + 15a become 08 + 09 in the canonical order). Refs: #331 (this issue), #247 (pattern reference — bp-crossplane split), Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scripts): register bp-external-secrets-stores in expected-bootstrap-deps.yaml The dependency-graph-audit CI step rejected PR #334 because the new bp-external-secrets-stores HR was on disk at slot 15a but missing from the expected DAG. This commit adds it with the same dependsOn shape as clusters/_template/bootstrap-kit/15a-external-secrets-stores.yaml: [bp-external-secrets, bp-openbao]. Refs: #331, #310 (Phase 0 minimum), PR #334. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(bp-external-secrets): retire CR cases from controller test, add stores-toggle (#331) After splitting the default ClusterSecretStore into bp-external-secrets-stores @1.0.0, the controller chart's observability-toggle integration test still expected the CR to render in the controller chart (Cases 4 + 5). Those assertions now belong on the new chart. Changes: - platform/external-secrets/chart/tests/observability-toggle.sh: Replace Cases 4+5 with a single inverted assertion — the controller chart MUST render ZERO ClusterSecretStore CRs (top-level kind:); only the upstream subchart's CRD definition (whose spec.names.kind value is "ClusterSecretStore" at non-zero indent) is allowed. - platform/external-secrets-stores/chart/tests/clustersecretstore-toggle.sh: NEW. Mirrors the retired Cases 4+5 against the stores chart, plus a Case 3 that asserts clusterSecretStore.server overrides propagate. Local smoke: bash platform/external-secrets/chart/tests/observability-toggle.sh → 4/4 PASS bash platform/external-secrets-stores/chart/tests/clustersecretstore-toggle.sh → 3/3 PASS Refs: #331, PR #334. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scripts): handle alphanumeric sub-slot suffixes in check-bootstrap-deps.sh PR #334 (issue #331) added slot 15a-external-secrets-stores as a sub-slot between numeric slots 15 and 16. The bootstrap-deps audit script's `printf '%02d'` formatter rejected `15a` with: scripts/check-bootstrap-deps.sh: line 390: printf: 15a: invalid number Fix: detect non-numeric slot tokens and pass them through verbatim. Numeric slots still render as zero-padded `01..49` for output alignment. Local smoke: $ bash scripts/check-bootstrap-deps.sh ... [P] slot 15 bp-external-secrets <-- bp-cert-manager bp-openbao [P] slot 15a bp-external-secrets-stores <-- bp-external-secrets bp-openbao ... OK: bootstrap-kit dependency graph audit PASSED Refs: #331, PR #334. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(wbs): tick #331 chart-released bp-external-secrets@1.1.0 (controller-only) + bp-external-secrets-stores@1.0.0 (NEW) shipped in PR #426. Helm-template acceptance + both toggle tests + dependency-graph-audit all green. Sovereign-impact deferred to Phase 8. Refs: #331, PR #426. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hatice Yildiz <hatice.yildiz@openova.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: hatiyildiz <hatiyildiz@noreply.github.com>
434 lines
14 KiB
Bash
Executable File
434 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-bootstrap-deps.sh — bootstrap-kit dependency-graph audit (W2.K0).
|
|
#
|
|
# Authoritative spec: docs/BOOTSTRAP-KIT-EXPANSION-PLAN.md §2 + §3.
|
|
#
|
|
# What this does:
|
|
# 1. Parses every clusters/_template/bootstrap-kit/*.yaml and extracts
|
|
# metadata.name + spec.dependsOn for the HelmRelease document(s).
|
|
# 2. Compares the actual deps graph against the expected DAG declared in
|
|
# scripts/expected-bootstrap-deps.yaml.
|
|
# 3. Fails (non-zero exit) on any drift: missing or extra edges, unknown HRs.
|
|
# 4. Detects cycles: asserts no HR transitively depends on itself.
|
|
# 5. On success, prints the rendered DAG as ASCII (per-tier topological view).
|
|
#
|
|
# Exit codes:
|
|
# 0 — actual graph matches expected, no cycles, all present HRs validated
|
|
# 1 — drift (missing/extra deps, unknown HR present, etc.)
|
|
# 2 — cycle detected
|
|
# 3 — input/parse/usage error
|
|
#
|
|
# Behaviour against an in-flight expansion (W2.K1..K4 staggered merges):
|
|
# HRs declared in expected-bootstrap-deps.yaml but not yet present on disk
|
|
# are reported as "deferred" (informational, not an error). HRs present on
|
|
# disk but not declared in expected-bootstrap-deps.yaml are an error — every
|
|
# new bootstrap-kit slot must update the expected file in the same PR.
|
|
#
|
|
# Usage:
|
|
# scripts/check-bootstrap-deps.sh
|
|
# scripts/check-bootstrap-deps.sh --kit-dir clusters/_template/bootstrap-kit \
|
|
# --expected scripts/expected-bootstrap-deps.yaml
|
|
#
|
|
# Dependencies: bash, yq (mikefarah, v4+), find, sort, awk.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Defaults + arg parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Resolve repo root from this script's location so the tool works from any cwd.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
KIT_DIR="${REPO_ROOT}/clusters/_template/bootstrap-kit"
|
|
EXPECTED_FILE="${REPO_ROOT}/scripts/expected-bootstrap-deps.yaml"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [--kit-dir DIR] [--expected FILE]
|
|
|
|
--kit-dir DIR Directory containing bootstrap-kit HR yaml files
|
|
(default: ${KIT_DIR})
|
|
--expected FILE Path to expected-DAG yaml data file
|
|
(default: ${EXPECTED_FILE})
|
|
-h, --help Show this message
|
|
|
|
See docs/BOOTSTRAP-KIT-EXPANSION-PLAN.md §2 + §3 for the design contract.
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--kit-dir)
|
|
KIT_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--expected)
|
|
EXPECTED_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 3
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if ! command -v yq >/dev/null 2>&1; then
|
|
echo "ERROR: yq is required but not installed." >&2
|
|
echo "Install: wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && chmod +x /usr/local/bin/yq" >&2
|
|
exit 3
|
|
fi
|
|
|
|
if [[ ! -d "${KIT_DIR}" ]]; then
|
|
echo "ERROR: kit directory does not exist: ${KIT_DIR}" >&2
|
|
exit 3
|
|
fi
|
|
if [[ ! -f "${EXPECTED_FILE}" ]]; then
|
|
echo "ERROR: expected DAG file does not exist: ${EXPECTED_FILE}" >&2
|
|
exit 3
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Print a coloured banner if stdout is a TTY; otherwise plain text.
|
|
_banner() {
|
|
local title="$1"
|
|
if [[ -t 1 ]]; then
|
|
printf '\n\033[1;36m== %s ==\033[0m\n' "${title}"
|
|
else
|
|
printf '\n== %s ==\n' "${title}"
|
|
fi
|
|
}
|
|
|
|
_err() {
|
|
if [[ -t 2 ]]; then
|
|
printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2
|
|
else
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
fi
|
|
}
|
|
|
|
_warn() {
|
|
if [[ -t 1 ]]; then
|
|
printf '\033[1;33mWARN:\033[0m %s\n' "$*"
|
|
else
|
|
printf 'WARN: %s\n' "$*"
|
|
fi
|
|
}
|
|
|
|
_ok() {
|
|
if [[ -t 1 ]]; then
|
|
printf '\033[1;32mOK:\033[0m %s\n' "$*"
|
|
else
|
|
printf 'OK: %s\n' "$*"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 1 — Parse expected DAG
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner "Phase 1: parse expected DAG (${EXPECTED_FILE#"${REPO_ROOT}/"})"
|
|
|
|
# Build two associative arrays:
|
|
# EXPECTED_DEPS[name]="dep1 dep2 ..." (space-separated, sorted)
|
|
# EXPECTED_SLOT[name]="<int>"
|
|
# EXPECTED_WAVE[name]="<wave-tag>"
|
|
declare -A EXPECTED_DEPS=()
|
|
declare -A EXPECTED_SLOT=()
|
|
declare -A EXPECTED_WAVE=()
|
|
EXPECTED_NAMES=()
|
|
|
|
# yq emits one record per line: "<slot>|<name>|<wave>|<dep1,dep2,...>"
|
|
while IFS='|' read -r slot name wave deps_csv; do
|
|
[[ -z "${name}" ]] && continue
|
|
EXPECTED_NAMES+=("${name}")
|
|
EXPECTED_SLOT["${name}"]="${slot}"
|
|
EXPECTED_WAVE["${name}"]="${wave}"
|
|
if [[ -z "${deps_csv}" ]]; then
|
|
EXPECTED_DEPS["${name}"]=""
|
|
else
|
|
# Sort deps so set comparison is order-insensitive.
|
|
EXPECTED_DEPS["${name}"]="$(echo "${deps_csv}" | tr ',' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')"
|
|
fi
|
|
done < <(
|
|
yq -r '
|
|
.slots[] |
|
|
[
|
|
(.slot | tostring),
|
|
.name,
|
|
.wave,
|
|
((.depends_on // []) | join(","))
|
|
] | join("|")
|
|
' "${EXPECTED_FILE}"
|
|
)
|
|
|
|
if [[ ${#EXPECTED_NAMES[@]} -eq 0 ]]; then
|
|
_err "expected DAG file declared no slots (empty .slots[])"
|
|
exit 3
|
|
fi
|
|
|
|
echo " Loaded ${#EXPECTED_NAMES[@]} expected HRs from ${EXPECTED_FILE#"${REPO_ROOT}/"}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 2 — Parse actual HR files
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner "Phase 2: parse actual HRs in ${KIT_DIR#"${REPO_ROOT}/"}"
|
|
|
|
declare -A ACTUAL_DEPS=()
|
|
declare -A ACTUAL_FILE=()
|
|
ACTUAL_NAMES=()
|
|
|
|
# Iterate in slot order to make the output deterministic.
|
|
shopt -s nullglob
|
|
HR_FILES=()
|
|
while IFS= read -r f; do
|
|
HR_FILES+=("$f")
|
|
done < <(find "${KIT_DIR}" -maxdepth 1 -type f -name '*.yaml' \
|
|
! -name 'kustomization.yaml' | sort)
|
|
shopt -u nullglob
|
|
|
|
if [[ ${#HR_FILES[@]} -eq 0 ]]; then
|
|
_err "no HR yaml files found in ${KIT_DIR}"
|
|
exit 3
|
|
fi
|
|
|
|
for f in "${HR_FILES[@]}"; do
|
|
# Each file may contain multiple yaml documents (Namespace, HelmRepository,
|
|
# HelmRelease). Extract the HelmRelease document(s).
|
|
while IFS='|' read -r name deps_csv; do
|
|
[[ -z "${name}" ]] && continue
|
|
if [[ -n "${ACTUAL_DEPS[${name}]+x}" ]]; then
|
|
_err "duplicate HelmRelease name '${name}' (in ${f#"${REPO_ROOT}/"} and ${ACTUAL_FILE[${name}]})"
|
|
exit 3
|
|
fi
|
|
ACTUAL_NAMES+=("${name}")
|
|
ACTUAL_FILE["${name}"]="${f#"${REPO_ROOT}/"}"
|
|
if [[ -z "${deps_csv}" ]]; then
|
|
ACTUAL_DEPS["${name}"]=""
|
|
else
|
|
ACTUAL_DEPS["${name}"]="$(echo "${deps_csv}" | tr ',' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')"
|
|
fi
|
|
done < <(
|
|
yq -r '
|
|
select(.kind == "HelmRelease") |
|
|
[
|
|
.metadata.name,
|
|
((.spec.dependsOn // []) | map(.name) | join(","))
|
|
] | join("|")
|
|
' "$f" 2>/dev/null || true
|
|
)
|
|
done
|
|
|
|
if [[ ${#ACTUAL_NAMES[@]} -eq 0 ]]; then
|
|
_err "no HelmRelease resources parsed from any file in ${KIT_DIR}"
|
|
exit 3
|
|
fi
|
|
|
|
echo " Parsed ${#ACTUAL_NAMES[@]} HelmRelease(s) across ${#HR_FILES[@]} file(s)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 3 — Compare actual vs expected (drift detection)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner "Phase 3: drift detection"
|
|
|
|
DRIFT_COUNT=0
|
|
DEFERRED_COUNT=0
|
|
|
|
# 3a — every actual HR must be declared in expected, with matching deps.
|
|
for name in "${ACTUAL_NAMES[@]}"; do
|
|
if [[ -z "${EXPECTED_DEPS[${name}]+x}" ]]; then
|
|
_err "HR '${name}' (file ${ACTUAL_FILE[${name}]}) is present on disk but NOT declared in ${EXPECTED_FILE#"${REPO_ROOT}/"}. Add it to the expected DAG."
|
|
DRIFT_COUNT=$((DRIFT_COUNT + 1))
|
|
continue
|
|
fi
|
|
|
|
exp="${EXPECTED_DEPS[${name}]}"
|
|
got="${ACTUAL_DEPS[${name}]}"
|
|
|
|
if [[ "${exp}" != "${got}" ]]; then
|
|
# Compute set differences for an actionable message.
|
|
missing=$(comm -23 <(echo "${exp}" | tr ' ' '\n' | sort -u) <(echo "${got}" | tr ' ' '\n' | sort -u) | tr '\n' ' ' | sed 's/ $//')
|
|
extra=$( comm -13 <(echo "${exp}" | tr ' ' '\n' | sort -u) <(echo "${got}" | tr ' ' '\n' | sort -u) | tr '\n' ' ' | sed 's/ $//')
|
|
_err "HR '${name}' (file ${ACTUAL_FILE[${name}]}): dependsOn drift"
|
|
[[ -n "${missing// /}" ]] && echo " missing edges (declared expected, NOT in HR): ${missing}" >&2
|
|
[[ -n "${extra// /}" ]] && echo " extra edges (in HR, NOT declared expected): ${extra}" >&2
|
|
DRIFT_COUNT=$((DRIFT_COUNT + 1))
|
|
fi
|
|
done
|
|
|
|
# 3b — expected HRs not yet on disk are reported as deferred (info, not error).
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
if [[ -z "${ACTUAL_DEPS[${name}]+x}" ]]; then
|
|
DEFERRED_COUNT=$((DEFERRED_COUNT + 1))
|
|
_slot_raw="${EXPECTED_SLOT[${name}]}"
|
|
if [[ "${_slot_raw}" =~ ^[0-9]+$ ]]; then
|
|
_slot_fmt="$(printf '%02d' "${_slot_raw}")"
|
|
else
|
|
_slot_fmt="${_slot_raw}"
|
|
fi
|
|
_warn "HR '${name}' (slot ${_slot_fmt}, wave ${EXPECTED_WAVE[${name}]}) declared expected but not yet on disk — will be added by ${EXPECTED_WAVE[${name}]}"
|
|
fi
|
|
done
|
|
|
|
if [[ ${DRIFT_COUNT} -gt 0 ]]; then
|
|
echo "" >&2
|
|
_err "${DRIFT_COUNT} drift(s) detected. Reconcile HR files with ${EXPECTED_FILE#"${REPO_ROOT}/"} (or vice versa) and re-run."
|
|
exit 1
|
|
fi
|
|
|
|
_ok "no drift between actual HRs and expected DAG (${DEFERRED_COUNT} deferred)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 4 — Cycle detection (Kahn's algorithm)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner "Phase 4: cycle detection"
|
|
|
|
# We check the *expected* graph (since it's the authoritative DAG and is the
|
|
# superset of what's currently on disk). A cycle in the expected graph is the
|
|
# bug; any subset on disk inherits the property.
|
|
declare -A INDEGREE=()
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
INDEGREE["${name}"]=0
|
|
done
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
for dep in ${EXPECTED_DEPS[${name}]}; do
|
|
[[ -z "${dep}" ]] && continue
|
|
if [[ -z "${INDEGREE[${dep}]+x}" ]]; then
|
|
_err "HR '${name}' depends on unknown HR '${dep}' (not declared in expected DAG)"
|
|
exit 1
|
|
fi
|
|
INDEGREE["${name}"]=$((INDEGREE["${name}"] + 1))
|
|
done
|
|
done
|
|
|
|
# Kahn's algorithm: repeatedly drain zero-in-degree nodes.
|
|
declare -a QUEUE=()
|
|
declare -a TOPO_ORDER=()
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
if [[ "${INDEGREE[${name}]}" -eq 0 ]]; then
|
|
QUEUE+=("${name}")
|
|
fi
|
|
done
|
|
|
|
while [[ ${#QUEUE[@]} -gt 0 ]]; do
|
|
current="${QUEUE[0]}"
|
|
QUEUE=("${QUEUE[@]:1}")
|
|
TOPO_ORDER+=("${current}")
|
|
# For each n that depends on current, decrement its indegree.
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
for dep in ${EXPECTED_DEPS[${name}]}; do
|
|
if [[ "${dep}" == "${current}" ]]; then
|
|
INDEGREE["${name}"]=$((INDEGREE["${name}"] - 1))
|
|
if [[ "${INDEGREE[${name}]}" -eq 0 ]]; then
|
|
QUEUE+=("${name}")
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
done
|
|
|
|
if [[ "${#TOPO_ORDER[@]}" -ne "${#EXPECTED_NAMES[@]}" ]]; then
|
|
_err "cycle detected in expected DAG"
|
|
echo " Topo-ordered ${#TOPO_ORDER[@]} of ${#EXPECTED_NAMES[@]} HRs before stalling." >&2
|
|
echo " Stalled HRs (transitively depend on themselves):" >&2
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
if [[ "${INDEGREE[${name}]}" -gt 0 ]]; then
|
|
echo " - ${name} (remaining in-degree=${INDEGREE[${name}]})" >&2
|
|
fi
|
|
done
|
|
exit 2
|
|
fi
|
|
|
|
_ok "no cycles (${#EXPECTED_NAMES[@]} HRs topologically ordered)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 5 — Render ASCII DAG (per-wave grouping, topological order within wave)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner "Phase 5: rendered DAG"
|
|
|
|
cat <<EOF
|
|
Bootstrap-kit dependency graph
|
|
(authoritative spec: docs/BOOTSTRAP-KIT-EXPANSION-PLAN.md §2)
|
|
|
|
Legend:
|
|
[P] present on disk and validated
|
|
[.] declared in expected DAG, deferred (file not yet added by W2.Kn)
|
|
|
|
EOF
|
|
|
|
# Group nodes by wave for the printout, in slot order.
|
|
declare -A WAVE_HEADERS=(
|
|
["present"]="Tier 0-4 — Foundation through Catalyst umbrella (post-PR-247 baseline)"
|
|
["W2.K1"]="Tier 5 — Storage + DB (Wave 2 batch K1, slots 15-19)"
|
|
["W2.K2"]="Tier 6 — Observability (Wave 2 batch K2, slots 20-26)"
|
|
["W2.K3"]="Tier 7 — Security + policy (Wave 2 batch K3, slots 27-34)"
|
|
["W2.K4"]="Tier 8+9 — Edge + apps + AI runtime (Wave 2 batch K4, slots 35-48)"
|
|
)
|
|
|
|
for wave in present W2.K1 W2.K2 W2.K3 W2.K4; do
|
|
echo "${WAVE_HEADERS[${wave}]}"
|
|
printf '%.0s-' {1..78}; echo
|
|
any=0
|
|
for name in "${EXPECTED_NAMES[@]}"; do
|
|
if [[ "${EXPECTED_WAVE[${name}]}" != "${wave}" ]]; then
|
|
continue
|
|
fi
|
|
any=1
|
|
if [[ -n "${ACTUAL_DEPS[${name}]+x}" ]]; then
|
|
marker="[P]"
|
|
else
|
|
marker="[.]"
|
|
fi
|
|
raw_slot="${EXPECTED_SLOT[${name}]}"
|
|
# Slots are mostly numeric (01..35, 49) but may carry an alphabetic
|
|
# sub-slot suffix (e.g. 15a) when a chart is wedged between two
|
|
# already-numbered slots without renumbering downstream consumers
|
|
# — see PR #334 (issue #331) for slot 15a-external-secrets-stores.
|
|
if [[ "${raw_slot}" =~ ^[0-9]+$ ]]; then
|
|
slot="$(printf '%02d' "${raw_slot}")"
|
|
else
|
|
slot="${raw_slot}"
|
|
fi
|
|
deps="${EXPECTED_DEPS[${name}]}"
|
|
if [[ -z "${deps}" ]]; then
|
|
printf ' %s slot %s %-26s (root, no deps)\n' "${marker}" "${slot}" "${name}"
|
|
else
|
|
printf ' %s slot %s %-26s <-- %s\n' "${marker}" "${slot}" "${name}" "${deps}"
|
|
fi
|
|
done
|
|
if [[ "${any}" -eq 0 ]]; then
|
|
echo " (none)"
|
|
fi
|
|
echo ""
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_banner "Summary"
|
|
present_count=${#ACTUAL_NAMES[@]}
|
|
expected_count=${#EXPECTED_NAMES[@]}
|
|
deferred_count=$((expected_count - present_count))
|
|
echo " Present on disk: ${present_count}"
|
|
echo " Declared expected: ${expected_count}"
|
|
echo " Deferred (W2.K1-K4): ${deferred_count}"
|
|
echo " Drift: 0"
|
|
echo " Cycles: 0"
|
|
echo ""
|
|
_ok "bootstrap-kit dependency graph audit PASSED"
|