NK-WP-0058-T07: conventions lint, server-side applies, one cluster route
All checks were successful
Bounded-execution contracts / contracts (push) Successful in 1m25s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

tools/conventions/lint.py (make conventions-lint, contract suite): server-side-apply, one-wait-condition,
one-cluster-route, tool-numbered-receipts, managing-controller (Argo-owned paths from railiance-platform),
with reasoned exceptions. 21 runbook applies made server-side. portal-client-rollout.py (and so
keycape-client.py and the rollout tools) and identity-provisioner-reconcile.py use NK_KUBECONFIG,
default ~/.kube/config-railiance01, without a second kubeconfig or --server override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 943911@bnt-lap001
Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
This commit is contained in:
tegwick 2026-11-06 00:34:00 +01:00
parent 80d280a340
commit cd265b84a4
13 changed files with 226 additions and 28 deletions

View file

@ -212,6 +212,9 @@ bounded-execution-admission-check: ## Validate the example admissions (read-only
execution-attribution-receipt-test: ## Receipt schema v0.1 validator; fixtures match the executor (NK-WP-0056-T15) execution-attribution-receipt-test: ## Receipt schema v0.1 validator; fixtures match the executor (NK-WP-0056-T15)
python3 -m pytest tools/execution-attribution-receipt/tests python3 -m pytest tools/execution-attribution-receipt/tests
conventions-lint: ## Runbook and tooling conventions from the 2026-11-04/05 pilot (NK-WP-0058-T07), read-only
python3 tools/conventions/lint.py
contract-test: ## Bounded-execution contract suite against the owner checkouts next to this repo; a skip fails (NK-WP-0058-T02) contract-test: ## Bounded-execution contract suite against the owner checkouts next to this repo; a skip fails (NK-WP-0058-T02)
bash tools/contract-tests/run.sh bash tools/contract-tests/run.sh

View file

@ -22,7 +22,7 @@ native scope/auth/namespace denials and reader revocation. It invokes this
repository's `sso-mfa/k8s/keycape/approval-clients-rollout.py` for image/config repository's `sso-mfa/k8s/keycape/approval-clients-rollout.py` for image/config
replacement and service acceptance. Do not run the platform child directly. replacement and service acceptance. Do not run the platform child directly.
On this workstation the verified Railiance kubeconfig is On this workstation the verified Railiance kubeconfig is
`/home/worsch/.kube/config-railiance01`; the default config targets another endpoint. `~/.kube/config-railiance01` (`NK_KUBECONFIG` overrides it); the default config targets another endpoint.
Use `activate` only for a first provision with absent custody and delivery. Use `activate` only for a first provision with absent custody and delivery.
`resume-activate --prior-receipt <metadata receipt>` requires a completed compatible `resume-activate --prior-receipt <metadata receipt>` requires a completed compatible

View file

@ -25,10 +25,10 @@ chmod +x create-secrets.sh
./create-secrets.sh ./create-secrets.sh
# 2. Apply manifests (order matters) # 2. Apply manifests (order matters)
kubectl apply -f pvc.yaml kubectl apply --server-side -f pvc.yaml
kubectl apply -f configmap.yaml kubectl apply --server-side -f configmap.yaml
kubectl apply -f deployment.yaml kubectl apply --server-side -f deployment.yaml
kubectl apply -f ingress.yaml kubectl apply --server-side -f ingress.yaml
# 3. Wait for pod to be ready # 3. Wait for pod to be ready
# The startup probe allows 90 s for the initial LLDAP connection. # The startup probe allows 90 s for the initial LLDAP connection.

View file

