8.3 KiB
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:
- Resolving operator flags from env vars (some hardcoded, some absent).
- Reporting which phase failed (often: silent until kubectl exits).
- Dumping diagnostic context on failure (often: nothing — operator had
to ssh in and run
kubectl describeby hand). - Suppressing routine output ("unchanged"/"configured") in normal use.
- 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)
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
R2 — Source diagnostics.sh (MUST)
# 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:
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.
_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):
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:
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.
_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.
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 -ffrom 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.
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)
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 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:
- Rename
install-fleet_daemon.sh→install-<your-name>.sh. - Rename
fleet_daemon-lib.sh→<your-name>-lib.sh(keep its existence). - Adjust the registered workload kind/name/namespace in the R5 block.
- Rewrite the phase functions for your manifest set.
- Run the linter — fix every FAIL it reports.
- Add a workspace
just <component>-up/down/update/statrecipe followinglibre-forge/justfiles/deploy.just::_fleet-daemon-opas 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.