-
Notifications
You must be signed in to change notification settings - Fork 0
Render hosted production environment from image lock #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| """Render a non-secret hosted managed production environment from an image lock.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| from datetime import datetime, timezone | ||
| import hashlib | ||
| import ipaddress | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
| import re | ||
| import tempfile | ||
| from typing import Any | ||
|
|
||
| try: | ||
| from scripts.validate_hosted_managed_image_lock import validate_image_lock | ||
| except ModuleNotFoundError: # Direct execution adds scripts/ rather than repo root. | ||
| from validate_hosted_managed_image_lock import validate_image_lock | ||
|
|
||
|
|
||
| SAFE_ABSOLUTE_PATH = re.compile(r"^/[A-Za-z0-9._/-]+$") | ||
| READ_ONLY_CANARY_OPERATION = "review_status_execute" | ||
| SECRET_FILENAMES = { | ||
| "PLATFORM_MANAGED_OPERATION_TOKEN_FILE": "service-token", | ||
| "PLATFORM_MANAGED_OPERATION_DB_PASSWORD_FILE": "database-password", | ||
| "PLATFORM_MANAGED_OPERATION_DATABASE_URL_FILE": "database-url", | ||
| "PLATFORM_MANAGED_OPERATION_GITHUB_TOKEN_FILE": "github-token", | ||
| "PLATFORM_MANAGED_OPERATION_TLS_CERTIFICATE_FILE": "tls-certificate.pem", | ||
| "PLATFORM_MANAGED_OPERATION_TLS_PRIVATE_KEY_FILE": "tls-private-key.pem", | ||
| } | ||
|
|
||
|
|
||
| class ProductionEnvRenderError(RuntimeError): | ||
| """The requested production environment is unsafe or incomplete.""" | ||
|
|
||
|
|
||
| def _load_image_lock(path: Path) -> dict[str, Any]: | ||
| try: | ||
| payload = json.loads(path.read_text(encoding="utf-8")) | ||
| except (OSError, json.JSONDecodeError) as exc: | ||
| raise ProductionEnvRenderError("image lock is unavailable or invalid") from exc | ||
| if not isinstance(payload, dict): | ||
| raise ProductionEnvRenderError("image lock must be an object") | ||
| diagnostics = validate_image_lock(payload) | ||
| if diagnostics: | ||
| raise ProductionEnvRenderError( | ||
| "image lock failed validation: " + ", ".join(diagnostics) | ||
| ) | ||
| return payload | ||
|
|
||
|
|
||
| def _safe_root(value: str, *, label: str) -> Path: | ||
| if not SAFE_ABSOLUTE_PATH.fullmatch(value) or "//" in value: | ||
| raise ProductionEnvRenderError(f"{label} must be a normalized absolute path") | ||
| path = Path(value) | ||
| if path == Path("/") or any(part in {".", ".."} for part in path.parts): | ||
| raise ProductionEnvRenderError(f"{label} must be a bounded absolute path") | ||
| return path | ||
|
|
||
|
|
||
| def _roots_overlap(left: Path, right: Path) -> bool: | ||
| return left == right or left in right.parents or right in left.parents | ||
|
|
||
|
|
||
| def render_environment( | ||
| *, | ||
| image_lock: dict[str, Any], | ||
| artifact_root: str, | ||
| state_dir: str, | ||
| backup_root: str, | ||
| secret_root: str, | ||
| ingress_bind_ip: str, | ||
| ingress_port: int, | ||
| ) -> tuple[str, dict[str, Any]]: | ||
| roots = { | ||
| "artifact_root": _safe_root(artifact_root, label="artifact root"), | ||
| "state_dir": _safe_root(state_dir, label="state directory"), | ||
| "backup_root": _safe_root(backup_root, label="backup root"), | ||
| "secret_root": _safe_root(secret_root, label="secret root"), | ||
| } | ||
| labels = list(roots) | ||
| for index, label in enumerate(labels): | ||
| for other_label in labels[index + 1 :]: | ||
| if _roots_overlap(roots[label], roots[other_label]): | ||
| raise ProductionEnvRenderError( | ||
| f"{label} and {other_label} must not overlap" | ||
| ) | ||
| try: | ||
| parsed_ip = ipaddress.ip_address(ingress_bind_ip) | ||
| except ValueError as exc: | ||
| raise ProductionEnvRenderError("ingress bind IP is invalid") from exc | ||
| if parsed_ip.version != 4: | ||
| raise ProductionEnvRenderError("ingress bind IP must be IPv4") | ||
| if not 1 <= ingress_port <= 65535: | ||
| raise ProductionEnvRenderError("ingress port is outside the valid range") | ||
|
|
||
| images = image_lock["images"] | ||
| values = { | ||
| "PLATFORM_MANAGED_OPERATION_IMAGE": images["platform"]["image_ref"], | ||
| "PLATFORM_MANAGED_OPERATION_POSTGRES_IMAGE": images["postgresql"][ | ||
| "image_ref" | ||
| ], | ||
| "PLATFORM_MANAGED_OPERATION_INGRESS_IMAGE": images["ingress"]["image_ref"], | ||
| "PLATFORM_MANAGED_OPERATION_ALLOWLIST": READ_ONLY_CANARY_OPERATION, | ||
| "PLATFORM_MANAGED_OPERATION_ARTIFACT_ROOT": str(roots["artifact_root"]), | ||
| "PLATFORM_MANAGED_OPERATION_STATE_DIR": str(roots["state_dir"]), | ||
| "PLATFORM_MANAGED_OPERATION_BACKUP_ROOT": str(roots["backup_root"]), | ||
| "PLATFORM_MANAGED_OPERATION_INGRESS_BIND_IP": str(parsed_ip), | ||
| "PLATFORM_MANAGED_OPERATION_INGRESS_PORT": str(ingress_port), | ||
| } | ||
| values.update( | ||
| { | ||
| key: str(roots["secret_root"] / filename) | ||
| for key, filename in SECRET_FILENAMES.items() | ||
| } | ||
| ) | ||
| rendered = "\n".join(f"{key}={value}" for key, value in values.items()) + "\n" | ||
| report = { | ||
| "artifact_kind": "platform_hosted_managed_production_env_render_report", | ||
| "contract_ref": "platform.hosted-managed.production-env.v1", | ||
| "generated_at": datetime.now(timezone.utc).isoformat(), | ||
| "ok": True, | ||
| "summary": { | ||
| "status": "production_env_rendered", | ||
| "source_commit": image_lock["source_commit"], | ||
| "image_count": 3, | ||
| "enabled_operation_ids": [READ_ONLY_CANARY_OPERATION], | ||
| "secret_ref_count": len(SECRET_FILENAMES), | ||
| "environment_sha256": hashlib.sha256(rendered.encode("utf-8")).hexdigest(), | ||
| }, | ||
| "privacy_boundary": { | ||
| "public_safe": True, | ||
| "includes_secret_values": False, | ||
| "includes_secret_paths": False, | ||
| "includes_local_paths": False, | ||
| }, | ||
| "authority_boundary": { | ||
| "may_deploy_production": False, | ||
| "may_create_secrets": False, | ||
| "may_execute_platform": False, | ||
| "may_enqueue_operations": False, | ||
| }, | ||
| } | ||
| return rendered, report | ||
|
|
||
|
|
||
| def _write_atomic(path: Path, content: str, *, overwrite: bool) -> None: | ||
| if path.exists() and not overwrite: | ||
| raise ProductionEnvRenderError("output already exists; use --overwrite") | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) | ||
| temporary = Path(temporary_name) | ||
| try: | ||
| with os.fdopen(descriptor, "w", encoding="utf-8") as handle: | ||
| handle.write(content) | ||
| handle.flush() | ||
| os.fsync(handle.fileno()) | ||
| temporary.chmod(0o440) | ||
| temporary.replace(path) | ||
| finally: | ||
| temporary.unlink(missing_ok=True) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--image-lock", required=True) | ||
| parser.add_argument("--output", required=True) | ||
| parser.add_argument("--artifact-root", default="/srv/0al/specgraph") | ||
| parser.add_argument("--state-dir", default="/srv/0al/specspace-state") | ||
| parser.add_argument("--backup-root", default="/srv/0al/backups") | ||
| parser.add_argument("--secret-root", default="/srv/0al/secrets") | ||
| parser.add_argument("--ingress-bind-ip", default="0.0.0.0") | ||
| parser.add_argument("--ingress-port", type=int, default=443) | ||
| parser.add_argument("--overwrite", action="store_true") | ||
| args = parser.parse_args(argv) | ||
| try: | ||
| image_lock = _load_image_lock(Path(args.image_lock)) | ||
| rendered, report = render_environment( | ||
| image_lock=image_lock, | ||
| artifact_root=args.artifact_root, | ||
| state_dir=args.state_dir, | ||
| backup_root=args.backup_root, | ||
| secret_root=args.secret_root, | ||
| ingress_bind_ip=args.ingress_bind_ip, | ||
| ingress_port=args.ingress_port, | ||
| ) | ||
| _write_atomic(Path(args.output), rendered, overwrite=args.overwrite) | ||
| except ProductionEnvRenderError as exc: | ||
| report = { | ||
| "artifact_kind": "platform_hosted_managed_production_env_render_report", | ||
| "ok": False, | ||
| "summary": {"status": "blocked"}, | ||
| "diagnostics": [str(exc)], | ||
| } | ||
| print(json.dumps(report, indent=2, sort_keys=True)) | ||
| return 0 if report.get("ok") is True else 1 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from contextlib import redirect_stdout | ||
| import io | ||
| import json | ||
| from pathlib import Path | ||
| import tempfile | ||
| import unittest | ||
|
|
||
| from scripts import render_hosted_managed_production_env as renderer | ||
|
|
||
|
|
||
| def image_lock() -> dict: | ||
| digest = "a" * 64 | ||
| return { | ||
| "artifact_kind": "platform_hosted_managed_image_lock", | ||
| "schema_version": 1, | ||
| "generated_at": "2026-07-13T00:00:00+00:00", | ||
| "source_commit": "b" * 40, | ||
| "platforms": ["linux/amd64", "linux/arm64"], | ||
| "images": { | ||
| "platform": { | ||
| "image_ref": f"ghcr.io/0al-spec/platform-hosted-managed@sha256:{digest}" | ||
| }, | ||
| "postgresql": {"image_ref": f"postgres@sha256:{digest}"}, | ||
| "ingress": { | ||
| "image_ref": f"ghcr.io/0al-spec/platform-hosted-managed-ingress@sha256:{digest}", | ||
| "base_image_ref": f"caddy@sha256:{digest}", | ||
| "upstream_file_capability_removed": True, | ||
| }, | ||
| }, | ||
| "supply_chain": {"provenance_attestation": True, "sbom_attestation": True}, | ||
| "privacy_boundary": {"public_safe": True, "includes_secrets": False}, | ||
| "authority_boundary": {"may_deploy_production": False}, | ||
| } | ||
|
|
||
|
|
||
| class HostedManagedProductionEnvTests(unittest.TestCase): | ||
| def test_runbook_uses_renderer_and_current_platform_image_repository(self) -> None: | ||
| runbook = ( | ||
| Path(__file__).resolve().parents[1] | ||
| / "docs" | ||
| / "hosted-managed-operations.md" | ||
| ).read_text(encoding="utf-8") | ||
| self.assertIn("render_hosted_managed_production_env.py", runbook) | ||
| self.assertIn("ghcr.io/0al-spec/platform-hosted-managed@sha256:", runbook) | ||
| self.assertNotIn("ghcr.io/0al-spec/platform@sha256:<digest>", runbook) | ||
|
|
||
| def test_renderer_uses_validated_lock_and_read_only_canary_scope(self) -> None: | ||
| rendered, report = renderer.render_environment( | ||
| image_lock=image_lock(), | ||
| artifact_root="/srv/0al/specgraph", | ||
| state_dir="/srv/0al/specspace-state", | ||
| backup_root="/srv/0al/backups", | ||
| secret_root="/srv/0al/secrets", | ||
| ingress_bind_ip="203.0.113.10", | ||
| ingress_port=443, | ||
| ) | ||
| self.assertIn( | ||
| "PLATFORM_MANAGED_OPERATION_ALLOWLIST=review_status_execute\n", rendered | ||
| ) | ||
| self.assertIn("PLATFORM_MANAGED_OPERATION_POSTGRES_IMAGE=postgres@sha256:", rendered) | ||
| self.assertIn( | ||
| "PLATFORM_MANAGED_OPERATION_TOKEN_FILE=/srv/0al/secrets/service-token", | ||
| rendered, | ||
| ) | ||
| self.assertNotIn("token-value", rendered) | ||
| self.assertEqual(report["summary"]["enabled_operation_ids"], ["review_status_execute"]) | ||
| self.assertFalse(report["authority_boundary"]["may_deploy_production"]) | ||
|
|
||
| def test_renderer_rejects_overlapping_roots_and_invalid_bind_address(self) -> None: | ||
| arguments = { | ||
| "image_lock": image_lock(), | ||
| "artifact_root": "/srv/0al/specgraph", | ||
| "state_dir": "/srv/0al/specgraph/state", | ||
| "backup_root": "/srv/0al/backups", | ||
| "secret_root": "/srv/0al/secrets", | ||
| "ingress_bind_ip": "managed.example.org", | ||
| "ingress_port": 443, | ||
| } | ||
| with self.assertRaisesRegex(renderer.ProductionEnvRenderError, "must not overlap"): | ||
| renderer.render_environment(**arguments) | ||
| arguments["state_dir"] = "/srv/0al/specspace-state" | ||
| with self.assertRaisesRegex(renderer.ProductionEnvRenderError, "bind IP is invalid"): | ||
| renderer.render_environment(**arguments) | ||
|
|
||
| def test_invalid_lock_does_not_replace_existing_environment(self) -> None: | ||
| with tempfile.TemporaryDirectory() as temp_dir: | ||
| root = Path(temp_dir) | ||
| lock_path = root / "image-lock.json" | ||
| lock_path.write_text(json.dumps({"artifact_kind": "invalid"}), encoding="utf-8") | ||
| output = root / "production.env" | ||
| output.write_text("existing=true\n", encoding="utf-8") | ||
| with redirect_stdout(io.StringIO()): | ||
| result = renderer.main( | ||
| [ | ||
| "--image-lock", | ||
| str(lock_path), | ||
| "--output", | ||
| str(output), | ||
| "--overwrite", | ||
| ] | ||
| ) | ||
| self.assertEqual(result, 1) | ||
| self.assertEqual(output.read_text(encoding="utf-8"), "existing=true\n") | ||
|
|
||
| def test_atomic_output_is_group_read_only(self) -> None: | ||
| with tempfile.TemporaryDirectory() as temp_dir: | ||
| output = Path(temp_dir) / "production.env" | ||
| renderer._write_atomic(output, "KEY=value\n", overwrite=False) | ||
| self.assertEqual(output.stat().st_mode & 0o777, 0o440) | ||
| with self.assertRaisesRegex(renderer.ProductionEnvRenderError, "already exists"): | ||
| renderer._write_atomic(output, "OTHER=value\n", overwrite=False) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When operators follow this new renderer path, the file is written to
/etc/0al/hosted-managed-production.env, but the later Start and probe commands still invokedocker composeand preflight from the process environment, and I found no--env-fileor source step for this output. Docker Compose documents non-default environment files as being selected with--env-file, so following the runbook as written leaves the requiredPLATFORM_MANAGED_OPERATION_*variables unset unless the operator manually infers an extra load step.Useful? React with 👍 / 👎.