Skip to content
Cockpit0.29.0
A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live

test/cases/84_deployment_contract.sh

An object of kind test

What it is

URI
majordomus://test/test/cases/84_deployment_contract.sh
Kind
test
Identity
test/cases/84_deployment_contract.sh
Path
test/cases/84_deployment_contract.sh
Provenance
declared
Media type
text/plain
As a capability
test.test/cases/84_deployment_contract.sh

Front matter

{}

Content

# majordomus-covers: doctor
# claims: deployment-contract-refuses-by-name
# A deployment is described once, in the layer, against a closed contract.
#
# `init` seeds the section with its contract and nothing in it, so a repository that later
# deploys something adds one file rather than a directory, a context document, a manifest
# entry and a source class. An object in it is read, closed and refused by name with no
# registration anywhere: the source class and the kind are data, and the index, the
# listing and the graph follow them.
#
# What this case exists to prove is the refusals. A deployment that cannot work must cost a
# test failure rather than a failed rollout, and a credential must never reach the layer.
. "$ROOT/test/lib.sh"
"$MJ" init >/dev/null
"$MJ" update >/dev/null

# ---------------------------------------------------------------- an empty section
# `init` seeds the section with its contract and no objects, so a repository that later
# deploys something adds one file. An empty section is not a finding: there is nothing
# that could be wrong.
git add -A >/dev/null; git commit -qm base
[ -f .ai/repo/deployments/README.md ] || { echo "    init did not seed the section's contract"; exit 1; }
"$MJ" doctor 2>&1 | grep -qE '^FAIL +deployment ' \
  && { echo "    an empty deployments section was reported as a failure"; exit 1; }
"$MJ" doctor 2>&1 | grep -q 'deployment(s)' \
  && { echo "    an empty section counted something"; exit 1; }

# ---------------------------------------------------------------- one deployment
cat > .ai/repo/deployments/example.yaml <<'MD'
schema: deployment/v1
kind: deployment
id: example
title: The example deployment
description: One machine that stops when idle.
application: example
build:
  package: example-cli
  binary: example
  profile: release
  inputs:
    - src
listen:
  port: 8080
  interface: all
health:
  liveness: /healthz
  readiness: /readyz
resources:
  cpu_kind: shared
  cpus: 1
  memory_mb: 256
machines:
  count: 1
  min_running: 0
  autostart: true
  autostop: true
region: fra
MD
git add -A >/dev/null

# read through the object surface, counted by doctor, refused by nothing
"$MJ" doctor 2>&1 | grep -q 'OK   deployment' || { echo "    doctor did not accept a valid deployment"; exit 1; }
# exactly one: the section's own contract is not an instance of the kind beside it
"$MJ" doctor 2>&1 | grep -q '1 deployment(s)' || { echo "    doctor did not count exactly the object"; exit 1; }

# ---------------------------------------------------------------- every refusal, by name
d=.ai/repo/deployments/example.yaml
cp "$d" keep.yaml
probe() {   # reason mutation...
  local want="$1"; shift
  "$@"
  git add -A >/dev/null
  "$MJ" doctor 2>&1 | grep -qE "^FAIL +deployment .*$want" || { echo "    doctor did not refuse: $want"; exit 1; }
  cp keep.yaml "$d"; git add -A >/dev/null
}
probe 'key\(s\) the contract does not have: fly_api_token' \
  sed -i.bak 's|^region: fra$|region: fra\nfly_api_token: "x"|' "$d"
# no hosting provider is a target: a provider block is a key the contract does not have
probe 'key\(s\) the contract does not have: provider' \
  sed -i.bak 's|^region: fra$|region: fra\nprovider:\n  name: fly|' "$d"
probe 'this executable reads deployment/v1' \
  sed -i.bak 's|^schema: deployment/v1$|schema: deployment/v2|' "$d"
probe 'lacks region' \
  sed -i.bak 's|^region: fra$|region_was: fra|' "$d"
probe 'is neither loopback nor all' \
  sed -i.bak 's|^  interface: all$|  interface: public|' "$d"
probe 'is outside 1024-65535; the process runs unprivileged' \
  sed -i.bak 's|^  port: 8080$|  port: 80|' "$d"
probe 'machines.min_running 2 exceeds machines.count 1' \
  sed -i.bak 's|^  min_running: 0$|  min_running: 2|' "$d"
probe 'is not a path beginning with /' \
  sed -i.bak 's|^  liveness: /healthz$|  liveness: http://example.invalid/healthz|' "$d"
probe 'carries what reads as a credential' \
  sed -i.bak 's|^description: .*$|description: "FlyV1 fm2_notarealtoken"|' "$d"
rm -f "$d".bak

# two objects, one identity
cp "$d" .ai/repo/deployments/a-second-claim.yaml
git add -A >/dev/null
"$MJ" doctor 2>&1 | grep -q 'two objects claim this identity' || { echo "    a duplicate identity was not refused"; exit 1; }
rm -f .ai/repo/deployments/a-second-claim.yaml; git add -A >/dev/null

# ---------------------------------------------------------------- the contract is generated
# The allow-list is a projection of the schema, never written by hand: remove it and the
# validator says which command puts it back rather than passing silently.
allow="$(dirname "$(dirname "$MJ")")/share/allow/deployment.txt"
[ -f "$allow" ] || { echo "    share/allow/deployment.txt was not projected from the schema"; exit 1; }
grep -qx '\^listen\\.port\$' "$allow" || { echo "    the allow-list does not close listen.port"; exit 1; }
grep -qx '\^budgets\\.blocking_check_ms\$' "$allow" || { echo "    the allow-list does not reach into the budgets block"; exit 1; }
# no hosting provider is a target, so the contract has no block for one
grep -q '^\^provider' "$allow" && { echo "    the allow-list still admits a provider block"; exit 1; }

# and the restored object is accepted again: every refusal above was the mutation, not the shape
"$MJ" doctor 2>&1 | grep -q 'OK   deployment' || { echo "    the restored object is no longer accepted"; exit 1; }