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

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:

  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)

#!/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 -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.

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:

  1. Rename install-fleet_daemon.shinstall-<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.