From 086d79a2736e34b916bd7de3c8a65e2ea38a3d64 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 17 Oct 2026 01:39:08 +0200 Subject: [PATCH] fix(deploy): keep secret dry-runs compatible with annotation policy Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa --- Makefile | 4 +-- tests/test_server_dryrun_commands.py | 30 +++++++++++++++++++ ...ATE-WP-0095-historical-source-receiving.md | 22 ++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/test_server_dryrun_commands.py diff --git a/Makefile b/Makefile index 52c2458..294d05c 100644 --- a/Makefile +++ b/Makefile @@ -174,12 +174,12 @@ railiance-state-hub-server-dry-run: --namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \ -f $(RAILIANCE_STATE_HUB_VALUES) \ --set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \ - $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \ + $(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \ $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \ $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \ $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \ if $(KUBECTL) get namespace $(RAILIANCE_STATE_HUB_NAMESPACE) >/dev/null 2>&1; then \ - $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \ + $(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \ $(KUBECTL) apply --dry-run=server -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \ else \ echo "Namespace $(RAILIANCE_STATE_HUB_NAMESPACE) does not exist; validating namespaced app manifests with client dry-run."; \ diff --git a/tests/test_server_dryrun_commands.py b/tests/test_server_dryrun_commands.py new file mode 100644 index 0000000..f337bc7 --- /dev/null +++ b/tests/test_server_dryrun_commands.py @@ -0,0 +1,30 @@ +"""Pure Make command fixture: run with --noconftest, no database or cluster.""" +import json +import os +from pathlib import Path +import subprocess +import sys +import pytest + +@pytest.mark.parametrize('namespace_exists',[True,False]) +def test_secret_server_dryrun_uses_ssa_without_force_or_other_phase_changes(tmp_path,namespace_exists): + makefile=Path(__file__).resolve().parents[1]/'Makefile' + log=tmp_path/'commands.jsonl';helm=tmp_path/'helm';kubectl=tmp_path/'kubectl' + helm.write_text('#!'+sys.executable+'\nprint("apiVersion: v1\\nkind: ConfigMap\\nmetadata: {name: fixture}")\n') + kubectl.write_text('#!'+sys.executable+'\nimport json,os,sys\nwith open(os.environ["NK_DRYRUN_LOG"],"a") as log:log.write(json.dumps(sys.argv[1:])+"\\n")\nif sys.argv[1:3]==["get","namespace"] and os.environ["NK_NAMESPACE_EXISTS"]=="0":raise SystemExit(1)\n') + helm.chmod(0o700);kubectl.chmod(0o700) + env=dict(os.environ);env['NK_DRYRUN_LOG']=str(log);env['NK_NAMESPACE_EXISTS']='1' if namespace_exists else '0' + p=subprocess.run(['make','--no-print-directory','-f',str(makefile),'railiance-state-hub-server-dry-run','HELM='+str(helm),'KUBECTL='+str(kubectl),'RAILIANCE_STATE_HUB_IMAGE_TAG=main-51bc70d'],cwd=tmp_path,env=env,capture_output=True,timeout=10) + assert p.returncode==0 + calls=[json.loads(line) for line in log.read_text().splitlines()] + applies=[args for args in calls if args[0]=='apply'] + secrets=[args for args in applies if any(p.endswith('.sops.yaml.template') for p in args)] + assert len(secrets)==2 + assert all('--force-conflicts' not in args and '--force' not in args for args in applies) + for args in secrets: + if '--dry-run=server' in args:assert '--server-side' in args + else:assert not namespace_exists and '--dry-run=client' in args and '--server-side' not in args + others=[args for args in applies if args not in secrets] + assert all('--server-side' not in args for args in others) + assert all('--dry-run=server' in args or '--dry-run=client' in args for args in applies) + assert not any(args[0] in {'create','replace','delete','patch'} for args in calls) diff --git a/workplans/STATE-WP-0095-historical-source-receiving.md b/workplans/STATE-WP-0095-historical-source-receiving.md index 384a2f7..d4019c3 100644 --- a/workplans/STATE-WP-0095-historical-source-receiving.md +++ b/workplans/STATE-WP-0095-historical-source-receiving.md @@ -237,3 +237,25 @@ unretirement or retirement is authorized for these histories. A rollback first fences cluster sync and restores registrar refusal before removing receiving support; it never rewrites archived IDs or UUIDs. This work does not establish original TLS recovery or owner-native admission. + +## Fix policy-compatible Secret-template server dry-run preparation + +```task +id: STATE-WP-0095-T08 +status: done +priority: high +state_hub_task_id: "2dd9b04f-2250-5a8a-b5bc-c8a009872858" +``` + +Prepared the narrow Makefile successor after the existing readiness command +refused its first Secret-template phase. Ordinary client-side apply adds the +last-applied annotation, correctly denied by the existing Platform +reject-secret-last-applied policy. Only the two Secret-template server +dry-run requests now use server-side apply with dry-run; no force-conflicts, +Secret value change, policy relaxation or actual Secret write is introduced. +Command-fixture tests verify both requests and preserve other dry-run phases. +Evidence and original refusal remain in the Custodian cluster historical +publication packet, revisions/r2 and r3-readiness. This is completed source +preparation; T06 remains the explicit native rollout gate. The runtime image +continues to be main-51bc70d; Makefile/workplan changes do not create a new +application image or native acceptance.