Files
aios/services/aios-autonomous-control-plane/aios_autonomous_control_plane/candidate_fixture.py
T

254 lines
9.7 KiB
Python

from __future__ import annotations
import argparse
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import sys
from typing import Any, Mapping
from .common import atomic_write_json, require_oid, sha256_file
class CandidateFixtureError(RuntimeError):
pass
FORBIDDEN_ROOTS = (
Path("/var/lib/aios-gitops/release"),
Path("/var/lib/aios-gitops/artifacts"),
Path("/opt/aios"),
Path("/data/aios-gitops/runtime/releases"),
)
@dataclass(frozen=True)
class CandidateFixtureConfig:
root: Path
host: str
port: int
repository: str
version: str
release_branch: str
expected_base_oid: str
target_oid: str
target_tree_oid: str
tag_oid: str
approval_plan_id: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "CandidateFixtureConfig":
required = {
"root", "host", "port", "repository", "version", "release_branch",
"expected_base_oid", "target_oid", "target_tree_oid", "tag_oid",
"approval_plan_id",
}
if set(value) != required:
raise CandidateFixtureError("CANDIDATE_FIXTURE_SCHEMA_INVALID")
root = Path(value["root"])
if not root.is_absolute() or root.is_symlink():
raise CandidateFixtureError("CANDIDATE_ROOT_INVALID")
resolved = root.resolve()
if any(resolved == denied or denied in resolved.parents for denied in FORBIDDEN_ROOTS):
raise CandidateFixtureError("PRODUCTION_ROOT_DENIED")
if value["host"] != "127.0.0.1" or int(value["port"]) != 8080:
raise CandidateFixtureError("CANDIDATE_ENDPOINT_INVALID")
if value["version"] != "V2R1C4":
raise CandidateFixtureError("CANDIDATE_VERSION_INVALID")
for key in ("expected_base_oid", "target_oid", "target_tree_oid", "tag_oid"):
require_oid(value[key], key)
for key in ("repository", "release_branch", "approval_plan_id"):
if not isinstance(value[key], str) or not value[key]:
raise CandidateFixtureError("CANDIDATE_BINDING_INVALID")
return cls(
root=root, host=value["host"], port=int(value["port"]),
repository=value["repository"], version=value["version"],
release_branch=value["release_branch"],
expected_base_oid=value["expected_base_oid"],
target_oid=value["target_oid"], target_tree_oid=value["target_tree_oid"],
tag_oid=value["tag_oid"], approval_plan_id=value["approval_plan_id"],
)
class CandidateIntegrationFixture:
def __init__(self, config: CandidateFixtureConfig):
self.config = config
def initialize(self) -> dict[str, Any]:
root = self.config.root
paths = {
"registry": root / "registry" / "V2R1C4.json",
"baseline": root / "baseline" / "CANDIDATE.json",
"artifact": root / "artifacts" / "V2R1C4.definition.json",
"binding": root / "releases" / "V2R1C4" / "binding.json",
}
for path in paths.values():
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
binding = {
"approval_plan_id": self.config.approval_plan_id,
"candidate_only": True,
"expected_base_oid": self.config.expected_base_oid,
"production_deployed": False,
"release_branch": self.config.release_branch,
"repository": self.config.repository,
"target_oid": self.config.target_oid,
"target_tree_oid": self.config.target_tree_oid,
"version": self.config.version,
}
atomic_write_json(paths["binding"], binding)
binding_sha256 = sha256_file(paths["binding"])
atomic_write_json(paths["artifact"], {
"binding_sha256": binding_sha256,
"candidate_only": True,
"production_artifact": False,
"target_oid": self.config.target_oid,
"target_tree_oid": self.config.target_tree_oid,
"version": self.config.version,
})
artifact_sha256 = sha256_file(paths["artifact"])
atomic_write_json(paths["registry"], {
"artifact_definition_sha256": artifact_sha256,
"candidate_only": True,
"status": "ISOLATED_CANDIDATE",
"target_oid": self.config.target_oid,
"version": self.config.version,
})
atomic_write_json(paths["baseline"], {
"candidate_only": True,
"expected_base_oid": self.config.expected_base_oid,
"production_baseline_modified": False,
"target_oid": self.config.target_oid,
"version": self.config.version,
})
return {
"artifact_definition_sha256": artifact_sha256,
"binding_sha256": binding_sha256,
"candidate_only": True,
"paths": {key: str(value) for key, value in sorted(paths.items())},
}
def merge(self, request: Mapping[str, Any]) -> dict[str, Any]:
required = {
"operation", "repository", "release_branch", "source_oid",
"target_oid", "expected_base_oid", "approval_plan_id",
}
if set(request) != required:
raise CandidateFixtureError("MERGE_REQUEST_SCHEMA_INVALID")
expected = {
"operation": "candidate_merge",
"repository": self.config.repository,
"release_branch": self.config.release_branch,
"source_oid": self.config.target_oid,
"target_oid": self.config.target_oid,
"expected_base_oid": self.config.expected_base_oid,
"approval_plan_id": self.config.approval_plan_id,
}
if dict(request) != expected:
raise CandidateFixtureError("MERGE_BINDING_MISMATCH")
return {
"candidate_only": True,
"release_oid": self.config.target_oid,
"target_tree_oid": self.config.target_tree_oid,
}
def tag(self, request: Mapping[str, Any]) -> dict[str, Any]:
expected = {
"approval_plan_id": self.config.approval_plan_id,
"execute": False,
"operation": "candidate_tag_plan",
"target_oid": self.config.target_oid,
"target_tree_oid": self.config.target_tree_oid,
"version": self.config.version,
}
if dict(request) != expected:
raise CandidateFixtureError("TAG_BINDING_MISMATCH")
return {
"candidate_only": True,
"execute": False,
"tag_oid": self.config.tag_oid,
"tag_target_oid": self.config.target_oid,
"version": self.config.version,
}
def _handler(fixture: CandidateIntegrationFixture):
class Handler(BaseHTTPRequestHandler):
server_version = "AIOSCandidateFixture/1"
def _json(self, status: int, value: Mapping[str, Any]):
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def do_GET(self):
if self.path != "/health":
self._json(404, {"status": "NOT_FOUND"})
return
self._json(200, {
"candidate_only": True,
"production_deployed": False,
"status": "ok",
"target_oid": fixture.config.target_oid,
"version": fixture.config.version,
})
def do_POST(self):
if self.path != "/api/create-tag":
self._json(404, {"status": "NOT_FOUND"})
return
try:
length = int(self.headers.get("Content-Length", "0"))
if length < 2 or length > 65536:
raise CandidateFixtureError("TAG_REQUEST_SIZE_INVALID")
value = json.loads(self.rfile.read(length))
if not isinstance(value, dict):
raise CandidateFixtureError("TAG_REQUEST_INVALID")
self._json(200, fixture.tag(value))
except (ValueError, json.JSONDecodeError, CandidateFixtureError) as exc:
self._json(409, {"error": str(exc), "status": "FAILED_CLOSED"})
def log_message(self, fmt, *args):
return
return Handler
def _read_config(path: Path) -> CandidateFixtureConfig:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise CandidateFixtureError("CANDIDATE_FIXTURE_SCHEMA_INVALID")
return CandidateFixtureConfig.from_mapping(value)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--merge-stdio", action="store_true")
args = parser.parse_args(argv)
fixture = CandidateIntegrationFixture(_read_config(args.config))
fixture.initialize()
if args.merge_stdio:
raw = sys.stdin.buffer.readline(1048577)
if not raw or len(raw) > 1048576:
raise CandidateFixtureError("MERGE_REQUEST_SIZE_INVALID")
value = json.loads(raw)
if not isinstance(value, dict):
raise CandidateFixtureError("MERGE_REQUEST_INVALID")
sys.stdout.write(json.dumps(fixture.merge(value), sort_keys=True) + "\n")
return 0
server = ThreadingHTTPServer(
(fixture.config.host, fixture.config.port), _handler(fixture))
try:
server.serve_forever()
finally:
server.server_close()
return 0
if __name__ == "__main__":
raise SystemExit(main())