diff --git a/CI.md b/CI.md index 299e175..2a69b93 100644 --- a/CI.md +++ b/CI.md @@ -57,7 +57,9 @@ It does not publish an image or require GHCR credentials. The suite checks: The Lease expiry timestamp is advanced explicitly in handoff tests to avoid a five-minute wait. These checks do not exercise analyzer migrations or prove cloud identity, database TLS, trace persistence, analysis, or Dashboard delivery. -Those remain the separately authorized three-cloud gates in [RELEASING.md](RELEASING.md). +The 0.3.0 publication gate requires real AWS installation and canonical acceptance; +Azure and GCP remain experimental. Later versions retain the full three-cloud +gate described in [RELEASING.md](RELEASING.md). `uv run pytest -q` skips cluster tests. To run them locally, reproduce the cluster/registry setup and image build in diff --git a/CONTRACT.md b/CONTRACT.md index b0aa9e0..910fba3 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -1,5 +1,14 @@ # Deployment contract +## Release support + +The 0.3.0 release requires a verified clean installation on AWS/EKS and canonical +pipeline acceptance through the Dashboard. Azure/AKS and GCP/GKE are experimental. +The lifecycle behavior below is the implementation contract; this first release's +cloud evidence does not certify upgrades, controller self-update, credential +rotation, interrupted-migration recovery, or long-lived identity refresh. See +[release acceptance](RELEASING.md) for the evidence required by each release. + ## Ownership Terraform owns cloud database, storage, network, IAM, sizing, and backup settings. diff --git a/README.md b/README.md index 8f21d5c..f1fb8c4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ # PIG deployment -Deploy Promptless Instruction Governance into an existing EKS, AKS, or GKE cluster. +Deploy Promptless Instruction Governance into an existing EKS cluster. The 0.3.0 +release targets AWS clean installations; Azure/AKS and GCP/GKE support is experimental. Terraform provisions the customer database, native object storage, network access, workload identity, and recovery settings. A namespace-scoped supervisor installs and updates the analyzer from immutable releases. diff --git a/RELEASING.md b/RELEASING.md index 4be66a5..434ba74 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,8 +2,10 @@ The first source candidate is 0.3.0. The stable catalog is empty until an accepted release is published and its catalog promotion is reviewed. Local tests are not -installation acceptance. No three-cloud acceptance record is supplied by this -change, so its publication workflow cannot release an untested candidate. +installation acceptance. Version 0.3.0 requires a real AWS clean installation and +canonical pipeline acceptance before publication. Azure and GCP support is +experimental; their native adapters and deployment modules are not cloud-validated +by the first release. ## Artifact and trust boundary @@ -40,16 +42,26 @@ check can pass. No customer secret belongs in this repository or its evidence. 2. With explicit publication authorization, run the supervisor candidate workflow on `main`. Record both image digests and the exact public source commit. The supervisor image embeds that commit as its OCI revision and reports its version. -3. In separately authorized existing EKS, AKS, and GKE test environments, exercise - installation and real canonical acceptance; minor and major upgrades; pause - and pin; blocked release and automatic resume; Secret rotation; supervisor - self-update; and recovery. Test native workload identity refresh and exact - database TLS/CA behavior. Use approved recovery points before destructive work. +3. For 0.3.0, install the exact candidate into a fresh EKS namespace using S3, + verified PostgreSQL TLS, native workload identity, and the public supervisor + path. Prove enrollment, a canonical object written and readable at its exact + native URI, successful analysis of the matching fingerprint, hosted + acknowledgement, and the matching trace and analysis in the Dashboard. + Zero findings is a successful analysis. An image pull, HTTP 200, healthy Pods, + or green CI does not establish this result. 4. Add `releases/acceptance/VERSION.json` in a reviewed PR. Its shape is generated in [schemas/acceptance.json](schemas/acceptance.json). Supply `version`, `sourceCommit`, exact `analyzerImage`/`supervisorImage`, `requirementsDigest`, - optional `rollbackTo`, and `eks`, `aks`, `gke` records. Each cloud record needs - `testedAt` and an `evidence` map with these exact keys: + optional `rollbackTo`, and an `eks` record with `testedAt` and an `evidence` + map containing `install` and `canonicalAcceptance`. + + For 0.3.0, `aks` and `gke` reports are optional experimental evidence. Any + supplied report must cover both checks and meet the same freshness and URL + rules. Additional lifecycle checks may be recorded when actually tested. + A nonempty `rollbackTo` requires a `recovery` report for every required cloud. + + The AWS installation exception applies only to 0.3.0. Later versions retain + the EKS, AKS, and GKE gate with all nine checks until a reviewed policy change: ```text install canonicalAcceptance minorUpdate majorUpdate pausePin @@ -82,7 +94,7 @@ it does not mean a schema-1 image can run against the resulting database. Recove with a schema-2-compatible image or a forward repair. Include a `rollbackTo` entry only for a tested recovery path between releases with the same schema revision. -Define upgrade fixtures before cloud acceptance. Each minor or major transition +Define upgrade fixtures before testing upgrades. Each minor or major transition needs immutable source and image identities, a compatible supervisor-capable baseline, and a real change to exercise. Relabelling the same image or using an incompatible worker does not prove an upgrade. @@ -117,7 +129,7 @@ Merging makes the release eligible for automatic updates, including major versions. Do not squash away or delete the manifest commit referenced by its URL. Keep releases, tags, and those commits reachable and protected. -## Release-required engineering acceptance +## Scope of acceptance The automated suite covers native chart rendering, CRD admission, scope-limited RBAC, fake-API restart/retry behavior, recovery/capacity binding, credential @@ -127,9 +139,16 @@ migration compatibility, and bounded maintenance failures. The credential-free [Kubernetes CI suite](CI.md) also exercises real Helm install/upgrade, process handoff, admission, status conflicts, SSA, and RBAC in disposable kind clusters. -Remaining release gates are real three-cloud installation/update/recovery -acceptance, cloud SDK token refresh under real federation, supervisor self-update -and recovery with the real analyzer, PostgreSQL certificate/network -validation, both runtime images and charts anonymously pullable, and full host -pipeline/Dashboard evidence. A Docker build runs in pull-request CI; local -validation does not depend on a working Docker daemon. +For 0.3.0, required live evidence covers a clean AWS installation, verified +PostgreSQL TLS and S3 access through workload identity, and the full host pipeline +through Dashboard confirmation. Both runtime images and charts must be anonymously +pullable, and publication and stable activation still require review of the +concrete artifacts and evidence. + +This first-release gate does not establish minor/major upgrade behavior, +pause/pin and rotation in a real cloud, interrupted-migration recovery, long-lived +workload identity refresh, supervisor self-update with the real analyzer, or +Azure/GCP installation. Preserve those engineering checks for subsequent release +validation and for promoting experimental cloud support. Define genuine upgrade +and recovery fixtures before running them; a version-label change is not an upgrade. +A Docker build runs in pull-request CI; local validation does not require Docker. diff --git a/examples/README.md b/examples/README.md index b42327f..d430cc2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,8 @@ # Provision infrastructure for an existing cluster +Version 0.3.0 targets a clean AWS/EKS installation. Azure/AKS and GCP/GKE examples +are experimental and are not covered by the first release's cloud acceptance. + Choose [AWS](aws/README.md), [Azure](azure/README.md), or [GCP](gcp/README.md). Each example uses Terraform 1.11.4 and committed provider locks. Cluster creation, state backend creation, DNS/TLS ingress, model endpoints, and Secret delivery diff --git a/examples/azure/README.md b/examples/azure/README.md index 2526128..50c2cd4 100644 --- a/examples/azure/README.md +++ b/examples/azure/README.md @@ -1,5 +1,9 @@ # Existing AKS cluster +This cloud is experimental in 0.3.0. Its deployment module and native storage +adapter are implemented, but the first release validates AWS clean installation +and canonical acceptance. See [release operations](../../RELEASING.md). + Follow the [shared workflow](../README.md) with AzureRM provider 4.36.0. Supply an existing resource group, AKS OIDC issuer with workload identity enabled, delegated PostgreSQL subnet, private endpoint subnet, and private PostgreSQL/Blob DNS zones diff --git a/examples/gcp/README.md b/examples/gcp/README.md index c2f32f7..1630a0e 100644 --- a/examples/gcp/README.md +++ b/examples/gcp/README.md @@ -1,5 +1,9 @@ # Existing GKE cluster +This cloud is experimental in 0.3.0. Its deployment module and native storage +adapter are implemented, but the first release validates AWS clean installation +and canonical acceptance. See [release operations](../../RELEASING.md). + Follow the [shared workflow](../README.md) with Google provider 6.49.0. Supply an existing project with Cloud SQL, IAM, and Storage APIs enabled, a GKE workload identity pool, and a VPC with private services access and its allocated range @@ -36,7 +40,8 @@ The Cloud SQL instance uses Google-managed regional CA service. Deliver its regional CA bundle, then use both output `host` (certificate DNS identity) and `hostaddr` (private IP) in the libpq DSN with `sslmode=verify-full`. This avoids relying on an IP certificate name. Confirm actual certificate/DNS behavior in the -cloud acceptance environment before publication. +cloud acceptance environment before claiming GCP acceptance or promoting GCP +support beyond experimental. State uses an existing encrypted, private, versioned GCS bucket with native locking and the configured state CMEK. State and application CMEK grants are diff --git a/schemas/acceptance.json b/schemas/acceptance.json index 0c3e0e9..c183835 100644 --- a/schemas/acceptance.json +++ b/schemas/acceptance.json @@ -62,10 +62,26 @@ "$ref": "#/$defs/CloudAcceptance" }, "aks": { - "$ref": "#/$defs/CloudAcceptance" + "anyOf": [ + { + "$ref": "#/$defs/CloudAcceptance" + }, + { + "type": "null" + } + ], + "default": null }, "gke": { - "$ref": "#/$defs/CloudAcceptance" + "anyOf": [ + { + "$ref": "#/$defs/CloudAcceptance" + }, + { + "type": "null" + } + ], + "default": null } }, "required": [ @@ -74,9 +90,7 @@ "analyzerImage", "supervisorImage", "requirementsDigest", - "eks", - "aks", - "gke" + "eks" ], "title": "AcceptanceEvidence", "type": "object" diff --git a/supervisor/pig_supervisor/publication.py b/supervisor/pig_supervisor/publication.py index fa3ff94..efa2d0b 100644 --- a/supervisor/pig_supervisor/publication.py +++ b/supervisor/pig_supervisor/publication.py @@ -24,6 +24,8 @@ REPO = "https://github.com/Promptless/pig-deploy" CHARTS = ("pig-supervisor", "pig-trace-analyzer") +INITIAL_RELEASE = "0.3.0" +INSTALL_CHECKS = frozenset({"install", "canonicalAcceptance"}) CHECKS = frozenset( { "install", @@ -45,8 +47,8 @@ class CloudAcceptance(Contract): @model_validator(mode="after") def complete(self): - if set(self.evidence) != CHECKS: - raise ValueError("cloud acceptance must include every required check") + if not self.evidence or not set(self.evidence) <= CHECKS: + raise ValueError("cloud acceptance must contain only recognized checks") if not all(re.fullmatch(r"https://[^\s]+", value) for value in self.evidence.values()): raise ValueError("every acceptance check needs a public sanitized evidence URL") if self.tested_at.tzinfo is None: @@ -62,12 +64,30 @@ class AcceptanceEvidence(Contract): requirements_digest: Digest rollback_to: list[Digest] = Field(default_factory=list) eks: CloudAcceptance - aks: CloudAcceptance - gke: CloudAcceptance + aks: CloudAcceptance | None = None + gke: CloudAcceptance | None = None + + @model_validator(mode="after") + def release_coverage(self): + """Scope the first release to AWS installation; retain later lifecycle gates.""" + initial = self.version == INITIAL_RELEASE + required_checks = INSTALL_CHECKS if initial else CHECKS + required_clouds = {"eks"} if initial else {"eks", "aks", "gke"} + for name in ("eks", "aks", "gke"): + cloud = getattr(self, name) + if cloud is None: + if name in required_clouds: + raise ValueError(f"{name} acceptance is required for this release") + continue + if not required_checks <= set(cloud.evidence): + raise ValueError(f"{name} acceptance is missing required checks") + if self.rollback_to and name in required_clouds and "recovery" not in cloud.evidence: + raise ValueError("rollback declarations require recovery acceptance") + return self def validate_evidence(data: dict, version: str, now: datetime) -> AcceptanceEvidence: - """Require recent, exact-artifact evidence for all three real cloud environments.""" + """Require recent, exact-artifact evidence for the release's cloud coverage.""" stable_version(version) evidence = AcceptanceEvidence.model_validate(data) if evidence.version != version: @@ -77,7 +97,7 @@ def validate_evidence(data: dict, version: str, now: datetime) -> AcceptanceEvid if not evidence.supervisor_image.startswith("ghcr.io/promptless/pig-supervisor@"): raise ValueError("supervisor must use its own image repository") for cloud in (evidence.eks, evidence.aks, evidence.gke): - if not now - timedelta(days=14) <= cloud.tested_at <= now: + if cloud is not None and not now - timedelta(days=14) <= cloud.tested_at <= now: raise ValueError("acceptance evidence must be from the last fourteen days") return evidence diff --git a/tests/test_publication.py b/tests/test_publication.py index fda2765..95396c9 100644 --- a/tests/test_publication.py +++ b/tests/test_publication.py @@ -14,6 +14,7 @@ from pig_supervisor.models import Requirements from pig_supervisor.publication import ( CHECKS, + INSTALL_CHECKS, Registry, assemble, chart_publication_needed, @@ -38,9 +39,9 @@ def evidence_data(commit="a" * 40): **{ cloud: { "testedAt": NOW.isoformat(), - "evidence": {check: "https://example.com/acceptance/" + check for check in CHECKS}, + "evidence": {check: "https://example.com/acceptance/" + check for check in INSTALL_CHECKS}, } - for cloud in ("eks", "aks", "gke") + for cloud in ("eks",) }, } return data, requirements @@ -53,9 +54,73 @@ def test_evidence_requires_real_cloud_checks_and_exact_artifacts(): validate_evidence(data, "0.3.1", NOW) with pytest.raises(ValueError): validate_evidence(data, "0.3.0", NOW + timedelta(days=15)) - del data["gke"]["evidence"]["recovery"] + + +@pytest.mark.parametrize("check", sorted(INSTALL_CHECKS)) +def test_initial_release_requires_both_install_and_canonical_acceptance(check): + data, _ = evidence_data() + del data["eks"]["evidence"][check] + with pytest.raises(ValueError, match="required checks"): + validate_evidence(data, "0.3.0", NOW) + + +def test_initial_release_cannot_omit_aws_or_advertise_untested_rollback(): + data, _ = evidence_data() + aws = data.pop("eks") with pytest.raises(ValueError): validate_evidence(data, "0.3.0", NOW) + data["eks"] = aws + data["rollbackTo"] = ["c" * 64] + with pytest.raises(ValueError, match="recovery acceptance"): + validate_evidence(data, "0.3.0", NOW) + aws["evidence"]["recovery"] = "https://example.com/acceptance/recovery" + assert validate_evidence(data, "0.3.0", NOW).rollback_to == ["c" * 64] + + +@pytest.mark.parametrize("version", ["0.3.1", "1.0.0"]) +def test_later_releases_retain_full_three_cloud_lifecycle_gate(version): + data, _ = evidence_data() + data["version"] = version + with pytest.raises(ValueError): + validate_evidence(data, version, NOW) + for cloud in ("eks", "aks", "gke"): + data[cloud] = { + "testedAt": NOW.isoformat(), + "evidence": {check: "https://example.com/acceptance/" + check for check in CHECKS}, + } + assert validate_evidence(data, version, NOW).version == version + for cloud in ("aks", "gke"): + report = data.pop(cloud) + with pytest.raises(ValueError, match=f"{cloud} acceptance is required"): + validate_evidence(data, version, NOW) + data[cloud] = None + with pytest.raises(ValueError, match=f"{cloud} acceptance is required"): + validate_evidence(data, version, NOW) + data[cloud] = report + del data["gke"]["evidence"]["recovery"] + with pytest.raises(ValueError, match="required checks"): + validate_evidence(data, version, NOW) + + +@pytest.mark.parametrize("cloud", ["aks", "gke"]) +def test_optional_experimental_cloud_reports_still_require_fresh_complete_evidence(cloud): + data, _ = evidence_data() + data[cloud] = {"testedAt": (NOW - timedelta(days=15)).isoformat(), "evidence": dict(data["eks"]["evidence"])} + with pytest.raises(ValueError, match="fourteen days"): + validate_evidence(data, "0.3.0", NOW) + data[cloud]["testedAt"] = NOW.isoformat() + assert getattr(validate_evidence(data, "0.3.0", NOW), cloud) is not None + del data[cloud]["evidence"]["canonicalAcceptance"] + with pytest.raises(ValueError, match="required checks"): + validate_evidence(data, "0.3.0", NOW) + + +@pytest.mark.parametrize("link", ["http://example.com/report", "https://example.com/private report"]) +def test_acceptance_reports_require_https_links(link): + data, _ = evidence_data() + data["eks"]["evidence"]["install"] = link + with pytest.raises(ValueError, match="public sanitized evidence URL"): + validate_evidence(data, "0.3.0", NOW) def test_stale_worker_image_cannot_be_relabelled_as_native_release():