Files
aios/tests/test_release_evidence_materializer.py
T

319 lines
12 KiB
Python

from __future__ import annotations
import dataclasses
import hashlib
import json
import os
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from aios_trusted_candidate_builder import ( # noqa: E402
BuilderError,
TrustedCandidateBuilderConfig,
load_builder_policy,
)
from release_evidence_materializer import ( # noqa: E402
EvidenceMaterializationError,
TrustedBuilderEvidenceMaterializer,
)
from silent_release_controller import ReleaseCandidate, ReleaseState # noqa: E402
from silent_release_hardening import ( # noqa: E402
DeploymentPlan,
FinalizationPlan,
ReleasePlan,
TagPlan,
TrustedCandidateBundle,
)
POLICY_PATH = ROOT / "config" / "gitops" / "trusted-candidate-builder-policy.json"
def canonical(value: dict) -> bytes:
return json.dumps(
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode()
def digest(label: str) -> str:
return hashlib.sha256(label.encode()).hexdigest()
def write_bytes(path: Path, value: bytes) -> str:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
path.write_bytes(value)
path.chmod(0o600)
return hashlib.sha256(value).hexdigest()
def write_json(path: Path, value: dict) -> str:
return write_bytes(path, canonical(value))
def build_valid_source(root: Path) -> tuple[dict, dict, dict]:
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
recipe = config.fixed_recipe("V2R2C2")
version = recipe["version"]
version_root = root / version
version_root.mkdir(parents=True, mode=0o700)
tree_oid = "a" * 40
artifact_sha = write_bytes(version_root / "artifact.tar.gz", b"artifact-bytes")
manifest_sha = write_bytes(version_root / "manifest.json", b'{"manifest":"v1"}\n')
closure_sha = write_bytes(version_root / "checksums.sha256", b"checksums-v1\n")
policy_sha = write_bytes(
version_root / "trusted-release-policy.json",
b'{"trusted":"release-policy"}\n',
)
tests = {"passed": True, "receipt": "real-tests"}
canary = {"passed": True, "receipt": "real-canary"}
gitea = {
"consistent": True,
"remote_ref_oid": recipe["source_commit"],
"repository": config.repository,
"source_commit": recipe["source_commit"],
"tree_oid": tree_oid,
}
rollback_contract_sha = digest("rollback-contract")
rollback = {
"ready": True,
"rollback_sha256": rollback_contract_sha,
"source_commit": recipe["source_commit"],
}
secret_scan = {"artifact_sha256": artifact_sha, "passed": True}
write_json(version_root / "tests.json", tests)
write_json(version_root / "canary.json", canary)
write_json(version_root / "gitea.json", gitea)
write_json(version_root / "rollback.json", rollback)
write_json(version_root / "secret-scan.json", secret_scan)
candidate = ReleaseCandidate(
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
tree_oid=tree_oid,
artifact_sha256=artifact_sha,
manifest_sha256=manifest_sha,
tests=tests,
canary=canary,
rollback_ready=True,
git_consistency={
"consistent": True,
"repository": config.repository,
"source_commit": recipe["source_commit"],
"tree_oid": tree_oid,
},
gitea_consistency=gitea,
state=ReleaseState.READY_FOR_APPROVAL,
)
plan = ReleasePlan(
deployment=DeploymentPlan(
service="aios-silent-release-controller",
release_path="/srv/release/r1",
release_root="/srv/release",
cutover_link="/srv/release/current",
dropin_path="/srv/release/r1/deploy/systemd/controller.service",
dropin_sha256=digest("dropin"),
systemd_unit_sha256=digest("unit"),
rollback_path="/srv/release/rollback-r1",
rollback_sha256=rollback_contract_sha,
source_commit=recipe["source_commit"],
tree_oid=tree_oid,
artifact_sha256=artifact_sha,
manifest_sha256=manifest_sha,
package_closure_sha256=closure_sha,
authoritative_git_dir="/data/aios-gitops/repo.git",
repository_identity=config.repository,
baseline_before_sha256=digest("baseline"),
registry_before_sha256=digest("registry"),
production_tag_refset_before_sha256=digest("tagset"),
health_contract_version="trusted-builder/v1",
timeout_policy={"seconds": 10},
retry_policy={"attempts": 1},
policy_sha256=policy_sha,
health_contract={"checked": True},
),
tag=TagPlan(
version="trusted-v2r2c2-001",
tag_type="annotated",
expected_tag_oid="b" * 40,
expected_target_oid=recipe["source_commit"],
evidence_closure_oid="c" * 40,
approval_plan_id="owner-plan-trusted-v2r2c2",
repository_identity=config.repository,
gitea_owner="aios",
gitea_repository="aios",
current_remote_tag_oid=None,
remote_absent_evidence_sha256=digest("tag-absence"),
verification_contract="trusted-builder/v1",
),
finalization=FinalizationPlan(
version="trusted-v2r2c2-001",
expected_tag_oid="b" * 40,
expected_target_oid=recipe["source_commit"],
expected_previous_baseline_sha256=digest("baseline"),
registry_expected_record={"base": recipe["source_commit"]},
baseline_expected_record={"base": recipe["source_commit"]},
final_consistency_contract="trusted-builder/v1",
),
policy_sha256=policy_sha,
)
checks = {name: True for name in config.required_checks}
TrustedCandidateBundle(candidate, plan, checks).validate()
receipt = {
"schema": "aios.trusted-candidate-builder.evidence.v1",
"version": version,
"candidate": candidate.to_dict(),
"release_plan": plan.to_dict(),
}
write_json(version_root / "receipt.json", receipt)
return recipe, candidate.to_dict(), plan.to_dict()
def fake_git(tree_oid: str):
def runner(argv, **_kwargs):
if "rev-parse" in argv:
return SimpleNamespace(stdout=tree_oid + "\n", returncode=0)
return SimpleNamespace(stdout="", returncode=0)
return runner
def make_materializer(tmp_path: Path, source_root: Path, output_root: Path, tree_oid: str):
authority = tmp_path / "repo.git"
authority.mkdir(mode=0o700)
output_root.mkdir(mode=0o700)
return TrustedBuilderEvidenceMaterializer(
builder_policy=POLICY_PATH,
authority_git_dir=authority,
source_root=source_root,
output_root=output_root,
expected_owner_uid=None,
git_runner=fake_git(tree_oid),
)
def test_materializer_projects_only_a_fully_validated_closure_and_is_idempotent(tmp_path: Path) -> None:
source_root = tmp_path / "source"
source_root.mkdir(mode=0o700)
recipe, candidate, _plan = build_valid_source(source_root)
output_root = tmp_path / "builder-evidence"
materializer = make_materializer(
tmp_path, source_root, output_root, candidate["tree_oid"]
)
result = materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
assert result["status"] == "EVIDENCE_MATERIALIZED"
assert result["idempotent"] is False
target = output_root / recipe["version"]
assert (target / "receipt.json").is_file()
assert (target / "materialization.json").is_file()
assert (target / "artifact.tar.gz").read_bytes() == b"artifact-bytes"
replay = materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
assert replay["status"] == "EVIDENCE_MATERIALIZED"
assert replay["idempotent"] is True
assert replay["materialization_id"] == result["materialization_id"]
def test_materializer_refuses_tampered_or_incomplete_source_without_writing_output(tmp_path: Path) -> None:
source_root = tmp_path / "source"
source_root.mkdir(mode=0o700)
recipe, candidate, _plan = build_valid_source(source_root)
(source_root / recipe["version"] / "manifest.json").write_bytes(b"tampered")
(source_root / recipe["version"] / "manifest.json").chmod(0o600)
output_root = tmp_path / "builder-evidence"
materializer = make_materializer(
tmp_path, source_root, output_root, candidate["tree_oid"]
)
with pytest.raises(EvidenceMaterializationError, match="COLLECTOR_DIGEST_MISMATCH"):
materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
assert not (output_root / recipe["version"]).exists()
def test_materializer_denies_symlinked_evidence(tmp_path: Path) -> None:
source_root = tmp_path / "source"
source_root.mkdir(mode=0o700)
recipe, candidate, _plan = build_valid_source(source_root)
manifest = source_root / recipe["version"] / "manifest.json"
real = source_root / recipe["version"] / "manifest.real"
manifest.rename(real)
manifest.symlink_to(real.name)
output_root = tmp_path / "builder-evidence"
materializer = make_materializer(
tmp_path, source_root, output_root, candidate["tree_oid"]
)
with pytest.raises(EvidenceMaterializationError):
materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
assert not (output_root / recipe["version"]).exists()
def test_materializer_rejects_conflicting_existing_destination(tmp_path: Path) -> None:
source_root = tmp_path / "source"
source_root.mkdir(mode=0o700)
recipe, candidate, _plan = build_valid_source(source_root)
output_root = tmp_path / "builder-evidence"
materializer = make_materializer(
tmp_path, source_root, output_root, candidate["tree_oid"]
)
materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
target = output_root / recipe["version"] / "artifact.tar.gz"
target.chmod(0o600)
target.write_bytes(b"conflict")
target.chmod(0o600)
with pytest.raises(
EvidenceMaterializationError, match="EVIDENCE_DESTINATION_CONFLICT"
):
materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
def test_materializer_does_not_create_receipt_when_source_receipt_is_missing(tmp_path: Path) -> None:
source_root = tmp_path / "source"
source_root.mkdir(mode=0o700)
recipe, candidate, _plan = build_valid_source(source_root)
(source_root / recipe["version"] / "receipt.json").unlink()
output_root = tmp_path / "builder-evidence"
materializer = make_materializer(
tmp_path, source_root, output_root, candidate["tree_oid"]
)
with pytest.raises(EvidenceMaterializationError):
materializer.materialize(
version=recipe["version"],
candidate_id=recipe["candidate_id"],
source_commit=recipe["source_commit"],
)
assert not (output_root / recipe["version"] / "receipt.json").exists()