Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions CONTRACT.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
53 changes: 36 additions & 17 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/azure/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 6 additions & 1 deletion examples/gcp/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 19 additions & 5 deletions schemas/acceptance.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -74,9 +90,7 @@
"analyzerImage",
"supervisorImage",
"requirementsDigest",
"eks",
"aks",
"gke"
"eks"
],
"title": "AcceptanceEvidence",
"type": "object"
Expand Down
32 changes: 26 additions & 6 deletions supervisor/pig_supervisor/publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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

Expand Down
71 changes: 68 additions & 3 deletions tests/test_publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pig_supervisor.models import Requirements
from pig_supervisor.publication import (
CHECKS,
INSTALL_CHECKS,
Registry,
assemble,
chart_publication_needed,
Expand All @@ -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
Expand All @@ -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():
Expand Down
Loading