feat(device-plane): add fail-closed deploy foundation

This commit is contained in:
Codex
2026-07-25 21:29:05 +03:00
parent e9e03143cd
commit e217723784
36 changed files with 3729 additions and 0 deletions
+408
View File
@@ -37,6 +37,21 @@ MAP_GATEWAY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-gateway-admin-secret"
MAP_EGRESS_PROXY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-egress-proxy-token"
PROXY_CONTUR_ENV_FILE = Path("/volume1/docker/proxy-contur/.env")
DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime")
DEVICE_PLANE_ROOT = Path("/volume1/docker/nodedc-device-plane")
DEVICE_PLANE_SECRET_DIR = DEVICE_PLANE_ROOT / "secrets"
DEVICE_PLANE_POSTGRES_PASSWORD_FILE = DEVICE_PLANE_SECRET_DIR / "postgres-password"
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE = DEVICE_PLANE_SECRET_DIR / "gateway-core-token"
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE = DEVICE_PLANE_SECRET_DIR / "identifier-pepper"
DEVICE_PLANE_CONTROL_CORE_IMAGE = "nodedc/device-control-core:local"
DEVICE_PLANE_GATEWAY_IMAGE = "nodedc/device-gateway:local"
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL = (
"deployment/device-postgres-bootstrap-v1.json"
)
DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES = (
"docker-compose.device-plane.yml",
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
)
DEVICE_PLANE_POSTGRES_VOLUME = "nodedc-device-plane-postgres-data"
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-provisioner"
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "token"
ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "engine-credential-provisioner-ed25519.pem"
@@ -1140,6 +1155,19 @@ COMPONENTS = {
"http://172.22.0.222:9920/healthz",
),
},
"device-plane": {
"payload_root": DEVICE_PLANE_ROOT,
"compose_root": DEVICE_PLANE_ROOT,
"compose_project": "nodedc-device-plane",
"compose_files": (
DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml",
),
"bootstrap_root": True,
"compose_no_deps": True,
# PostgreSQL is durable state infrastructure. Normal application
# overlays can rebuild/recreate only these two stateless services.
"services": ("device-control-core", "device-gateway"),
},
"proxy-contur": {
"payload_root": Path("/volume1/docker/proxy-contur"),
"compose_root": Path("/volume1/docker/proxy-contur"),
@@ -2297,6 +2325,12 @@ def denied_payload_path(component, rel):
"infra/docker-compose.module-foundry.yml",
):
pass
elif component == "device-plane" and rel in (
"docker-compose.device-plane.yml",
"services/device-control-core/Dockerfile",
"services/device-gateway/Dockerfile",
):
pass
elif component == "proxy-contur" and rel in (
"Dockerfile",
"docker-compose.yml",
@@ -2426,6 +2460,25 @@ def denied_payload_path(component, rel):
return "module-foundry env file"
if rel.startswith(("Dockerfile.bak", "infra/docker-compose.module-foundry.yml.bak")):
return "module-foundry backup file"
elif component == "device-plane":
runtime_prefixes = (
"runtime",
"secrets",
"data",
"logs",
"backups",
"node_modules",
)
if rel == ".env" or rel.startswith(".env."):
return "device-plane env file"
if "test" in parts:
return "device-plane test path"
if rel.startswith((
"docker-compose.device-plane.yml.bak",
"services/device-control-core/Dockerfile.bak",
"services/device-gateway/Dockerfile.bak",
)):
return "device-plane backup file"
elif component == "n8n-private-extension":
# An extension release is inert data at this boundary. Activation is
# Engine-owned and must never be smuggled into the artifact as a script,
@@ -2676,6 +2729,27 @@ def allowed_payload_path(component, rel):
)):
return True
if component == "device-plane":
if rel in (
".dockerignore",
"package.json",
"package-lock.json",
"docker-compose.device-plane.yml",
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
"packages/device-protocol-contract",
"packages/arusnavi-b2-adapter",
"services/device-control-core",
"services/device-gateway",
):
return True
if rel.startswith((
"packages/device-protocol-contract/",
"packages/arusnavi-b2-adapter/",
"services/device-control-core/",
"services/device-gateway/",
)):
return True
if component == "n8n-private-extension":
parts = PurePosixPath(rel).parts
if (
@@ -7681,6 +7755,11 @@ def load_artifact(artifact, work_dir):
die(f"files.txt entry missing in payload: {rel}")
validate_payload_tree(manifest["component"], payload_dir, entries)
if is_device_plane_postgres_bootstrap_slice(
manifest["component"],
entries,
):
validate_device_plane_postgres_bootstrap_payload(payload_dir)
if manifest["component"] == "n8n-private-extension":
validate_n8n_private_extension_release(payload_dir, entries)
if manifest["component"] == "engine":
@@ -7972,6 +8051,79 @@ def component_root(component):
return COMPONENTS[component]["payload_root"]
def is_device_plane_postgres_bootstrap_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES
)
def validate_device_plane_postgres_bootstrap_payload(payload_dir):
descriptor = read_strict_json(
payload_dir / DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
"Device Plane PostgreSQL bootstrap descriptor",
max_bytes=8 * 1024,
)
expected = {
"schemaVersion": "nodedc.device-plane.postgres-bootstrap.v1",
"service": "device-postgres",
"volume": DEVICE_PLANE_POSTGRES_VOLUME,
"mode": "create-if-absent",
"ordinaryApplicationSelection": "forbidden",
"rollbackVolumePolicy": "preserve",
}
if descriptor != expected:
die("Device Plane PostgreSQL bootstrap descriptor mismatch")
return descriptor
def preflight_device_plane_postgres_bootstrap():
container_result = subprocess.run(
[
str(DOCKER),
"container",
"ls",
"-a",
"--filter",
"label=com.docker.compose.project=nodedc-device-plane",
"--filter",
"label=com.docker.compose.service=device-postgres",
"--format",
"{{.ID}}",
],
check=False,
capture_output=True,
text=True,
)
if container_result.returncode != 0:
die("Device Plane PostgreSQL container preflight failed")
container_ids = [
line.strip()
for line in container_result.stdout.splitlines()
if line.strip()
]
if container_ids:
die("Device Plane PostgreSQL container already exists")
volume_result = subprocess.run(
[
str(DOCKER),
"volume",
"inspect",
DEVICE_PLANE_POSTGRES_VOLUME,
],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if volume_result.returncode == 0:
die("Device Plane PostgreSQL volume already exists")
if volume_result.returncode != 1:
die("Device Plane PostgreSQL volume preflight failed")
return "absent"
def component_compose_root(component):
return COMPONENTS[component].get("compose_root", component_root(component))
@@ -9491,6 +9643,13 @@ def is_platform_provider_catalog_only(entries):
def component_services(component, entries=None):
if is_device_plane_postgres_bootstrap_slice(component, entries):
# This exact one-time transition is the only Device Plane artifact that
# may select durable state. Its preflight requires both container and
# named volume to be absent, so --force-recreate cannot touch an
# installed database.
return ("device-postgres",)
if is_engine_l2_closed_loop_slice(component, entries):
# The failed 030 apply published both the backend source and the built
# UI before Compose rejected the descriptor/source mismatch. The exact
@@ -9549,6 +9708,45 @@ def component_services(component, entries=None):
# authorization normalization inside the already-active backend.
return ("nodedc-backend",)
if component == "device-plane" and entries is not None:
selected = []
def add(*services):
for service in services:
if service not in selected:
selected.append(service)
touches_common = any(
rel in (
".dockerignore",
"package.json",
"package-lock.json",
"docker-compose.device-plane.yml",
"packages/device-protocol-contract",
"packages/arusnavi-b2-adapter",
)
or rel.startswith((
"packages/device-protocol-contract/",
"packages/arusnavi-b2-adapter/",
))
for rel in entries
)
touches_core = any(
rel == "services/device-control-core"
or rel.startswith("services/device-control-core/")
for rel in entries
)
touches_gateway = any(
rel == "services/device-gateway"
or rel.startswith("services/device-gateway/")
for rel in entries
)
if touches_common or touches_core:
add("device-control-core")
if touches_common or touches_gateway:
add("device-gateway")
return tuple(selected)
if component == "dc-cms" and entries is not None:
selected = []
@@ -9783,6 +9981,42 @@ def component_build_args(component, entries=None):
def component_builds(component, entries=None):
if is_device_plane_postgres_bootstrap_slice(component, entries):
return ()
if component == "device-plane" and entries is not None:
selected_services = component_services(component, entries)
builds = []
if "device-control-core" in selected_services:
builds.append((
DEVICE_PLANE_ROOT,
(
"build",
"--no-cache",
"--network=host",
"-f",
"services/device-control-core/Dockerfile",
"-t",
DEVICE_PLANE_CONTROL_CORE_IMAGE,
".",
),
))
if "device-gateway" in selected_services:
builds.append((
DEVICE_PLANE_ROOT,
(
"build",
"--no-cache",
"--network=host",
"-f",
"services/device-gateway/Dockerfile",
"-t",
DEVICE_PLANE_GATEWAY_IMAGE,
".",
),
))
return tuple(builds)
if component == "platform" and entries is not None:
touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries)
touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries)
@@ -11739,6 +11973,7 @@ def plan_artifact(artifact):
mcp_registered_execution_profiles_preflight = None
mcp_gelios_units_items_preflight = None
provider_catalog_preflight = None
device_plane_postgres_preflight = None
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
reject_terminal_engine_l2_failed_artifact(manifest, sha)
@@ -11875,6 +12110,13 @@ def plan_artifact(artifact):
)
if is_engine_provider_security_catalog_slice(manifest["component"], entries):
provider_catalog_preflight = preflight_engine_provider_security_catalog_predecessor()
if is_device_plane_postgres_bootstrap_slice(
manifest["component"],
entries,
):
device_plane_postgres_preflight = (
preflight_device_plane_postgres_bootstrap()
)
component = manifest["component"]
root = component_root(component)
@@ -13246,6 +13488,22 @@ def plan_artifact(artifact):
print(f"runtime_grants=runner-managed:{FOUNDRY_BINDING_GRANTS_DIR}")
print(f"runtime_private_key=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}")
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
if component == "device-plane":
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_POSTGRES_PASSWORD_FILE}")
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE}")
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_IDENTIFIER_PEPPER_FILE}")
print("device_postgres=preserved-prerequisite:not-selected")
print("device_postgres_volume=preserved:nodedc-device-plane-postgres-data")
print("device_gateway_public_ingress=disabled")
print("device_gateway_command_transport=disabled")
print("gelios=untouched")
if device_plane_postgres_preflight is not None:
print(
"device_postgres_bootstrap="
f"{device_plane_postgres_preflight}"
)
print("device_postgres_bootstrap_mode=create-if-absent")
print("device_postgres_rollback_volume=preserve")
if touches_external_data_plane:
print(f"runtime_secret=runner-managed:{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
@@ -13527,6 +13785,71 @@ def rollback_platform_apply(root, backup_dir, entries, current_stamp, runtime_st
return f"source+runtime-restored:{restored_count}"
def rollback_device_plane_apply(
root,
backup_dir,
entries,
current_stamp,
runtime_started,
applied_services,
):
existing = read_backup_path_list(backup_dir / "existing-files.txt")
missing = read_backup_path_list(backup_dir / "missing-files.txt")
existing_set, _missing_set = validate_backup_partition(
entries,
existing,
missing,
"Device Plane runtime rollback",
)
baseline_entries = [rel for rel in entries if rel in existing_set]
compose_was_installed = "docker-compose.device-plane.yml" in existing_set
baseline_services = (
component_services("device-plane", baseline_entries)
if compose_was_installed
else ()
)
candidate_only_services = tuple(
service
for service in applied_services
if service not in baseline_services
)
candidate_cleanup_failed = False
if runtime_started and candidate_only_services:
# Remove only candidate services while the candidate Compose file is
# still present. PostgreSQL can appear only in the exact one-time
# bootstrap slice; volume flags are deliberately never used.
try:
stop_and_remove_compose_services(
"device-plane",
candidate_only_services,
)
except Exception:
candidate_cleanup_failed = True
restored_count = restore_platform_overlay(
root,
backup_dir,
entries,
current_stamp,
)
if candidate_cleanup_failed:
die("Device Plane candidate-only runtime cleanup failed after source restore")
if not runtime_started or not baseline_services:
return f"source-restored-runtime-unchanged:{restored_count}"
run_component_runtime(
"device-plane",
baseline_entries,
baseline_services,
)
run_healthchecks(
"device-plane",
baseline_entries,
baseline_services,
)
return f"source+runtime-restored:{restored_count}"
def rollback_engine_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services):
existing = read_backup_path_list(backup_dir / "existing-files.txt")
missing = read_backup_path_list(backup_dir / "missing-files.txt")
@@ -14130,6 +14453,24 @@ def prepare_component_runtime(component, entries=None):
)
return
if component == "device-plane":
ensure_platform_runtime_secret(
DEVICE_PLANE_POSTGRES_PASSWORD_FILE,
MAP_GATEWAY_SECRET_RE,
"device plane PostgreSQL",
)
ensure_platform_runtime_secret(
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE,
MAP_GATEWAY_SECRET_RE,
"device plane Gateway to Core",
)
ensure_platform_runtime_secret(
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE,
MAP_GATEWAY_SECRET_RE,
"device plane identifier pepper",
)
return
if component == "proxy-contur":
sync_map_egress_proxy_secret()
return
@@ -14309,6 +14650,8 @@ def module_foundry_healthcheck():
def component_healthchecks(component, entries=None, services=None):
if is_device_plane_postgres_bootstrap_slice(component, entries):
return ()
if is_engine_n8n_transition(component, entries):
# The transition does not restart the Engine UI/backend generation.
# Its own acceptance below verifies n8n readiness, image, mount, logs,
@@ -14323,6 +14666,36 @@ def component_healthchecks(component, entries=None, services=None):
return ()
if component == "module-foundry":
return (module_foundry_healthcheck(),)
if component == "device-plane":
selected_services = (
tuple(services)
if services is not None
else component_services(component, entries)
)
checks = []
if "device-control-core" in selected_services:
checks.append({
"url": "http://127.0.0.1:18120/healthz",
"expected_json": {
"ok": True,
"service": "nodedc-device-control-core",
"database": "ready",
"discoveryIngest": "disabled",
"commandTransport": "disabled",
},
})
if "device-gateway" in selected_services:
checks.append({
"url": "http://127.0.0.1:18121/healthz",
"expected_json": {
"ok": True,
"service": "nodedc-device-gateway",
"tcpListener": "disabled",
"publicIngress": "disabled",
"commandTransport": "disabled",
},
})
return tuple(checks)
if (
(
is_engine_data_product_publish_grant_slice(component, entries)
@@ -14712,6 +15085,11 @@ process.stdout.write('engine-l2-closed-loop:0.7.0:cas+safe-profile+external-plan
def run_healthchecks(component, entries=None, services=None):
if is_device_plane_postgres_bootstrap_slice(component, entries):
if tuple(services or ()) != ("device-postgres",):
die("Device Plane PostgreSQL bootstrap service set mismatch")
healthcheck_compose_service("device-plane", "device-postgres")
return
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
return
if is_engine_l2_closed_loop_slice(component, entries):
@@ -15504,6 +15882,11 @@ def apply_artifact(artifact):
die(f"artifact sha already applied: {sha}")
if state_has_patch_id(patch_id):
die(f"patch id already applied: {patch_id}")
if is_device_plane_postgres_bootstrap_slice(
component,
entries,
):
preflight_device_plane_postgres_bootstrap()
if not root.is_dir():
if bootstrap_root:
root.mkdir(parents=True, exist_ok=True)
@@ -16064,6 +16447,31 @@ def apply_artifact(artifact):
except Exception as rollback_exc:
rollback_status = f"failed:{type(rollback_exc).__name__}"
print("platform-automatic-rollback=failed", file=sys.stderr)
elif (
component == "device-plane"
and entries is not None
and services is not None
):
try:
restored_state = rollback_device_plane_apply(
root,
backup_dir,
entries,
current_stamp,
runtime_started,
services,
)
rollback_status = f"ok:device-plane-overlay:{restored_state}"
print(
f"device-plane-automatic-rollback={rollback_status}",
file=sys.stderr,
)
except Exception as rollback_exc:
rollback_status = f"failed:{type(rollback_exc).__name__}"
print(
"device-plane-automatic-rollback=failed",
file=sys.stderr,
)
failed_path = None
if artifact.exists() and rollback_status != "deferred:reconciliation-required":
try: