248 lines
8.3 KiB
Markdown
248 lines
8.3 KiB
Markdown
|
|
# 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.
|