diff --git a/charts/access-engine/templates/deployment.yaml b/charts/access-engine/templates/deployment.yaml index 744c8b2..a899ab3 100644 --- a/charts/access-engine/templates/deployment.yaml +++ b/charts/access-engine/templates/deployment.yaml @@ -29,8 +29,10 @@ metadata: {{- include "access-engine.labels" . | nindent 4 }} spec: {{- if .Values.outbox.enabled }} - {{- if ne (int .Values.replicaCount) 1 }} - {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} + {{- $replicas := .Values.replicaCount }} + {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} + {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} + {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} {{- end }} strategy: type: Recreate diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml index 62f84ba..3afef53 100644 --- a/charts/flex-auth/templates/deployment.yaml +++ b/charts/flex-auth/templates/deployment.yaml @@ -28,8 +28,10 @@ metadata: {{- include "flex-auth.labels" . | nindent 4 }} spec: {{- if .Values.outbox.enabled }} - {{- if ne (int .Values.replicaCount) 1 }} - {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} + {{- $replicas := .Values.replicaCount }} + {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} + {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} + {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} {{- end }} strategy: type: Recreate diff --git a/tests/test_outbox_replicas_chart.py b/tests/test_outbox_replicas_chart.py new file mode 100644 index 0000000..aa4a155 --- /dev/null +++ b/tests/test_outbox_replicas_chart.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Actual Helm stop/start renders and unsafe replica value rejection; offline only.""" +import copy +import json +from pathlib import Path +import subprocess +import tempfile +import yaml + +ROOT = Path(__file__).resolve().parents[1] + +def render(chart, value, encoding="yaml", success=True): + with tempfile.TemporaryDirectory() as directory: + command = ["helm", "template", "flex-auth-secrets-engine", str(ROOT / "charts" / chart), + "--namespace", "flex-auth", "-f", str(ROOT / "values/secrets-engine.yaml"), + "--set", "outbox.enabled=true", "--set", "outbox.source=flex-auth.secrets-engine", + "--set", "envelopeSigning.enabled=true", "--set", "envelopeSigning.kid=test-kid", + "--set", "envelopeSigning.secretName=proposed-seed", "--set", "envelopeSigning.secretKey=seed"] + if encoding == "yaml": + overlay = Path(directory) / "values.yaml" + overlay.write_text(yaml.safe_dump({"replicaCount": value})) + command += ["-f", str(overlay)] + elif encoding == "json": + command += ["--set-json", "replicaCount=" + json.dumps(value)] + elif encoding == "string": + command += ["--set-string", "replicaCount=" + str(value)] + else: + command += ["--set", "replicaCount=" + str(value)] + result = subprocess.run(command, capture_output=True, text=True) + if not success: + assert result.returncode != 0, (chart, encoding, value) + assert "replicaCount" in result.stderr, result.stderr + return + assert result.returncode == 0, result.stderr + documents = [d for d in yaml.safe_load_all(result.stdout) if d] + deployment = next(d for d in documents if d["kind"] == "Deployment") + assert deployment["spec"]["strategy"] == {"type": "Recreate"} + assert type(deployment["spec"]["replicas"]) is int + assert deployment["spec"]["replicas"] == value + return documents + +count = 0 +for chart in ("flex-auth", "access-engine"): + for encoding in ("yaml", "json", "cli"): + stopped = render(chart, 0, encoding) + ready = render(chart, 1, encoding) + normalized = copy.deepcopy(ready) + next(d for d in normalized if d["kind"] == "Deployment")["spec"]["replicas"] = 0 + assert stopped == normalized, "stop must change replicas only, including identical pod projection/PVC" + count += 2 + for encoding in ("yaml", "json"): + for value in (-1, 2, 1.5, 0.5, True, False, None, "0", "1", [], {}): + render(chart, value, encoding, success=False) + count += 1 + # Helm loses lexical 0 vs0.0 and1 vs1.0 before template evaluation. + for value in (0.0, 1.0): + render(chart, value, encoding) + count += 1 + for value in ("-1", "2", "0.5", "1.5", "true", "false", "null", "0.0", "1.0"): + render(chart, value, "cli", success=False) + count += 1 + for value in ("0", "1"): + render(chart, value, "string", success=False) + count += 1 +print(f"stopped/single-writer chart checks passed: {count} actual Helm renders") diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md index 1481c1d..f737349 100644 --- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md @@ -302,3 +302,26 @@ unchanged. Exact local source resolution is no central/private read-placement, reef binding, custody, native rollout or consumer trust admission. Future signer image/config adoption remains separate. T06 remains `progress`; no second work item duplicates this bounded dependency. + + +2026-10-16 bounded stopped-chart source preparation (Custodian +`access-native-admission/stopped-chart-r2`): both outbox charts admit numeric +replicaCount exactly 0 (maintenance stop) or 1 (single writer), with Recreate, +process flock and bounded persistence unchanged. Fractions, strings, booleans, +negative values and 2+ refuse; Helm decodes YAML/JSON integral 0.0/1.0 as the same +numeric values as 0/1, so lexical float spelling is not distinguished. Actual +Helm stopped/ready renders differ only in replicas and retain the same Platform +source-rendered deployment projection. Owner preparation and independent +coordinator checks approve this bounded source change only; 86 Helm renders and +the existing outbox/delivery/receiver-port suite pass. No native stop, bootstrap, +rollout, custody, public trust, resource or protected-effect approval follows. +The proposed immutable image remains source revision +`84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0`, ordinary CI402 verified digest +`sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`. +This chart-only successor requires its own exact source/chart pin and reviewed +runtime scope; it can explicitly select the unchanged verified 84 image without +rebuilding it or claiming that its revision label names the new chart commit. +These chart/test/workplan-only paths do not trigger the existing image workflow. +Frozen rollout R1, Platform P2 and the package extraction2b/current05a remain +unchanged. T06 stays `progress`; actual stopped-fence verification and all native +admission gates remain open under the same existing task.