provisioning-catalog/components/_renderer/lib/COMPONENT_CONTRACT.md

248 lines
8.3 KiB
Markdown
Raw Normal View History

# Catalog component install script contract
Every shell script under `provisioning/catalog/components/<name>/cluster/install-<name>.sh`
(and the equivalent for taskserv components) MUST follow this contract so
operators get uniform error capture, flag handling, and diagnostic output
across all components. The contract is checked by
`provisioning/catalog/components/_renderer/lib/check-contract.sh`.
## Why this exists
Before this contract, each component's install script invented its own way
of doing the same five things:
1. Resolving operator flags from env vars (some hardcoded, some absent).
2. Reporting which phase failed (often: silent until kubectl exits).
3. Dumping diagnostic context on failure (often: nothing — operator had
to ssh in and run `kubectl describe` by hand).
4. Suppressing routine output ("unchanged"/"configured") in normal use.
5. Force-recreating a workload when k0s/k3s rollout hangs.
Each component duplicating these is brittle. The contract centralises them
in `diagnostics.sh` (sourced once) and demands every install script use it.
## Required clauses
Every `install-<name>.sh` MUST contain ALL of the following. The order is
prescriptive — `set -euo pipefail` first, source second, flag resolution
third, kubeconfig resolution fourth, workload registration + ERR trap fifth,
phase functions sixth, dispatcher last.
### R1 — Strict shell + set guards (MUST)
```bash
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
```
### R2 — Source diagnostics.sh (MUST)
```bash
# shellcheck source=/dev/null
[ -f "${SCRIPT_DIR}/diagnostics.sh" ] && source "${SCRIPT_DIR}/diagnostics.sh"
```
The bundle ships `diagnostics.sh` alongside the install script. Sourcing is
guarded so a hand-run from a partial bundle still works (with degraded
diagnostics — the local fallbacks in R3 cover that path).
### R3 — Resolve operator flags (MUST)
Read `PROVISIONING_VERBOSE`/`PRVNG_VERBOSE`, `PROVISIONING_FORCE`/`PRVNG_FORCE`,
and `PROVISIONING_DEBUG`/`PRVNG_DEBUG`. Prefer the helpers from `diagnostics.sh`,
fall back to a local truthy check when the lib is absent:
```bash
if command -v _diag_is_force >/dev/null 2>&1; then
_diag_is_force && FORCE=true || FORCE=false
_diag_is_debug && DEBUG=true || DEBUG=false
_diag_is_verbose && VERBOSE=true || VERBOSE=false
else
# Local fallback (one case per flag — keep verbatim for the linter).
case "$(printf '%s' "${PROVISIONING_FORCE:-${PRVNG_FORCE:-}}" | tr '[:upper:]' '[:lower:]')" in
1|true|yes) FORCE=true ;; *) FORCE=false ;;
esac
case "$(printf '%s' "${PROVISIONING_DEBUG:-${PRVNG_DEBUG:-}}" | tr '[:upper:]' '[:lower:]')" in
1|true|yes) DEBUG=true ;; *) DEBUG=false ;;
esac
case "$(printf '%s' "${PROVISIONING_VERBOSE:-${PRVNG_VERBOSE:-}}" | tr '[:upper:]' '[:lower:]')" in
1|true|yes) VERBOSE=true ;; *) VERBOSE=false ;;
esac
fi
```
### R4 — Define `_log` and `_apply_stdin` (MUST)
Step markers ALWAYS show. Kubectl apply output is gated by `VERBOSE`. Errors
are NEVER hidden — `kubectl apply` non-zero exit prints stderr unconditionally.
```bash
_log() {
printf ' → %s\n' "$*"
}
_apply_stdin() {
if [ "$VERBOSE" = "true" ]; then
_kubectl apply -f -
return $?
fi
local out rc
out=$(_kubectl apply -f - 2>&1)
rc=$?
if [ $rc -ne 0 ]; then
printf '%s\n' "$out" >&2
fi
return $rc
}
```
Use `_apply_stdin` (NOT bare `_kubectl apply -f -`) for ALL manifest applies.
Use `_log "<phase>"` (NOT bare `echo " → ..."`) for ALL phase markers.
### R5 — Register the workload + install the ERR trap (MUST)
For cluster components (after `_resolve_kubeconfig`):
```bash
if command -v _diag_register_cluster_workload >/dev/null 2>&1; then
_diag_register_cluster_workload deployment "$<COMPONENT>_NAME" "$<COMPONENT>_NAMESPACE" \
"app.kubernetes.io/name=${<COMPONENT>_NAME}"
_diag_install_err_trap
fi
```
Kind can be `deployment`, `statefulset`, `daemonset`, or `cronjob`. Pick the
one that owns the pods.
For taskserv components:
```bash
if command -v _diag_register_systemd_unit >/dev/null 2>&1; then
_diag_register_systemd_unit "<component>.service"
_diag_install_err_trap
fi
```
### R6 — Honour `FORCE` when re-applying Deployments/StatefulSets (MUST)
Wrap the workload apply (Deployment/StatefulSet) so that, when `FORCE=true`,
the existing resource is deleted before apply. Skip for stateless resources
(Service, ConfigMap, NetworkPolicy) — apply alone is fine there.
```bash
_apply_deployment_and_service() {
if [ "$FORCE" = "true" ]; then
if command -v _diag_force_delete_workload >/dev/null 2>&1; then
_diag_force_delete_workload deployment "$NAME" "$NAMESPACE" 30
else
_kubectl delete deployment "$NAME" --namespace "$NAMESPACE" \
--wait=true --timeout=30s --ignore-not-found 2>&1 || true
fi
fi
_apply_stdin <<MANIFEST
...
MANIFEST
}
```
### R7 — Wait via `_diag_wait_for_*` (MUST)
The rollout wait MUST use the diagnostic helpers, which auto-dump pod
logs/describe on failure. Bare `kubectl rollout status` is NOT acceptable
because it produces only `exceeded its progress deadline` — no context.
```bash
if command -v _diag_wait_for_deployment >/dev/null 2>&1; then
_diag_wait_for_deployment "$NAME" "$NAMESPACE" 180
else
_kubectl rollout status deployment/"$NAME" --namespace "$NAMESPACE" --timeout=180s
fi
```
### R8 — Never write secrets to disk (MUST)
Use `kubectl create secret … --from-literal=… --dry-run=client -o yaml |
_apply_stdin`. The `_credentials.env` file is the sole on-disk surface for
plaintext secrets, and it lives only in a `mode 0700` bundle dir, `mode
0600` itself, removed after the op completes (unless `DEBUG=true`).
NEVER:
- Write rendered Secret YAML to disk.
- `echo "$SECRET" > /some/file`.
- Embed plaintext in a heredoc that's later `kubectl apply -f` from a path.
### R9 — Local validation before apply (SHOULD)
If a manifest references structured user-supplied data (JSON access policy,
YAML config, etc.), validate it locally BEFORE building the manifest. A bad
input that reaches the cluster as a ConfigMap will manifest as a
CrashLoopBackOff inside the workload — much harder to diagnose than a
local `jq -e` rejection.
```bash
if command -v jq >/dev/null 2>&1; then
local err
if ! err=$(printf '%s' "$ACCESS_POLICY_JSON" | jq -e . 2>&1 >/dev/null); then
echo "ERROR: access-policy JSON failed local validation — refusing to apply." >&2
echo " jq: $err" >&2
[ "$DEBUG" = "true" ] && printf ' raw: %s\n' "$ACCESS_POLICY_JSON" >&2
exit 1
fi
fi
```
### R10 — Dispatcher accepts `install|update|delete|purge|restart|health` (MUST)
```bash
case "$CMD_TSK" in
install) _do_install ;;
update) _do_update ;;
delete) _do_delete ;;
purge) shift; _do_purge "${1:-}" ;;
health) _do_health ;;
restart) _do_restart ;;
*)
echo "ERROR: unknown CMD_TSK='$CMD_TSK'. Valid: install|update|delete|purge|health|restart" >&2
exit 1
;;
esac
```
`purge` MUST require `--confirm`.
## Linter
`provisioning/catalog/components/_renderer/lib/check-contract.sh` scans
every component's install script and reports R1..R10 compliance. Run via:
```bash
bash provisioning/catalog/components/_renderer/lib/check-contract.sh
# OR (from any workspace that imports the libre-forge deploy.just shape):
just check-component-contract
```
Exit code 0 = all components compliant. Exit code 1 = at least one
violation. Reports per-component per-clause PASS/FAIL.
## Adding a new component
Copy `provisioning/catalog/components/fleet_daemon/` as a template — it is
the reference implementation of this contract. Then:
1. Rename `install-fleet_daemon.sh``install-<your-name>.sh`.
2. Rename `fleet_daemon-lib.sh``<your-name>-lib.sh` (keep its existence).
3. Adjust the registered workload kind/name/namespace in the R5 block.
4. Rewrite the phase functions for your manifest set.
5. Run the linter — fix every FAIL it reports.
6. Add a workspace `just <component>-up/down/update/stat` recipe following
`libre-forge/justfiles/deploy.just::_fleet-daemon-op` as the template.
## See also
- `FLAGS.md` — what each flag does, combinability, examples.
- `diagnostics.sh` — the implementation of every `_diag_*` helper.
- `provisioning/catalog/components/fleet_daemon/cluster/install-fleet_daemon.sh`
— reference implementation.