4678 lines
192 KiB
Python
4678 lines
192 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify, plan, and explicitly apply one immutable Yuqei backend package.
|
|
|
|
The default operation is read-only verification. Install, rollback, and
|
|
forward operations are plans unless ``--apply`` and the operation's exact
|
|
confirmation string are supplied; those controls are necessary but not
|
|
sufficient. Production apply currently fails closed while the declared
|
|
release blockers remain unresolved. The executor is deliberately offline: it
|
|
never builds or pulls images and it never manages traffic, DNS, volumes, or
|
|
database downgrades. Contract Web identity is package-bound but is deployed by
|
|
its separate web orchestrator, so no result from this tool claims completion of
|
|
the full cross-host release.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any, Callable, Iterator, Mapping
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
PACKAGE_FILE = "release-package.json"
|
|
BASE_COMPOSE_RELATIVE = "yuqei-infra/compose/docker-compose.yml"
|
|
PRODUCTION_COMPOSE_RELATIVE = "yuqei-infra/compose/docker-compose.production.yml"
|
|
OFFLINE_COMPOSE_RELATIVE = "yuqei-infra/compose/docker-compose.production.offline.yml"
|
|
|
|
PACKAGE_SCHEMA_V1 = "yuqei.release-package.v1"
|
|
PACKAGE_SCHEMA_V2 = "yuqei.release-package.v2"
|
|
# Historical fixtures import this name. New installable candidates must opt in
|
|
# to v2 explicitly; v1 remains read-only compatibility only.
|
|
PACKAGE_SCHEMA = PACKAGE_SCHEMA_V1
|
|
CHECKSUM_SCHEMA = "yuqei.release-package-checksums.v1"
|
|
SOURCE_MANIFEST_SCHEMA = "yuqei.release-source-manifest.v1"
|
|
IMAGE_IDENTITY_SCHEMA_V1 = "yuqei.release-image-identity.v1"
|
|
IMAGE_IDENTITY_SCHEMA_V2 = "yuqei.release-image-identity.v2"
|
|
# Historical callers and fixtures import this name. Keep it pinned to v1;
|
|
# v2 is opt-in and is never inferred from the payload shape.
|
|
IMAGE_IDENTITY_SCHEMA = IMAGE_IDENTITY_SCHEMA_V1
|
|
BACKUP_PROOF_SCHEMA = "yuqei.release-backup-proof.v1"
|
|
COMPATIBILITY_PROOF_SCHEMA = "yuqei.release-rollback-compatibility.v1"
|
|
INSTALLABLE_MANIFEST_SCHEMA = "yuqei.installable-release-manifest.v1"
|
|
DOCKER_IMPORT_PLAN_SCHEMA = "yuqei.docker-image-import-plan.v1"
|
|
PLAN_SCHEMA = "yuqei.release-package-plan.v4"
|
|
RECEIPT_SCHEMA = "yuqei.release-package-receipt.v3"
|
|
PENDING_SCHEMA = "yuqei.release-package-pending.v3"
|
|
POST_APPLY_SCHEMA = "yuqei.release-post-apply-check.v2"
|
|
PREFLIGHT_BINDING_SCHEMA = "yuqei.production-preflight-binding.v2"
|
|
INFRASTRUCTURE_SOURCE_SET_SCHEMA = "yuqei.infrastructure-image-source-set.v2"
|
|
INFRASTRUCTURE_BUNDLE_SCHEMA = "yuqei.infrastructure-image-bundle.v2"
|
|
THIRD_PARTY_INPUT_SCHEMA = "yuqei.third-party-materials-input.v1"
|
|
THIRD_PARTY_MANIFEST_SCHEMA = "yuqei.third-party-materials.v1"
|
|
THIRD_PARTY_VERIFICATION_SCHEMA = "yuqei.third-party-materials-verification.v1"
|
|
RELEASE_IMPORT_RESULT_SCHEMA = "yuqei.docker-image-import-result.v2"
|
|
RECOVERY_READINESS_RESULT = "PASS_RECOVERY_READINESS"
|
|
|
|
DOCKER_CLI = "/usr/bin/docker"
|
|
LOCAL_DOCKER_HOST = "unix:///var/run/docker.sock"
|
|
FIXED_DOCKER_HOME = "/var/empty"
|
|
REAL_APPLY_BLOCKERS = (
|
|
"infrastructure_docker_import_plan_not_available",
|
|
"backup_and_compatibility_external_signatures_not_verified",
|
|
)
|
|
FIRST_RELEASE_REAL_APPLY_BLOCKERS = (
|
|
"infrastructure_docker_import_plan_not_available",
|
|
"preinstall_system_and_database_snapshot_evidence_not_verified",
|
|
)
|
|
LEGACY_PACKAGE_APPLY_BLOCKER = "legacy_package_schema_not_apply_eligible"
|
|
THIRD_PARTY_MATERIALS_BLOCKER = "third_party_materials_not_independently_verified"
|
|
|
|
RECOVERY_STRATEGY_PREDECESSOR = "predecessor_release"
|
|
RECOVERY_STRATEGY_FIRST_RELEASE = "first_release_snapshot_restore"
|
|
FIRST_RELEASE_REQUIRED_RESTORE_SCOPES = (
|
|
"host_runtime_state",
|
|
"postgres_consistent_backup",
|
|
"object_storage_state",
|
|
"release_environment_and_tls_bindings",
|
|
)
|
|
FIRST_RELEASE_REQUIRED_POSTGRES_DATABASES = ("contract", "ai", "operations")
|
|
FIRST_RELEASE_REQUIRED_STORAGE_TARGETS = (
|
|
"contract_files_volume",
|
|
"minio_object_store",
|
|
)
|
|
PREDECESSOR_EXTERNAL_EVIDENCE_REQUIREMENTS = (
|
|
"externally_signed_backup_proof",
|
|
"externally_signed_schema_compatibility_proof",
|
|
"approved_predecessor_application_import_plan",
|
|
)
|
|
FIRST_RELEASE_EXTERNAL_EVIDENCE_REQUIREMENTS = (
|
|
"signed_no_prior_successful_release_receipt",
|
|
"preinstall_quiesced_system_and_data_snapshot",
|
|
"independent_snapshot_restore_test",
|
|
"single_named_release_owner_approval",
|
|
)
|
|
|
|
CONFIRMATIONS = {
|
|
"install": "I_AUTHORIZE_THIS_EXACT_RELEASE_INSTALL",
|
|
"rollback": "I_AUTHORIZE_THIS_EXACT_RELEASE_ROLLBACK",
|
|
"forward": "I_AUTHORIZE_THIS_EXACT_RELEASE_FORWARD",
|
|
}
|
|
|
|
APPLICATION_IMAGE_KEYS = (
|
|
"CONTRACT_API_IMAGE",
|
|
"CONTRACT_WEB_IMAGE",
|
|
"AI_PLATFORM_API_IMAGE",
|
|
"AI_WORKER_IMAGE",
|
|
)
|
|
BACKEND_APPLICATION_IMAGE_KEYS = (
|
|
"CONTRACT_API_IMAGE",
|
|
"AI_PLATFORM_API_IMAGE",
|
|
"AI_WORKER_IMAGE",
|
|
)
|
|
INFRASTRUCTURE_IMAGE_KEYS = (
|
|
"POSTGRES_IMAGE",
|
|
"POSTGRES_CLIENT_IMAGE",
|
|
"REDIS_IMAGE",
|
|
"MINIO_IMAGE",
|
|
"MINIO_MC_IMAGE",
|
|
"NGINX_IMAGE",
|
|
"BACKUP_UTILITY_IMAGE",
|
|
)
|
|
BACKEND_INSTALL_INFRASTRUCTURE_IMAGE_KEYS = (
|
|
"POSTGRES_IMAGE",
|
|
"POSTGRES_CLIENT_IMAGE",
|
|
"REDIS_IMAGE",
|
|
"MINIO_IMAGE",
|
|
)
|
|
DEPLOYMENT_SCOPE = "backend_only"
|
|
SEPARATELY_DEPLOYED_IMAGE_KEYS = ("CONTRACT_WEB_IMAGE",)
|
|
RUNTIME_SERVICES = (
|
|
"contract-api",
|
|
"contract-review-worker",
|
|
"contract-playbook-governance-worker",
|
|
"ai-platform-api",
|
|
"worker",
|
|
)
|
|
INSTALL_SERVICES = (
|
|
"postgres",
|
|
"postgres-init-databases",
|
|
"redis",
|
|
"ai-redis",
|
|
"minio",
|
|
"contract-api-migrate",
|
|
"ai-platform-api-migrate",
|
|
"ai-platform-prompt-assets-import",
|
|
*RUNTIME_SERVICES,
|
|
)
|
|
V1_REQUIRED_ARTIFACTS = (
|
|
"release_manifest",
|
|
"checksum_manifest",
|
|
"image_identity",
|
|
"installable_manifest",
|
|
)
|
|
V2_REQUIRED_ARTIFACTS = (
|
|
*V1_REQUIRED_ARTIFACTS,
|
|
"infrastructure_source_set",
|
|
"infrastructure_image_bundle",
|
|
"third_party_materials_input",
|
|
"third_party_materials_manifest",
|
|
"third_party_materials_verification_receipt",
|
|
)
|
|
# Backwards compatible export used by older callers.
|
|
REQUIRED_ARTIFACTS = V1_REQUIRED_ARTIFACTS
|
|
OPTIONAL_EVIDENCE_ARTIFACTS = ("backup_proof", "compatibility_proof")
|
|
|
|
IMPORT_ROLE_ENV_KEYS = (
|
|
("contract-web", "CONTRACT_WEB_IMAGE"),
|
|
("contract-api", "CONTRACT_API_IMAGE"),
|
|
("ai-platform-api", "AI_PLATFORM_API_IMAGE"),
|
|
("ai-worker", "AI_WORKER_IMAGE"),
|
|
)
|
|
|
|
RELEASE_IMPORT_ROLE_ENV_SERVICES = (
|
|
("contract-web", "CONTRACT_WEB_IMAGE", ()),
|
|
(
|
|
"contract-api",
|
|
"CONTRACT_API_IMAGE",
|
|
(
|
|
"contract-api",
|
|
"contract-api-migrate",
|
|
"contract-review-worker",
|
|
"contract-playbook-governance-worker",
|
|
),
|
|
),
|
|
(
|
|
"ai-platform-api",
|
|
"AI_PLATFORM_API_IMAGE",
|
|
(
|
|
"ai-platform-api",
|
|
"ai-platform-api-migrate",
|
|
"ai-platform-prompt-assets-import",
|
|
),
|
|
),
|
|
("ai-worker", "AI_WORKER_IMAGE", ("worker",)),
|
|
("postgres-runtime", "POSTGRES_IMAGE", ("postgres",)),
|
|
("postgres-client", "POSTGRES_CLIENT_IMAGE", ("postgres-init-databases",)),
|
|
("redis", "REDIS_IMAGE", ("redis", "ai-redis")),
|
|
("minio", "MINIO_IMAGE", ("minio",)),
|
|
("minio-client", "MINIO_MC_IMAGE", ("minio-backup",)),
|
|
("nginx-edge", "NGINX_IMAGE", ("edge",)),
|
|
("backup-utility", "BACKUP_UTILITY_IMAGE", ("contract-files-backup",)),
|
|
)
|
|
|
|
LONG_RUNNING_INSTALL_SERVICES = (
|
|
"postgres",
|
|
"redis",
|
|
"ai-redis",
|
|
"minio",
|
|
*RUNTIME_SERVICES,
|
|
)
|
|
ONE_SHOT_INSTALL_SERVICES = (
|
|
"postgres-init-databases",
|
|
"contract-api-migrate",
|
|
"ai-platform-api-migrate",
|
|
"ai-platform-prompt-assets-import",
|
|
)
|
|
|
|
POST_APPLY_WAIT_TIMEOUT_SECONDS = 300
|
|
POST_APPLY_POLL_INTERVAL_SECONDS = 2
|
|
|
|
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
COMMIT_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
|
RELEASE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
PROJECT_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,62}$")
|
|
IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
|
APPROVAL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$")
|
|
ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
IMAGE_REFERENCE_RE = re.compile(
|
|
r"^[a-z0-9](?:[a-z0-9._:/-]{0,510}[a-z0-9])?@sha256:[0-9a-f]{64}$"
|
|
)
|
|
IMAGE_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
MAX_JSON_BYTES = 4 * 1024 * 1024
|
|
MAX_ENV_BYTES = 1024 * 1024
|
|
|
|
FORBIDDEN_COMMAND_TOKENS = frozenset(
|
|
{
|
|
"down",
|
|
"rm",
|
|
"prune",
|
|
"build",
|
|
"pull",
|
|
"push",
|
|
"volume",
|
|
"volumes",
|
|
"downgrade",
|
|
"edge",
|
|
"dns",
|
|
"--volumes",
|
|
"-v",
|
|
}
|
|
)
|
|
|
|
|
|
class ReleasePackageError(RuntimeError):
|
|
"""Raised when a package or requested transition fails closed."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ArtifactPin:
|
|
path: str
|
|
sha256: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ValidatedPackage:
|
|
root: Path
|
|
package_schema_version: str
|
|
package: dict[str, Any]
|
|
package_sha256: str
|
|
checksum_manifest: dict[str, Any]
|
|
checksum_manifest_sha256: str
|
|
release_manifest: dict[str, Any]
|
|
release_manifest_sha256: str
|
|
image_identity: dict[str, Any]
|
|
image_identity_sha256: str
|
|
installable_manifest: dict[str, Any]
|
|
installable_manifest_sha256: str
|
|
installable_root: Path
|
|
trusted_build_receipt_sha256: str
|
|
docker_import_plan: tuple[dict[str, Any], ...]
|
|
offline_image_overrides: dict[str, str]
|
|
infrastructure_source_set: dict[str, Any] | None
|
|
infrastructure_source_set_sha256: str | None
|
|
infrastructure_bundle: dict[str, Any] | None
|
|
infrastructure_bundle_sha256: str | None
|
|
third_party_materials_input: dict[str, Any] | None
|
|
third_party_materials_input_sha256: str | None
|
|
third_party_materials_manifest: dict[str, Any] | None
|
|
third_party_materials_manifest_sha256: str | None
|
|
third_party_verification_receipt: dict[str, Any] | None
|
|
third_party_verification_receipt_sha256: str | None
|
|
third_party_profile: str | None
|
|
backup_proof: dict[str, Any] | None
|
|
backup_proof_sha256: str | None
|
|
compatibility_proof: dict[str, Any] | None
|
|
compatibility_proof_sha256: str | None
|
|
application_images: dict[str, dict[str, str]]
|
|
infrastructure_images: dict[str, dict[str, str]]
|
|
recovery_strategy: str
|
|
recovery_policy: dict[str, Any]
|
|
rollback_images: dict[str, dict[str, str]] | None
|
|
rollback_release_id: str | None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExecutionOutcome:
|
|
payload: dict[str, Any]
|
|
exit_code: int = 0
|
|
replayed: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimeEvidence:
|
|
env_file: Path
|
|
env_values: dict[str, str]
|
|
env_sha256: str
|
|
preflight_summary: dict[str, Any]
|
|
compose_gate_summary: dict[str, Any]
|
|
daemon_identity: dict[str, str]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SnapshotInputs:
|
|
root: Path
|
|
source_env_file: Path
|
|
env_file: Path
|
|
compose_files: tuple[Path, Path, Path]
|
|
docker_archives: dict[str, Path]
|
|
tls_files: dict[str, Path]
|
|
manifest: dict[str, Any]
|
|
|
|
|
|
_PREFLIGHT_MODULE: Any | None = None
|
|
_COMPOSE_GATE_MODULE: Any | None = None
|
|
_INSTALLABLE_VALIDATOR_MODULE: Any | None = None
|
|
_INFRASTRUCTURE_BUNDLE_MODULE: Any | None = None
|
|
_THIRD_PARTY_MATERIALS_MODULE: Any | None = None
|
|
_SNAPSHOT_RESTORE_PLANNER_MODULE: Any | None = None
|
|
|
|
|
|
def _exec_module_without_bytecode(spec: Any, module: Any) -> None:
|
|
"""Execute a local validator without mutating its source tree."""
|
|
previous = sys.dont_write_bytecode
|
|
sys.dont_write_bytecode = True
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
finally:
|
|
sys.dont_write_bytecode = previous
|
|
|
|
|
|
def _load_local_module(filename: str, module_name: str) -> Any:
|
|
path = Path(__file__).resolve().with_name(filename)
|
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise ReleasePackageError(f"Cannot load required deployment gate: {filename}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[module_name] = module
|
|
try:
|
|
_exec_module_without_bytecode(spec, module)
|
|
except Exception as exc:
|
|
sys.modules.pop(module_name, None)
|
|
raise ReleasePackageError(f"Cannot load required deployment gate: {filename}") from exc
|
|
return module
|
|
|
|
|
|
def _load_preflight_module() -> Any:
|
|
global _PREFLIGHT_MODULE
|
|
if _PREFLIGHT_MODULE is None:
|
|
_PREFLIGHT_MODULE = _load_local_module(
|
|
"preflight-production.py",
|
|
"yuqei_release_package_preflight",
|
|
)
|
|
return _PREFLIGHT_MODULE
|
|
|
|
|
|
def _load_compose_gate_module() -> Any:
|
|
global _COMPOSE_GATE_MODULE
|
|
if _COMPOSE_GATE_MODULE is None:
|
|
_COMPOSE_GATE_MODULE = _load_local_module(
|
|
"verify-production-compose.py",
|
|
"yuqei_release_package_compose_gate",
|
|
)
|
|
return _COMPOSE_GATE_MODULE
|
|
|
|
|
|
def _load_installable_validator_module() -> Any:
|
|
global _INSTALLABLE_VALIDATOR_MODULE
|
|
if _INSTALLABLE_VALIDATOR_MODULE is None:
|
|
path = Path(__file__).resolve().parents[1] / "scripts" / "build-installable-release.py"
|
|
spec = importlib.util.spec_from_file_location(
|
|
"yuqei_release_package_installable_validator",
|
|
path,
|
|
)
|
|
if spec is None or spec.loader is None:
|
|
raise ReleasePackageError("Cannot load the strict installable manifest validator.")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
try:
|
|
_exec_module_without_bytecode(spec, module)
|
|
except Exception as exc:
|
|
sys.modules.pop(spec.name, None)
|
|
raise ReleasePackageError(
|
|
"Cannot load the strict installable manifest validator."
|
|
) from exc
|
|
_INSTALLABLE_VALIDATOR_MODULE = module
|
|
return _INSTALLABLE_VALIDATOR_MODULE
|
|
|
|
|
|
def _load_script_module(relative: str, module_name: str, *, label: str) -> Any:
|
|
path = Path(__file__).resolve().parents[1] / relative
|
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise ReleasePackageError(f"Cannot load the fixed {label}.")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[module_name] = module
|
|
try:
|
|
_exec_module_without_bytecode(spec, module)
|
|
except Exception as exc:
|
|
sys.modules.pop(module_name, None)
|
|
raise ReleasePackageError(f"Cannot load the fixed {label}.") from exc
|
|
return module
|
|
|
|
|
|
def _load_infrastructure_bundle_module() -> Any:
|
|
global _INFRASTRUCTURE_BUNDLE_MODULE
|
|
if _INFRASTRUCTURE_BUNDLE_MODULE is None:
|
|
_INFRASTRUCTURE_BUNDLE_MODULE = _load_script_module(
|
|
"scripts/build-infrastructure-image-bundle.py",
|
|
"yuqei_release_package_infrastructure_bundle",
|
|
label="infrastructure image bundle validator",
|
|
)
|
|
return _INFRASTRUCTURE_BUNDLE_MODULE
|
|
|
|
|
|
def _load_third_party_materials_module() -> Any:
|
|
global _THIRD_PARTY_MATERIALS_MODULE
|
|
if _THIRD_PARTY_MATERIALS_MODULE is None:
|
|
_THIRD_PARTY_MATERIALS_MODULE = _load_script_module(
|
|
"scripts/build-third-party-materials.py",
|
|
"yuqei_release_package_third_party_materials",
|
|
label="third-party materials validator",
|
|
)
|
|
return _THIRD_PARTY_MATERIALS_MODULE
|
|
|
|
|
|
def _load_snapshot_restore_planner_module() -> Any:
|
|
global _SNAPSHOT_RESTORE_PLANNER_MODULE
|
|
if _SNAPSHOT_RESTORE_PLANNER_MODULE is None:
|
|
_SNAPSHOT_RESTORE_PLANNER_MODULE = _load_local_module(
|
|
"plan-first-release-snapshot-restore.py",
|
|
"yuqei_release_package_snapshot_restore_planner",
|
|
)
|
|
return _SNAPSHOT_RESTORE_PLANNER_MODULE
|
|
|
|
|
|
def canonical_json_bytes(value: object) -> bytes:
|
|
try:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
except (TypeError, ValueError) as exc:
|
|
raise ReleasePackageError("Value is not canonical JSON.") from exc
|
|
|
|
|
|
def canonical_sha256(value: object) -> str:
|
|
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
|
|
|
|
|
def file_sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def confirmation_for(operation: str) -> str:
|
|
try:
|
|
return CONFIRMATIONS[operation]
|
|
except KeyError as exc:
|
|
raise ReleasePackageError(f"Unsupported write operation: {operation}") from exc
|
|
|
|
|
|
def _real_apply_blockers(
|
|
operation: str,
|
|
*,
|
|
recovery_strategy: str = RECOVERY_STRATEGY_PREDECESSOR,
|
|
package_schema_version: str = PACKAGE_SCHEMA_V1,
|
|
infrastructure_ready: bool = False,
|
|
third_party_ready: bool = False,
|
|
recovery_readiness_ready: bool = False,
|
|
) -> list[str]:
|
|
if package_schema_version == PACKAGE_SCHEMA_V1:
|
|
blockers = [LEGACY_PACKAGE_APPLY_BLOCKER]
|
|
blockers.extend(
|
|
FIRST_RELEASE_REAL_APPLY_BLOCKERS
|
|
if recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else REAL_APPLY_BLOCKERS
|
|
)
|
|
else:
|
|
blockers = []
|
|
if not infrastructure_ready:
|
|
blockers.append("infrastructure_docker_import_plan_not_available")
|
|
if not third_party_ready:
|
|
blockers.append(THIRD_PARTY_MATERIALS_BLOCKER)
|
|
if recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE:
|
|
if not recovery_readiness_ready:
|
|
blockers.append(
|
|
"preinstall_system_and_database_snapshot_evidence_not_verified"
|
|
)
|
|
else:
|
|
blockers.append(
|
|
"backup_and_compatibility_external_signatures_not_verified"
|
|
)
|
|
if (
|
|
operation == "rollback"
|
|
and recovery_strategy == RECOVERY_STRATEGY_PREDECESSOR
|
|
):
|
|
blockers.append("rollback_docker_import_plan_not_available")
|
|
return blockers
|
|
|
|
|
|
def _reject_json_constant(value: str) -> None:
|
|
raise ReleasePackageError(f"Non-finite JSON value is forbidden: {value}")
|
|
|
|
|
|
def _reject_duplicate_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
result: dict[str, Any] = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise ReleasePackageError(f"Duplicate JSON key is forbidden: {key}")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _read_json(path: Path, *, label: str) -> dict[str, Any]:
|
|
try:
|
|
size = path.stat().st_size
|
|
if size <= 0 or size > MAX_JSON_BYTES:
|
|
raise ReleasePackageError(f"{label} must be non-empty and no larger than 4 MiB.")
|
|
raw = path.read_bytes()
|
|
payload = json.loads(
|
|
raw.decode("utf-8"),
|
|
object_pairs_hook=_reject_duplicate_pairs,
|
|
parse_constant=_reject_json_constant,
|
|
)
|
|
except ReleasePackageError:
|
|
raise
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise ReleasePackageError(f"{label} is not readable strict UTF-8 JSON.") from exc
|
|
if not isinstance(payload, dict):
|
|
raise ReleasePackageError(f"{label} must be a JSON object.")
|
|
return payload
|
|
|
|
|
|
def _verify_embedded_hash(
|
|
payload: Mapping[str, Any],
|
|
field: str,
|
|
*,
|
|
label: str,
|
|
) -> None:
|
|
expected = payload.get(field)
|
|
if not isinstance(expected, str) or not SHA256_RE.fullmatch(expected):
|
|
raise ReleasePackageError(f"{label} has no valid {field}.")
|
|
unsigned = {key: value for key, value in payload.items() if key != field}
|
|
if canonical_sha256(unsigned) != expected:
|
|
raise ReleasePackageError(f"{label} canonical payload hash mismatch.")
|
|
|
|
|
|
def _absolute_lexical(path: Path) -> Path:
|
|
return Path(os.path.abspath(os.fspath(path)))
|
|
|
|
|
|
def _reject_symlink_components(path: Path, *, label: str) -> None:
|
|
absolute = _absolute_lexical(path)
|
|
current = Path(absolute.anchor)
|
|
for part in absolute.parts[1:]:
|
|
current = current / part
|
|
try:
|
|
if current.exists() and current.is_symlink():
|
|
raise ReleasePackageError(f"{label} must not contain symbolic links: {current}")
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"Unable to inspect {label} path components.") from exc
|
|
|
|
|
|
def _validated_directory(path: Path, *, label: str) -> Path:
|
|
lexical = _absolute_lexical(path)
|
|
_reject_symlink_components(lexical, label=label)
|
|
try:
|
|
resolved = lexical.resolve(strict=True)
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"{label} does not exist.") from exc
|
|
if lexical.is_symlink() or not resolved.is_dir():
|
|
raise ReleasePackageError(f"{label} must be a real directory, not a symbolic link.")
|
|
return resolved
|
|
|
|
|
|
def _validated_file(path: Path, *, label: str, max_bytes: int | None = None) -> Path:
|
|
lexical = _absolute_lexical(path)
|
|
if not lexical.is_absolute(): # pragma: no cover - os.path.abspath guarantees this.
|
|
raise ReleasePackageError(f"{label} must be absolute.")
|
|
_reject_symlink_components(lexical, label=label)
|
|
try:
|
|
resolved = lexical.resolve(strict=True)
|
|
size = resolved.stat().st_size
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"{label} is unavailable.") from exc
|
|
if lexical.is_symlink() or not resolved.is_file():
|
|
raise ReleasePackageError(f"{label} must be a real regular file, not a symbolic link.")
|
|
if max_bytes is not None and (size <= 0 or size > max_bytes):
|
|
raise ReleasePackageError(f"{label} has an invalid size.")
|
|
return resolved
|
|
|
|
|
|
def _safe_relative_path(value: object, *, label: str) -> str:
|
|
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value:
|
|
raise ReleasePackageError(f"{label} must use a non-empty canonical POSIX relative path.")
|
|
pure = PurePosixPath(value)
|
|
if (
|
|
pure.is_absolute()
|
|
or pure.as_posix() != value
|
|
or any(part in {"", ".", ".."} for part in pure.parts)
|
|
):
|
|
raise ReleasePackageError(f"{label} contains path traversal or is not canonical: {value!r}")
|
|
return value
|
|
|
|
|
|
def _artifact_file(root: Path, relative_path: str, *, label: str) -> Path:
|
|
relative_path = _safe_relative_path(relative_path, label=label)
|
|
lexical = root.joinpath(*PurePosixPath(relative_path).parts)
|
|
_reject_symlink_components(lexical, label=label)
|
|
try:
|
|
resolved = lexical.resolve(strict=True)
|
|
resolved.relative_to(root)
|
|
except (OSError, ValueError) as exc:
|
|
raise ReleasePackageError(f"{label} escapes the release package or is missing.") from exc
|
|
if lexical.is_symlink() or not resolved.is_file():
|
|
raise ReleasePackageError(f"{label} must be a real regular file, not a symbolic link.")
|
|
return resolved
|
|
|
|
|
|
def _artifact_pin(package: Mapping[str, Any], name: str) -> ArtifactPin:
|
|
artifacts = package.get("artifacts")
|
|
if not isinstance(artifacts, dict):
|
|
raise ReleasePackageError("release-package.json has no artifacts object.")
|
|
raw = artifacts.get(name)
|
|
if not isinstance(raw, dict):
|
|
raise ReleasePackageError(f"Required release package artifact is missing: {name}")
|
|
if set(raw) != {"path", "sha256"}:
|
|
raise ReleasePackageError(f"Artifact {name} fields are missing or unknown.")
|
|
path = _safe_relative_path(raw.get("path"), label=f"artifact {name} path")
|
|
digest = raw.get("sha256")
|
|
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
|
|
raise ReleasePackageError(f"Artifact {name} has no valid lowercase SHA-256.")
|
|
return ArtifactPin(path=path, sha256=digest)
|
|
|
|
|
|
def _validate_identifier(value: object, pattern: re.Pattern[str], *, label: str) -> str:
|
|
if not isinstance(value, str) or not pattern.fullmatch(value):
|
|
raise ReleasePackageError(f"{label} is missing or unsafe.")
|
|
return value
|
|
|
|
|
|
def _validate_image_map(
|
|
value: object,
|
|
expected_keys: tuple[str, ...],
|
|
*,
|
|
label: str,
|
|
) -> dict[str, dict[str, str]]:
|
|
if not isinstance(value, dict) or set(value) != set(expected_keys):
|
|
raise ReleasePackageError(f"{label} must contain exactly: {', '.join(expected_keys)}")
|
|
result: dict[str, dict[str, str]] = {}
|
|
for key in expected_keys:
|
|
raw = value[key]
|
|
if not isinstance(raw, dict):
|
|
raise ReleasePackageError(f"{label}.{key} must be an object.")
|
|
reference = raw.get("reference")
|
|
digest = raw.get("digest")
|
|
if not isinstance(reference, str) or not IMAGE_REFERENCE_RE.fullmatch(reference):
|
|
raise ReleasePackageError(f"{label}.{key} is not an immutable safe image reference.")
|
|
if not isinstance(digest, str) or not IMAGE_DIGEST_RE.fullmatch(digest):
|
|
raise ReleasePackageError(f"{label}.{key} has no valid image digest.")
|
|
if reference.rsplit("@", 1)[1] != digest:
|
|
raise ReleasePackageError(f"{label}.{key} reference and digest do not match.")
|
|
result[key] = {"reference": reference, "digest": digest}
|
|
return result
|
|
|
|
|
|
def _validate_image_identity_recovery(
|
|
image_identity: Mapping[str, Any],
|
|
*,
|
|
application_images: Mapping[str, Mapping[str, str]],
|
|
) -> tuple[
|
|
str,
|
|
dict[str, Any],
|
|
dict[str, dict[str, str]] | None,
|
|
str | None,
|
|
]:
|
|
schema = image_identity.get("schema_version")
|
|
if schema == IMAGE_IDENTITY_SCHEMA_V1:
|
|
if "recovery" in image_identity:
|
|
raise ReleasePackageError(
|
|
"Image identity v1 must not mix the v2 recovery contract with legacy rollback fields."
|
|
)
|
|
rollback = image_identity.get("rollback")
|
|
if not isinstance(rollback, dict) or set(rollback) != {
|
|
"target_release_id",
|
|
"evidence_status",
|
|
"application_images",
|
|
}:
|
|
raise ReleasePackageError("Image identity has no complete rollback candidate.")
|
|
if rollback.get("evidence_status") != "external_approval_missing":
|
|
raise ReleasePackageError(
|
|
"Rollback image identity must remain unapproved until external evidence exists."
|
|
)
|
|
rollback_release_id = _validate_identifier(
|
|
rollback.get("target_release_id"),
|
|
RELEASE_ID_RE,
|
|
label="rollback target release_id",
|
|
)
|
|
rollback_images = _validate_image_map(
|
|
rollback.get("application_images"),
|
|
APPLICATION_IMAGE_KEYS,
|
|
label="rollback.application_images",
|
|
)
|
|
if rollback_images == application_images:
|
|
raise ReleasePackageError(
|
|
"Rollback image identity must differ from the install image identity."
|
|
)
|
|
return (
|
|
RECOVERY_STRATEGY_PREDECESSOR,
|
|
{
|
|
"strategy": RECOVERY_STRATEGY_PREDECESSOR,
|
|
"evidence_status": "external_approval_missing",
|
|
"predecessor_package_required": True,
|
|
"database_downgrade_allowed": False,
|
|
"target_release_id": rollback_release_id,
|
|
},
|
|
rollback_images,
|
|
rollback_release_id,
|
|
)
|
|
|
|
if schema != IMAGE_IDENTITY_SCHEMA_V2:
|
|
raise ReleasePackageError("Unsupported image identity schema.")
|
|
expected_identity_fields = {
|
|
"schema_version",
|
|
"release_id",
|
|
"source_commit",
|
|
"source_manifest_sha256",
|
|
"application_images",
|
|
"infrastructure_images",
|
|
"recovery",
|
|
"identity_payload_sha256",
|
|
}
|
|
if set(image_identity) != expected_identity_fields:
|
|
raise ReleasePackageError(
|
|
"Image identity v2 fields are missing or unknown; legacy rollback fields are forbidden."
|
|
)
|
|
recovery = image_identity.get("recovery")
|
|
if not isinstance(recovery, dict):
|
|
raise ReleasePackageError("Image identity v2 has no recovery contract.")
|
|
strategy = recovery.get("strategy")
|
|
if strategy == RECOVERY_STRATEGY_PREDECESSOR:
|
|
expected_recovery_fields = {
|
|
"strategy",
|
|
"target_release_id",
|
|
"evidence_status",
|
|
"application_images",
|
|
}
|
|
if set(recovery) != expected_recovery_fields:
|
|
raise ReleasePackageError(
|
|
"Predecessor recovery fields are missing or unknown."
|
|
)
|
|
if recovery.get("evidence_status") != "external_approval_missing":
|
|
raise ReleasePackageError(
|
|
"Predecessor recovery must remain unapproved until external evidence exists."
|
|
)
|
|
rollback_release_id = _validate_identifier(
|
|
recovery.get("target_release_id"),
|
|
RELEASE_ID_RE,
|
|
label="recovery target release_id",
|
|
)
|
|
rollback_images = _validate_image_map(
|
|
recovery.get("application_images"),
|
|
APPLICATION_IMAGE_KEYS,
|
|
label="recovery.application_images",
|
|
)
|
|
if rollback_images == application_images:
|
|
raise ReleasePackageError(
|
|
"Predecessor recovery image identity must differ from the install image identity."
|
|
)
|
|
return (
|
|
strategy,
|
|
{
|
|
"strategy": strategy,
|
|
"evidence_status": "external_approval_missing",
|
|
"predecessor_package_required": True,
|
|
"database_downgrade_allowed": False,
|
|
"target_release_id": rollback_release_id,
|
|
},
|
|
rollback_images,
|
|
rollback_release_id,
|
|
)
|
|
|
|
if strategy == RECOVERY_STRATEGY_FIRST_RELEASE:
|
|
expected_recovery_fields = {
|
|
"strategy",
|
|
"evidence_status",
|
|
"first_release_only",
|
|
"predecessor_package_required",
|
|
"database_downgrade_allowed",
|
|
"required_restore_scopes",
|
|
"required_postgres_databases",
|
|
"required_storage_targets",
|
|
}
|
|
if set(recovery) != expected_recovery_fields:
|
|
raise ReleasePackageError(
|
|
"First-release snapshot recovery fields are missing or unknown; a predecessor is forbidden."
|
|
)
|
|
if (
|
|
recovery.get("evidence_status") != "runtime_signed_evidence_required"
|
|
or recovery.get("first_release_only") is not True
|
|
or recovery.get("predecessor_package_required") is not False
|
|
or recovery.get("database_downgrade_allowed") is not False
|
|
or recovery.get("required_restore_scopes")
|
|
!= list(FIRST_RELEASE_REQUIRED_RESTORE_SCOPES)
|
|
or recovery.get("required_postgres_databases")
|
|
!= list(FIRST_RELEASE_REQUIRED_POSTGRES_DATABASES)
|
|
or recovery.get("required_storage_targets")
|
|
!= list(FIRST_RELEASE_REQUIRED_STORAGE_TARGETS)
|
|
):
|
|
raise ReleasePackageError(
|
|
"First-release snapshot recovery must declare the fixed fail-closed evidence and restore scope."
|
|
)
|
|
return strategy, dict(recovery), None, None
|
|
|
|
raise ReleasePackageError("Image identity v2 has an unsupported recovery strategy.")
|
|
|
|
|
|
def _recovery_contract(package: ValidatedPackage) -> dict[str, Any]:
|
|
first_release = package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
return {
|
|
"strategy": package.recovery_strategy,
|
|
"predecessor_package_required": not first_release,
|
|
"rollback_plan_available": not first_release,
|
|
"forward_plan_available": not first_release,
|
|
"rollback_target_release_id": package.rollback_release_id,
|
|
"required_restore_scopes": (
|
|
list(FIRST_RELEASE_REQUIRED_RESTORE_SCOPES) if first_release else []
|
|
),
|
|
"required_postgres_databases": (
|
|
list(FIRST_RELEASE_REQUIRED_POSTGRES_DATABASES) if first_release else []
|
|
),
|
|
"required_storage_targets": (
|
|
list(FIRST_RELEASE_REQUIRED_STORAGE_TARGETS) if first_release else []
|
|
),
|
|
"external_evidence_requirements": list(
|
|
FIRST_RELEASE_EXTERNAL_EVIDENCE_REQUIREMENTS
|
|
if first_release
|
|
else PREDECESSOR_EXTERNAL_EVIDENCE_REQUIREMENTS
|
|
),
|
|
"external_restore_executor_required": first_release,
|
|
"post_restore_current_release_operation": "install" if first_release else "forward",
|
|
}
|
|
|
|
|
|
def _assert_operation_supported_for_recovery(
|
|
package: ValidatedPackage,
|
|
*,
|
|
operation: str,
|
|
) -> None:
|
|
if (
|
|
package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
and operation in {"rollback", "forward"}
|
|
):
|
|
raise ReleasePackageError(
|
|
"First-release snapshot recovery forbids package rollback/forward; use the "
|
|
"externally approved snapshot-restore planner, then run a new full install."
|
|
)
|
|
|
|
|
|
def _validate_installable_manifest(
|
|
root: Path,
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
source_commit: str,
|
|
application_images: Mapping[str, Mapping[str, str]],
|
|
) -> tuple[dict[str, Any], ...]:
|
|
if payload.get("schema_version") != INSTALLABLE_MANIFEST_SCHEMA:
|
|
raise ReleasePackageError("Unsupported installable release manifest schema.")
|
|
_verify_embedded_hash(
|
|
payload,
|
|
"manifest_payload_sha256",
|
|
label="installable release manifest",
|
|
)
|
|
candidate = payload.get("candidate")
|
|
if not isinstance(candidate, dict) or candidate.get("commit") != source_commit:
|
|
raise ReleasePackageError("Installable manifest is not bound to the release source commit.")
|
|
images = payload.get("images")
|
|
expected_roles = [role for role, _key in IMPORT_ROLE_ENV_KEYS]
|
|
if (
|
|
not isinstance(images, list)
|
|
or any(not isinstance(item, dict) for item in images)
|
|
or [item.get("role") for item in images] != expected_roles
|
|
):
|
|
raise ReleasePackageError("Installable manifest must contain the four application roles in fixed order.")
|
|
images_by_role = {item["role"]: item for item in images}
|
|
|
|
raw_plan = payload.get("docker_import_plan")
|
|
if not isinstance(raw_plan, dict) or raw_plan.get("schema_version") != DOCKER_IMPORT_PLAN_SCHEMA:
|
|
raise ReleasePackageError("Installable manifest has no supported docker_import_plan.")
|
|
if raw_plan.get("implicit_preloaded_images_allowed") is not False:
|
|
raise ReleasePackageError("Docker import plan must forbid implicit preloaded images.")
|
|
raw_imports = raw_plan.get("images")
|
|
if not isinstance(raw_imports, list) or len(raw_imports) != len(IMPORT_ROLE_ENV_KEYS):
|
|
raise ReleasePackageError("Docker import plan must contain exactly four application archives.")
|
|
|
|
normalized: list[dict[str, Any]] = []
|
|
seen_paths: set[str] = set()
|
|
seen_ids: set[str] = set()
|
|
for sequence, ((expected_role, expected_key), raw) in enumerate(
|
|
zip(IMPORT_ROLE_ENV_KEYS, raw_imports, strict=True),
|
|
start=1,
|
|
):
|
|
if not isinstance(raw, dict):
|
|
raise ReleasePackageError("Docker import plan entries must be objects.")
|
|
if (
|
|
raw.get("sequence") != sequence
|
|
or raw.get("role") != expected_role
|
|
or raw.get("environment_key") != expected_key
|
|
):
|
|
raise ReleasePackageError("Docker import plan role, sequence, or environment key drifted.")
|
|
image = images_by_role[expected_role]
|
|
if image.get("environment_key") != expected_key:
|
|
raise ReleasePackageError("Installable image environment key drifted.")
|
|
|
|
archive = raw.get("archive")
|
|
docker_archive = image.get("docker_archive")
|
|
if not isinstance(archive, dict) or archive != docker_archive:
|
|
raise ReleasePackageError("Docker import plan archive is not bound to its image record.")
|
|
if set(archive) != {"path", "sha256", "size_bytes"}:
|
|
raise ReleasePackageError("Docker archive record has unexpected or missing fields.")
|
|
relative = _safe_relative_path(archive.get("path"), label="Docker archive path")
|
|
digest = archive.get("sha256")
|
|
size = archive.get("size_bytes")
|
|
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
|
|
raise ReleasePackageError("Docker archive record has an invalid SHA-256.")
|
|
if not isinstance(size, int) or isinstance(size, bool) or size <= 0:
|
|
raise ReleasePackageError("Docker archive record has an invalid size.")
|
|
if relative in seen_paths:
|
|
raise ReleasePackageError("Docker import plan reuses an archive path.")
|
|
seen_paths.add(relative)
|
|
archive_path = _artifact_file(root, relative, label=f"{expected_role} Docker archive")
|
|
archive_stat = archive_path.stat()
|
|
if archive_stat.st_nlink != 1:
|
|
raise ReleasePackageError(f"{expected_role} Docker archive must not be hard linked.")
|
|
if archive_stat.st_size != size or file_sha256(archive_path) != digest:
|
|
raise ReleasePackageError(f"{expected_role} Docker archive size or SHA-256 mismatch.")
|
|
|
|
config_digest = image.get("config_digest")
|
|
manifest_digest = image.get("manifest_digest")
|
|
registry_reference = image.get("registry_reference")
|
|
expected_tag = raw.get("expected_repo_tag")
|
|
if (
|
|
not isinstance(expected_tag, str)
|
|
or not expected_tag
|
|
or len(expected_tag) > 512
|
|
or any(character.isspace() or ord(character) < 32 for character in expected_tag)
|
|
):
|
|
raise ReleasePackageError("Docker import plan has an unsafe expected repository tag.")
|
|
if not isinstance(config_digest, str) or not IMAGE_DIGEST_RE.fullmatch(config_digest):
|
|
raise ReleasePackageError("Installable image has an invalid config digest.")
|
|
if config_digest in seen_ids:
|
|
raise ReleasePackageError("Docker import plan reuses a config-based image ID.")
|
|
seen_ids.add(config_digest)
|
|
if not isinstance(manifest_digest, str) or not IMAGE_DIGEST_RE.fullmatch(manifest_digest):
|
|
raise ReleasePackageError("Installable image has an invalid OCI manifest digest.")
|
|
if not isinstance(registry_reference, str) or not IMAGE_REFERENCE_RE.fullmatch(
|
|
registry_reference
|
|
):
|
|
raise ReleasePackageError("Installable image has an invalid online registry reference.")
|
|
if (
|
|
raw.get("command_argv")
|
|
!= [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"image",
|
|
"load",
|
|
"--input",
|
|
relative,
|
|
]
|
|
or raw.get("expected_loaded_image_id") != config_digest
|
|
or raw.get("expected_oci_manifest_digest") != manifest_digest
|
|
or raw.get("offline_compose_override") != config_digest
|
|
or raw.get("registry_reference_for_online_release") != registry_reference
|
|
or raw.get("repo_digests_expected_after_load") != []
|
|
or image.get("offline_image_id") != config_digest
|
|
or image.get("image_ref") != expected_tag
|
|
):
|
|
raise ReleasePackageError("Docker import plan does not match the installable image identity.")
|
|
identity = application_images[expected_key]
|
|
if identity["reference"] != registry_reference or identity["digest"] != manifest_digest:
|
|
raise ReleasePackageError(
|
|
f"Docker import plan online identity does not match application_images.{expected_key}."
|
|
)
|
|
normalized.append(
|
|
{
|
|
"sequence": sequence,
|
|
"image_class": "application",
|
|
"role": expected_role,
|
|
"environment_key": expected_key,
|
|
"services": list(RELEASE_IMPORT_ROLE_ENV_SERVICES[sequence - 1][2]),
|
|
"archive_path": relative,
|
|
"archive_sha256": digest,
|
|
"archive_size_bytes": size,
|
|
"expected_repo_tag": expected_tag,
|
|
"expected_loaded_image_id": config_digest,
|
|
"expected_oci_manifest_digest": manifest_digest,
|
|
"expected_platform": "linux/amd64",
|
|
"tag_command_argv": [],
|
|
"offline_compose_override": config_digest,
|
|
"registry_reference_for_online_release": registry_reference,
|
|
}
|
|
)
|
|
return tuple(normalized)
|
|
|
|
|
|
def _validate_checksum_manifest(
|
|
root: Path,
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
package_file_path: str,
|
|
checksum_file_path: str,
|
|
) -> dict[str, dict[str, Any]]:
|
|
if payload.get("schema_version") != CHECKSUM_SCHEMA:
|
|
raise ReleasePackageError("Unsupported checksum manifest schema.")
|
|
_verify_embedded_hash(payload, "manifest_payload_sha256", label="checksum manifest")
|
|
raw_items = payload.get("files")
|
|
if not isinstance(raw_items, list) or not raw_items:
|
|
raise ReleasePackageError("Checksum manifest must list package artifacts.")
|
|
records: dict[str, dict[str, Any]] = {}
|
|
casefolded: set[str] = set()
|
|
observed_order: list[str] = []
|
|
for raw in raw_items:
|
|
if not isinstance(raw, dict):
|
|
raise ReleasePackageError("Checksum manifest file entries must be objects.")
|
|
relative = _safe_relative_path(raw.get("path"), label="checksum entry path")
|
|
digest = raw.get("sha256")
|
|
size = raw.get("size_bytes")
|
|
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
|
|
raise ReleasePackageError(f"Checksum entry has invalid SHA-256: {relative}")
|
|
if not isinstance(size, int) or isinstance(size, bool) or size < 0:
|
|
raise ReleasePackageError(f"Checksum entry has invalid size: {relative}")
|
|
if relative in records or relative.casefold() in casefolded:
|
|
raise ReleasePackageError(f"Checksum manifest has a duplicate path: {relative}")
|
|
if relative in {package_file_path, checksum_file_path}:
|
|
raise ReleasePackageError("The package and checksum manifests must not checksum themselves.")
|
|
records[relative] = {"sha256": digest, "size_bytes": size}
|
|
casefolded.add(relative.casefold())
|
|
observed_order.append(relative)
|
|
if observed_order != sorted(observed_order, key=lambda item: item.encode("utf-8")):
|
|
raise ReleasePackageError("Checksum manifest file entries must be bytewise path sorted.")
|
|
|
|
for relative, record in records.items():
|
|
target = _artifact_file(root, relative, label=f"checksummed artifact {relative}")
|
|
if target.stat().st_size != record["size_bytes"]:
|
|
raise ReleasePackageError(f"Artifact size mismatch: {relative}")
|
|
if file_sha256(target) != record["sha256"]:
|
|
raise ReleasePackageError(f"Artifact checksum mismatch: {relative}")
|
|
|
|
actual_files: set[str] = set()
|
|
for path in root.rglob("*"):
|
|
if path.is_symlink():
|
|
raise ReleasePackageError(f"Release package contains a symbolic link: {path}")
|
|
if path.is_file():
|
|
actual_files.add(path.relative_to(root).as_posix())
|
|
elif not path.is_dir():
|
|
raise ReleasePackageError(f"Release package contains a non-regular entry: {path}")
|
|
expected_files = {package_file_path, checksum_file_path, *records}
|
|
if actual_files != expected_files:
|
|
missing = sorted(expected_files - actual_files)
|
|
unexpected = sorted(actual_files - expected_files)
|
|
raise ReleasePackageError(
|
|
"Release package file set differs from its checksum manifest: "
|
|
f"missing={missing}, unexpected={unexpected}"
|
|
)
|
|
return records
|
|
|
|
|
|
def _validate_source_manifest(payload: Mapping[str, Any]) -> str:
|
|
if payload.get("schema_version") != SOURCE_MANIFEST_SCHEMA:
|
|
raise ReleasePackageError("Unsupported release source manifest schema.")
|
|
_verify_embedded_hash(payload, "manifest_payload_sha256", label="release source manifest")
|
|
candidate = payload.get("candidate")
|
|
if not isinstance(candidate, dict):
|
|
raise ReleasePackageError("Release source manifest has no candidate identity.")
|
|
commit = _validate_identifier(candidate.get("commit"), COMMIT_RE, label="source commit")
|
|
if candidate.get("worktree_was_clean") is not True:
|
|
raise ReleasePackageError("Release source manifest is not from a clean candidate.")
|
|
files = payload.get("files")
|
|
if not isinstance(files, dict) or not isinstance(files.get("items"), list):
|
|
raise ReleasePackageError("Release source manifest has no file inventory.")
|
|
if files.get("count") != len(files["items"]):
|
|
raise ReleasePackageError("Release source manifest file count is inconsistent.")
|
|
return commit
|
|
|
|
|
|
def _source_candidate_tag(payload: Mapping[str, Any]) -> str:
|
|
candidate = payload.get("candidate")
|
|
tag = candidate.get("tag") if isinstance(candidate, Mapping) else None
|
|
if not isinstance(tag, str) or not tag.strip() or len(tag) > 255:
|
|
raise ReleasePackageError("Release source manifest has no valid candidate tag.")
|
|
return tag
|
|
|
|
|
|
def _validate_source_bound_tool(
|
|
source_manifest: Mapping[str, Any],
|
|
repo_root: Path,
|
|
relative: str,
|
|
*,
|
|
loaded_file: Path,
|
|
) -> None:
|
|
by_path: dict[str, Mapping[str, Any]] = {}
|
|
for raw in source_manifest["files"]["items"]:
|
|
if isinstance(raw, Mapping) and isinstance(raw.get("path"), str):
|
|
by_path[raw["path"]] = raw
|
|
record = by_path.get(relative)
|
|
if record is None:
|
|
raise ReleasePackageError(
|
|
f"Release source manifest does not bind required validator: {relative}"
|
|
)
|
|
expected = record.get("sha256")
|
|
if not isinstance(expected, str) or not SHA256_RE.fullmatch(expected):
|
|
raise ReleasePackageError(f"Release source validator hash is invalid: {relative}")
|
|
repository_copy = _artifact_file(
|
|
_validated_directory(repo_root, label="repository root"),
|
|
relative,
|
|
label=f"source-bound validator {relative}",
|
|
)
|
|
executable_copy = _validated_file(loaded_file, label=f"loaded validator {relative}")
|
|
if file_sha256(repository_copy) != expected or file_sha256(executable_copy) != expected:
|
|
raise ReleasePackageError(
|
|
f"Loaded validator differs from the frozen source manifest: {relative}"
|
|
)
|
|
|
|
|
|
def _validate_infrastructure_bundle(
|
|
*,
|
|
package_root: Path,
|
|
artifact_root: Path,
|
|
source_set_path: Path,
|
|
bundle_path: Path,
|
|
installable_manifest: Mapping[str, Any],
|
|
source_commit: str,
|
|
candidate_tag: str,
|
|
infrastructure_images: Mapping[str, Mapping[str, str]],
|
|
application_plan: tuple[dict[str, Any], ...],
|
|
) -> tuple[dict[str, Any], dict[str, Any], tuple[dict[str, Any], ...], dict[str, str]]:
|
|
source_set = _read_json(source_set_path, label="infrastructure source set")
|
|
included_bundle = _read_json(bundle_path, label="infrastructure image bundle")
|
|
module = _load_infrastructure_bundle_module()
|
|
try:
|
|
rebuilt = module.build_bundle(
|
|
artifact_root=artifact_root,
|
|
source_set=source_set,
|
|
application_manifest=installable_manifest,
|
|
)
|
|
except Exception as exc:
|
|
if isinstance(exc, ReleasePackageError):
|
|
raise
|
|
raise ReleasePackageError(
|
|
f"Infrastructure image bundle validation failed: {exc}"
|
|
) from exc
|
|
if rebuilt != included_bundle:
|
|
raise ReleasePackageError(
|
|
"Infrastructure image bundle differs from independent deterministic recomputation."
|
|
)
|
|
if (
|
|
included_bundle.get("schema_version") != INFRASTRUCTURE_BUNDLE_SCHEMA
|
|
or source_set.get("schema_version") != INFRASTRUCTURE_SOURCE_SET_SCHEMA
|
|
or included_bundle.get("candidate")
|
|
!= {"commit": source_commit, "tag": candidate_tag}
|
|
):
|
|
raise ReleasePackageError("Infrastructure bundle candidate identity is invalid.")
|
|
|
|
raw_imports = included_bundle.get("docker_import_plan", {}).get("images")
|
|
raw_images = included_bundle.get("infrastructure_images")
|
|
if (
|
|
not isinstance(raw_imports, list)
|
|
or not isinstance(raw_images, list)
|
|
or len(raw_imports) != 7
|
|
or len(raw_images) != 7
|
|
):
|
|
raise ReleasePackageError("Infrastructure bundle must contain exactly seven images.")
|
|
normalized = [dict(item) for item in application_plan]
|
|
for offset, ((role, environment_key, services), raw_import, raw_image) in enumerate(
|
|
zip(RELEASE_IMPORT_ROLE_ENV_SERVICES[4:], raw_imports, raw_images, strict=True),
|
|
start=5,
|
|
):
|
|
if not isinstance(raw_import, Mapping) or not isinstance(raw_image, Mapping):
|
|
raise ReleasePackageError("Infrastructure image records must be objects.")
|
|
if (
|
|
raw_import.get("sequence") != offset - 4
|
|
or raw_import.get("role") != role
|
|
or raw_import.get("environment_key") != environment_key
|
|
or raw_image.get("role") != role
|
|
or raw_image.get("environment_key") != environment_key
|
|
or raw_image.get("services") != list(services)
|
|
):
|
|
raise ReleasePackageError("Infrastructure role, order, or service mapping drifted.")
|
|
archive = raw_import.get("archive")
|
|
if not isinstance(archive, Mapping) or set(archive) != {
|
|
"path",
|
|
"sha256",
|
|
"size_bytes",
|
|
}:
|
|
raise ReleasePackageError("Infrastructure Docker archive record is invalid.")
|
|
relative = _safe_relative_path(
|
|
archive.get("path"), label=f"{role} Docker archive path"
|
|
)
|
|
archive_path = _artifact_file(
|
|
artifact_root, relative, label=f"{role} Docker archive"
|
|
)
|
|
if (
|
|
archive_path.stat().st_nlink != 1
|
|
or archive_path.stat().st_size != archive.get("size_bytes")
|
|
or file_sha256(archive_path) != archive.get("sha256")
|
|
):
|
|
raise ReleasePackageError(f"{role} Docker archive is not immutable and bound.")
|
|
manifest_digest = raw_image.get("manifest_digest")
|
|
config_digest = raw_image.get("config_digest")
|
|
canonical_reference = raw_image.get("canonical_reference")
|
|
expected_tag = raw_image.get("expected_repo_tag")
|
|
identity = infrastructure_images[environment_key]
|
|
expected_tag_command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"image",
|
|
"tag",
|
|
config_digest,
|
|
expected_tag,
|
|
]
|
|
if (
|
|
not isinstance(manifest_digest, str)
|
|
or not IMAGE_DIGEST_RE.fullmatch(manifest_digest)
|
|
or not isinstance(config_digest, str)
|
|
or not IMAGE_DIGEST_RE.fullmatch(config_digest)
|
|
or not isinstance(canonical_reference, str)
|
|
or not IMAGE_REFERENCE_RE.fullmatch(canonical_reference)
|
|
or identity.get("reference") != canonical_reference
|
|
or identity.get("digest") != manifest_digest
|
|
or raw_import.get("expected_loaded_image_id") != config_digest
|
|
or raw_import.get("expected_manifest_digest") != manifest_digest
|
|
or raw_import.get("expected_platform") != "linux/amd64"
|
|
or raw_import.get("offline_compose_override") != config_digest
|
|
or raw_import.get("tag_command_argv") != expected_tag_command
|
|
):
|
|
raise ReleasePackageError(
|
|
f"Infrastructure import plan does not match image identity: {role}"
|
|
)
|
|
normalized.append(
|
|
{
|
|
"sequence": offset,
|
|
"image_class": "infrastructure",
|
|
"role": role,
|
|
"environment_key": environment_key,
|
|
"services": list(services),
|
|
"archive_path": relative,
|
|
"archive_package_path": archive_path.relative_to(package_root).as_posix(),
|
|
"archive_sha256": archive["sha256"],
|
|
"archive_size_bytes": archive["size_bytes"],
|
|
"expected_repo_tag": expected_tag,
|
|
"expected_loaded_image_id": config_digest,
|
|
"expected_oci_manifest_digest": manifest_digest,
|
|
"expected_platform": "linux/amd64",
|
|
"tag_command_argv": expected_tag_command,
|
|
"offline_compose_override": config_digest,
|
|
"registry_reference_for_online_release": canonical_reference,
|
|
}
|
|
)
|
|
|
|
if len(normalized) != len(RELEASE_IMPORT_ROLE_ENV_SERVICES):
|
|
raise ReleasePackageError("Release import plan does not cover exactly eleven images.")
|
|
for field in (
|
|
"role",
|
|
"environment_key",
|
|
"archive_package_path",
|
|
"expected_loaded_image_id",
|
|
"expected_oci_manifest_digest",
|
|
):
|
|
values = [str(item[field]).casefold() for item in normalized]
|
|
if len(values) != len(set(values)):
|
|
raise ReleasePackageError(f"Release import plan repeats or collides on {field}.")
|
|
overrides = included_bundle.get("offline_compose_overrides")
|
|
expected_overrides = {
|
|
item["environment_key"]: item["expected_loaded_image_id"]
|
|
for item in normalized
|
|
}
|
|
if overrides != expected_overrides:
|
|
raise ReleasePackageError("Infrastructure bundle offline override map is incomplete.")
|
|
return source_set, included_bundle, tuple(normalized), expected_overrides
|
|
|
|
|
|
def _validate_third_party_materials(
|
|
*,
|
|
input_path: Path,
|
|
manifest_path: Path,
|
|
receipt_path: Path,
|
|
source_commit: str,
|
|
candidate_tag: str,
|
|
source_manifest_sha256: str,
|
|
release_import_plan: tuple[dict[str, Any], ...],
|
|
installable_manifest: Mapping[str, Any],
|
|
infrastructure_bundle: Mapping[str, Any],
|
|
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], str]:
|
|
materials_input = _read_json(input_path, label="third-party materials input")
|
|
if materials_input.get("schema_version") != THIRD_PARTY_INPUT_SCHEMA:
|
|
raise ReleasePackageError("Unsupported third-party materials input schema.")
|
|
profile = materials_input.get("profile")
|
|
if profile not in {"internal-hosted", "redistributable"}:
|
|
raise ReleasePackageError("Third-party materials profile is invalid.")
|
|
if manifest_path.name != "materials-manifest.json":
|
|
raise ReleasePackageError("Third-party materials manifest must use its fixed name.")
|
|
module = _load_third_party_materials_module()
|
|
try:
|
|
recomputed_receipt = module.verify(input_path, manifest_path.parent, profile)
|
|
except Exception as exc:
|
|
raise ReleasePackageError(
|
|
f"Third-party materials independent verification failed: {exc}"
|
|
) from exc
|
|
materials_manifest = _read_json(manifest_path, label="third-party materials manifest")
|
|
included_receipt = _read_json(
|
|
receipt_path, label="third-party materials verification receipt"
|
|
)
|
|
if included_receipt != recomputed_receipt:
|
|
raise ReleasePackageError(
|
|
"Third-party verification receipt differs from independent recomputation."
|
|
)
|
|
if (
|
|
materials_manifest.get("schema_version") != THIRD_PARTY_MANIFEST_SCHEMA
|
|
or materials_manifest.get("result") != "PASS_TECHNICAL_MATERIALS_COMPLETE"
|
|
or materials_manifest.get("distribution_clearance_claimed") is not False
|
|
or included_receipt.get("schema_version") != THIRD_PARTY_VERIFICATION_SCHEMA
|
|
or included_receipt.get("result") != "PASS_INDEPENDENT_RECOMPUTATION"
|
|
or included_receipt.get("unresolved_occurrence_count") != 0
|
|
or included_receipt.get("distribution_clearance_claimed") is not False
|
|
):
|
|
raise ReleasePackageError(
|
|
"Third-party materials are incomplete or make an unsafe clearance claim."
|
|
)
|
|
candidate = materials_manifest.get("candidate")
|
|
if not isinstance(candidate, Mapping) or any(
|
|
candidate.get(key) != expected
|
|
for key, expected in (
|
|
("commit", source_commit),
|
|
("tag", candidate_tag),
|
|
("source_manifest_sha256", source_manifest_sha256),
|
|
)
|
|
):
|
|
raise ReleasePackageError("Third-party materials bind a different candidate.")
|
|
raw_images = candidate.get("images")
|
|
if not isinstance(raw_images, list) or len(raw_images) != 11:
|
|
raise ReleasePackageError("Third-party materials must bind all eleven images.")
|
|
expected_images = {
|
|
item["role"]: (
|
|
item["expected_oci_manifest_digest"],
|
|
item["expected_loaded_image_id"],
|
|
)
|
|
for item in release_import_plan
|
|
}
|
|
|
|
def report_fingerprint(raw: object, *, label: str) -> tuple[str, int]:
|
|
if not isinstance(raw, Mapping):
|
|
raise ReleasePackageError(f"{label} record is missing.")
|
|
digest = raw.get("sha256")
|
|
size = raw.get("size_bytes")
|
|
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
|
|
raise ReleasePackageError(f"{label} record has an invalid SHA-256.")
|
|
if not isinstance(size, int) or isinstance(size, bool) or size <= 0:
|
|
raise ReleasePackageError(f"{label} record has an invalid size.")
|
|
return digest, size
|
|
|
|
formal_reports: dict[str, dict[str, tuple[str, int]]] = {}
|
|
formal_image_groups = (
|
|
("application", installable_manifest.get("images")),
|
|
("infrastructure", infrastructure_bundle.get("infrastructure_images")),
|
|
)
|
|
for image_class, raw_group in formal_image_groups:
|
|
if not isinstance(raw_group, list):
|
|
raise ReleasePackageError(
|
|
f"Formal {image_class} image security-report inventory is missing."
|
|
)
|
|
for raw_image in raw_group:
|
|
if not isinstance(raw_image, Mapping):
|
|
raise ReleasePackageError(
|
|
f"Formal {image_class} image security-report entry is invalid."
|
|
)
|
|
role = raw_image.get("role")
|
|
if not isinstance(role, str) or role not in expected_images:
|
|
raise ReleasePackageError(
|
|
f"Formal {image_class} image security-report role is invalid."
|
|
)
|
|
if role in formal_reports:
|
|
raise ReleasePackageError(
|
|
f"Formal image security-report inventory repeats role: {role}"
|
|
)
|
|
expected_manifest_digest, expected_config_digest = expected_images[role]
|
|
if (
|
|
raw_image.get("manifest_digest") != expected_manifest_digest
|
|
or raw_image.get("config_digest") != expected_config_digest
|
|
):
|
|
raise ReleasePackageError(
|
|
f"Formal image security reports bind a different image: {role}"
|
|
)
|
|
formal_reports[role] = {
|
|
"sbom": report_fingerprint(
|
|
raw_image.get("sbom"), label=f"formal {role} SBOM"
|
|
),
|
|
"trivy_license": report_fingerprint(
|
|
raw_image.get("license_scan"),
|
|
label=f"formal {role} Trivy license report",
|
|
),
|
|
}
|
|
if set(formal_reports) != set(expected_images):
|
|
raise ReleasePackageError(
|
|
"Formal image security reports do not cover exactly all eleven release images."
|
|
)
|
|
|
|
observed_images: dict[str, tuple[object, object]] = {}
|
|
for item in raw_images:
|
|
if not isinstance(item, Mapping) or not isinstance(item.get("role"), str):
|
|
raise ReleasePackageError("Third-party image identity is invalid.")
|
|
role = item["role"]
|
|
if role in observed_images:
|
|
raise ReleasePackageError(f"Third-party image role is repeated: {role}")
|
|
observed_images[role] = (
|
|
item.get("manifest_digest"),
|
|
item.get("config_digest"),
|
|
)
|
|
expected_reports = formal_reports.get(role)
|
|
if expected_reports is None:
|
|
raise ReleasePackageError(
|
|
f"Third-party image role has no formal security reports: {role}"
|
|
)
|
|
for report_kind in ("sbom", "trivy_license"):
|
|
observed_report = report_fingerprint(
|
|
item.get(report_kind),
|
|
label=f"third-party {role} {report_kind}",
|
|
)
|
|
if observed_report != expected_reports[report_kind]:
|
|
raise ReleasePackageError(
|
|
"Third-party materials use a different raw image report than the "
|
|
f"formal build gate: {role} {report_kind}."
|
|
)
|
|
if observed_images != expected_images:
|
|
raise ReleasePackageError("Third-party image identities differ from the release plan.")
|
|
return materials_input, materials_manifest, included_receipt, profile
|
|
|
|
|
|
def _validate_compose_inputs(source_manifest: Mapping[str, Any], repo_root: Path) -> None:
|
|
root = _validated_directory(repo_root, label="repository root")
|
|
raw_items = source_manifest["files"]["items"]
|
|
by_path: dict[str, Mapping[str, Any]] = {}
|
|
for item in raw_items:
|
|
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
|
|
raise ReleasePackageError("Release source manifest contains an invalid file record.")
|
|
path = _safe_relative_path(item["path"], label="release source file path")
|
|
if path in by_path:
|
|
raise ReleasePackageError(f"Duplicate release source file path: {path}")
|
|
by_path[path] = item
|
|
for relative in (
|
|
BASE_COMPOSE_RELATIVE,
|
|
PRODUCTION_COMPOSE_RELATIVE,
|
|
OFFLINE_COMPOSE_RELATIVE,
|
|
):
|
|
item = by_path.get(relative)
|
|
if not isinstance(item, Mapping):
|
|
raise ReleasePackageError(f"Release source manifest does not bind Compose input: {relative}")
|
|
expected = item.get("sha256")
|
|
if not isinstance(expected, str) or not SHA256_RE.fullmatch(expected):
|
|
raise ReleasePackageError(f"Release source manifest has an invalid Compose hash: {relative}")
|
|
target = _artifact_file(root, relative, label=f"repository Compose input {relative}")
|
|
if file_sha256(target) != expected:
|
|
raise ReleasePackageError(f"Repository Compose input differs from release manifest: {relative}")
|
|
|
|
|
|
def _validate_backup_proof(
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
release_id: str,
|
|
rollback_release_id: str,
|
|
) -> None:
|
|
if payload.get("schema_version") != BACKUP_PROOF_SCHEMA:
|
|
raise ReleasePackageError("Unsupported backup proof schema.")
|
|
_verify_embedded_hash(payload, "proof_payload_sha256", label="backup proof")
|
|
if payload.get("release_id") != release_id or payload.get("rollback_target_release_id") != rollback_release_id:
|
|
raise ReleasePackageError("Backup proof is not bound to this release transition.")
|
|
if payload.get("status") != "unverified" or payload.get("restore_tested") is not False:
|
|
raise ReleasePackageError(
|
|
"Self-hashed backup material must remain explicitly unverified."
|
|
)
|
|
backup_id = payload.get("backup_id")
|
|
if backup_id is not None:
|
|
_validate_identifier(backup_id, APPROVAL_RE, label="backup proof backup_id")
|
|
backup_manifest_sha = payload.get("backup_manifest_sha256")
|
|
if backup_manifest_sha is not None and (
|
|
not isinstance(backup_manifest_sha, str)
|
|
or not SHA256_RE.fullmatch(backup_manifest_sha)
|
|
):
|
|
raise ReleasePackageError("Backup proof has an invalid backup manifest SHA-256.")
|
|
|
|
|
|
def _validate_compatibility_proof(
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
release_id: str,
|
|
rollback_release_id: str,
|
|
rollback_images: Mapping[str, Mapping[str, str]],
|
|
) -> None:
|
|
if payload.get("schema_version") != COMPATIBILITY_PROOF_SCHEMA:
|
|
raise ReleasePackageError("Unsupported rollback compatibility proof schema.")
|
|
_verify_embedded_hash(payload, "proof_payload_sha256", label="rollback compatibility proof")
|
|
if payload.get("from_release_id") != release_id or payload.get("to_release_id") != rollback_release_id:
|
|
raise ReleasePackageError("Compatibility proof is not bound to this release transition.")
|
|
if payload.get("status") != "unverified":
|
|
raise ReleasePackageError(
|
|
"Self-hashed compatibility material must remain explicitly unverified."
|
|
)
|
|
if payload.get("approval_id") is not None:
|
|
raise ReleasePackageError(
|
|
"Untrusted compatibility material must not claim an approval identifier."
|
|
)
|
|
if payload.get("database_downgrade_required") is not False:
|
|
raise ReleasePackageError("Database downgrade is forbidden.")
|
|
if payload.get("current_schema_is_backward_compatible") not in {None, False}:
|
|
raise ReleasePackageError(
|
|
"Untrusted compatibility material must not claim schema compatibility."
|
|
)
|
|
approved = payload.get("approved_application_digests")
|
|
if approved is not None and approved != {} and approved != []:
|
|
raise ReleasePackageError(
|
|
"Untrusted compatibility material must not claim approved image digests."
|
|
)
|
|
|
|
|
|
def validate_release_package(
|
|
package_dir: Path,
|
|
*,
|
|
expected_package_sha256: str,
|
|
repo_root: Path = REPO_ROOT,
|
|
) -> ValidatedPackage:
|
|
if not SHA256_RE.fullmatch(expected_package_sha256):
|
|
raise ReleasePackageError("--expected-package-sha256 must be a lowercase SHA-256.")
|
|
root = _validated_directory(package_dir, label="release package directory")
|
|
package_path = _artifact_file(root, PACKAGE_FILE, label="release-package.json")
|
|
package_sha = file_sha256(package_path)
|
|
if package_sha != expected_package_sha256:
|
|
raise ReleasePackageError("release-package.json does not match the externally approved SHA-256.")
|
|
package = _read_json(package_path, label="release-package.json")
|
|
package_schema = package.get("schema_version")
|
|
if package_schema not in {PACKAGE_SCHEMA_V1, PACKAGE_SCHEMA_V2}:
|
|
raise ReleasePackageError("Unsupported release package schema.")
|
|
_verify_embedded_hash(package, "package_payload_sha256", label="release-package.json")
|
|
expected_package_fields = {
|
|
"schema_version",
|
|
"release_id",
|
|
"compose_project_name",
|
|
"deployment_scope",
|
|
"deployed_service_set",
|
|
"separately_deployed_artifacts",
|
|
"trusted_build_receipt_sha256",
|
|
"artifacts",
|
|
"package_payload_sha256",
|
|
}
|
|
if set(package) != expected_package_fields:
|
|
raise ReleasePackageError("release-package.json fields are missing or unknown.")
|
|
trusted_build_receipt_sha256 = package.get("trusted_build_receipt_sha256")
|
|
if not isinstance(trusted_build_receipt_sha256, str) or not SHA256_RE.fullmatch(
|
|
trusted_build_receipt_sha256
|
|
):
|
|
raise ReleasePackageError(
|
|
"release-package.json has no trusted build receipt raw SHA-256."
|
|
)
|
|
release_id = _validate_identifier(package.get("release_id"), RELEASE_ID_RE, label="release_id")
|
|
_validate_identifier(package.get("compose_project_name"), PROJECT_RE, label="compose_project_name")
|
|
if package.get("deployment_scope") != DEPLOYMENT_SCOPE:
|
|
raise ReleasePackageError("release-package.json must declare deployment_scope=backend_only.")
|
|
if package.get("deployed_service_set") != list(INSTALL_SERVICES):
|
|
raise ReleasePackageError(
|
|
"release-package.json deployed_service_set must equal the fixed backend install allowlist."
|
|
)
|
|
if package.get("separately_deployed_artifacts") != list(SEPARATELY_DEPLOYED_IMAGE_KEYS):
|
|
raise ReleasePackageError(
|
|
"release-package.json must identify Contract Web as a separately deployed artifact."
|
|
)
|
|
artifacts = package.get("artifacts")
|
|
artifact_names = set(artifacts) if isinstance(artifacts, dict) else set()
|
|
required_artifacts = (
|
|
V2_REQUIRED_ARTIFACTS
|
|
if package_schema == PACKAGE_SCHEMA_V2
|
|
else V1_REQUIRED_ARTIFACTS
|
|
)
|
|
if (
|
|
not isinstance(artifacts, dict)
|
|
or not set(required_artifacts).issubset(artifact_names)
|
|
or not artifact_names.issubset(
|
|
set(required_artifacts + OPTIONAL_EVIDENCE_ARTIFACTS)
|
|
)
|
|
):
|
|
raise ReleasePackageError(
|
|
"release-package.json artifact closure is incomplete or contains unknown pins."
|
|
)
|
|
pins = {name: _artifact_pin(package, name) for name in artifacts}
|
|
if len({pin.path.casefold() for pin in pins.values()}) != len(pins):
|
|
raise ReleasePackageError("Release package artifact paths must be unique.")
|
|
|
|
checksum_path = _artifact_file(root, pins["checksum_manifest"].path, label="checksum manifest")
|
|
checksum_sha = file_sha256(checksum_path)
|
|
if checksum_sha != pins["checksum_manifest"].sha256:
|
|
raise ReleasePackageError("Checksum manifest SHA-256 does not match release-package.json.")
|
|
checksum_payload = _read_json(checksum_path, label="checksum manifest")
|
|
if checksum_payload.get("release_id") != release_id:
|
|
raise ReleasePackageError("Checksum manifest release_id mismatch.")
|
|
checksum_records = _validate_checksum_manifest(
|
|
root,
|
|
checksum_payload,
|
|
package_file_path=PACKAGE_FILE,
|
|
checksum_file_path=pins["checksum_manifest"].path,
|
|
)
|
|
for name, pin in pins.items():
|
|
if name == "checksum_manifest":
|
|
continue
|
|
record = checksum_records.get(pin.path)
|
|
if record is None or record["sha256"] != pin.sha256:
|
|
raise ReleasePackageError(f"Checksum manifest does not bind artifact {name}.")
|
|
|
|
release_path = _artifact_file(root, pins["release_manifest"].path, label="release source manifest")
|
|
release_sha = file_sha256(release_path)
|
|
if release_sha != pins["release_manifest"].sha256:
|
|
raise ReleasePackageError("Release source manifest SHA-256 mismatch.")
|
|
release_manifest = _read_json(release_path, label="release source manifest")
|
|
source_commit = _validate_source_manifest(release_manifest)
|
|
_validate_compose_inputs(release_manifest, repo_root)
|
|
|
|
identity_path = _artifact_file(root, pins["image_identity"].path, label="image identity")
|
|
identity_sha = file_sha256(identity_path)
|
|
if identity_sha != pins["image_identity"].sha256:
|
|
raise ReleasePackageError("Image identity SHA-256 mismatch.")
|
|
image_identity = _read_json(identity_path, label="image identity")
|
|
_verify_embedded_hash(image_identity, "identity_payload_sha256", label="image identity")
|
|
if (
|
|
package_schema == PACKAGE_SCHEMA_V2
|
|
and image_identity.get("schema_version") != IMAGE_IDENTITY_SCHEMA_V2
|
|
):
|
|
raise ReleasePackageError("Release package v2 requires image identity v2.")
|
|
if (
|
|
image_identity.get("release_id") != release_id
|
|
or image_identity.get("source_commit") != source_commit
|
|
or image_identity.get("source_manifest_sha256") != release_sha
|
|
):
|
|
raise ReleasePackageError("Image identity is not bound to the release manifest and source commit.")
|
|
application_images = _validate_image_map(
|
|
image_identity.get("application_images"),
|
|
APPLICATION_IMAGE_KEYS,
|
|
label="application_images",
|
|
)
|
|
infrastructure_images = _validate_image_map(
|
|
image_identity.get("infrastructure_images"),
|
|
INFRASTRUCTURE_IMAGE_KEYS,
|
|
label="infrastructure_images",
|
|
)
|
|
(
|
|
recovery_strategy,
|
|
recovery_policy,
|
|
rollback_images,
|
|
rollback_release_id,
|
|
) = _validate_image_identity_recovery(
|
|
image_identity,
|
|
application_images=application_images,
|
|
)
|
|
|
|
installable_path = _artifact_file(
|
|
root,
|
|
pins["installable_manifest"].path,
|
|
label="installable release manifest",
|
|
)
|
|
installable_sha = file_sha256(installable_path)
|
|
if installable_sha != pins["installable_manifest"].sha256:
|
|
raise ReleasePackageError("Installable release manifest SHA-256 mismatch.")
|
|
installable_validator = _load_installable_validator_module()
|
|
try:
|
|
installable_manifest = installable_validator.validate_installable_release_manifest(
|
|
artifact_root=installable_path.parent,
|
|
manifest_path=installable_path,
|
|
expected_receipt_sha256=trusted_build_receipt_sha256,
|
|
repo=_validated_directory(repo_root, label="repository root"),
|
|
)
|
|
except installable_validator.ReleaseValidationError as exc:
|
|
raise ReleasePackageError(f"Strict installable manifest validation failed: {exc}") from exc
|
|
bundle_prefix = installable_path.parent.relative_to(root)
|
|
for record in installable_manifest.get("files", []):
|
|
if not isinstance(record, dict):
|
|
raise ReleasePackageError("Installable manifest file inventory is invalid.")
|
|
relative = _safe_relative_path(
|
|
record.get("path"),
|
|
label="installable manifest file path",
|
|
)
|
|
package_relative = (bundle_prefix / PurePosixPath(relative)).as_posix()
|
|
outer = checksum_records.get(package_relative)
|
|
if outer != {
|
|
"sha256": record.get("sha256"),
|
|
"size_bytes": record.get("size_bytes"),
|
|
}:
|
|
raise ReleasePackageError(
|
|
f"Outer checksum manifest does not bind installable file: {package_relative}"
|
|
)
|
|
receipt_record = installable_manifest.get("build_receipt")
|
|
if (
|
|
not isinstance(receipt_record, dict)
|
|
or receipt_record.get("sha256") != trusted_build_receipt_sha256
|
|
):
|
|
raise ReleasePackageError(
|
|
"Installable manifest is not bound to the trusted build receipt raw SHA-256."
|
|
)
|
|
build_receipt_path = _artifact_file(
|
|
installable_path.parent,
|
|
str(receipt_record["path"]),
|
|
label="trusted build receipt",
|
|
)
|
|
build_receipt = _read_json(build_receipt_path, label="trusted build receipt")
|
|
receipt_candidate = build_receipt.get("candidate")
|
|
source_record = (
|
|
receipt_candidate.get("source_manifest")
|
|
if isinstance(receipt_candidate, dict)
|
|
else None
|
|
)
|
|
if not isinstance(source_record, dict):
|
|
raise ReleasePackageError("Trusted build receipt has no source manifest record.")
|
|
receipt_source_path = _artifact_file(
|
|
installable_path.parent,
|
|
str(source_record.get("path")),
|
|
label="trusted build receipt source manifest",
|
|
)
|
|
if receipt_source_path != release_path or source_record.get("sha256") != release_sha:
|
|
raise ReleasePackageError(
|
|
"Release package source manifest is not the build receipt source manifest."
|
|
)
|
|
application_import_plan = tuple(
|
|
{
|
|
**item,
|
|
"archive_package_path": (
|
|
bundle_prefix / PurePosixPath(item["archive_path"])
|
|
).as_posix(),
|
|
}
|
|
for item in _validate_installable_manifest(
|
|
installable_path.parent,
|
|
installable_manifest,
|
|
source_commit=source_commit,
|
|
application_images=application_images,
|
|
)
|
|
)
|
|
docker_import_plan = application_import_plan
|
|
offline_image_overrides = {
|
|
item["environment_key"]: item["offline_compose_override"]
|
|
for item in application_import_plan
|
|
}
|
|
|
|
infrastructure_source_set: dict[str, Any] | None = None
|
|
infrastructure_source_set_sha: str | None = None
|
|
infrastructure_bundle: dict[str, Any] | None = None
|
|
infrastructure_bundle_sha: str | None = None
|
|
third_party_input: dict[str, Any] | None = None
|
|
third_party_input_sha: str | None = None
|
|
third_party_manifest: dict[str, Any] | None = None
|
|
third_party_manifest_sha: str | None = None
|
|
third_party_receipt: dict[str, Any] | None = None
|
|
third_party_receipt_sha: str | None = None
|
|
third_party_profile: str | None = None
|
|
if package_schema == PACKAGE_SCHEMA_V2:
|
|
infrastructure_module = _load_infrastructure_bundle_module()
|
|
third_party_module = _load_third_party_materials_module()
|
|
snapshot_module = _load_snapshot_restore_planner_module()
|
|
for relative, loaded in (
|
|
(
|
|
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
|
Path(infrastructure_module.__file__),
|
|
),
|
|
(
|
|
"yuqei-infra/scripts/build-third-party-materials.py",
|
|
Path(third_party_module.__file__),
|
|
),
|
|
(
|
|
"yuqei-infra/release-trust/third-party-materials-baseline.json",
|
|
Path(third_party_module.REPOSITORY_TRUST_BASELINE_PATH),
|
|
),
|
|
(
|
|
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
|
Path(snapshot_module.__file__),
|
|
),
|
|
):
|
|
_validate_source_bound_tool(
|
|
release_manifest,
|
|
repo_root,
|
|
relative,
|
|
loaded_file=loaded,
|
|
)
|
|
candidate = installable_manifest.get("candidate")
|
|
candidate_tag = candidate.get("tag") if isinstance(candidate, Mapping) else None
|
|
if not isinstance(candidate_tag, str) or not candidate_tag:
|
|
raise ReleasePackageError("Installable manifest candidate tag is invalid.")
|
|
infrastructure_source_path = _artifact_file(
|
|
root,
|
|
pins["infrastructure_source_set"].path,
|
|
label="infrastructure source set",
|
|
)
|
|
infrastructure_bundle_path = _artifact_file(
|
|
root,
|
|
pins["infrastructure_image_bundle"].path,
|
|
label="infrastructure image bundle",
|
|
)
|
|
infrastructure_source_set_sha = file_sha256(infrastructure_source_path)
|
|
infrastructure_bundle_sha = file_sha256(infrastructure_bundle_path)
|
|
(
|
|
infrastructure_source_set,
|
|
infrastructure_bundle,
|
|
docker_import_plan,
|
|
offline_image_overrides,
|
|
) = _validate_infrastructure_bundle(
|
|
package_root=root,
|
|
artifact_root=infrastructure_source_path.parent,
|
|
source_set_path=infrastructure_source_path,
|
|
bundle_path=infrastructure_bundle_path,
|
|
installable_manifest=installable_manifest,
|
|
source_commit=source_commit,
|
|
candidate_tag=candidate_tag,
|
|
infrastructure_images=infrastructure_images,
|
|
application_plan=application_import_plan,
|
|
)
|
|
third_party_input_path = _artifact_file(
|
|
root,
|
|
pins["third_party_materials_input"].path,
|
|
label="third-party materials input",
|
|
)
|
|
third_party_manifest_path = _artifact_file(
|
|
root,
|
|
pins["third_party_materials_manifest"].path,
|
|
label="third-party materials manifest",
|
|
)
|
|
third_party_receipt_path = _artifact_file(
|
|
root,
|
|
pins["third_party_materials_verification_receipt"].path,
|
|
label="third-party verification receipt",
|
|
)
|
|
third_party_input_sha = file_sha256(third_party_input_path)
|
|
third_party_manifest_sha = file_sha256(third_party_manifest_path)
|
|
third_party_receipt_sha = file_sha256(third_party_receipt_path)
|
|
(
|
|
third_party_input,
|
|
third_party_manifest,
|
|
third_party_receipt,
|
|
third_party_profile,
|
|
) = _validate_third_party_materials(
|
|
input_path=third_party_input_path,
|
|
manifest_path=third_party_manifest_path,
|
|
receipt_path=third_party_receipt_path,
|
|
source_commit=source_commit,
|
|
candidate_tag=candidate_tag,
|
|
source_manifest_sha256=release_sha,
|
|
release_import_plan=docker_import_plan,
|
|
installable_manifest=installable_manifest,
|
|
infrastructure_bundle=infrastructure_bundle,
|
|
)
|
|
|
|
backup_proof: dict[str, Any] | None = None
|
|
backup_sha: str | None = None
|
|
if recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE and (
|
|
"backup_proof" in pins or "compatibility_proof" in pins
|
|
):
|
|
raise ReleasePackageError(
|
|
"First-release snapshot recovery forbids predecessor backup/compatibility artifacts; "
|
|
"runtime signed snapshot evidence is required."
|
|
)
|
|
if "backup_proof" in pins:
|
|
if rollback_release_id is None:
|
|
raise ReleasePackageError("Backup proof requires a predecessor release target.")
|
|
backup_path = _artifact_file(root, pins["backup_proof"].path, label="backup proof")
|
|
backup_sha = file_sha256(backup_path)
|
|
if backup_sha != pins["backup_proof"].sha256:
|
|
raise ReleasePackageError("Backup proof SHA-256 mismatch.")
|
|
backup_proof = _read_json(backup_path, label="backup proof")
|
|
_validate_backup_proof(
|
|
backup_proof,
|
|
release_id=release_id,
|
|
rollback_release_id=rollback_release_id,
|
|
)
|
|
|
|
compatibility_proof: dict[str, Any] | None = None
|
|
compatibility_sha: str | None = None
|
|
if "compatibility_proof" in pins:
|
|
if rollback_release_id is None or rollback_images is None:
|
|
raise ReleasePackageError(
|
|
"Rollback compatibility proof requires a predecessor release target."
|
|
)
|
|
compatibility_path = _artifact_file(
|
|
root, pins["compatibility_proof"].path, label="rollback compatibility proof"
|
|
)
|
|
compatibility_sha = file_sha256(compatibility_path)
|
|
if compatibility_sha != pins["compatibility_proof"].sha256:
|
|
raise ReleasePackageError("Rollback compatibility proof SHA-256 mismatch.")
|
|
compatibility_proof = _read_json(
|
|
compatibility_path,
|
|
label="rollback compatibility proof",
|
|
)
|
|
_validate_compatibility_proof(
|
|
compatibility_proof,
|
|
release_id=release_id,
|
|
rollback_release_id=rollback_release_id,
|
|
rollback_images=rollback_images,
|
|
)
|
|
|
|
return ValidatedPackage(
|
|
root=root,
|
|
package_schema_version=package_schema,
|
|
package=package,
|
|
package_sha256=package_sha,
|
|
checksum_manifest=checksum_payload,
|
|
checksum_manifest_sha256=checksum_sha,
|
|
release_manifest=release_manifest,
|
|
release_manifest_sha256=release_sha,
|
|
image_identity=image_identity,
|
|
image_identity_sha256=identity_sha,
|
|
installable_manifest=installable_manifest,
|
|
installable_manifest_sha256=installable_sha,
|
|
installable_root=installable_path.parent,
|
|
trusted_build_receipt_sha256=trusted_build_receipt_sha256,
|
|
docker_import_plan=docker_import_plan,
|
|
offline_image_overrides=offline_image_overrides,
|
|
infrastructure_source_set=infrastructure_source_set,
|
|
infrastructure_source_set_sha256=infrastructure_source_set_sha,
|
|
infrastructure_bundle=infrastructure_bundle,
|
|
infrastructure_bundle_sha256=infrastructure_bundle_sha,
|
|
third_party_materials_input=third_party_input,
|
|
third_party_materials_input_sha256=third_party_input_sha,
|
|
third_party_materials_manifest=third_party_manifest,
|
|
third_party_materials_manifest_sha256=third_party_manifest_sha,
|
|
third_party_verification_receipt=third_party_receipt,
|
|
third_party_verification_receipt_sha256=third_party_receipt_sha,
|
|
third_party_profile=third_party_profile,
|
|
backup_proof=backup_proof,
|
|
backup_proof_sha256=backup_sha,
|
|
compatibility_proof=compatibility_proof,
|
|
compatibility_proof_sha256=compatibility_sha,
|
|
application_images=application_images,
|
|
infrastructure_images=infrastructure_images,
|
|
recovery_strategy=recovery_strategy,
|
|
recovery_policy=recovery_policy,
|
|
rollback_images=rollback_images,
|
|
rollback_release_id=rollback_release_id,
|
|
)
|
|
|
|
|
|
def parse_env_file(path: Path) -> dict[str, str]:
|
|
compose_gate = _load_compose_gate_module()
|
|
try:
|
|
_target, values = compose_gate.parse_strict_env(path)
|
|
except compose_gate.ComposeGateError as exc:
|
|
raise ReleasePackageError(str(exc)) from exc
|
|
return values
|
|
|
|
|
|
def _fixed_runtime_environment(overrides: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
environment = {
|
|
"HOME": FIXED_DOCKER_HOME,
|
|
"LANG": "C.UTF-8",
|
|
"LC_ALL": "C.UTF-8",
|
|
}
|
|
if overrides:
|
|
reserved = {
|
|
"PATH",
|
|
"DOCKER_HOST",
|
|
"DOCKER_CONTEXT",
|
|
"DOCKER_CONFIG",
|
|
"COMPOSE_FILE",
|
|
"COMPOSE_PATH_SEPARATOR",
|
|
"COMPOSE_PROFILES",
|
|
"COMPOSE_PROJECT_NAME",
|
|
}
|
|
conflict = sorted(reserved.intersection(overrides))
|
|
if conflict:
|
|
raise ReleasePackageError(
|
|
f"Runtime overrides contain forbidden ambient-control keys: {', '.join(conflict)}"
|
|
)
|
|
environment.update(overrides)
|
|
return environment
|
|
|
|
|
|
def _run_production_preflight(env_values: dict[str, str]) -> dict[str, Any]:
|
|
preflight = _load_preflight_module()
|
|
try:
|
|
environment_checks = preflight.validate_environment(dict(env_values))
|
|
file_checks = preflight.validate_files(dict(env_values))
|
|
except Exception as exc:
|
|
raise ReleasePackageError("Production preflight raised while validating protected inputs.") from exc
|
|
|
|
def bound_record(check: Any) -> dict[str, Any]:
|
|
record: dict[str, Any] = {"key": check.key, "status": check.status}
|
|
details = getattr(check, "details", None)
|
|
if details:
|
|
record["details"] = details
|
|
return record
|
|
|
|
environment_records = [bound_record(check) for check in environment_checks]
|
|
file_records = [bound_record(check) for check in file_checks]
|
|
failed = [
|
|
f"environment:{record['key']}"
|
|
for record in environment_records
|
|
if record["status"] == "fail"
|
|
] + [
|
|
f"files:{record['key']}"
|
|
for record in file_records
|
|
if record["status"] == "fail"
|
|
]
|
|
if failed:
|
|
raise ReleasePackageError(
|
|
"Production preflight failed before any state write: " + ", ".join(failed)
|
|
)
|
|
summary: dict[str, Any] = {
|
|
"schema_version": PREFLIGHT_BINDING_SCHEMA,
|
|
"status": "passed",
|
|
"environment_checks": environment_records,
|
|
"file_checks": file_records,
|
|
}
|
|
summary["summary_sha256"] = canonical_sha256(summary)
|
|
return summary
|
|
|
|
|
|
def _tls_material_binding(preflight_summary: Mapping[str, Any]) -> dict[str, str]:
|
|
records = preflight_summary.get("file_checks")
|
|
if not isinstance(records, list):
|
|
raise ReleasePackageError("Production preflight has no TLS file-check inventory.")
|
|
matches = [
|
|
record
|
|
for record in records
|
|
if isinstance(record, dict) and record.get("key") == "tls_material_binding"
|
|
]
|
|
if len(matches) != 1 or matches[0].get("status") != "pass":
|
|
raise ReleasePackageError("Production preflight has no unique passing TLS material binding.")
|
|
details = matches[0].get("details")
|
|
expected_fields = {
|
|
"certificate_sha256",
|
|
"private_key_sha256",
|
|
"trusted_ca_sha256",
|
|
"server_name",
|
|
}
|
|
if not isinstance(details, dict) or set(details) != expected_fields:
|
|
raise ReleasePackageError("Production preflight TLS material binding is incomplete.")
|
|
for field in (
|
|
"certificate_sha256",
|
|
"private_key_sha256",
|
|
"trusted_ca_sha256",
|
|
):
|
|
value = details.get(field)
|
|
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
|
|
raise ReleasePackageError(f"Production preflight has an invalid TLS {field}.")
|
|
server_name = details.get("server_name")
|
|
if (
|
|
not isinstance(server_name, str)
|
|
or not server_name
|
|
or server_name != server_name.strip()
|
|
or any(ord(character) < 32 or ord(character) == 127 for character in server_name)
|
|
):
|
|
raise ReleasePackageError("Production preflight has an invalid TLS server name.")
|
|
return {field: str(details[field]) for field in sorted(expected_fields)}
|
|
|
|
|
|
def _parse_json_text(value: object, *, label: str, max_bytes: int = MAX_JSON_BYTES) -> dict[str, Any]:
|
|
raw = _output_bytes(value)
|
|
if len(raw) > max_bytes:
|
|
raise ReleasePackageError(f"{label} exceeds the maximum allowed size.")
|
|
try:
|
|
payload = json.loads(
|
|
raw.decode("utf-8"),
|
|
object_pairs_hook=_reject_duplicate_pairs,
|
|
parse_constant=_reject_json_constant,
|
|
)
|
|
except ReleasePackageError:
|
|
raise
|
|
except (UnicodeError, ValueError, TypeError) as exc:
|
|
raise ReleasePackageError(f"{label} is not strict UTF-8 JSON.") from exc
|
|
if not isinstance(payload, dict):
|
|
raise ReleasePackageError(f"{label} must be a JSON object.")
|
|
return payload
|
|
|
|
|
|
def _docker_daemon_identity(
|
|
*,
|
|
runner: Callable[..., Any],
|
|
repo_root: Path,
|
|
) -> dict[str, str]:
|
|
command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"info",
|
|
"--format",
|
|
"{{json .}}",
|
|
]
|
|
try:
|
|
completed = runner(
|
|
command,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
shell=False,
|
|
cwd=str(repo_root),
|
|
env=_fixed_runtime_environment(),
|
|
timeout=30,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
raise ReleasePackageError("Cannot query the fixed local Docker daemon identity.") from exc
|
|
if int(completed.returncode) != 0:
|
|
raise ReleasePackageError("Cannot query the fixed local Docker daemon identity.")
|
|
payload = _parse_json_text(completed.stdout, label="Docker daemon identity")
|
|
fields = {
|
|
"daemon_id": payload.get("ID"),
|
|
"daemon_name": payload.get("Name"),
|
|
"docker_root_dir": payload.get("DockerRootDir"),
|
|
"server_version": payload.get("ServerVersion"),
|
|
}
|
|
if any(not isinstance(value, str) or not value.strip() for value in fields.values()):
|
|
raise ReleasePackageError("Docker daemon identity is incomplete.")
|
|
identity: dict[str, str] = {
|
|
"docker_cli": str(DOCKER_CLI),
|
|
"docker_host": LOCAL_DOCKER_HOST,
|
|
**{key: str(value) for key, value in fields.items()},
|
|
}
|
|
identity["identity_sha256"] = canonical_sha256(identity)
|
|
return identity
|
|
|
|
|
|
def _collect_runtime_evidence(
|
|
*,
|
|
env_file: Path,
|
|
repo_root: Path,
|
|
inspection_runner: Callable[..., Any],
|
|
) -> RuntimeEvidence:
|
|
env_target = _validated_file(env_file, label="production env file", max_bytes=MAX_ENV_BYTES)
|
|
env_values = parse_env_file(env_target)
|
|
preflight_summary = _run_production_preflight(env_values)
|
|
|
|
compose_gate = _load_compose_gate_module()
|
|
try:
|
|
compose_report = compose_gate.verify_production_compose(
|
|
env_file=env_target,
|
|
repo_root=repo_root,
|
|
runner=inspection_runner,
|
|
)
|
|
except compose_gate.ComposeGateError as exc:
|
|
raise ReleasePackageError(f"Production Compose merge gate failed: {exc}") from exc
|
|
compose_summary = dict(compose_report)
|
|
compose_summary["summary_sha256"] = canonical_sha256(compose_summary)
|
|
daemon_identity = _docker_daemon_identity(runner=inspection_runner, repo_root=repo_root)
|
|
return RuntimeEvidence(
|
|
env_file=env_target,
|
|
env_values=env_values,
|
|
env_sha256=file_sha256(env_target),
|
|
preflight_summary=preflight_summary,
|
|
compose_gate_summary=compose_summary,
|
|
daemon_identity=daemon_identity,
|
|
)
|
|
|
|
|
|
def _run_offline_execution_gate(
|
|
*,
|
|
env_file: Path,
|
|
repo_root: Path,
|
|
compose_files: tuple[Path, Path, Path],
|
|
project_name: str,
|
|
offline_image_overrides: Mapping[str, str],
|
|
inspection_runner: Callable[..., Any],
|
|
) -> dict[str, Any]:
|
|
compose_gate = _load_compose_gate_module()
|
|
env_values = parse_env_file(env_file)
|
|
env_values.update(offline_image_overrides)
|
|
child_env = _fixed_runtime_environment(offline_image_overrides)
|
|
cases: list[dict[str, Any]] = []
|
|
try:
|
|
for profile_case in compose_gate.PROFILE_CASES:
|
|
command = compose_gate._command(
|
|
project_name=project_name,
|
|
env_file=env_file,
|
|
compose_files=compose_files,
|
|
profile_case=profile_case,
|
|
)
|
|
payload = compose_gate._run_config(
|
|
command,
|
|
repo_root=repo_root,
|
|
runner=inspection_runner,
|
|
child_env=child_env,
|
|
label=f"offline-execution/{profile_case.name}",
|
|
)
|
|
cases.append(
|
|
compose_gate._validate_model(
|
|
payload,
|
|
env_values=env_values,
|
|
mode="offline",
|
|
profile_case=profile_case,
|
|
)
|
|
)
|
|
except compose_gate.ComposeGateError as exc:
|
|
raise ReleasePackageError(f"Offline execution Compose gate failed: {exc}") from exc
|
|
summary: dict[str, Any] = {
|
|
"schema_version": "yuqei.offline-compose-execution-gate.v1",
|
|
"status": "passed",
|
|
"docker_host": LOCAL_DOCKER_HOST,
|
|
"project_name": project_name,
|
|
"offline_image_overrides": dict(offline_image_overrides),
|
|
"cases": cases,
|
|
}
|
|
summary["summary_sha256"] = canonical_sha256(summary)
|
|
return summary
|
|
|
|
|
|
def _assert_safe_compose_command(command: list[str], *, operation: str) -> None:
|
|
if command[:4] != [str(DOCKER_CLI), "--host", LOCAL_DOCKER_HOST, "compose"]:
|
|
raise ReleasePackageError("Only the fixed local Docker Compose target is allowed.")
|
|
if any("\x00" in item or "\r" in item or "\n" in item for item in command):
|
|
raise ReleasePackageError("Deployment argv contains a forbidden control character.")
|
|
if command.count("up") != 1 or command.count("--pull") != 1 or "--no-build" not in command:
|
|
raise ReleasePackageError("Offline execution requires compose up --pull never --no-build.")
|
|
pull_index = command.index("--pull")
|
|
if pull_index + 1 >= len(command) or command[pull_index + 1] != "never":
|
|
raise ReleasePackageError("Offline execution requires the exact --pull never policy.")
|
|
lowered = [item.casefold() for item in command]
|
|
for index, token in enumerate(lowered):
|
|
if token in FORBIDDEN_COMMAND_TOKENS:
|
|
pull_pair = token == "pull" and index > 0 and lowered[index - 1] == "--pull"
|
|
if not pull_pair:
|
|
raise ReleasePackageError(f"Forbidden deployment command token: {command[index]}")
|
|
if "edge" in lowered or any("dns" in item for item in lowered):
|
|
raise ReleasePackageError("Automatic edge or DNS operations are forbidden.")
|
|
if operation in {"rollback", "forward"} and "--no-deps" not in command:
|
|
raise ReleasePackageError("Rollback and forward must switch application services with --no-deps.")
|
|
expected_services = RUNTIME_SERVICES if operation in {"rollback", "forward"} else INSTALL_SERVICES
|
|
if command[-len(expected_services) :] != list(expected_services):
|
|
raise ReleasePackageError("Deployment command service set is not the fixed operation allowlist.")
|
|
|
|
|
|
def _compose_command(
|
|
*,
|
|
operation: str,
|
|
project_name: str,
|
|
env_file: Path,
|
|
repo_root: Path,
|
|
compose_files: tuple[Path, Path, Path] | None = None,
|
|
) -> list[str]:
|
|
if compose_files is None:
|
|
root = _validated_directory(repo_root, label="repository root")
|
|
base = _artifact_file(root, BASE_COMPOSE_RELATIVE, label="base Compose file")
|
|
production = _artifact_file(
|
|
root,
|
|
PRODUCTION_COMPOSE_RELATIVE,
|
|
label="production Compose file",
|
|
)
|
|
offline = _artifact_file(
|
|
root,
|
|
OFFLINE_COMPOSE_RELATIVE,
|
|
label="offline Compose override",
|
|
)
|
|
else:
|
|
base, production, offline = compose_files
|
|
for label, path in (
|
|
("base Compose snapshot", base),
|
|
("production Compose snapshot", production),
|
|
("offline Compose snapshot", offline),
|
|
):
|
|
_validated_file(path, label=label, max_bytes=MAX_JSON_BYTES)
|
|
command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"compose",
|
|
"--project-name",
|
|
project_name,
|
|
"--env-file",
|
|
str(env_file),
|
|
"-f",
|
|
str(base),
|
|
"-f",
|
|
str(production),
|
|
"-f",
|
|
str(offline),
|
|
"up",
|
|
"-d",
|
|
"--pull",
|
|
"never",
|
|
"--no-build",
|
|
]
|
|
if operation in {"rollback", "forward"}:
|
|
command.append("--no-deps")
|
|
command.extend(RUNTIME_SERVICES)
|
|
else:
|
|
command.extend(INSTALL_SERVICES)
|
|
_assert_safe_compose_command(command, operation=operation)
|
|
return command
|
|
|
|
|
|
def _assert_fixed_subprocess_argv(command: object) -> list[str]:
|
|
if (
|
|
not isinstance(command, list)
|
|
or any(not isinstance(item, str) for item in command)
|
|
or command[:3] != [str(DOCKER_CLI), "--host", LOCAL_DOCKER_HOST]
|
|
or any("\x00" in item or "\r" in item or "\n" in item for item in command)
|
|
):
|
|
raise ReleasePackageError("Fixed runner received an unsafe Docker argv.")
|
|
if command[3:] == ["info", "--format", "{{json .}}"]:
|
|
return command
|
|
if len(command) >= 5 and command[3] == "image":
|
|
tail = command[4:]
|
|
if (
|
|
len(tail) == 3
|
|
and tail[:2] == ["load", "--input"]
|
|
and Path(tail[2]).is_absolute()
|
|
):
|
|
return command
|
|
if (
|
|
len(tail) == 3
|
|
and tail[0] == "tag"
|
|
and IMAGE_DIGEST_RE.fullmatch(tail[1])
|
|
and tail[2]
|
|
):
|
|
return command
|
|
if (
|
|
len(tail) == 4
|
|
and tail[:2] == ["inspect", "--format"]
|
|
and tail[2] in {"{{.Id}}", "{{json .}}"}
|
|
and tail[3]
|
|
):
|
|
return command
|
|
raise ReleasePackageError("Fixed runner rejected a Docker image command.")
|
|
if len(command) == 7 and command[3:6] == ["inspect", "--format", "{{json .}}"]:
|
|
return command
|
|
if len(command) < 8 or command[3] != "compose":
|
|
raise ReleasePackageError("Fixed runner only permits Docker info, image, inspect, and Compose.")
|
|
lowered = [item.casefold() for item in command]
|
|
mutating = {"down", "rm", "pull", "build", "push", "run", "exec", "restart", "kill"}
|
|
if any(item in mutating for item in lowered):
|
|
pull_never = any(
|
|
lowered[index] == "pull"
|
|
and index > 0
|
|
and lowered[index - 1] == "--pull"
|
|
for index in range(len(lowered))
|
|
)
|
|
forbidden = [
|
|
item
|
|
for index, item in enumerate(lowered)
|
|
if item in mutating
|
|
and not (item == "pull" and index > 0 and lowered[index - 1] == "--pull")
|
|
]
|
|
if forbidden or ("pull" in lowered and not pull_never):
|
|
raise ReleasePackageError("Fixed runner rejected a mutating Compose command.")
|
|
if "config" in command:
|
|
if command[-3:] != ["config", "--format", "json"]:
|
|
raise ReleasePackageError("Fixed runner permits only Compose config JSON.")
|
|
return command
|
|
if "up" in command:
|
|
operation = "forward" if "--no-deps" in command else "install"
|
|
_assert_safe_compose_command(command, operation=operation)
|
|
return command
|
|
if command[-4:] == ["ps", "--all", "--format", "json"]:
|
|
return command
|
|
raise ReleasePackageError("Fixed runner rejected an unsupported Compose command.")
|
|
|
|
|
|
def _fixed_subprocess_runner(command: list[str], **kwargs: Any) -> Any:
|
|
safe_command = _assert_fixed_subprocess_argv(command)
|
|
if (
|
|
kwargs.get("shell") is not False
|
|
or kwargs.get("check") is not False
|
|
or kwargs.get("capture_output") is not True
|
|
):
|
|
raise ReleasePackageError("Fixed runner requires non-shell captured execution.")
|
|
timeout = kwargs.get("timeout")
|
|
if (
|
|
not isinstance(timeout, (int, float))
|
|
or isinstance(timeout, bool)
|
|
or timeout <= 0
|
|
or timeout > 1800
|
|
):
|
|
raise ReleasePackageError("Fixed runner timeout is outside the approved bound.")
|
|
cwd = kwargs.get("cwd")
|
|
if cwd is not None and (not isinstance(cwd, str) or not Path(cwd).is_absolute()):
|
|
raise ReleasePackageError("Fixed runner cwd must be an absolute path.")
|
|
environment = kwargs.get("env")
|
|
allowed_env = {
|
|
"HOME",
|
|
"LANG",
|
|
"LC_ALL",
|
|
"AI_PLATFORM_RELEASE_IMAGE_DIGEST",
|
|
*(key for _role, key, _services in RELEASE_IMPORT_ROLE_ENV_SERVICES),
|
|
}
|
|
if (
|
|
not isinstance(environment, Mapping)
|
|
or not set(environment).issubset(allowed_env)
|
|
or environment.get("HOME") != FIXED_DOCKER_HOME
|
|
or environment.get("LANG") != "C.UTF-8"
|
|
or environment.get("LC_ALL") != "C.UTF-8"
|
|
or any(
|
|
not isinstance(value, str)
|
|
or "\x00" in value
|
|
or "\r" in value
|
|
or "\n" in value
|
|
for value in environment.values()
|
|
)
|
|
):
|
|
raise ReleasePackageError("Fixed runner environment is not the approved clean environment.")
|
|
return subprocess.run(safe_command, **kwargs)
|
|
|
|
|
|
def _parse_json_value(value: object, *, label: str) -> Any:
|
|
raw = _output_bytes(value)
|
|
if len(raw) > MAX_JSON_BYTES:
|
|
raise ReleasePackageError(f"{label} exceeds the maximum allowed size.")
|
|
try:
|
|
return json.loads(
|
|
raw.decode("utf-8"),
|
|
object_pairs_hook=_reject_duplicate_pairs,
|
|
parse_constant=_reject_json_constant,
|
|
)
|
|
except (UnicodeError, ValueError, TypeError) as exc:
|
|
raise ReleasePackageError(f"{label} is not strict UTF-8 JSON.") from exc
|
|
|
|
|
|
def _builtin_post_apply_checker(
|
|
*,
|
|
plan: Mapping[str, Any],
|
|
command_argv: list[str],
|
|
runtime_environment: Mapping[str, str],
|
|
daemon_identity: Mapping[str, str],
|
|
) -> dict[str, Any]:
|
|
up_index = command_argv.index("up")
|
|
compose_prefix = command_argv[:up_index]
|
|
ps_command = [*compose_prefix, "ps", "--all", "--format", "json"]
|
|
expected_services = list(plan["deployed_service_set"])
|
|
service_image_ids: dict[str, str] = {}
|
|
for item in plan["docker_import_plan"]:
|
|
for service in item["services"]:
|
|
if service not in expected_services:
|
|
continue
|
|
if service in service_image_ids:
|
|
raise ReleasePackageError(
|
|
f"Post-apply image plan repeats service binding: {service}."
|
|
)
|
|
service_image_ids[service] = item["expected_loaded_image_id"]
|
|
if set(service_image_ids) != set(expected_services):
|
|
raise ReleasePackageError("Post-apply image plan does not cover every deployed service.")
|
|
|
|
deadline = time.monotonic() + POST_APPLY_WAIT_TIMEOUT_SECONDS
|
|
records: list[dict[str, Any]] = []
|
|
release_binding_verified = False
|
|
while True:
|
|
completed = _fixed_subprocess_runner(
|
|
ps_command,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
shell=False,
|
|
cwd=str(Path(command_argv[command_argv.index("-f") + 1]).parent),
|
|
env=dict(runtime_environment),
|
|
timeout=60,
|
|
)
|
|
if int(completed.returncode) != 0:
|
|
raise ReleasePackageError("Built-in post-apply Compose inventory check failed.")
|
|
parsed = _parse_json_value(completed.stdout, label="Compose post-apply inventory")
|
|
if isinstance(parsed, dict):
|
|
rows = [parsed]
|
|
elif isinstance(parsed, list) and all(isinstance(item, dict) for item in parsed):
|
|
rows = parsed
|
|
else:
|
|
raise ReleasePackageError("Compose post-apply inventory has an invalid shape.")
|
|
by_service: dict[str, Mapping[str, Any]] = {}
|
|
for row in rows:
|
|
service = row.get("Service")
|
|
if not isinstance(service, str) or service in by_service:
|
|
raise ReleasePackageError(
|
|
"Compose post-apply inventory repeats or has an invalid service."
|
|
)
|
|
by_service[service] = row
|
|
unexpected = set(by_service).difference(expected_services)
|
|
if unexpected:
|
|
raise ReleasePackageError("Compose post-apply service inventory exceeds the plan.")
|
|
|
|
pending_services = set(expected_services).difference(by_service)
|
|
current_records: list[dict[str, Any]] = []
|
|
current_release_binding_verified = False
|
|
for service in expected_services:
|
|
row = by_service.get(service)
|
|
if row is None:
|
|
continue
|
|
container_id = row.get("ID")
|
|
if not isinstance(container_id, str) or not container_id:
|
|
raise ReleasePackageError(f"Post-apply container ID is missing for {service}.")
|
|
inspect_command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"inspect",
|
|
"--format",
|
|
"{{json .}}",
|
|
container_id,
|
|
]
|
|
inspected = _fixed_subprocess_runner(
|
|
inspect_command,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
shell=False,
|
|
env=dict(runtime_environment),
|
|
timeout=30,
|
|
)
|
|
if int(inspected.returncode) != 0:
|
|
raise ReleasePackageError(f"Cannot inspect post-apply container {service}.")
|
|
detail = _parse_json_text(inspected.stdout, label=f"{service} container inspect")
|
|
config = detail.get("Config")
|
|
state = detail.get("State")
|
|
labels = config.get("Labels") if isinstance(config, Mapping) else None
|
|
if (
|
|
not isinstance(config, Mapping)
|
|
or not isinstance(state, Mapping)
|
|
or not isinstance(labels, Mapping)
|
|
or labels.get("com.docker.compose.project") != plan["compose_project_name"]
|
|
or labels.get("com.docker.compose.service") != service
|
|
or detail.get("Image") != service_image_ids[service]
|
|
):
|
|
raise ReleasePackageError(
|
|
f"Post-apply project or image binding failed for {service}."
|
|
)
|
|
status = state.get("Status")
|
|
health_payload = state.get("Health")
|
|
health = (
|
|
health_payload.get("Status")
|
|
if isinstance(health_payload, Mapping)
|
|
else None
|
|
)
|
|
exit_code = state.get("ExitCode")
|
|
if service in ONE_SHOT_INSTALL_SERVICES:
|
|
if status == "exited":
|
|
if exit_code != 0:
|
|
raise ReleasePackageError(
|
|
f"Post-apply migration/init failed for {service}."
|
|
)
|
|
elif status in {"created", "running", "restarting"}:
|
|
pending_services.add(service)
|
|
else:
|
|
raise ReleasePackageError(
|
|
f"Post-apply migration/init entered an invalid state: {service}."
|
|
)
|
|
elif status == "running" and health == "healthy":
|
|
pass
|
|
elif status in {"created", "running", "restarting"} and health in {
|
|
None,
|
|
"starting",
|
|
"unhealthy",
|
|
}:
|
|
pending_services.add(service)
|
|
else:
|
|
raise ReleasePackageError(
|
|
f"Post-apply service entered an invalid state: {service}."
|
|
)
|
|
env_items = config.get("Env")
|
|
if service == "ai-platform-api":
|
|
if (
|
|
not isinstance(env_items, list)
|
|
or f"AI_PLATFORM_RELEASE_ID={plan['target_release_id']}" not in env_items
|
|
):
|
|
raise ReleasePackageError("AI runtime release identity is not bound.")
|
|
current_release_binding_verified = True
|
|
current_records.append(
|
|
{
|
|
"service": service,
|
|
"container_id_sha256": hashlib.sha256(
|
|
container_id.encode("utf-8")
|
|
).hexdigest(),
|
|
"status": status,
|
|
"health": health,
|
|
"exit_code": exit_code,
|
|
"image_id": detail.get("Image"),
|
|
}
|
|
)
|
|
|
|
if not pending_services:
|
|
records = current_records
|
|
release_binding_verified = current_release_binding_verified
|
|
break
|
|
if time.monotonic() >= deadline:
|
|
raise ReleasePackageError("Post-apply services did not become ready before timeout.")
|
|
time.sleep(POST_APPLY_POLL_INTERVAL_SECONDS)
|
|
if "ai-platform-api" in expected_services and not release_binding_verified:
|
|
raise ReleasePackageError("Post-apply release identity check did not run.")
|
|
payload: dict[str, Any] = {
|
|
"schema_version": POST_APPLY_SCHEMA,
|
|
"status": "verified",
|
|
"deployment_scope": DEPLOYMENT_SCOPE,
|
|
"operation": plan["operation"],
|
|
"target_release_id": plan["target_release_id"],
|
|
"compose_project_name": plan["compose_project_name"],
|
|
"deployed_service_set": expected_services,
|
|
"daemon_identity_sha256": daemon_identity["identity_sha256"],
|
|
"container_count": len(records),
|
|
"services": records,
|
|
"release_binding_verified": release_binding_verified,
|
|
}
|
|
payload["post_check_payload_sha256"] = canonical_sha256(payload)
|
|
return payload
|
|
|
|
|
|
def build_plan(
|
|
package: ValidatedPackage,
|
|
*,
|
|
operation: str,
|
|
env_file: Path,
|
|
repo_root: Path = REPO_ROOT,
|
|
inspection_runner: Callable[..., Any] = subprocess.run,
|
|
recovery_readiness_receipt: Mapping[str, Any] | None = None,
|
|
recovery_readiness_receipt_sha256: str | None = None,
|
|
) -> dict[str, Any]:
|
|
if operation not in CONFIRMATIONS:
|
|
raise ReleasePackageError("Operation must be install, rollback, or forward.")
|
|
_assert_operation_supported_for_recovery(package, operation=operation)
|
|
if not env_file.is_absolute():
|
|
raise ReleasePackageError("--env-file must be an absolute path.")
|
|
evidence = _collect_runtime_evidence(
|
|
env_file=env_file,
|
|
repo_root=repo_root,
|
|
inspection_runner=inspection_runner,
|
|
)
|
|
tls_binding = _tls_material_binding(evidence.preflight_summary)
|
|
env_target = evidence.env_file
|
|
env_values = evidence.env_values
|
|
project_name = package.package["compose_project_name"]
|
|
if env_values.get("COMPOSE_PROJECT_NAME") != project_name:
|
|
raise ReleasePackageError("Production env COMPOSE_PROJECT_NAME does not match the release package.")
|
|
|
|
if operation == "rollback":
|
|
if package.rollback_release_id is None or package.rollback_images is None:
|
|
raise ReleasePackageError("Rollback requires an approved predecessor identity.")
|
|
target_release_id = package.rollback_release_id
|
|
application_images = package.rollback_images
|
|
else:
|
|
target_release_id = package.package["release_id"]
|
|
application_images = package.application_images
|
|
environment_overrides = {
|
|
key: application_images[key]["reference"] for key in BACKEND_APPLICATION_IMAGE_KEYS
|
|
}
|
|
environment_overrides["AI_PLATFORM_RELEASE_IMAGE_DIGEST"] = application_images[
|
|
"AI_PLATFORM_API_IMAGE"
|
|
]["digest"]
|
|
if operation == "install":
|
|
environment_overrides.update(
|
|
{
|
|
key: package.infrastructure_images[key]["reference"]
|
|
for key in BACKEND_INSTALL_INFRASTRUCTURE_IMAGE_KEYS
|
|
}
|
|
)
|
|
import_plan = [dict(item) for item in package.docker_import_plan]
|
|
offline_image_overrides = (
|
|
dict(package.offline_image_overrides)
|
|
if operation in {"install", "forward"}
|
|
else {}
|
|
)
|
|
apply_blockers = _real_apply_blockers(
|
|
operation,
|
|
recovery_strategy=package.recovery_strategy,
|
|
package_schema_version=package.package_schema_version,
|
|
infrastructure_ready=len(package.docker_import_plan) == 11,
|
|
third_party_ready=package.third_party_verification_receipt is not None,
|
|
recovery_readiness_ready=recovery_readiness_receipt is not None,
|
|
)
|
|
source_root = _validated_directory(repo_root, label="repository root")
|
|
source_compose_files = (
|
|
_artifact_file(source_root, BASE_COMPOSE_RELATIVE, label="base Compose file"),
|
|
_artifact_file(
|
|
source_root,
|
|
PRODUCTION_COMPOSE_RELATIVE,
|
|
label="production Compose file",
|
|
),
|
|
_artifact_file(
|
|
source_root,
|
|
OFFLINE_COMPOSE_RELATIVE,
|
|
label="offline Compose override",
|
|
),
|
|
)
|
|
offline_execution_gate = _run_offline_execution_gate(
|
|
env_file=env_target,
|
|
repo_root=source_root,
|
|
compose_files=source_compose_files,
|
|
project_name=project_name,
|
|
offline_image_overrides=offline_image_overrides,
|
|
inspection_runner=inspection_runner,
|
|
)
|
|
command = _compose_command(
|
|
operation=operation,
|
|
project_name=project_name,
|
|
env_file=env_target,
|
|
repo_root=repo_root,
|
|
)
|
|
unsigned: dict[str, Any] = {
|
|
"schema_version": PLAN_SCHEMA,
|
|
"mode": "plan",
|
|
"operation": operation,
|
|
"deployment_scope": DEPLOYMENT_SCOPE,
|
|
"deployed_service_set": list(
|
|
RUNTIME_SERVICES if operation in {"rollback", "forward"} else INSTALL_SERVICES
|
|
),
|
|
"separately_deployed_artifacts": [
|
|
{
|
|
"image_key": key,
|
|
"reference": application_images[key]["reference"],
|
|
"digest": application_images[key]["digest"],
|
|
"deployment_scope": "separate_web_orchestrator",
|
|
"applied_by_this_operation": False,
|
|
}
|
|
for key in SEPARATELY_DEPLOYED_IMAGE_KEYS
|
|
],
|
|
"identity_bound_image_keys": list(APPLICATION_IMAGE_KEYS + INFRASTRUCTURE_IMAGE_KEYS),
|
|
"release_completion_claimed": False,
|
|
"package_schema_version": package.package_schema_version,
|
|
"release_id": package.package["release_id"],
|
|
"target_release_id": target_release_id,
|
|
"recovery_strategy": package.recovery_strategy,
|
|
"recovery_contract": _recovery_contract(package),
|
|
"recovery_readiness_receipt_sha256": recovery_readiness_receipt_sha256,
|
|
"recovery_readiness_status": (
|
|
RECOVERY_READINESS_RESULT
|
|
if recovery_readiness_receipt is not None
|
|
else "missing"
|
|
),
|
|
"package_manifest_sha256": package.package_sha256,
|
|
"release_manifest_sha256": package.release_manifest_sha256,
|
|
"checksum_manifest_sha256": package.checksum_manifest_sha256,
|
|
"image_identity_sha256": package.image_identity_sha256,
|
|
"installable_manifest_sha256": package.installable_manifest_sha256,
|
|
"trusted_build_receipt_sha256": package.trusted_build_receipt_sha256,
|
|
"infrastructure_source_set_sha256": package.infrastructure_source_set_sha256,
|
|
"infrastructure_bundle_sha256": package.infrastructure_bundle_sha256,
|
|
"third_party_materials_input_sha256": package.third_party_materials_input_sha256,
|
|
"third_party_materials_manifest_sha256": package.third_party_materials_manifest_sha256,
|
|
"third_party_verification_receipt_sha256": (
|
|
package.third_party_verification_receipt_sha256
|
|
),
|
|
"third_party_profile": package.third_party_profile,
|
|
"distribution_clearance_claimed": False,
|
|
"backup_proof_sha256": package.backup_proof_sha256,
|
|
"compatibility_proof_sha256": package.compatibility_proof_sha256,
|
|
"env_file": str(env_target),
|
|
"env_file_sha256": evidence.env_sha256,
|
|
"compose_project_name": project_name,
|
|
"docker_daemon_identity": evidence.daemon_identity,
|
|
"production_preflight": evidence.preflight_summary,
|
|
"tls_material_binding": tls_binding,
|
|
"tls_material_binding_sha256": canonical_sha256(tls_binding),
|
|
"compose_merge_gate": evidence.compose_gate_summary,
|
|
"offline_execution_gate": offline_execution_gate,
|
|
"docker_import_plan": import_plan,
|
|
"offline_image_overrides": offline_image_overrides,
|
|
"apply_eligible": not apply_blockers,
|
|
"backup_compatibility_evidence_status": (
|
|
"runtime_signed_snapshot_restore_evidence_required_blocked"
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else "optional_self_hashed_material_untrusted_external_signatures_missing_blocked"
|
|
),
|
|
"backup_evidence_status": (
|
|
"not_applicable_runtime_snapshot_evidence_required"
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else "missing" if package.backup_proof is None else "unverified_self_hashed"
|
|
),
|
|
"compatibility_evidence_status": (
|
|
"not_applicable_exact_snapshot_restore_required"
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else (
|
|
"missing"
|
|
if package.compatibility_proof is None
|
|
else "unverified_self_hashed"
|
|
)
|
|
),
|
|
"real_apply_feature_gate": {
|
|
"status": "blocked",
|
|
"blockers": apply_blockers,
|
|
},
|
|
"offline": True,
|
|
"pull_policy": "never",
|
|
"build_allowed": False,
|
|
"database_downgrade_allowed": False,
|
|
"edge_or_dns_change_allowed": False,
|
|
"environment_overrides": environment_overrides,
|
|
"command_argv": command,
|
|
}
|
|
unsigned["plan_sha256"] = canonical_sha256(unsigned)
|
|
return unsigned
|
|
|
|
|
|
def verification_payload(package: ValidatedPackage) -> dict[str, Any]:
|
|
apply_blockers = _real_apply_blockers(
|
|
"install",
|
|
recovery_strategy=package.recovery_strategy,
|
|
package_schema_version=package.package_schema_version,
|
|
infrastructure_ready=len(package.docker_import_plan) == 11,
|
|
third_party_ready=package.third_party_verification_receipt is not None,
|
|
)
|
|
payload: dict[str, Any] = {
|
|
"schema_version": PLAN_SCHEMA,
|
|
"mode": "verify",
|
|
"status": "structurally_verified_not_apply_eligible",
|
|
"package_structure_valid": True,
|
|
"apply_eligible": False,
|
|
"real_apply_feature_gate": {
|
|
"status": "eligible" if not apply_blockers else "blocked",
|
|
"blockers": apply_blockers,
|
|
},
|
|
"deployment_scope": DEPLOYMENT_SCOPE,
|
|
"deployed_service_set": [],
|
|
"managed_backend_service_set": list(INSTALL_SERVICES),
|
|
"separately_deployed_artifacts": [
|
|
{
|
|
"image_key": key,
|
|
"reference": package.application_images[key]["reference"],
|
|
"digest": package.application_images[key]["digest"],
|
|
"deployment_scope": "separate_web_orchestrator",
|
|
"applied_by_this_operation": False,
|
|
}
|
|
for key in SEPARATELY_DEPLOYED_IMAGE_KEYS
|
|
],
|
|
"identity_bound_image_keys": list(APPLICATION_IMAGE_KEYS + INFRASTRUCTURE_IMAGE_KEYS),
|
|
"release_completion_claimed": False,
|
|
"package_schema_version": package.package_schema_version,
|
|
"release_id": package.package["release_id"],
|
|
"rollback_target_release_id": package.rollback_release_id,
|
|
"recovery_strategy": package.recovery_strategy,
|
|
"recovery_contract": _recovery_contract(package),
|
|
"package_manifest_sha256": package.package_sha256,
|
|
"release_manifest_sha256": package.release_manifest_sha256,
|
|
"checksum_manifest_sha256": package.checksum_manifest_sha256,
|
|
"image_identity_sha256": package.image_identity_sha256,
|
|
"installable_manifest_sha256": package.installable_manifest_sha256,
|
|
"trusted_build_receipt_sha256": package.trusted_build_receipt_sha256,
|
|
"infrastructure_source_set_sha256": package.infrastructure_source_set_sha256,
|
|
"infrastructure_bundle_sha256": package.infrastructure_bundle_sha256,
|
|
"third_party_materials_input_sha256": package.third_party_materials_input_sha256,
|
|
"third_party_materials_manifest_sha256": package.third_party_materials_manifest_sha256,
|
|
"third_party_verification_receipt_sha256": (
|
|
package.third_party_verification_receipt_sha256
|
|
),
|
|
"third_party_profile": package.third_party_profile,
|
|
"third_party_materials_status": (
|
|
"independently_verified_technical_materials"
|
|
if package.third_party_verification_receipt is not None
|
|
else "missing_blocked"
|
|
),
|
|
"distribution_clearance_claimed": False,
|
|
"docker_import_plan_status": (
|
|
"validated_for_eleven_release_images"
|
|
if len(package.docker_import_plan) == 11
|
|
else "legacy_four_application_images_only_blocked"
|
|
),
|
|
"infrastructure_import_plan_status": (
|
|
"validated_for_seven_infrastructure_images"
|
|
if package.infrastructure_bundle is not None
|
|
else "missing_blocked"
|
|
),
|
|
"backup_compatibility_evidence_status": (
|
|
"runtime_signed_snapshot_restore_evidence_required_blocked"
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else "optional_self_hashed_material_untrusted_external_signatures_missing_blocked"
|
|
),
|
|
"backup_evidence_status": (
|
|
"not_applicable_runtime_snapshot_evidence_required"
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else "missing" if package.backup_proof is None else "unverified_self_hashed"
|
|
),
|
|
"compatibility_evidence_status": (
|
|
"not_applicable_exact_snapshot_restore_required"
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE
|
|
else (
|
|
"missing"
|
|
if package.compatibility_proof is None
|
|
else "unverified_self_hashed"
|
|
)
|
|
),
|
|
"backup_proof_sha256": package.backup_proof_sha256,
|
|
"compatibility_proof_sha256": package.compatibility_proof_sha256,
|
|
"offline": True,
|
|
"writes_performed": False,
|
|
}
|
|
payload["plan_sha256"] = canonical_sha256(payload)
|
|
return payload
|
|
|
|
|
|
def _validate_state_target(state_dir: Path, *, package_root: Path) -> Path:
|
|
lexical = _absolute_lexical(state_dir)
|
|
if not state_dir.is_absolute():
|
|
raise ReleasePackageError("--state-dir must be an absolute path.")
|
|
if lexical == Path(lexical.anchor):
|
|
raise ReleasePackageError("--state-dir must not be a filesystem root.")
|
|
_reject_symlink_components(lexical, label="state directory")
|
|
existing = lexical
|
|
while not existing.exists() and existing != existing.parent:
|
|
existing = existing.parent
|
|
try:
|
|
existing_resolved = existing.resolve(strict=True)
|
|
except OSError as exc:
|
|
raise ReleasePackageError("State directory parent is unavailable.") from exc
|
|
if not existing_resolved.is_dir():
|
|
raise ReleasePackageError("State directory parent is not a directory.")
|
|
try:
|
|
lexical.resolve(strict=False).relative_to(package_root)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise ReleasePackageError("State directory must be outside the immutable release package.")
|
|
if lexical.exists() and not lexical.is_dir():
|
|
raise ReleasePackageError("State directory path is not a directory.")
|
|
return lexical
|
|
|
|
|
|
def _fsync_directory(path: Path) -> None:
|
|
if os.name == "nt":
|
|
return
|
|
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
descriptor = os.open(path, flags)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _prepare_secure_state_root(state_dir: Path, *, require_root_owner: bool) -> None:
|
|
existed = state_dir.exists()
|
|
state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
_reject_symlink_components(state_dir, label="state directory")
|
|
if not state_dir.is_dir() or state_dir.is_symlink():
|
|
raise ReleasePackageError("State root must be a real directory.")
|
|
if os.name != "nt":
|
|
if not existed:
|
|
os.chmod(state_dir, 0o700)
|
|
state_stat = state_dir.stat()
|
|
expected_uid = 0 if require_root_owner else os.geteuid()
|
|
if state_stat.st_uid != expected_uid:
|
|
raise ReleasePackageError("State root has the wrong owner.")
|
|
if stat.S_IMODE(state_stat.st_mode) != 0o700:
|
|
raise ReleasePackageError("State root must have mode 0700.")
|
|
_fsync_directory(state_dir.parent)
|
|
|
|
|
|
def _secure_snapshot_copy(
|
|
source: Path,
|
|
destination: Path,
|
|
*,
|
|
expected_sha256: str,
|
|
expected_size: int | None = None,
|
|
label: str,
|
|
) -> dict[str, Any]:
|
|
source = _validated_file(source, label=label)
|
|
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
source_descriptor = os.open(source, flags)
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"Cannot open {label} for an immutable snapshot.") from exc
|
|
destination_descriptor = -1
|
|
try:
|
|
before = os.fstat(source_descriptor)
|
|
if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1:
|
|
raise ReleasePackageError(f"{label} must be a single-link regular file.")
|
|
if expected_size is not None and before.st_size != expected_size:
|
|
raise ReleasePackageError(f"{label} size changed before snapshot.")
|
|
destination.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
_reject_symlink_components(destination.parent, label="snapshot directory")
|
|
destination_descriptor = os.open(
|
|
destination,
|
|
os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0),
|
|
0o600,
|
|
)
|
|
digest = hashlib.sha256()
|
|
copied = 0
|
|
while True:
|
|
chunk = os.read(source_descriptor, 1024 * 1024)
|
|
if not chunk:
|
|
break
|
|
digest.update(chunk)
|
|
copied += len(chunk)
|
|
view = memoryview(chunk)
|
|
while view:
|
|
written = os.write(destination_descriptor, view)
|
|
view = view[written:]
|
|
after = os.fstat(source_descriptor)
|
|
if (
|
|
before.st_dev,
|
|
before.st_ino,
|
|
before.st_size,
|
|
getattr(before, "st_mtime_ns", None),
|
|
) != (
|
|
after.st_dev,
|
|
after.st_ino,
|
|
after.st_size,
|
|
getattr(after, "st_mtime_ns", None),
|
|
):
|
|
raise ReleasePackageError(f"{label} changed while being snapshotted.")
|
|
observed_sha256 = digest.hexdigest()
|
|
if observed_sha256 != expected_sha256:
|
|
raise ReleasePackageError(f"{label} SHA-256 changed before snapshot.")
|
|
if expected_size is not None and copied != expected_size:
|
|
raise ReleasePackageError(f"{label} size changed while being snapshotted.")
|
|
os.fsync(destination_descriptor)
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"Cannot create immutable snapshot for {label}.") from exc
|
|
finally:
|
|
os.close(source_descriptor)
|
|
if destination_descriptor >= 0:
|
|
os.close(destination_descriptor)
|
|
if os.name != "nt":
|
|
os.chmod(destination, 0o600)
|
|
destination_stat = destination.stat()
|
|
if destination_stat.st_nlink != 1:
|
|
raise ReleasePackageError(f"Snapshot for {label} is unexpectedly hard linked.")
|
|
return {
|
|
"path": destination.as_posix(),
|
|
"sha256": expected_sha256,
|
|
"size_bytes": destination_stat.st_size,
|
|
}
|
|
|
|
|
|
def _write_exclusive_bytes(path: Path, payload: bytes, *, label: str) -> dict[str, Any]:
|
|
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
_reject_symlink_components(path.parent, label=f"{label} directory")
|
|
descriptor = -1
|
|
try:
|
|
descriptor = os.open(
|
|
path,
|
|
os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0),
|
|
0o600,
|
|
)
|
|
view = memoryview(payload)
|
|
while view:
|
|
written = os.write(descriptor, view)
|
|
view = view[written:]
|
|
os.fsync(descriptor)
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"Cannot create immutable {label}.") from exc
|
|
finally:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
metadata = os.lstat(path)
|
|
if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
|
|
raise ReleasePackageError(f"Immutable {label} is not a single-link regular file.")
|
|
return {
|
|
"path": path.as_posix(),
|
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
"size_bytes": len(payload),
|
|
}
|
|
|
|
|
|
def _rewrite_execution_env(
|
|
source: Path,
|
|
destination: Path,
|
|
*,
|
|
replacements: Mapping[str, str],
|
|
) -> dict[str, Any]:
|
|
try:
|
|
text = source.read_text(encoding="utf-8-sig")
|
|
except (OSError, UnicodeError) as exc:
|
|
raise ReleasePackageError("Snapshotted production env is not readable UTF-8 text.") from exc
|
|
remaining = set(replacements)
|
|
rewritten: list[str] = []
|
|
for raw_line in text.splitlines(keepends=True):
|
|
ending = ""
|
|
body = raw_line
|
|
for candidate in ("\r\n", "\n", "\r"):
|
|
if body.endswith(candidate):
|
|
body = body[: -len(candidate)]
|
|
ending = candidate
|
|
break
|
|
normalized = body.strip()
|
|
if normalized and not normalized.startswith("#") and "=" in normalized:
|
|
key = normalized.split("=", 1)[0]
|
|
if key in replacements:
|
|
value = replacements[key]
|
|
if not value or any(character in value for character in "\r\n\x00"):
|
|
raise ReleasePackageError(f"Execution env replacement for {key} is unsafe.")
|
|
body = f"{key}={value}"
|
|
remaining.discard(key)
|
|
rewritten.append(body + ending)
|
|
if remaining:
|
|
raise ReleasePackageError(
|
|
"Production env is missing TLS keys required for execution: "
|
|
+ ", ".join(sorted(remaining))
|
|
)
|
|
payload = "".join(rewritten).encode("utf-8")
|
|
record = _write_exclusive_bytes(destination, payload, label="execution env snapshot")
|
|
parsed = parse_env_file(destination)
|
|
if any(parsed.get(key) != value for key, value in replacements.items()):
|
|
raise ReleasePackageError("Execution env TLS paths do not match the immutable snapshot.")
|
|
return record
|
|
|
|
|
|
def _verify_immutable_snapshot_file(
|
|
path: Path,
|
|
*,
|
|
expected_sha256: str,
|
|
expected_size: int,
|
|
label: str,
|
|
) -> None:
|
|
target = _validated_file(path, label=label)
|
|
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
descriptor = -1
|
|
try:
|
|
descriptor = os.open(target, flags)
|
|
before = os.fstat(descriptor)
|
|
if (
|
|
not stat.S_ISREG(before.st_mode)
|
|
or before.st_nlink != 1
|
|
or before.st_size != expected_size
|
|
):
|
|
raise ReleasePackageError(f"{label} metadata changed before execution.")
|
|
digest = hashlib.sha256()
|
|
while True:
|
|
chunk = os.read(descriptor, 1024 * 1024)
|
|
if not chunk:
|
|
break
|
|
digest.update(chunk)
|
|
after = os.fstat(descriptor)
|
|
if (
|
|
before.st_dev,
|
|
before.st_ino,
|
|
before.st_size,
|
|
getattr(before, "st_mtime_ns", None),
|
|
) != (
|
|
after.st_dev,
|
|
after.st_ino,
|
|
after.st_size,
|
|
getattr(after, "st_mtime_ns", None),
|
|
):
|
|
raise ReleasePackageError(f"{label} changed during execution verification.")
|
|
if digest.hexdigest() != expected_sha256:
|
|
raise ReleasePackageError(f"{label} SHA-256 changed before execution.")
|
|
except OSError as exc:
|
|
raise ReleasePackageError(f"Cannot verify immutable {label} before execution.") from exc
|
|
finally:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _verify_execution_snapshot(snapshot: SnapshotInputs, plan: Mapping[str, Any]) -> None:
|
|
files = snapshot.manifest.get("files")
|
|
if not isinstance(files, list):
|
|
raise ReleasePackageError("Execution snapshot has no file inventory.")
|
|
by_path = {
|
|
str(record.get("path")): record
|
|
for record in files
|
|
if isinstance(record, dict) and isinstance(record.get("path"), str)
|
|
}
|
|
required = [snapshot.env_file, *snapshot.tls_files.values()]
|
|
for path in required:
|
|
record = by_path.get(path.as_posix())
|
|
if (
|
|
not isinstance(record, dict)
|
|
or not isinstance(record.get("sha256"), str)
|
|
or not SHA256_RE.fullmatch(str(record.get("sha256")))
|
|
or not isinstance(record.get("size_bytes"), int)
|
|
):
|
|
raise ReleasePackageError("Execution snapshot TLS or env inventory is incomplete.")
|
|
_verify_immutable_snapshot_file(
|
|
path,
|
|
expected_sha256=str(record["sha256"]),
|
|
expected_size=int(record["size_bytes"]),
|
|
label=f"execution snapshot {path.name}",
|
|
)
|
|
if snapshot.manifest.get("tls_material_binding_sha256") != plan.get(
|
|
"tls_material_binding_sha256"
|
|
):
|
|
raise ReleasePackageError("Execution snapshot TLS binding does not match the release plan.")
|
|
|
|
|
|
def _freeze_snapshot_tree(root: Path) -> None:
|
|
if os.name == "nt":
|
|
return
|
|
files = [path for path in root.rglob("*") if path.is_file()]
|
|
directories = [path for path in root.rglob("*") if path.is_dir()]
|
|
for path in files:
|
|
os.chmod(path, 0o400)
|
|
for path in sorted(directories, key=lambda item: len(item.parts), reverse=True):
|
|
os.chmod(path, 0o500)
|
|
os.chmod(root, 0o500)
|
|
|
|
|
|
def _snapshot_inputs(
|
|
*,
|
|
state_dir: Path,
|
|
idempotency_key: str,
|
|
plan: Mapping[str, Any],
|
|
package: ValidatedPackage,
|
|
repo_root: Path,
|
|
) -> SnapshotInputs:
|
|
key_digest = hashlib.sha256(idempotency_key.encode("utf-8")).hexdigest()
|
|
snapshots_root = state_dir / "snapshots"
|
|
snapshots_root.mkdir(mode=0o700, exist_ok=True)
|
|
_reject_symlink_components(snapshots_root, label="snapshot root")
|
|
snapshot_root = snapshots_root / key_digest
|
|
try:
|
|
snapshot_root.mkdir(mode=0o700)
|
|
except FileExistsError as exc:
|
|
raise ReleasePackageError(
|
|
"A prior snapshot exists for this idempotency key; reconciliation is required."
|
|
) from exc
|
|
|
|
records: list[dict[str, Any]] = []
|
|
source_env_destination = snapshot_root / "production.source.env"
|
|
records.append(
|
|
_secure_snapshot_copy(
|
|
Path(str(plan["env_file"])),
|
|
source_env_destination,
|
|
expected_sha256=str(plan["env_file_sha256"]),
|
|
label="production env file",
|
|
)
|
|
)
|
|
|
|
binding = _tls_material_binding(plan["production_preflight"])
|
|
binding_sha256 = canonical_sha256(binding)
|
|
if (
|
|
binding != plan.get("tls_material_binding")
|
|
or binding_sha256 != plan.get("tls_material_binding_sha256")
|
|
):
|
|
raise ReleasePackageError("Release plan TLS material binding is inconsistent.")
|
|
source_values = parse_env_file(source_env_destination)
|
|
source_tls_dir = Path(source_values.get("BACKEND_TLS_CERT_DIR", ""))
|
|
source_ca_path = Path(source_values.get("BACKEND_TLS_TRUSTED_CA_FILE", ""))
|
|
tls_directory = snapshot_root / "tls"
|
|
tls_files = {
|
|
"certificate": tls_directory / "tls.crt",
|
|
"private_key": tls_directory / "tls.key",
|
|
"trusted_ca": tls_directory / "trusted-ca.pem",
|
|
}
|
|
records.append(
|
|
_secure_snapshot_copy(
|
|
source_tls_dir / "tls.crt",
|
|
tls_files["certificate"],
|
|
expected_sha256=binding["certificate_sha256"],
|
|
label="TLS certificate",
|
|
)
|
|
)
|
|
records.append(
|
|
_secure_snapshot_copy(
|
|
source_tls_dir / "tls.key",
|
|
tls_files["private_key"],
|
|
expected_sha256=binding["private_key_sha256"],
|
|
label="TLS private key",
|
|
)
|
|
)
|
|
records.append(
|
|
_secure_snapshot_copy(
|
|
source_ca_path,
|
|
tls_files["trusted_ca"],
|
|
expected_sha256=binding["trusted_ca_sha256"],
|
|
label="TLS trusted CA",
|
|
)
|
|
)
|
|
env_destination = snapshot_root / "production.env"
|
|
execution_env_record = _rewrite_execution_env(
|
|
source_env_destination,
|
|
env_destination,
|
|
replacements={
|
|
"BACKEND_TLS_CERT_DIR": tls_directory.as_posix(),
|
|
"BACKEND_TLS_TRUSTED_CA_FILE": tls_files["trusted_ca"].as_posix(),
|
|
},
|
|
)
|
|
records.append(execution_env_record)
|
|
|
|
compose_hashes = {
|
|
item["path"]: item["sha256"] for item in plan["compose_merge_gate"]["compose_inputs"]
|
|
}
|
|
compose_destinations: list[Path] = []
|
|
for relative in (
|
|
BASE_COMPOSE_RELATIVE,
|
|
PRODUCTION_COMPOSE_RELATIVE,
|
|
OFFLINE_COMPOSE_RELATIVE,
|
|
):
|
|
destination = snapshot_root.joinpath(*PurePosixPath(relative).parts)
|
|
records.append(
|
|
_secure_snapshot_copy(
|
|
repo_root.joinpath(*PurePosixPath(relative).parts),
|
|
destination,
|
|
expected_sha256=compose_hashes[relative],
|
|
label=f"Compose input {relative}",
|
|
)
|
|
)
|
|
compose_destinations.append(destination)
|
|
|
|
archive_destinations: dict[str, Path] = {}
|
|
for item in plan["docker_import_plan"]:
|
|
role = item["role"]
|
|
destination = snapshot_root / "docker-archives" / f"{role}.docker.tar"
|
|
records.append(
|
|
_secure_snapshot_copy(
|
|
package.root.joinpath(
|
|
*PurePosixPath(item["archive_package_path"]).parts
|
|
),
|
|
destination,
|
|
expected_sha256=item["archive_sha256"],
|
|
expected_size=item["archive_size_bytes"],
|
|
label=f"{role} Docker archive",
|
|
)
|
|
)
|
|
archive_destinations[role] = destination
|
|
|
|
manifest: dict[str, Any] = {
|
|
"schema_version": "yuqei.release-input-snapshot.v2",
|
|
"plan_sha256": plan["plan_sha256"],
|
|
"tls_material_binding_sha256": binding_sha256,
|
|
"execution_env_sha256": execution_env_record["sha256"],
|
|
"files": sorted(records, key=lambda item: item["path"].encode("utf-8")),
|
|
}
|
|
manifest["snapshot_payload_sha256"] = canonical_sha256(manifest)
|
|
_write_exclusive_canonical_json(snapshot_root / "snapshot-manifest.json", manifest)
|
|
for directory in {
|
|
snapshot_root,
|
|
source_env_destination.parent,
|
|
env_destination.parent,
|
|
*(path.parent for path in tls_files.values()),
|
|
*(path.parent for path in compose_destinations),
|
|
*(path.parent for path in archive_destinations.values()),
|
|
}:
|
|
_fsync_directory(directory)
|
|
snapshot = SnapshotInputs(
|
|
root=snapshot_root,
|
|
source_env_file=source_env_destination,
|
|
env_file=env_destination,
|
|
compose_files=(
|
|
compose_destinations[0],
|
|
compose_destinations[1],
|
|
compose_destinations[2],
|
|
),
|
|
docker_archives=archive_destinations,
|
|
tls_files=tls_files,
|
|
manifest=manifest,
|
|
)
|
|
_verify_execution_snapshot(snapshot, plan)
|
|
_freeze_snapshot_tree(snapshot_root)
|
|
return snapshot
|
|
|
|
|
|
@contextmanager
|
|
def _exclusive_lock(state_dir: Path) -> Iterator[Path]:
|
|
_reject_symlink_components(state_dir, label="state directory")
|
|
lock_path = state_dir / ".release-package.lock"
|
|
try:
|
|
descriptor = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
except FileExistsError as exc:
|
|
raise ReleasePackageError("Another release package operation holds the exclusive lock.") from exc
|
|
except OSError as exc:
|
|
raise ReleasePackageError("Unable to acquire the release package lock.") from exc
|
|
try:
|
|
os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
|
|
os.fsync(descriptor)
|
|
_fsync_directory(state_dir)
|
|
os.close(descriptor)
|
|
descriptor = -1
|
|
yield lock_path
|
|
finally:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
try:
|
|
lock_path.unlink(missing_ok=True)
|
|
except OSError as exc: # pragma: no cover - an unhealthy filesystem should remain visible.
|
|
raise ReleasePackageError("Unable to release the release package lock.") from exc
|
|
|
|
|
|
def _receipt_path(state_dir: Path, idempotency_key: str) -> Path:
|
|
key_digest = hashlib.sha256(idempotency_key.encode("utf-8")).hexdigest()
|
|
return state_dir / "receipts" / f"{key_digest}.json"
|
|
|
|
|
|
def _pending_path(state_dir: Path, idempotency_key: str) -> Path:
|
|
key_digest = hashlib.sha256(idempotency_key.encode("utf-8")).hexdigest()
|
|
return state_dir / "pending" / f"{key_digest}.json"
|
|
|
|
|
|
def _pending_journal(
|
|
*,
|
|
plan: Mapping[str, Any],
|
|
idempotency_key: str,
|
|
snapshot: SnapshotInputs,
|
|
command: list[str],
|
|
recorded_at: datetime,
|
|
) -> dict[str, Any]:
|
|
if recorded_at.tzinfo is None:
|
|
raise ReleasePackageError("Pending journal time must include a timezone.")
|
|
payload: dict[str, Any] = {
|
|
"schema_version": PENDING_SCHEMA,
|
|
"status": "reconcile_required",
|
|
"phase": "pending_image_import_and_backend_apply",
|
|
"operation": plan["operation"],
|
|
"package_schema_version": plan["package_schema_version"],
|
|
"deployment_scope": DEPLOYMENT_SCOPE,
|
|
"deployed_service_set": plan["deployed_service_set"],
|
|
"idempotency_key": idempotency_key,
|
|
"recorded_at": recorded_at.astimezone(UTC).isoformat().replace("+00:00", "Z"),
|
|
"plan_sha256": plan["plan_sha256"],
|
|
"snapshot_payload_sha256": snapshot.manifest["snapshot_payload_sha256"],
|
|
"tls_material_binding_sha256": plan["tls_material_binding_sha256"],
|
|
"execution_env_sha256": snapshot.manifest["execution_env_sha256"],
|
|
"docker_daemon_identity": plan["docker_daemon_identity"],
|
|
"installable_manifest_sha256": plan["installable_manifest_sha256"],
|
|
"trusted_build_receipt_sha256": plan["trusted_build_receipt_sha256"],
|
|
"infrastructure_source_set_sha256": plan["infrastructure_source_set_sha256"],
|
|
"infrastructure_bundle_sha256": plan["infrastructure_bundle_sha256"],
|
|
"third_party_materials_input_sha256": plan[
|
|
"third_party_materials_input_sha256"
|
|
],
|
|
"third_party_materials_manifest_sha256": plan[
|
|
"third_party_materials_manifest_sha256"
|
|
],
|
|
"third_party_verification_receipt_sha256": plan[
|
|
"third_party_verification_receipt_sha256"
|
|
],
|
|
"recovery_readiness_receipt_sha256": plan.get(
|
|
"recovery_readiness_receipt_sha256"
|
|
),
|
|
"docker_import_plan_sha256": canonical_sha256(plan["docker_import_plan"]),
|
|
"command_argv": command,
|
|
}
|
|
payload["journal_payload_sha256"] = canonical_sha256(payload)
|
|
return payload
|
|
|
|
|
|
def _read_pending_journal(path: Path) -> dict[str, Any]:
|
|
payload = _read_json(path, label="pending reconciliation journal")
|
|
if payload.get("schema_version") != PENDING_SCHEMA:
|
|
raise ReleasePackageError("Pending reconciliation journal has an unsupported schema.")
|
|
expected_fields = {
|
|
"schema_version",
|
|
"status",
|
|
"phase",
|
|
"operation",
|
|
"package_schema_version",
|
|
"deployment_scope",
|
|
"deployed_service_set",
|
|
"idempotency_key",
|
|
"recorded_at",
|
|
"plan_sha256",
|
|
"snapshot_payload_sha256",
|
|
"tls_material_binding_sha256",
|
|
"execution_env_sha256",
|
|
"docker_daemon_identity",
|
|
"installable_manifest_sha256",
|
|
"trusted_build_receipt_sha256",
|
|
"infrastructure_source_set_sha256",
|
|
"infrastructure_bundle_sha256",
|
|
"third_party_materials_input_sha256",
|
|
"third_party_materials_manifest_sha256",
|
|
"third_party_verification_receipt_sha256",
|
|
"recovery_readiness_receipt_sha256",
|
|
"docker_import_plan_sha256",
|
|
"command_argv",
|
|
"journal_payload_sha256",
|
|
}
|
|
if set(payload) != expected_fields:
|
|
raise ReleasePackageError("Pending reconciliation journal has unexpected or missing fields.")
|
|
_verify_embedded_hash(
|
|
payload,
|
|
"journal_payload_sha256",
|
|
label="pending reconciliation journal",
|
|
)
|
|
if payload.get("status") != "reconcile_required":
|
|
raise ReleasePackageError("Pending journal has an invalid status.")
|
|
for field in (
|
|
"plan_sha256",
|
|
"snapshot_payload_sha256",
|
|
"tls_material_binding_sha256",
|
|
"execution_env_sha256",
|
|
"installable_manifest_sha256",
|
|
"trusted_build_receipt_sha256",
|
|
"docker_import_plan_sha256",
|
|
"infrastructure_source_set_sha256",
|
|
"infrastructure_bundle_sha256",
|
|
"third_party_materials_input_sha256",
|
|
"third_party_materials_manifest_sha256",
|
|
"third_party_verification_receipt_sha256",
|
|
):
|
|
value = payload.get(field)
|
|
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
|
|
raise ReleasePackageError(f"Pending reconciliation journal has an invalid {field}.")
|
|
readiness_hash = payload.get("recovery_readiness_receipt_sha256")
|
|
if readiness_hash is not None and (
|
|
not isinstance(readiness_hash, str) or not SHA256_RE.fullmatch(readiness_hash)
|
|
):
|
|
raise ReleasePackageError(
|
|
"Pending reconciliation journal has an invalid recovery readiness receipt hash."
|
|
)
|
|
if path.read_bytes() != canonical_json_bytes(payload) + b"\n":
|
|
raise ReleasePackageError("Pending reconciliation journal is not canonical JSON.")
|
|
return payload
|
|
|
|
|
|
def _validate_receipt_payload(payload: Mapping[str, Any], *, label: str) -> None:
|
|
expected_fields = {
|
|
"schema_version",
|
|
"operation",
|
|
"status",
|
|
"execution_mode",
|
|
"package_schema_version",
|
|
"deployment_scope",
|
|
"deployed_service_set",
|
|
"separately_deployed_artifacts",
|
|
"identity_bound_image_keys",
|
|
"release_completion_claimed",
|
|
"release_id",
|
|
"target_release_id",
|
|
"compose_project_name",
|
|
"idempotency_key",
|
|
"recorded_at",
|
|
"plan_sha256",
|
|
"package_manifest_sha256",
|
|
"release_manifest_sha256",
|
|
"checksum_manifest_sha256",
|
|
"image_identity_sha256",
|
|
"installable_manifest_sha256",
|
|
"trusted_build_receipt_sha256",
|
|
"infrastructure_source_set_sha256",
|
|
"infrastructure_bundle_sha256",
|
|
"third_party_materials_input_sha256",
|
|
"third_party_materials_manifest_sha256",
|
|
"third_party_verification_receipt_sha256",
|
|
"third_party_profile",
|
|
"distribution_clearance_claimed",
|
|
"recovery_readiness_receipt_sha256",
|
|
"backup_proof_sha256",
|
|
"compatibility_proof_sha256",
|
|
"docker_daemon_identity",
|
|
"production_preflight_summary_sha256",
|
|
"compose_merge_gate_summary_sha256",
|
|
"offline_execution_gate_summary_sha256",
|
|
"snapshot_payload_sha256",
|
|
"tls_material_binding_sha256",
|
|
"execution_env_sha256",
|
|
"docker_image_import_result",
|
|
"post_apply_check",
|
|
"offline",
|
|
"pull_policy",
|
|
"build_allowed",
|
|
"database_downgrade_performed",
|
|
"edge_or_dns_change_performed",
|
|
"environment_overrides",
|
|
"command_argv",
|
|
"result",
|
|
"receipt_payload_sha256",
|
|
}
|
|
if set(payload) != expected_fields:
|
|
raise ReleasePackageError(f"{label} has unexpected or missing fields.")
|
|
if payload.get("schema_version") != RECEIPT_SCHEMA:
|
|
raise ReleasePackageError(f"{label} has an unsupported schema.")
|
|
_verify_embedded_hash(payload, "receipt_payload_sha256", label=label)
|
|
operation = payload.get("operation")
|
|
if operation not in CONFIRMATIONS:
|
|
raise ReleasePackageError(f"{label} has an invalid operation.")
|
|
if payload.get("status") != "backend_verified":
|
|
raise ReleasePackageError(f"{label} has an invalid status.")
|
|
if payload.get("execution_mode") not in {
|
|
"test_injected_runner",
|
|
"builtin_fixed_subprocess_runner",
|
|
}:
|
|
raise ReleasePackageError(f"{label} has an invalid execution mode.")
|
|
if payload.get("package_schema_version") != PACKAGE_SCHEMA_V2:
|
|
raise ReleasePackageError(f"{label} is not from an apply-eligible package schema.")
|
|
if payload.get("deployment_scope") != DEPLOYMENT_SCOPE:
|
|
raise ReleasePackageError(f"{label} does not declare the backend-only deployment scope.")
|
|
expected_services = RUNTIME_SERVICES if operation in {"rollback", "forward"} else INSTALL_SERVICES
|
|
if payload.get("deployed_service_set") != list(expected_services):
|
|
raise ReleasePackageError(f"{label} has an invalid backend deployed_service_set.")
|
|
if payload.get("release_completion_claimed") is not False:
|
|
raise ReleasePackageError(f"{label} must not claim completion of the full release.")
|
|
if payload.get("identity_bound_image_keys") != list(
|
|
APPLICATION_IMAGE_KEYS + INFRASTRUCTURE_IMAGE_KEYS
|
|
):
|
|
raise ReleasePackageError(f"{label} does not bind the complete production image inventory.")
|
|
separate = payload.get("separately_deployed_artifacts")
|
|
if not isinstance(separate, list) or len(separate) != len(SEPARATELY_DEPLOYED_IMAGE_KEYS):
|
|
raise ReleasePackageError(f"{label} has an invalid separately deployed artifact inventory.")
|
|
for expected_key, artifact in zip(SEPARATELY_DEPLOYED_IMAGE_KEYS, separate, strict=True):
|
|
if (
|
|
not isinstance(artifact, dict)
|
|
or set(artifact)
|
|
!= {
|
|
"image_key",
|
|
"reference",
|
|
"digest",
|
|
"deployment_scope",
|
|
"applied_by_this_operation",
|
|
}
|
|
or artifact.get("image_key") != expected_key
|
|
or artifact.get("deployment_scope") != "separate_web_orchestrator"
|
|
or artifact.get("applied_by_this_operation") is not False
|
|
or not IMAGE_REFERENCE_RE.fullmatch(str(artifact.get("reference", "")))
|
|
or not IMAGE_DIGEST_RE.fullmatch(str(artifact.get("digest", "")))
|
|
or str(artifact.get("reference", "")).rsplit("@", 1)[-1]
|
|
!= artifact.get("digest")
|
|
):
|
|
raise ReleasePackageError(f"{label} has an invalid separately deployed artifact record.")
|
|
|
|
_validate_identifier(payload.get("release_id"), RELEASE_ID_RE, label=f"{label} release_id")
|
|
_validate_identifier(
|
|
payload.get("target_release_id"),
|
|
RELEASE_ID_RE,
|
|
label=f"{label} target_release_id",
|
|
)
|
|
_validate_identifier(
|
|
payload.get("compose_project_name"),
|
|
PROJECT_RE,
|
|
label=f"{label} compose_project_name",
|
|
)
|
|
_validate_identifier(
|
|
payload.get("idempotency_key"),
|
|
IDEMPOTENCY_RE,
|
|
label=f"{label} idempotency_key",
|
|
)
|
|
recorded_at = payload.get("recorded_at")
|
|
if not isinstance(recorded_at, str) or not recorded_at.endswith("Z"):
|
|
raise ReleasePackageError(f"{label} has an invalid recorded_at timestamp.")
|
|
try:
|
|
parsed_time = datetime.fromisoformat(recorded_at[:-1] + "+00:00")
|
|
except ValueError as exc:
|
|
raise ReleasePackageError(f"{label} has an invalid recorded_at timestamp.") from exc
|
|
if parsed_time.tzinfo is None or parsed_time.utcoffset() != UTC.utcoffset(parsed_time):
|
|
raise ReleasePackageError(f"{label} recorded_at must be UTC.")
|
|
|
|
for hash_field in (
|
|
"plan_sha256",
|
|
"package_manifest_sha256",
|
|
"release_manifest_sha256",
|
|
"checksum_manifest_sha256",
|
|
"image_identity_sha256",
|
|
"installable_manifest_sha256",
|
|
"trusted_build_receipt_sha256",
|
|
"infrastructure_source_set_sha256",
|
|
"infrastructure_bundle_sha256",
|
|
"third_party_materials_input_sha256",
|
|
"third_party_materials_manifest_sha256",
|
|
"third_party_verification_receipt_sha256",
|
|
"production_preflight_summary_sha256",
|
|
"compose_merge_gate_summary_sha256",
|
|
"offline_execution_gate_summary_sha256",
|
|
"snapshot_payload_sha256",
|
|
"tls_material_binding_sha256",
|
|
"execution_env_sha256",
|
|
):
|
|
value = payload.get(hash_field)
|
|
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
|
|
raise ReleasePackageError(f"{label} has an invalid {hash_field}.")
|
|
for optional_hash_field in (
|
|
"backup_proof_sha256",
|
|
"compatibility_proof_sha256",
|
|
"recovery_readiness_receipt_sha256",
|
|
):
|
|
value = payload.get(optional_hash_field)
|
|
if value is not None and (
|
|
not isinstance(value, str) or not SHA256_RE.fullmatch(value)
|
|
):
|
|
raise ReleasePackageError(f"{label} has an invalid {optional_hash_field}.")
|
|
if (
|
|
payload.get("third_party_profile") not in {"internal-hosted", "redistributable"}
|
|
or payload.get("distribution_clearance_claimed") is not False
|
|
):
|
|
raise ReleasePackageError(
|
|
f"{label} makes an invalid third-party materials or clearance claim."
|
|
)
|
|
|
|
daemon = payload.get("docker_daemon_identity")
|
|
daemon_fields = {
|
|
"docker_cli",
|
|
"docker_host",
|
|
"daemon_id",
|
|
"daemon_name",
|
|
"docker_root_dir",
|
|
"server_version",
|
|
"identity_sha256",
|
|
}
|
|
if not isinstance(daemon, dict) or set(daemon) != daemon_fields:
|
|
raise ReleasePackageError(f"{label} has an invalid Docker daemon identity.")
|
|
_verify_embedded_hash(daemon, "identity_sha256", label=f"{label} Docker daemon identity")
|
|
if daemon.get("docker_cli") != str(DOCKER_CLI) or daemon.get("docker_host") != LOCAL_DOCKER_HOST:
|
|
raise ReleasePackageError(f"{label} is not bound to the fixed local Docker target.")
|
|
if any(
|
|
not isinstance(daemon.get(key), str) or not str(daemon.get(key)).strip()
|
|
for key in ("daemon_id", "daemon_name", "docker_root_dir", "server_version")
|
|
):
|
|
raise ReleasePackageError(f"{label} has an incomplete Docker daemon identity.")
|
|
|
|
import_result = payload.get("docker_image_import_result")
|
|
if not isinstance(import_result, dict) or set(import_result) != {
|
|
"schema_version",
|
|
"status",
|
|
"images",
|
|
"summary_sha256",
|
|
}:
|
|
raise ReleasePackageError(f"{label} has an invalid Docker image import result.")
|
|
_verify_embedded_hash(
|
|
import_result,
|
|
"summary_sha256",
|
|
label=f"{label} Docker image import result",
|
|
)
|
|
imported_images = import_result.get("images")
|
|
if (
|
|
import_result.get("schema_version") != RELEASE_IMPORT_RESULT_SCHEMA
|
|
or import_result.get("status") != "verified"
|
|
or not isinstance(imported_images, list)
|
|
or len(imported_images) != len(RELEASE_IMPORT_ROLE_ENV_SERVICES)
|
|
):
|
|
raise ReleasePackageError(f"{label} has an invalid Docker image import result.")
|
|
import_fields = {
|
|
"sequence",
|
|
"role",
|
|
"archive_sha256",
|
|
"loaded_image_id",
|
|
"offline_compose_override",
|
|
"load_stdout_sha256",
|
|
"load_stderr_sha256",
|
|
"tag_stdout_sha256",
|
|
"tag_stderr_sha256",
|
|
}
|
|
for sequence, ((role, env_key, _services), record) in enumerate(
|
|
zip(RELEASE_IMPORT_ROLE_ENV_SERVICES, imported_images, strict=True),
|
|
start=1,
|
|
):
|
|
if (
|
|
not isinstance(record, dict)
|
|
or set(record) != import_fields
|
|
or record.get("sequence") != sequence
|
|
or record.get("role") != role
|
|
or not IMAGE_DIGEST_RE.fullmatch(str(record.get("loaded_image_id", "")))
|
|
or record.get("offline_compose_override") != record.get("loaded_image_id")
|
|
or any(
|
|
not isinstance(record.get(field), str)
|
|
or not SHA256_RE.fullmatch(str(record.get(field)))
|
|
for field in (
|
|
"archive_sha256",
|
|
"load_stdout_sha256",
|
|
"load_stderr_sha256",
|
|
"tag_stdout_sha256",
|
|
"tag_stderr_sha256",
|
|
)
|
|
)
|
|
):
|
|
raise ReleasePackageError(f"{label} has an invalid imported image record.")
|
|
|
|
overrides = payload.get("environment_overrides")
|
|
expected_override_keys = {
|
|
*(key for _role, key, _services in RELEASE_IMPORT_ROLE_ENV_SERVICES),
|
|
"AI_PLATFORM_RELEASE_IMAGE_DIGEST",
|
|
}
|
|
if not isinstance(overrides, dict) or set(overrides) != expected_override_keys:
|
|
raise ReleasePackageError(f"{label} has an invalid environment override set.")
|
|
for (_role, env_key, _services), record in zip(
|
|
RELEASE_IMPORT_ROLE_ENV_SERVICES, imported_images, strict=True
|
|
):
|
|
if overrides.get(env_key) != record["loaded_image_id"]:
|
|
raise ReleasePackageError(f"{label} does not bind {env_key} to its loaded image ID.")
|
|
if not IMAGE_DIGEST_RE.fullmatch(str(overrides.get("AI_PLATFORM_RELEASE_IMAGE_DIGEST", ""))):
|
|
raise ReleasePackageError(f"{label} has an invalid AI release image digest.")
|
|
|
|
command = payload.get("command_argv")
|
|
if not isinstance(command, list) or any(not isinstance(item, str) for item in command):
|
|
raise ReleasePackageError(f"{label} has an invalid command argv.")
|
|
_assert_safe_compose_command(command, operation=operation)
|
|
|
|
post_check = payload.get("post_apply_check")
|
|
expected_post_fields = {
|
|
"schema_version",
|
|
"status",
|
|
"deployment_scope",
|
|
"operation",
|
|
"target_release_id",
|
|
"compose_project_name",
|
|
"deployed_service_set",
|
|
"daemon_identity_sha256",
|
|
"container_count",
|
|
"services",
|
|
"release_binding_verified",
|
|
"post_check_payload_sha256",
|
|
}
|
|
if not isinstance(post_check, dict) or set(post_check) != expected_post_fields:
|
|
raise ReleasePackageError(f"{label} has an invalid post-apply check.")
|
|
_verify_embedded_hash(post_check, "post_check_payload_sha256", label=f"{label} post-apply check")
|
|
if (
|
|
post_check.get("schema_version") != POST_APPLY_SCHEMA
|
|
or post_check.get("status") != "verified"
|
|
or post_check.get("deployment_scope") != DEPLOYMENT_SCOPE
|
|
or post_check.get("operation") != operation
|
|
or post_check.get("target_release_id") != payload.get("target_release_id")
|
|
or post_check.get("compose_project_name") != payload.get("compose_project_name")
|
|
or post_check.get("deployed_service_set") != payload.get("deployed_service_set")
|
|
or post_check.get("daemon_identity_sha256") != daemon.get("identity_sha256")
|
|
or post_check.get("container_count") != len(payload.get("deployed_service_set", []))
|
|
or not isinstance(post_check.get("services"), list)
|
|
or len(post_check.get("services")) != len(payload.get("deployed_service_set", []))
|
|
or post_check.get("release_binding_verified") is not True
|
|
):
|
|
raise ReleasePackageError(f"{label} has an invalid post-apply check binding.")
|
|
|
|
if (
|
|
payload.get("offline") is not True
|
|
or payload.get("pull_policy") != "never"
|
|
or payload.get("build_allowed") is not False
|
|
or payload.get("database_downgrade_performed") is not False
|
|
or payload.get("edge_or_dns_change_performed") is not False
|
|
):
|
|
raise ReleasePackageError(f"{label} violates offline or destructive-action boundaries.")
|
|
result = payload.get("result")
|
|
if (
|
|
not isinstance(result, dict)
|
|
or set(result) != {"returncode", "stdout_sha256", "stderr_sha256"}
|
|
or result.get("returncode") != 0
|
|
or any(
|
|
not isinstance(result.get(field), str)
|
|
or not SHA256_RE.fullmatch(str(result.get(field)))
|
|
for field in ("stdout_sha256", "stderr_sha256")
|
|
)
|
|
):
|
|
raise ReleasePackageError(f"{label} has an invalid successful execution result.")
|
|
|
|
|
|
def _read_existing_receipt(path: Path) -> dict[str, Any]:
|
|
if path.is_symlink():
|
|
raise ReleasePackageError("Idempotency receipt must not be a symbolic link.")
|
|
payload = _read_json(path, label="idempotency receipt")
|
|
_validate_receipt_payload(payload, label="idempotency receipt")
|
|
if path.read_bytes() != canonical_json_bytes(payload) + b"\n":
|
|
raise ReleasePackageError("Existing idempotency receipt is not canonical JSON.")
|
|
return payload
|
|
|
|
|
|
def _write_exclusive_canonical_json(path: Path, payload: Mapping[str, Any]) -> None:
|
|
data = canonical_json_bytes(payload) + b"\n"
|
|
try:
|
|
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
except FileExistsError as exc:
|
|
raise ReleasePackageError(f"Refusing to overwrite existing receipt: {path}") from exc
|
|
try:
|
|
with os.fdopen(descriptor, "wb") as handle:
|
|
descriptor = -1
|
|
handle.write(data)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
_fsync_directory(path.parent)
|
|
finally:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _output_bytes(value: object) -> bytes:
|
|
if value is None:
|
|
return b""
|
|
if isinstance(value, bytes):
|
|
return value
|
|
return str(value).encode("utf-8", "replace")
|
|
|
|
|
|
def _run_release_image_imports(
|
|
*,
|
|
plan: Mapping[str, Any],
|
|
snapshot: SnapshotInputs,
|
|
runner: Callable[..., Any],
|
|
) -> tuple[bool, dict[str, Any]]:
|
|
results: list[dict[str, Any]] = []
|
|
runtime_environment = _fixed_runtime_environment()
|
|
for item in plan["docker_import_plan"]:
|
|
role = item["role"]
|
|
archive = snapshot.docker_archives[role]
|
|
if file_sha256(archive) != item["archive_sha256"]:
|
|
return False, {"stage": "archive_recheck", "role": role}
|
|
load_command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"image",
|
|
"load",
|
|
"--input",
|
|
str(archive),
|
|
]
|
|
try:
|
|
loaded = runner(
|
|
load_command,
|
|
check=False,
|
|
capture_output=True,
|
|
shell=False,
|
|
env=runtime_environment,
|
|
timeout=1800,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
return False, {"stage": "docker_load", "role": role, "error": type(exc).__name__}
|
|
loaded_stdout = _output_bytes(getattr(loaded, "stdout", b""))
|
|
loaded_stderr = _output_bytes(getattr(loaded, "stderr", b""))
|
|
if int(loaded.returncode) != 0:
|
|
return False, {
|
|
"stage": "docker_load",
|
|
"role": role,
|
|
"returncode": int(loaded.returncode),
|
|
"stdout_sha256": hashlib.sha256(loaded_stdout).hexdigest(),
|
|
"stderr_sha256": hashlib.sha256(loaded_stderr).hexdigest(),
|
|
}
|
|
tag_stdout = b""
|
|
tag_stderr = b""
|
|
if item["tag_command_argv"]:
|
|
tag_command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"image",
|
|
"tag",
|
|
item["expected_loaded_image_id"],
|
|
item["expected_repo_tag"],
|
|
]
|
|
try:
|
|
tagged = runner(
|
|
tag_command,
|
|
check=False,
|
|
capture_output=True,
|
|
shell=False,
|
|
env=runtime_environment,
|
|
timeout=60,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
return False, {
|
|
"stage": "docker_tag",
|
|
"role": role,
|
|
"error": type(exc).__name__,
|
|
}
|
|
tag_stdout = _output_bytes(getattr(tagged, "stdout", b""))
|
|
tag_stderr = _output_bytes(getattr(tagged, "stderr", b""))
|
|
if int(tagged.returncode) != 0:
|
|
return False, {
|
|
"stage": "docker_tag",
|
|
"role": role,
|
|
"returncode": int(tagged.returncode),
|
|
"stdout_sha256": hashlib.sha256(tag_stdout).hexdigest(),
|
|
"stderr_sha256": hashlib.sha256(tag_stderr).hexdigest(),
|
|
}
|
|
inspect_command = [
|
|
str(DOCKER_CLI),
|
|
"--host",
|
|
LOCAL_DOCKER_HOST,
|
|
"image",
|
|
"inspect",
|
|
"--format",
|
|
"{{.Id}}",
|
|
item["expected_repo_tag"],
|
|
]
|
|
try:
|
|
inspected = runner(
|
|
inspect_command,
|
|
check=False,
|
|
capture_output=True,
|
|
shell=False,
|
|
env=runtime_environment,
|
|
timeout=60,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
return False, {"stage": "docker_inspect", "role": role, "error": type(exc).__name__}
|
|
observed_id = _output_bytes(getattr(inspected, "stdout", b"")).decode(
|
|
"utf-8",
|
|
errors="replace",
|
|
).strip()
|
|
if int(inspected.returncode) != 0 or observed_id != item["expected_loaded_image_id"]:
|
|
return False, {
|
|
"stage": "docker_inspect",
|
|
"role": role,
|
|
"returncode": int(inspected.returncode),
|
|
"expected_loaded_image_id": item["expected_loaded_image_id"],
|
|
"observed_image_id_sha256": hashlib.sha256(
|
|
observed_id.encode("utf-8")
|
|
).hexdigest(),
|
|
}
|
|
results.append(
|
|
{
|
|
"sequence": item["sequence"],
|
|
"role": role,
|
|
"archive_sha256": item["archive_sha256"],
|
|
"loaded_image_id": observed_id,
|
|
"offline_compose_override": item["offline_compose_override"],
|
|
"load_stdout_sha256": hashlib.sha256(loaded_stdout).hexdigest(),
|
|
"load_stderr_sha256": hashlib.sha256(loaded_stderr).hexdigest(),
|
|
"tag_stdout_sha256": hashlib.sha256(tag_stdout).hexdigest(),
|
|
"tag_stderr_sha256": hashlib.sha256(tag_stderr).hexdigest(),
|
|
}
|
|
)
|
|
summary: dict[str, Any] = {
|
|
"schema_version": RELEASE_IMPORT_RESULT_SCHEMA,
|
|
"status": "verified",
|
|
"images": results,
|
|
}
|
|
summary["summary_sha256"] = canonical_sha256(summary)
|
|
return True, summary
|
|
|
|
|
|
def _validate_post_apply_result(
|
|
value: object,
|
|
*,
|
|
plan: Mapping[str, Any],
|
|
) -> dict[str, Any]:
|
|
if not isinstance(value, dict):
|
|
raise ReleasePackageError("Post-apply checker must return an object.")
|
|
_verify_embedded_hash(value, "post_check_payload_sha256", label="post-apply check")
|
|
expected = {
|
|
"schema_version": POST_APPLY_SCHEMA,
|
|
"status": "verified",
|
|
"deployment_scope": DEPLOYMENT_SCOPE,
|
|
"operation": plan["operation"],
|
|
"target_release_id": plan["target_release_id"],
|
|
"compose_project_name": plan["compose_project_name"],
|
|
"deployed_service_set": plan["deployed_service_set"],
|
|
"daemon_identity_sha256": plan["docker_daemon_identity"]["identity_sha256"],
|
|
"container_count": len(plan["deployed_service_set"]),
|
|
"release_binding_verified": True,
|
|
}
|
|
for key, expected_value in expected.items():
|
|
if value.get(key) != expected_value:
|
|
raise ReleasePackageError(f"Post-apply check has invalid {key}.")
|
|
services = value.get("services")
|
|
if not isinstance(services, list) or len(services) != len(plan["deployed_service_set"]):
|
|
raise ReleasePackageError("Post-apply check has an invalid service inventory.")
|
|
return dict(value)
|
|
|
|
|
|
def _receipt(
|
|
*,
|
|
plan: Mapping[str, Any],
|
|
snapshot: SnapshotInputs,
|
|
idempotency_key: str,
|
|
command_argv: list[str],
|
|
runtime_overrides: Mapping[str, str],
|
|
image_import_result: Mapping[str, Any],
|
|
post_apply_check: Mapping[str, Any],
|
|
snapshot_offline_gate: Mapping[str, Any],
|
|
returncode: int,
|
|
stdout: bytes,
|
|
stderr: bytes,
|
|
recorded_at: datetime,
|
|
execution_mode: str,
|
|
) -> dict[str, Any]:
|
|
if recorded_at.tzinfo is None:
|
|
raise ReleasePackageError("Receipt time must include a timezone.")
|
|
payload: dict[str, Any] = {
|
|
"schema_version": RECEIPT_SCHEMA,
|
|
"operation": plan["operation"],
|
|
"status": "backend_verified",
|
|
"execution_mode": execution_mode,
|
|
"package_schema_version": plan["package_schema_version"],
|
|
"deployment_scope": plan["deployment_scope"],
|
|
"deployed_service_set": plan["deployed_service_set"],
|
|
"separately_deployed_artifacts": plan["separately_deployed_artifacts"],
|
|
"identity_bound_image_keys": plan["identity_bound_image_keys"],
|
|
"release_completion_claimed": False,
|
|
"release_id": plan["release_id"],
|
|
"target_release_id": plan["target_release_id"],
|
|
"compose_project_name": plan["compose_project_name"],
|
|
"idempotency_key": idempotency_key,
|
|
"recorded_at": recorded_at.astimezone(UTC).isoformat().replace("+00:00", "Z"),
|
|
"plan_sha256": plan["plan_sha256"],
|
|
"package_manifest_sha256": plan["package_manifest_sha256"],
|
|
"release_manifest_sha256": plan["release_manifest_sha256"],
|
|
"checksum_manifest_sha256": plan["checksum_manifest_sha256"],
|
|
"image_identity_sha256": plan["image_identity_sha256"],
|
|
"installable_manifest_sha256": plan["installable_manifest_sha256"],
|
|
"trusted_build_receipt_sha256": plan["trusted_build_receipt_sha256"],
|
|
"infrastructure_source_set_sha256": plan["infrastructure_source_set_sha256"],
|
|
"infrastructure_bundle_sha256": plan["infrastructure_bundle_sha256"],
|
|
"third_party_materials_input_sha256": plan[
|
|
"third_party_materials_input_sha256"
|
|
],
|
|
"third_party_materials_manifest_sha256": plan[
|
|
"third_party_materials_manifest_sha256"
|
|
],
|
|
"third_party_verification_receipt_sha256": plan[
|
|
"third_party_verification_receipt_sha256"
|
|
],
|
|
"third_party_profile": plan["third_party_profile"],
|
|
"distribution_clearance_claimed": False,
|
|
"recovery_readiness_receipt_sha256": plan.get(
|
|
"recovery_readiness_receipt_sha256"
|
|
),
|
|
"backup_proof_sha256": plan["backup_proof_sha256"],
|
|
"compatibility_proof_sha256": plan["compatibility_proof_sha256"],
|
|
"docker_daemon_identity": plan["docker_daemon_identity"],
|
|
"production_preflight_summary_sha256": plan["production_preflight"][
|
|
"summary_sha256"
|
|
],
|
|
"compose_merge_gate_summary_sha256": plan["compose_merge_gate"]["summary_sha256"],
|
|
"offline_execution_gate_summary_sha256": snapshot_offline_gate["summary_sha256"],
|
|
"snapshot_payload_sha256": snapshot.manifest["snapshot_payload_sha256"],
|
|
"tls_material_binding_sha256": plan["tls_material_binding_sha256"],
|
|
"execution_env_sha256": snapshot.manifest["execution_env_sha256"],
|
|
"docker_image_import_result": dict(image_import_result),
|
|
"post_apply_check": dict(post_apply_check),
|
|
"offline": True,
|
|
"pull_policy": "never",
|
|
"build_allowed": False,
|
|
"database_downgrade_performed": False,
|
|
"edge_or_dns_change_performed": False,
|
|
"environment_overrides": dict(runtime_overrides),
|
|
"command_argv": command_argv,
|
|
"result": {
|
|
"returncode": returncode,
|
|
"stdout_sha256": hashlib.sha256(stdout).hexdigest(),
|
|
"stderr_sha256": hashlib.sha256(stderr).hexdigest(),
|
|
},
|
|
}
|
|
payload["receipt_payload_sha256"] = canonical_sha256(payload)
|
|
return payload
|
|
|
|
|
|
def _require_apply_authorization(
|
|
*,
|
|
operation: str,
|
|
confirmation: str,
|
|
idempotency_key: str | None,
|
|
) -> str:
|
|
expected = confirmation_for(operation)
|
|
if confirmation != expected:
|
|
raise ReleasePackageError(f"Apply confirmation must exactly equal: {expected}")
|
|
if idempotency_key is None or not IDEMPOTENCY_RE.fullmatch(idempotency_key):
|
|
raise ReleasePackageError("--idempotency-key is required and must use the safe identifier syntax.")
|
|
return idempotency_key
|
|
|
|
|
|
def _validate_first_release_install_readiness(
|
|
*,
|
|
package: ValidatedPackage,
|
|
expected_package_sha256: str,
|
|
evidence_file: Path | None,
|
|
keyring_file: Path | None,
|
|
trusted_root_public_key_file: Path | None,
|
|
receipt_file: Path | None,
|
|
expected_receipt_sha256: str | None,
|
|
now: datetime,
|
|
) -> tuple[dict[str, Any], str, dict[str, str]]:
|
|
inputs = {
|
|
"evidence": evidence_file,
|
|
"keyring": keyring_file,
|
|
"trusted_root_public_key": trusted_root_public_key_file,
|
|
"receipt": receipt_file,
|
|
}
|
|
if (
|
|
any(path is None for path in inputs.values())
|
|
or not isinstance(expected_receipt_sha256, str)
|
|
or not SHA256_RE.fullmatch(expected_receipt_sha256)
|
|
):
|
|
raise ReleasePackageError(
|
|
"First-release install apply requires the raw evidence, keyring, pinned root key, "
|
|
"readiness receipt, and externally approved receipt SHA-256."
|
|
)
|
|
validated_paths = {
|
|
name: _validated_file(path, label=f"first-release {name}")
|
|
for name, path in inputs.items()
|
|
if path is not None
|
|
}
|
|
actual_receipt_sha = file_sha256(validated_paths["receipt"])
|
|
if actual_receipt_sha != expected_receipt_sha256:
|
|
raise ReleasePackageError(
|
|
"First-release readiness receipt does not match its approved SHA-256."
|
|
)
|
|
module = _load_snapshot_restore_planner_module()
|
|
validator = getattr(module, "validate_install_readiness", None)
|
|
if not callable(validator):
|
|
raise ReleasePackageError(
|
|
"The frozen snapshot planner has no validated install-readiness API; apply remains closed."
|
|
)
|
|
try:
|
|
validated = validator(
|
|
package_file=package.root / PACKAGE_FILE,
|
|
expected_package_sha256=expected_package_sha256,
|
|
evidence_file=validated_paths["evidence"],
|
|
keyring_file=validated_paths["keyring"],
|
|
trusted_root_public_key_file=validated_paths["trusted_root_public_key"],
|
|
receipt_file=validated_paths["receipt"],
|
|
expected_receipt_sha256=expected_receipt_sha256,
|
|
now=now,
|
|
)
|
|
except Exception as exc:
|
|
raise ReleasePackageError(
|
|
f"First-release install readiness validation failed: {exc}"
|
|
) from exc
|
|
included = _read_json(validated_paths["receipt"], label="first-release readiness receipt")
|
|
if not isinstance(validated, Mapping) or dict(validated) != included:
|
|
raise ReleasePackageError(
|
|
"First-release readiness receipt differs from cryptographic recomputation."
|
|
)
|
|
release_binding = included.get("release_binding")
|
|
if (
|
|
included.get("result") != RECOVERY_READINESS_RESULT
|
|
or included.get("purpose") != "release_package_install_apply_gate"
|
|
or included.get("install_apply_gate") is not True
|
|
or included.get("snapshot_restore_apply_allowed") is not False
|
|
or not isinstance(release_binding, Mapping)
|
|
or release_binding.get("release_id") != package.package["release_id"]
|
|
or release_binding.get("package_sha256") != package.package_sha256
|
|
or release_binding.get("source_commit")
|
|
!= package.image_identity.get("source_commit")
|
|
or release_binding.get("image_identity_sha256") != package.image_identity_sha256
|
|
):
|
|
raise ReleasePackageError(
|
|
"First-release readiness receipt is not an install-only PASS for this package."
|
|
)
|
|
input_hashes = {name: file_sha256(path) for name, path in validated_paths.items()}
|
|
return dict(included), actual_receipt_sha, input_hashes
|
|
|
|
|
|
def _validate_readiness_runtime_binding(
|
|
*,
|
|
readiness: Mapping[str, Any],
|
|
env_values: Mapping[str, str],
|
|
env_sha256: str,
|
|
daemon_identity: Mapping[str, str],
|
|
tls_binding: Mapping[str, str],
|
|
project_name: str,
|
|
) -> None:
|
|
bindings = readiness.get("release_environment_and_tls_bindings")
|
|
target = readiness.get("target")
|
|
expected_bindings = {
|
|
"production_env_sha256": env_sha256,
|
|
"tls_certificate_sha256": tls_binding.get("certificate_sha256"),
|
|
"tls_private_key_sha256": tls_binding.get("private_key_sha256"),
|
|
"tls_trusted_ca_sha256": tls_binding.get("trusted_ca_sha256"),
|
|
}
|
|
planner = _load_snapshot_restore_planner_module()
|
|
host_identity = getattr(planner, "runtime_host_identity_sha256", None)
|
|
if not callable(host_identity):
|
|
raise ReleasePackageError(
|
|
"The frozen snapshot planner has no runtime host identity algorithm."
|
|
)
|
|
expected_target = {
|
|
"environment_id": env_values.get("YUQEI_RELEASE_ENVIRONMENT_ID"),
|
|
"environment_class": env_values.get("YUQEI_RELEASE_ENVIRONMENT_CLASS"),
|
|
"host_id": env_values.get("YUQEI_RELEASE_HOST_ID"),
|
|
"host_identity_sha256": host_identity(
|
|
env_values.get("YUQEI_RELEASE_HOST_ID"),
|
|
daemon_identity,
|
|
),
|
|
"docker_daemon_identity_sha256": daemon_identity.get("identity_sha256"),
|
|
"compose_project_name": project_name,
|
|
}
|
|
if (
|
|
not isinstance(bindings, Mapping)
|
|
or dict(bindings) != expected_bindings
|
|
or not isinstance(target, Mapping)
|
|
or dict(target) != expected_target
|
|
):
|
|
raise ReleasePackageError(
|
|
"First-release readiness receipt does not match the exact runtime "
|
|
"environment/host/daemon/TLS/project."
|
|
)
|
|
|
|
|
|
def execute(
|
|
*,
|
|
package_dir: Path,
|
|
expected_package_sha256: str,
|
|
operation: str = "verify",
|
|
env_file: Path | None = None,
|
|
apply: bool = False,
|
|
confirmation: str = "",
|
|
idempotency_key: str | None = None,
|
|
state_dir: Path = Path("/var/lib/yuqei/release-package"),
|
|
repo_root: Path = REPO_ROOT,
|
|
runner: Callable[..., Any] | None = None,
|
|
inspection_runner: Callable[..., Any] = subprocess.run,
|
|
post_apply_checker: Callable[..., Any] | None = None,
|
|
first_release_evidence_file: Path | None = None,
|
|
first_release_keyring_file: Path | None = None,
|
|
first_release_trusted_root_public_key_file: Path | None = None,
|
|
first_release_readiness_receipt_file: Path | None = None,
|
|
expected_first_release_readiness_receipt_sha256: str | None = None,
|
|
now: datetime | None = None,
|
|
) -> ExecutionOutcome:
|
|
current_time = now or datetime.now(UTC)
|
|
package = validate_release_package(
|
|
package_dir,
|
|
expected_package_sha256=expected_package_sha256,
|
|
repo_root=repo_root,
|
|
)
|
|
if operation == "verify":
|
|
if apply:
|
|
raise ReleasePackageError("The verify operation is read-only and does not accept --apply.")
|
|
return ExecutionOutcome(verification_payload(package))
|
|
if operation not in CONFIRMATIONS:
|
|
raise ReleasePackageError("Operation must be verify, install, rollback, or forward.")
|
|
_assert_operation_supported_for_recovery(package, operation=operation)
|
|
if env_file is None:
|
|
raise ReleasePackageError("--env-file is required for install, rollback, and forward plans.")
|
|
safe_key: str | None = None
|
|
readiness: dict[str, Any] | None = None
|
|
readiness_sha256: str | None = None
|
|
readiness_input_hashes: dict[str, str] = {}
|
|
execution_mode = "test_injected_runner"
|
|
if apply:
|
|
safe_key = _require_apply_authorization(
|
|
operation=operation,
|
|
confirmation=confirmation,
|
|
idempotency_key=idempotency_key,
|
|
)
|
|
if package.package_schema_version != PACKAGE_SCHEMA_V2:
|
|
raise ReleasePackageError(
|
|
"Release package v1 is permanently read-only; real or injected apply requires v2."
|
|
)
|
|
if package.recovery_strategy == RECOVERY_STRATEGY_FIRST_RELEASE:
|
|
readiness, readiness_sha256, readiness_input_hashes = (
|
|
_validate_first_release_install_readiness(
|
|
package=package,
|
|
expected_package_sha256=expected_package_sha256,
|
|
evidence_file=first_release_evidence_file,
|
|
keyring_file=first_release_keyring_file,
|
|
trusted_root_public_key_file=(
|
|
first_release_trusted_root_public_key_file
|
|
),
|
|
receipt_file=first_release_readiness_receipt_file,
|
|
expected_receipt_sha256=(
|
|
expected_first_release_readiness_receipt_sha256
|
|
),
|
|
now=current_time,
|
|
)
|
|
)
|
|
if runner is None:
|
|
raise ReleasePackageError(
|
|
"Apply requires the built-in fixed subprocess runner or an explicitly injected test runner."
|
|
)
|
|
if post_apply_checker is None:
|
|
raise ReleasePackageError(
|
|
"Injected apply tests require the fixed post-apply checker interface."
|
|
)
|
|
builtin_mode = (
|
|
runner is _fixed_subprocess_runner
|
|
and inspection_runner is _fixed_subprocess_runner
|
|
and post_apply_checker is _builtin_post_apply_checker
|
|
)
|
|
execution_mode = (
|
|
"builtin_fixed_subprocess_runner"
|
|
if builtin_mode
|
|
else "test_injected_runner"
|
|
)
|
|
if builtin_mode:
|
|
blockers = _real_apply_blockers(
|
|
operation,
|
|
recovery_strategy=package.recovery_strategy,
|
|
package_schema_version=package.package_schema_version,
|
|
infrastructure_ready=len(package.docker_import_plan) == 11,
|
|
third_party_ready=package.third_party_verification_receipt is not None,
|
|
recovery_readiness_ready=readiness is not None,
|
|
)
|
|
if blockers:
|
|
raise ReleasePackageError(
|
|
"Real apply is fail-closed until all release blockers are resolved: "
|
|
+ ", ".join(blockers)
|
|
)
|
|
if operation == "rollback":
|
|
raise ReleasePackageError(
|
|
"Rollback apply is fail-closed because no approved rollback Docker import "
|
|
"plan exists."
|
|
)
|
|
plan = build_plan(
|
|
package,
|
|
operation=operation,
|
|
env_file=env_file,
|
|
repo_root=repo_root,
|
|
inspection_runner=inspection_runner,
|
|
recovery_readiness_receipt=readiness,
|
|
recovery_readiness_receipt_sha256=readiness_sha256,
|
|
)
|
|
if readiness is not None:
|
|
_validate_readiness_runtime_binding(
|
|
readiness=readiness,
|
|
env_values=parse_env_file(Path(plan["env_file"])),
|
|
env_sha256=plan["env_file_sha256"],
|
|
daemon_identity=plan["docker_daemon_identity"],
|
|
tls_binding=plan["tls_material_binding"],
|
|
project_name=plan["compose_project_name"],
|
|
)
|
|
if not apply:
|
|
return ExecutionOutcome(plan)
|
|
assert safe_key is not None # Authorization above is required for every write path.
|
|
|
|
for name, expected_hash in readiness_input_hashes.items():
|
|
source_path = {
|
|
"evidence": first_release_evidence_file,
|
|
"keyring": first_release_keyring_file,
|
|
"trusted_root_public_key": first_release_trusted_root_public_key_file,
|
|
"receipt": first_release_readiness_receipt_file,
|
|
}[name]
|
|
if source_path is None or file_sha256(_validated_file(source_path, label=name)) != expected_hash:
|
|
raise ReleasePackageError(
|
|
"First-release readiness input changed after validation and before state write."
|
|
)
|
|
|
|
state = _validate_state_target(state_dir, package_root=package.root)
|
|
_prepare_secure_state_root(state, require_root_owner=False)
|
|
with _exclusive_lock(state):
|
|
locked_package = validate_release_package(
|
|
package_dir,
|
|
expected_package_sha256=expected_package_sha256,
|
|
repo_root=repo_root,
|
|
)
|
|
locked_plan = build_plan(
|
|
locked_package,
|
|
operation=operation,
|
|
env_file=env_file,
|
|
repo_root=repo_root,
|
|
inspection_runner=inspection_runner,
|
|
recovery_readiness_receipt=readiness,
|
|
recovery_readiness_receipt_sha256=readiness_sha256,
|
|
)
|
|
if locked_plan["plan_sha256"] != plan["plan_sha256"]:
|
|
raise ReleasePackageError("Release package, env, or execution plan changed while acquiring the lock.")
|
|
|
|
receipt_path = _receipt_path(state, safe_key)
|
|
pending_path = _pending_path(state, safe_key)
|
|
receipts_dir = receipt_path.parent
|
|
pending_dir = pending_path.parent
|
|
if receipts_dir.exists() and (receipts_dir.is_symlink() or not receipts_dir.is_dir()):
|
|
raise ReleasePackageError("Receipt directory is unsafe.")
|
|
if pending_dir.exists() and (pending_dir.is_symlink() or not pending_dir.is_dir()):
|
|
raise ReleasePackageError("Pending journal directory is unsafe.")
|
|
receipts_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
pending_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
_reject_symlink_components(receipts_dir, label="receipt directory")
|
|
_reject_symlink_components(pending_dir, label="pending journal directory")
|
|
if pending_path.exists() or pending_path.is_symlink():
|
|
pending = _read_pending_journal(pending_path)
|
|
if pending.get("idempotency_key") != safe_key:
|
|
raise ReleasePackageError("Pending reconciliation journal is bound to another key.")
|
|
return ExecutionOutcome(pending, exit_code=1, replayed=True)
|
|
if receipt_path.exists() or receipt_path.is_symlink():
|
|
existing = _read_existing_receipt(receipt_path)
|
|
if existing.get("idempotency_key") != safe_key:
|
|
raise ReleasePackageError("Idempotency receipt is bound to another key.")
|
|
if existing.get("execution_mode") != execution_mode:
|
|
raise ReleasePackageError(
|
|
"Idempotency receipt execution mode does not match the current apply mode."
|
|
)
|
|
if existing.get("plan_sha256") != plan["plan_sha256"]:
|
|
raise ReleasePackageError(
|
|
"Idempotency key is already bound to a different release operation."
|
|
)
|
|
return ExecutionOutcome(existing, exit_code=0, replayed=True)
|
|
|
|
snapshot = _snapshot_inputs(
|
|
state_dir=state,
|
|
idempotency_key=safe_key,
|
|
plan=plan,
|
|
package=locked_package,
|
|
repo_root=_validated_directory(repo_root, label="repository root"),
|
|
)
|
|
source_snapshot_evidence = _collect_runtime_evidence(
|
|
env_file=snapshot.source_env_file,
|
|
repo_root=snapshot.root,
|
|
inspection_runner=inspection_runner,
|
|
)
|
|
if source_snapshot_evidence.env_sha256 != plan["env_file_sha256"]:
|
|
raise ReleasePackageError("Source production env snapshot SHA-256 does not match the plan.")
|
|
if source_snapshot_evidence.preflight_summary != plan["production_preflight"]:
|
|
raise ReleasePackageError("Source production preflight summary changed.")
|
|
if source_snapshot_evidence.daemon_identity != plan["docker_daemon_identity"]:
|
|
raise ReleasePackageError("Docker daemon identity changed before apply.")
|
|
|
|
execution_snapshot_evidence = _collect_runtime_evidence(
|
|
env_file=snapshot.env_file,
|
|
repo_root=snapshot.root,
|
|
inspection_runner=inspection_runner,
|
|
)
|
|
if execution_snapshot_evidence.env_sha256 != snapshot.manifest["execution_env_sha256"]:
|
|
raise ReleasePackageError("Execution env snapshot SHA-256 is inconsistent.")
|
|
if execution_snapshot_evidence.preflight_summary != plan["production_preflight"]:
|
|
raise ReleasePackageError("Execution TLS snapshot does not match the production preflight.")
|
|
if execution_snapshot_evidence.daemon_identity != plan["docker_daemon_identity"]:
|
|
raise ReleasePackageError("Docker daemon identity changed while preparing execution.")
|
|
_verify_execution_snapshot(snapshot, plan)
|
|
|
|
snapshot_offline_gate = _run_offline_execution_gate(
|
|
env_file=snapshot.env_file,
|
|
repo_root=snapshot.root,
|
|
compose_files=snapshot.compose_files,
|
|
project_name=plan["compose_project_name"],
|
|
offline_image_overrides=plan["offline_image_overrides"],
|
|
inspection_runner=inspection_runner,
|
|
)
|
|
runtime_overrides = dict(plan["environment_overrides"])
|
|
runtime_overrides.update(plan["offline_image_overrides"])
|
|
runtime_environment = _fixed_runtime_environment(runtime_overrides)
|
|
command = _compose_command(
|
|
operation=operation,
|
|
project_name=plan["compose_project_name"],
|
|
env_file=snapshot.env_file,
|
|
repo_root=snapshot.root,
|
|
compose_files=snapshot.compose_files,
|
|
)
|
|
_assert_safe_compose_command(command, operation=operation)
|
|
|
|
journal = _pending_journal(
|
|
plan=plan,
|
|
idempotency_key=safe_key,
|
|
snapshot=snapshot,
|
|
command=command,
|
|
recorded_at=current_time,
|
|
)
|
|
_write_exclusive_canonical_json(pending_path, journal)
|
|
|
|
imported, import_result = _run_release_image_imports(
|
|
plan=plan,
|
|
snapshot=snapshot,
|
|
runner=runner,
|
|
)
|
|
if not imported:
|
|
return ExecutionOutcome(journal, exit_code=1)
|
|
_verify_execution_snapshot(snapshot, plan)
|
|
try:
|
|
completed = runner(
|
|
command,
|
|
check=False,
|
|
capture_output=True,
|
|
shell=False,
|
|
env=runtime_environment,
|
|
timeout=1800,
|
|
)
|
|
returncode = int(completed.returncode)
|
|
stdout = _output_bytes(getattr(completed, "stdout", b""))
|
|
stderr = _output_bytes(getattr(completed, "stderr", b""))
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
returncode = 127
|
|
stdout = b""
|
|
stderr = type(exc).__name__.encode("ascii", "replace")
|
|
if returncode != 0:
|
|
return ExecutionOutcome(journal, exit_code=1)
|
|
|
|
post_daemon_identity = _docker_daemon_identity(
|
|
runner=inspection_runner,
|
|
repo_root=snapshot.root,
|
|
)
|
|
if post_daemon_identity != plan["docker_daemon_identity"]:
|
|
return ExecutionOutcome(journal, exit_code=1)
|
|
try:
|
|
post_apply = _validate_post_apply_result(
|
|
post_apply_checker(
|
|
plan=dict(plan),
|
|
command_argv=list(command),
|
|
runtime_environment=dict(runtime_environment),
|
|
daemon_identity=dict(post_daemon_identity),
|
|
),
|
|
plan=plan,
|
|
)
|
|
except Exception:
|
|
return ExecutionOutcome(journal, exit_code=1)
|
|
receipt = _receipt(
|
|
plan=plan,
|
|
snapshot=snapshot,
|
|
idempotency_key=safe_key,
|
|
command_argv=command,
|
|
runtime_overrides=runtime_overrides,
|
|
image_import_result=import_result,
|
|
post_apply_check=post_apply,
|
|
snapshot_offline_gate=snapshot_offline_gate,
|
|
returncode=returncode,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
recorded_at=current_time,
|
|
execution_mode=execution_mode,
|
|
)
|
|
_write_exclusive_canonical_json(receipt_path, receipt)
|
|
try:
|
|
pending_path.unlink()
|
|
_fsync_directory(pending_dir)
|
|
except OSError as exc:
|
|
raise ReleasePackageError(
|
|
"Backend was verified but pending reconciliation journal could not be cleared."
|
|
) from exc
|
|
return ExecutionOutcome(receipt, exit_code=0)
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--package-dir", type=Path, required=True)
|
|
parser.add_argument(
|
|
"--expected-package-sha256",
|
|
required=True,
|
|
help="Externally approved raw SHA-256 of release-package.json.",
|
|
)
|
|
parser.add_argument(
|
|
"--operation",
|
|
choices=("verify", "install", "rollback", "forward"),
|
|
default="verify",
|
|
help="Defaults to read-only package verification; non-verify operations are plans without --apply.",
|
|
)
|
|
parser.add_argument("--env-file", type=Path, help="Absolute protected production env file.")
|
|
parser.add_argument("--state-dir", type=Path, default=Path("/var/lib/yuqei/release-package"))
|
|
parser.add_argument(
|
|
"--apply",
|
|
action="store_true",
|
|
help=(
|
|
"Request execution after authorization and all feature gates; "
|
|
"the current production gate is closed."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--confirmation",
|
|
default="",
|
|
help="Exact operation confirmation; see the dry-run plan or --help.",
|
|
)
|
|
parser.add_argument("--idempotency-key", default=None)
|
|
parser.add_argument("--first-release-evidence-file", type=Path)
|
|
parser.add_argument("--first-release-keyring-file", type=Path)
|
|
parser.add_argument("--first-release-trusted-root-public-key-file", type=Path)
|
|
parser.add_argument("--first-release-readiness-receipt-file", type=Path)
|
|
parser.add_argument("--expected-first-release-readiness-receipt-sha256")
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
try:
|
|
outcome = execute(
|
|
package_dir=args.package_dir,
|
|
expected_package_sha256=args.expected_package_sha256,
|
|
operation=args.operation,
|
|
env_file=args.env_file,
|
|
apply=args.apply,
|
|
confirmation=args.confirmation,
|
|
idempotency_key=args.idempotency_key,
|
|
state_dir=args.state_dir,
|
|
runner=_fixed_subprocess_runner if args.apply else None,
|
|
inspection_runner=_fixed_subprocess_runner,
|
|
post_apply_checker=_builtin_post_apply_checker if args.apply else None,
|
|
first_release_evidence_file=args.first_release_evidence_file,
|
|
first_release_keyring_file=args.first_release_keyring_file,
|
|
first_release_trusted_root_public_key_file=(
|
|
args.first_release_trusted_root_public_key_file
|
|
),
|
|
first_release_readiness_receipt_file=(
|
|
args.first_release_readiness_receipt_file
|
|
),
|
|
expected_first_release_readiness_receipt_sha256=(
|
|
args.expected_first_release_readiness_receipt_sha256
|
|
),
|
|
)
|
|
except (ReleasePackageError, OSError) as exc:
|
|
print(
|
|
canonical_json_bytes(
|
|
{"schema_version": RECEIPT_SCHEMA, "status": "rejected", "error": str(exc)}
|
|
).decode("utf-8"),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
print(canonical_json_bytes(outcome.payload).decode("utf-8"))
|
|
return outcome.exit_code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|