# Catalog component install script contract Every shell script under `provisioning/catalog/components//cluster/install-.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-.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 ""` (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 "$_NAME" "$_NAMESPACE" \ "app.kubernetes.io/name=${_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 ".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 </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-.sh`. 2. Rename `fleet_daemon-lib.sh` → `-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 -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.