Files
aios/services/aios-release-controller/aios_release_controller/manifest.py
T

70 lines
7.4 KiB
Python

from __future__ import annotations
import hashlib,json,re,tarfile
from release_version import RELEASE_VERSION_RE
from pathlib import Path
from typing import Any
OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=RELEASE_VERSION_RE
REQUIRED_SOURCE={"schema","version","repository","base_ref","base_oid","binding_mode","release_readiness","production_deployed","tag_created","baseline_changed","production_service","production_port","rollback_metadata","controller_schema","required_checks"}
REQUEST_KEYS={"schema","version","repository","base_oid","target_oid","artifact_uri","artifact_sha256","artifact_size","evidence_closure_oid","approval_plan_id","execute","production_deployed","production_service","production_port","rollback_metadata","current_tag_oid","source_manifest_sha256"}
class ManifestValidationError(ValueError):
def __init__(self,code:str,message:str): super().__init__(message); self.code=code
def _canonical(value:Any)->bytes: return (json.dumps(value,sort_keys=True,separators=(",",":"))+"\n").encode()
def sha256_bytes(value:bytes)->str: return hashlib.sha256(value).hexdigest()
def sha256_file(path:Path)->str: return sha256_bytes(path.read_bytes())
def _json_object(path:Path)->dict[str,Any]:
try: value=json.loads(path.read_text(encoding="utf-8"))
except (OSError,ValueError) as exc: raise ManifestValidationError("INVALID_JSON",str(exc)) from exc
if not isinstance(value,dict): raise ManifestValidationError("INVALID_JSON","expected object")
return value
def _oid(value:Any,name:str)->str:
if not isinstance(value,str) or not OID_RE.fullmatch(value): raise ManifestValidationError("INVALID_OID",name)
return value
def _sha(value:Any,name:str)->str:
if not isinstance(value,str) or not SHA_RE.fullmatch(value): raise ManifestValidationError("INVALID_SHA256",name)
return value
def validate_deployment_state(state:str,previous:str|None=None)->str:
if state not in {"STAGED","ACTIVE","ROLLED_BACK"}: raise ManifestValidationError("DEPLOYMENT_STATE","unsupported state")
if previous=="ACTIVE" and state=="STAGED": raise ManifestValidationError("DEPLOYMENT_STATE_TRANSITION","active cannot return to staged")
return state
def validate_source_manifest(path:Path,expected_base_oid:str)->dict[str,Any]:
data=_json_object(path)
if not REQUIRED_SOURCE <= set(data): raise ManifestValidationError("SOURCE_SCHEMA_KEYS","required manifest keys missing")
if data["schema"]!="aios.release-source.v2": raise ManifestValidationError("SOURCE_SCHEMA","unsupported source schema")
if not VERSION_RE.fullmatch(str(data["version"])) or data["repository"]!="aios/aios": raise ManifestValidationError("SOURCE_IDENTITY","unsupported version/repository")
if data["base_ref"]!="refs/heads/main": raise ManifestValidationError("SOURCE_BASE_REF","base ref mismatch")
if _oid(data["base_oid"],"base_oid")!=expected_base_oid: raise ManifestValidationError("BASE_OID_MISMATCH","authorized base mismatch")
if data["binding_mode"]!="external_exact_oid": raise ManifestValidationError("BINDING_MODE","exact OID required")
if data["release_readiness"] not in {"READY","ENGINEERING_READY"}: raise ManifestValidationError("SOURCE_NOT_READY","source not ready")
if any(data[k] is not False for k in ("production_deployed","tag_created","baseline_changed")): raise ManifestValidationError("PRODUCTION_FLAG_SET","production mutation flag set")
if not isinstance(data["production_service"],str) or not data["production_service"] or not isinstance(data["production_port"],int) or not 1024<=data["production_port"]<=65535: raise ManifestValidationError("SERVICE_IDENTITY","service binding invalid")
if not isinstance(data["required_checks"],list) or not {"source_manifest","deterministic_artifact","health_probe","rollback"} <= set(data["required_checks"]): raise ManifestValidationError("REQUIRED_CHECKS","required checks incomplete")
return data
def _safe_member(name:str)->bool:
p=Path(name); return not p.is_absolute() and ".." not in p.parts and not name.startswith("/")
def verify_deployable_artifact(path:Path,required_members:set[str])->list[str]:
if not path.is_file(): raise ManifestValidationError("ARTIFACT_MISSING",str(path))
try:
with tarfile.open(path,mode="r:*") as archive:
members=archive.getmembers()
if any(not _safe_member(m.name) or m.issym() or m.islnk() or not (m.isfile() or m.isdir()) for m in members): raise ManifestValidationError("ARTIFACT_MEMBER","unsafe member")
names={m.name for m in members}
except (OSError,tarfile.TarError) as exc: raise ManifestValidationError("ARTIFACT_FORMAT",str(exc)) from exc
missing=sorted(required_members-names)
if missing: raise ManifestValidationError("ARTIFACT_CONTENT",",".join(missing))
return sorted(names)
def validate_release_request(source_manifest:dict[str,Any],request_path:Path,expected_main_oid:str|None=None)->dict[str,Any]:
request=_json_object(request_path)
if set(request)!=REQUEST_KEYS: raise ManifestValidationError("REQUEST_SCHEMA_KEYS","request keys mismatch")
if request["schema"]!="aios.release-request.v2" or request["version"]!=source_manifest["version"] or request["repository"]!="aios/aios": raise ManifestValidationError("REQUEST_IDENTITY","request identity mismatch")
if request["base_oid"]!=source_manifest["base_oid"]: raise ManifestValidationError("REQUEST_BASE_OID","base differs")
_oid(request["target_oid"],"target_oid")
if expected_main_oid is not None and request["target_oid"]!=expected_main_oid: raise ManifestValidationError("TARGET_OID_MISMATCH","target differs")
_sha(request["artifact_sha256"],"artifact_sha256"); _oid(request["evidence_closure_oid"],"evidence_closure_oid")
if not isinstance(request["approval_plan_id"],str) or not request["approval_plan_id"].startswith("sha256:"): raise ManifestValidationError("APPROVAL_PLAN","invalid approval plan")
if request["execute"] is not False or request["production_deployed"] is not False: raise ManifestValidationError("EXECUTION_NOT_ALLOWED","must be non-executing")
if request["production_service"]!=source_manifest["production_service"] or request["production_port"]!=source_manifest["production_port"]: raise ManifestValidationError("SERVICE_IDENTITY","request service mismatch")
if not isinstance(request["current_tag_oid"],str) or request["current_tag_oid"] not in {"V2R1C7","82792347937c0df59a8e02d6162775252bcad2e2"}: raise ManifestValidationError("PROTECTED_TAG","previous production tag binding changed")
_sha(request["source_manifest_sha256"],"source_manifest_sha256"); return request
def build_approval_request(*,source_manifest_path:Path,target_oid:str,artifact_uri:str,artifact_sha256:str,artifact_size:int,evidence_closure_oid:str,rollback_metadata:str,current_tag_oid:str)->dict[str,Any]:
source=_json_object(source_manifest_path); _oid(target_oid,"target_oid"); _sha(artifact_sha256,"artifact_sha256"); _oid(evidence_closure_oid,"evidence_closure_oid"); request={"schema":"aios.release-request.v2","version":source["version"],"repository":source["repository"],"base_oid":source["base_oid"],"target_oid":target_oid,"artifact_uri":artifact_uri,"artifact_sha256":artifact_sha256,"artifact_size":artifact_size,"evidence_closure_oid":evidence_closure_oid,"execute":False,"production_deployed":False,"production_service":source["production_service"],"production_port":source["production_port"],"rollback_metadata":rollback_metadata,"current_tag_oid":current_tag_oid,"source_manifest_sha256":sha256_file(source_manifest_path)}; request["approval_plan_id"]="sha256:"+sha256_bytes(_canonical(request)); return request