@ -30,7 +30,7 @@ is not in its file. Applying the whole file would therefore be a broader change
K="kubectl --kubeconfig $HOME/.kube/config-railiance01" K="kubectl --kubeconfig $HOME/.kube/config-railiance01"
cd ~/approval-engine && git pull --ff-only cd ~/approval-engine && git pull --ff-only
python3 -c "import yaml,sys; yaml.safe_dump([d for d in yaml.safe_load_all(open('deploy/networkpolicies.yaml')) if d and d['metadata']['name']=='approval-engine-bounded-executor'][0], sys.stdout)" > /tmp/ae-bx.yaml python3 -c "import yaml,sys; yaml.safe_dump([d for d in yaml.safe_load_all(open('deploy/networkpolicies.yaml')) if d and d['metadata']['name']=='approval-engine-bounded-executor'][0], sys.stdout)" > /tmp/ae-bx.yaml
$K apply --dry-run=server -f /tmp/ae-bx.yaml && $K apply -f /tmp/ae-bx.yaml $K apply --server-side --dry-run=server -f /tmp/ae-bx.yaml && $K apply --server-side -f /tmp/ae-bx.yaml
``` ```
audit-core: run steps 1 and 2 of audit-core `docs/bounded-executor-receipts.md`, "Founder runbook — audit-core: run steps 1 and 2 of audit-core `docs/bounded-executor-receipts.md`, "Founder runbook —
@ -86,7 +86,7 @@ C=$(mktemp); python3 sso-mfa/k8s/keycape/keycape-client.py client-from-case secu
B=$HOME/.local/share/key-cape/verified-bin/e2b5fa1 # check-config binary from the deployed image (T09 runbook) B=$HOME/.local/share/key-cape/verified-bin/e2b5fa1 # check-config binary from the deployed image (T09 runbook)
test -x "$B/keycape" || { mkdir -p "$B"; cid=$(docker create forgejo.coulomb.social/coulomb/key-cape@sha256:9e082dca78f65a4af3e92d3de3705b99e49ca9b298d9562ad0d37511c4306f8d); docker cp "$cid:/keycape" "$B/keycape"; docker rm "$cid"; } test -x "$B/keycape" || { mkdir -p "$B"; cid=$(docker create forgejo.coulomb.social/coulomb/key-cape@sha256:9e082dca78f65a4af3e92d3de3705b99e49ca9b298d9562ad0d37511c4306f8d); docker cp "$cid:/keycape" "$B/keycape"; docker rm "$cid"; }
# keycape-client.py uses its built-in kubeconfig through 127.0.0.1:16444, exactly as in the T09 runbook; # keycape-client.py uses the same route as $K (NK_KUBECONFIG, default ~/.kube/config-railiance01; NK-WP-0058-T07);
# it pins the cluster identity and refuses another cluster. # it pins the cluster identity and refuses another cluster.
$K -n sso rollout pause deployment/keycape $K -n sso rollout pause deployment/keycape
$K -n sso patch deployment keycape --type=json -p '[ $K -n sso patch deployment keycape --type=json -p '[

View file

@ -8,6 +8,7 @@ import copy
import importlib.util import importlib.util
import json import json
from pathlib import Path from pathlib import Path
import os
import subprocess import subprocess
import yaml import yaml
@ -91,9 +92,13 @@ def replacement(secret):
return base64.b64encode(updated.encode()).decode(), True return base64.b64encode(updated.encode()).decode(), True
# The runbooks' cluster route (NK-WP-0058-T07): NK_KUBECONFIG, else the admin kubeconfig they name. No second file and
# no --server override: until 2026-11-06 this used a second kubeconfig file with a server override to the same API.
KUBECONFIG = os.environ.get('NK_KUBECONFIG', os.path.expanduser('~/.kube/config-railiance01'))
def kubectl(arguments, payload=None): def kubectl(arguments, payload=None):
args = ['kubectl', '--kubeconfig', '/home/worsch/.kube/config-hosteurope', args = ['kubectl', '--kubeconfig', KUBECONFIG, '--request-timeout=20s', *arguments]
'--server', 'https://127.0.0.1:16444', '--request-timeout=20s', *arguments]
result = subprocess.run(args, input=None if payload is None else json.dumps(payload), result = subprocess.run(args, input=None if payload is None else json.dumps(payload),
text=True, capture_output=True, timeout=30) text=True, capture_output=True, timeout=30)
require(result.returncode == 0, 'kubernetes_operation_failed') require(result.returncode == 0, 'kubernetes_operation_failed')

View file

@ -23,7 +23,7 @@ receipts under `~/nk54/`; they are value-free and go to `docs/evidence/` afterwa
```bash ```bash
cd ~/net-kingdom && git pull --ff-only cd ~/net-kingdom && git pull --ff-only
bridge up k3s-api-railiance01 bridge up k3s-api-railiance01
K="kubectl --kubeconfig $HOME/.kube/config-hosteurope --server https://127.0.0.1:16444" K="kubectl --kubeconfig $HOME/.kube/config-railiance01" # one route for runbooks and tools (NK-WP-0058-T07)
# The Secret scripts call plain kubectl; the default config is the limited ops-mason-builder. # The Secret scripts call plain kubectl; the default config is the limited ops-mason-builder.
export KUBECONFIG=$HOME/.kube/config-railiance01 export KUBECONFIG=$HOME/.kube/config-railiance01
kubectl auth can-i create secrets -n sso # must print yes kubectl auth can-i create secrets -n sso # must print yes
@ -94,7 +94,7 @@ python3 sso-mfa/k8s/lldap/bind-split.py verify --endpoint https://lldap.coulomb.
```bash ```bash
bash sso-mfa/k8s/identity-provisioner/create-secrets.sh "$CUSTODY" bash sso-mfa/k8s/identity-provisioner/create-secrets.sh "$CUSTODY"
$K apply -f sso-mfa/k8s/identity-provisioner/deployment.yaml $K apply --server-side -f sso-mfa/k8s/identity-provisioner/deployment.yaml
$K -n sso rollout status deploy/identity-provisioner --timeout=180s $K -n sso rollout status deploy/identity-provisioner --timeout=180s
``` ```
Its readiness includes a directory login, so `rollout status` succeeding means `svc-provisioner` Its readiness includes a directory login, so `rollout status` succeeding means `svc-provisioner`
@ -104,13 +104,13 @@ works. Rollback: `$K -n sso rollout undo deploy/identity-provisioner`.
```bash ```bash
bash sso-mfa/k8s/authelia/create-secrets.sh "$CUSTODY" --rotate-key LLDAP_SVC_AUTHELIA_PASS bash sso-mfa/k8s/authelia/create-secrets.sh "$CUSTODY" --rotate-key LLDAP_SVC_AUTHELIA_PASS
$K apply -f sso-mfa/k8s/authelia/configmap.yaml $K apply --server-side -f sso-mfa/k8s/authelia/configmap.yaml
$K -n sso rollout restart deploy/authelia && $K -n sso rollout status deploy/authelia --timeout=180s $K -n sso rollout restart deploy/authelia && $K -n sso rollout status deploy/authelia --timeout=180s
``` ```
Check: sign in at `https://auth.coulomb.social` as yourself (password and OTP). Check: sign in at `https://auth.coulomb.social` as yourself (password and OTP).
Rollback (the `admin` password still works until step 10): Rollback (the `admin` password still works until step 10):
```bash ```bash
git show 145ca8e~1:sso-mfa/k8s/authelia/configmap.yaml | $K apply -f - git show 145ca8e~1:sso-mfa/k8s/authelia/configmap.yaml | $K apply --server-side -f -
read -rsp 'admin password: ' P; echo read -rsp 'admin password: ' P; echo
printf '[{"op":"replace","path":"/data/ldap_password","value":"%s"}]' "$(printf %s "$P" | base64 -w0)" \ printf '[{"op":"replace","path":"/data/ldap_password","value":"%s"}]' "$(printf %s "$P" | base64 -w0)" \
| $K -n sso patch secret authelia-secrets --type=json --patch-file=/dev/stdin; unset P | $K -n sso patch secret authelia-secrets --type=json --patch-file=/dev/stdin; unset P
@ -148,7 +148,7 @@ automated rollback; the old bind is the `admin` account, which still works, so t
```bash ```bash
bash sso-mfa/k8s/lldap/create-secrets.sh "$CUSTODY" --rotate-key LLDAP_LDAP_USER_PASS bash sso-mfa/k8s/lldap/create-secrets.sh "$CUSTODY" --rotate-key LLDAP_LDAP_USER_PASS
$K apply -f sso-mfa/k8s/lldap/deployment.yaml $K apply --server-side -f sso-mfa/k8s/lldap/deployment.yaml
$K -n sso rollout status deploy/lldap --timeout=180s $K -n sso rollout status deploy/lldap --timeout=180s
``` ```
The configured super-admin is now `ld-admin`. Check: sign in to the LLDAP UI as `ld-admin` once, then The configured super-admin is now `ld-admin`. Check: sign in to the LLDAP UI as `ld-admin` once, then

View file

@ -24,10 +24,10 @@ chmod +x create-secrets.sh
./create-secrets.sh ./create-secrets.sh
# 3. Apply manifests (order matters) # 3. Apply manifests (order matters)
kubectl apply -f pvc.yaml kubectl apply --server-side -f pvc.yaml
kubectl apply -f middleware.yaml kubectl apply --server-side -f middleware.yaml
kubectl apply -f deployment.yaml kubectl apply --server-side -f deployment.yaml
kubectl apply -f ingress.yaml kubectl apply --server-side -f ingress.yaml
# 4. Wait for pod to be ready # 4. Wait for pod to be ready
kubectl rollout status deployment/lldap -n sso --timeout=120s kubectl rollout status deployment/lldap -n sso --timeout=120s

View file

@ -8,14 +8,16 @@ from __future__ import annotations
import argparse import argparse
import base64 import base64
import getpass import getpass
import os
import json import json
import re import re
import subprocess import subprocess
import sys import sys
CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786'
KUBE = ['kubectl', '--kubeconfig', '/home/worsch/.kube/config-hosteurope', # The runbooks' cluster route (NK-WP-0058-T07): NK_KUBECONFIG, else the admin kubeconfig they name; no override.
'--server', 'https://127.0.0.1:16444', '--request-timeout=20s'] KUBE = ['kubectl', '--kubeconfig', os.environ.get('NK_KUBECONFIG', os.path.expanduser('~/.kube/config-railiance01')),
'--request-timeout=20s']
NAMESPACE = 'sso' NAMESPACE = 'sso'
# The provisioner's own directory account (NK-WP-0054-T05); needs the image with LLDAP_ADMIN_USERNAME. # The provisioner's own directory account (NK-WP-0054-T05); needs the image with LLDAP_ADMIN_USERNAME.
SECRET = 'identity-provisioner-ldap' SECRET = 'identity-provisioner-ldap'

View file

@ -54,7 +54,7 @@ kubectl create secret generic net-kingdom-pg-privacyidea-app \
### 3. Deploy the cluster ### 3. Deploy the cluster
```bash ```bash
kubectl apply -f cluster.yaml kubectl apply --server-side -f cluster.yaml
``` ```
Wait for cluster to become ready (this provisions PVCs and runs initdb — allow 2–3 minutes): Wait for cluster to become ready (this provisions PVCs and runs initdb — allow 2–3 minutes):
@ -100,7 +100,7 @@ kubectl create secret generic net-kingdom-pg-backup-s3 \
Apply the updated cluster.yaml, then: Apply the updated cluster.yaml, then:
```bash ```bash
kubectl apply -f scheduled-backup.yaml kubectl apply --server-side -f scheduled-backup.yaml
``` ```
### 6. Run the restore drill ### 6. Run the restore drill

View file

@ -34,11 +34,11 @@ PI_SQLALCHEMY_DATABASE_URI).
```bash ```bash
# From sso-mfa/k8s/privacyidea/ # From sso-mfa/k8s/privacyidea/
kubectl apply -f pvc.yaml kubectl apply --server-side -f pvc.yaml
kubectl apply -f configmap.yaml kubectl apply --server-side -f configmap.yaml
kubectl apply -f middleware.yaml kubectl apply --server-side -f middleware.yaml
kubectl apply -f deployment.yaml kubectl apply --server-side -f deployment.yaml
kubectl apply -f ingress.yaml kubectl apply --server-side -f ingress.yaml
``` ```
**Wait for the pod to reach Running state** (DB migrations run on first start — **Wait for the pod to reach Running state** (DB migrations run on first start —
@ -185,7 +185,7 @@ Update `middleware.yaml` `privacyidea-admin-allowlist.spec.ipWhiteList.sourceRan
to your actual VPN/office CIDRs and reapply: to your actual VPN/office CIDRs and reapply:
```bash ```bash
kubectl apply -f middleware.yaml kubectl apply --server-side -f middleware.yaml
``` ```
### Self-service portal ### Self-service portal

View file

@ -15,6 +15,7 @@ PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q -rs -p no:cacheprovider \
tools/execution-attribution-receipt/tests \ tools/execution-attribution-receipt/tests \
sso-mfa/k8s/bounded-executor \ sso-mfa/k8s/bounded-executor \
tools/contract-tests/tests \ tools/contract-tests/tests \
tools/conventions/tests \
"$@" | tee "$out" "$@" | tee "$out"
if grep -q '^SKIPPED' "$out"; then if grep -q '^SKIPPED' "$out"; then
echo "contract-test: skipped tests above; every contract must run (are the owner checkouts present?)" >&2 echo "contract-test: skipped tests above; every contract must run (are the owner checkouts present?)" >&2

116
tools/conventions/lint.py Normal file
View file

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Runbook and tooling conventions from the 2026-11-04/05 pilot, as checks (NK-WP-0058-T07). Read-only.
python3 tools/conventions/lint.py [ROOT ...] # default: this repository; the rapp CI passes its own root
server-side-apply every `kubectl apply` in a runbook or script is server-side. A client-side apply leaves a
last-applied annotation; ESO copied it and the Secret admission guard refused (2026-11-04).
one-wait-condition no `kubectl wait` with more than one --for: they are ANDed, and the completed run hung it
(2026-11-05). Poll the status instead.
one-cluster-route tools and runbooks reach the cluster through one route: NK_KUBECONFIG, default
~/.kube/config-railiance01. No other kubeconfig file, absolute home path or --server override.
keycape-client.py used a second, hidden route (2026-11-04).
tool-numbered-receipts runbooks never number receipts by hand (`--receipt .../NN-...`); the tool numbers them.
managing-controller no runbook applies a path that an Argo CD Application owns (railiance-platform argocd/):
a manual write there is reverted or drifts (informed-decision, 2026-11-05).
Records (evidence, history, proposals, workplans, wiki) are not instructions and are not checked. A known exception is
listed in EXCEPTIONS with its reason. Exit 0 when clean; otherwise one line per finding: file:line rule.
"""
import re
import sys
from pathlib import Path
import yaml
HERE = Path(__file__).resolve()
RECORDS = re.compile(r'(^|/)(docs/evidence|docs/proposals|history|workplans|wiki|\.git)/|WORK-RECORDS|HISTORICAL')
RUNBOOK = {'.md', '.sh', '.mk'}
TOOL = {'.py', '.sh'}
KUBECTL = r'(kubectl|\$K\b|\$\{K\}|\$\(KUBECTL\)|\$KUBECTL|\$\{KUBECTL\})'
APPLY = re.compile(KUBECTL + r'[^|;&\n]*\sapply(\s|$)')
WAIT = re.compile(KUBECTL + r'[^|;&\n]*\swait\s')
ROUTE = re.compile(r'config-hosteurope|/etc/rancher/k3s/k3s\.yaml|/home/[^/\s\'"]+/\.kube/|'
r'--server[ =,\'"]+[\'"]?https?://127\.0\.0\.1:(16443|16444|6443)')
RECEIPT = re.compile(r'--receipt[ =]+["\']?[^ "\']*/\d{2}-')
SOURCE = re.compile(r'(?:~|\$HOME|/home/[^/\s]+)/([a-z0-9-]+)/([^\s"\']+)')
# (path relative to the root, rule): reason. Keep this short; each entry is a decision.
EXCEPTIONS = {
('sso-mfa/k8s/tenant-engine/README.md', 'server-side-apply'):
"quotes tenant-engine's own deploy command; the owner's runbook, not ours",
('sso-mfa/WORKPLAN.md', 'server-side-apply'): 'legacy checklist from the first SSO setup, not a runbook',
('sso-mfa/k8s/keycape/openbao-negative-session.py', 'one-cluster-route'):
"runs on railiance01 itself: /etc/rancher/k3s/k3s.yaml is the node's own cluster config",
('tools/bootstrap-recovery/install-identity-backup.py', 'one-cluster-route'):
"installs the node's backup job on railiance01: the node's own cluster config",
('sso-mfa/k8s/keycape/openbao-negative-node-route.md', 'one-cluster-route'):
'the command runs on railiance01 itself (the node route of openbao-negative-session.py)',
('docs/bounded-execution-contract-table.md', 'one-cluster-route'):
'the interface inventory describes the old routes on purpose',
('sso-mfa/k8s/bounded-executor/T10-LANES-RUNBOOK.md', 'tool-numbered-receipts'):
'one-time lane setup, completed 2026-10-31 (NK-WP-0056-T10)',
('sso-mfa/k8s/lldap/BIND-SPLIT-RUNBOOK.md', 'tool-numbered-receipts'):
'one-time bind split, completed (NK-WP-0054)',
('sso-mfa/k8s/synthetic-journey/T09-RUNBOOK.md', 'tool-numbered-receipts'):
'one-time journey lane setup, completed (NK-WP-0056-T09)',
}
def argo_paths(platform):
"""(repository, path) pairs that an Argo CD Application in railiance-platform deploys."""
owned = set()
for app in sorted((platform / 'argocd').rglob('*.yaml')) if platform.is_dir() else []:
for doc in yaml.safe_load_all(app.read_text()):
if not isinstance(doc, dict) or doc.get('kind') != 'Application':
continue
for source in [doc['spec'].get('source')] + list(doc['spec'].get('sources') or []):
if source and source.get('path'):
repo = source['repoURL'].rstrip('/').rsplit('/', 1)[-1].removesuffix('.git')
owned.add((repo, source['path'].strip('/')))
return owned
def check_file(rel, text, owned=frozenset()):
findings, suffix = [], Path(rel).suffix or Path(rel).name
runbook = suffix in RUNBOOK or Path(rel).name == 'Makefile'
tool = suffix in TOOL and '/tests/' not in rel and not Path(rel).name.startswith('test_')
for n, full in enumerate(text.splitlines(), 1):
line = full.split(' #')[0] # a trailing shell comment is prose, not part of the command
if runbook and APPLY.search(line) and '--server-side' not in line:
findings.append((rel, n, 'server-side-apply'))
if WAIT.search(line) and line.count('--for') > 1:
findings.append((rel, n, 'one-wait-condition'))
if (tool or runbook) and ROUTE.search(line):
findings.append((rel, n, 'one-cluster-route'))
if suffix == '.md' and RECEIPT.search(line):
findings.append((rel, n, 'tool-numbered-receipts'))
if runbook and APPLY.search(line):
for repo, path in SOURCE.findall(line):
if any(repo == r and (path == p or path.startswith(p + '/')) for r, p in owned):
findings.append((rel, n, 'managing-controller'))
return [f for f in findings if (f[0], f[2]) not in EXCEPTIONS]
def lint(root, owned):
findings = []
for path in sorted(root.rglob('*')):
rel = path.relative_to(root).as_posix()
if not path.is_file() or RECORDS.search(rel) or path == HERE or HERE.parent / 'tests' in path.parents:
continue
if path.suffix in RUNBOOK | TOOL or path.name == 'Makefile':
findings += check_file(rel, path.read_text(errors='replace'), owned)
return findings
def main(argv=None):
roots = [Path(a).resolve() for a in (argv if argv is not None else sys.argv[1:])] or [HERE.parents[2]]
owned = argo_paths(roots[0].parent / 'railiance-platform')
findings = [(root.name, *f) for root in roots for f in lint(root, owned)]
for repo, rel, n, rule in findings:
print(f'{repo}/{rel}:{n} {rule}')
return 1 if findings else 0
if __name__ == '__main__':
sys.exit(main())

View file

@ -0,0 +1,71 @@
"""tools/conventions/lint.py (NK-WP-0058-T07): each rule on synthetic lines, and this repository is clean."""
import importlib.util
from pathlib import Path
import pytest
import yaml
REPO = Path(__file__).resolve().parents[3]
spec = importlib.util.spec_from_file_location('conventions_lint', REPO / 'tools/conventions/lint.py')
lint = importlib.util.module_from_spec(spec)
spec.loader.exec_module(lint)
def rules(rel, line, owned=frozenset()):
return [rule for _, _, rule in lint.check_file(rel, line + '\n', owned)]
@pytest.mark.parametrize('line, found', [
('kubectl apply -f deployment.yaml', ['server-side-apply']),
('$K apply --dry-run=server -f x.yaml && $K apply -f x.yaml', ['server-side-apply']),
('$(KUBECTL) apply -f manifests/rbac.yaml', ['server-side-apply']),
('kubectl apply --server-side -f deployment.yaml', []),
('kubectl get pods # apply nothing', []),
])
def test_server_side_apply(line, found):
assert rules('runbooks/x.md', line) == found
def test_one_wait_condition():
assert rules('x.sh', 'kubectl wait --for=condition=complete --for=condition=failed job/x') == [
'one-wait-condition']
assert rules('x.sh', 'kubectl wait --for=condition=available deploy/x') == []
@pytest.mark.parametrize('line, found', [
("args = ['kubectl', '--kubeconfig', '/home/worsch/.kube/config-hosteurope']", True),
("args = ['kubectl', '--server', 'https://127.0.0.1:16444']", True),
("KUBE = '/etc/rancher/k3s/k3s.yaml'", True),
("KUBECONFIG = os.environ.get('NK_KUBECONFIG', os.path.expanduser('~/.kube/config-railiance01'))", False),
])
def test_one_cluster_route(line, found):
assert ('one-cluster-route' in rules('tools/x.py', line)) is found
assert rules('tools/tests/test_x.py', line) == [] # tests may build synthetic routes
def test_tool_numbered_receipts():
assert rules('runbooks/x.md', 'python3 lane.py setup --receipt "$E/01-setup.json"') == ['tool-numbered-receipts']
assert rules('runbooks/x.md', 'python3 bx.py run') == []
def test_managing_controller(tmp_path):
app = tmp_path / 'railiance-platform/argocd/apps/x.yaml'
app.parent.mkdir(parents=True)
app.write_text(yaml.safe_dump({'kind': 'Application', 'spec': {'source': {
'repoURL': 'https://forgejo.coulomb.social/coulomb/informed-decision.git', 'path': 'deploy/lifecycle'}}}))
owned = lint.argo_paths(tmp_path / 'railiance-platform')
assert owned == {('informed-decision', 'deploy/lifecycle')}
line = 'kubectl apply --server-side -f ~/informed-decision/deploy/lifecycle/review-groups.yaml'
assert rules('runbooks/x.md', line, owned) == ['managing-controller']
assert rules('runbooks/x.md', 'kubectl apply --server-side -f ~/approval-engine/deploy/np.yaml', owned) == []
def test_records_and_exceptions_are_not_findings():
assert lint.RECORDS.search('docs/evidence/2026-11-05-x/README.md')
assert rules('sso-mfa/WORKPLAN.md', 'kubectl apply -f a.yaml') == [] # a listed exception with its reason
assert all(reason for reason in lint.EXCEPTIONS.values())
def test_this_repository_is_clean():
findings = lint.lint(REPO, lint.argo_paths(REPO.parent / 'railiance-platform'))
assert findings == [], findings