feat(edp): provision Foundry reader grants
This commit is contained in:
@@ -182,13 +182,14 @@ force-recreates only `external-data-plane`; the already healthy
|
||||
independent deploy prerequisite and are never selected by an EDP application
|
||||
artifact. The reviewed Compose source pins the Timescale image, named volume
|
||||
and target, internal database-only network, absence of database host ports,
|
||||
healthy dependency, localhost-only EDP bind and the two read-only
|
||||
healthy dependency, localhost-only EDP bind and the three read-only
|
||||
provisioner/trust mounts in the reviewed Compose source. The runner does not
|
||||
reinterpret version-dependent `docker compose config` JSON as a second deploy
|
||||
schema. Its canonical enforcement remains the artifact/path allowlist plus
|
||||
hard-coded build command, selected service set, runtime-secret preparation and
|
||||
health acceptance. Post-apply acceptance also requires
|
||||
`database=ready`; a first-rollout failure removes only the candidate EDP
|
||||
`database=ready`; the managed Foundry slice additionally requires
|
||||
`foundryReaderBindingProvisioning=digest+server-resolved-source`. A first-rollout failure removes only the candidate EDP
|
||||
container without volumes and restores the source overlay. It never contains a
|
||||
provider credential, provider endpoint,
|
||||
collection schedule or command
|
||||
@@ -218,6 +219,21 @@ symlinks and permissive modes fail closed. `plan` discloses both paths without
|
||||
printing key material. The private key must not be broadened into an L2 graph,
|
||||
MCP surface, artifact or shared-token boundary.
|
||||
|
||||
Module Foundry has a separate Ed25519 managed-provisioner identity for
|
||||
target-scoped Data Product consumer grants. On a relevant `platform` or
|
||||
`module-foundry` apply, the runner creates or validates the Foundry-only private
|
||||
key at
|
||||
`/volume1/docker/nodedc-platform/secrets/foundry-edp-managed-provisioner/private-key.pem`
|
||||
as `root:root 0400` and the matching EDP trust copy at
|
||||
`/volume1/docker/nodedc-platform/trust/foundry-managed-provisioner/public-key.pem`
|
||||
as `root:11006 0440`. Foundry generates the opaque reader token only inside its
|
||||
persistent private runtime; its signed EDP request contains only a SHA-256
|
||||
digest and Data Product id. EDP resolves the unique active writer source scope
|
||||
server-side and fails closed on missing or ambiguous coverage. No provider,
|
||||
tenant, connection, token, private key or endpoint is admitted to the Foundry
|
||||
MCP plan, application state or browser response. The Engine signing identity,
|
||||
native n8n credentials and legacy issuance bearer cannot call this endpoint.
|
||||
|
||||
The reviewed Engine source candidate has dedicated server-derived MCP
|
||||
plan/apply handling for the exact `ndcDataProductWriterApi` + Data Product
|
||||
Publish tuple. It is separate from the generic HTTP safe-ref path and accepts no
|
||||
@@ -340,6 +356,12 @@ never enters an artifact. The component reuses the existing internal platform
|
||||
credential for Launcher handoff validation and requires that runtime
|
||||
configuration before its first `apply`.
|
||||
|
||||
When the managed reader-grant source is present, Module Foundry acceptance also
|
||||
requires `/healthz` to report the dedicated Ed25519 provisioner as configured.
|
||||
The check is source-aware: if an apply rolls back to the prior source, rollback
|
||||
acceptance uses that prior health contract instead of falsely requiring a
|
||||
feature which the restored generation does not contain.
|
||||
|
||||
The Foundry ↔ Map Gateway signing key is not an application or `.env` setting.
|
||||
On the first relevant `platform` or `module-foundry` apply, the root-owned
|
||||
runner creates `/volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret`
|
||||
|
||||
@@ -77,6 +77,10 @@ async function assertSourceBoundary() {
|
||||
"source: /volume1/docker/nodedc-platform/trust/engine-managed-provisioner",
|
||||
"target: /run/nodedc-trust/engine-managed-provisioner",
|
||||
"EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/engine-managed-provisioner/public-key.pem",
|
||||
"source: /volume1/docker/nodedc-platform/trust/foundry-managed-provisioner",
|
||||
"target: /run/nodedc-trust/foundry-managed-provisioner",
|
||||
"EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/foundry-managed-provisioner/public-key.pem",
|
||||
"EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED:-true}",
|
||||
"create_host_path: false",
|
||||
]) {
|
||||
if (!compose.includes(fragment)) throw new Error(`platform_compose_boundary_missing:${fragment}`);
|
||||
@@ -97,6 +101,9 @@ async function assertSourceBoundary() {
|
||||
'managedReaderBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"',
|
||||
'managedReaderBindings: "digest+idempotent-generation+explicit-revoke"',
|
||||
'readerSourceScope: "writer-resolved+fail-closed-ambiguity"',
|
||||
'app.post("/internal/data-plane/v1/consumer-reader-bindings/plan"',
|
||||
'app.put("/internal/data-plane/v1/consumer-reader-bindings/by-key/:bindingKey"',
|
||||
'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"',
|
||||
'source_connection_id as "sourceConnectionId"',
|
||||
'where id = $1 and binding_key is null and active = true',
|
||||
]) {
|
||||
@@ -109,6 +116,8 @@ async function assertSourceBoundary() {
|
||||
for (const marker of [
|
||||
"managed_reader_source_scope_not_found",
|
||||
"managed_reader_source_scope_ambiguous",
|
||||
"managed_consumer_reader_source_scope_not_found",
|
||||
"managed_consumer_reader_source_scope_ambiguous",
|
||||
"external_data_plane_writer_bindings",
|
||||
]) {
|
||||
if (!readerSourceScope.includes(marker)) throw new Error(`reader_source_scope_boundary_missing:${marker}`);
|
||||
|
||||
@@ -38,6 +38,8 @@ const stage = await mkdtemp(join(tmpdir(), "nodedc-module-foundry-artifact-"));
|
||||
const payload = join(stage, "payload");
|
||||
const target = join(artifactDir, `nodedc-module-foundry-${patchId}.tgz`);
|
||||
|
||||
await assertSourceBoundary();
|
||||
|
||||
try {
|
||||
await mkdir(payload, { recursive: true });
|
||||
for (const sourceRelative of files) {
|
||||
@@ -60,6 +62,31 @@ try {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertSourceBoundary() {
|
||||
const compose = await readFile(resolve(foundryRoot, "infra/docker-compose.module-foundry.yml"), "utf8");
|
||||
for (const fragment of [
|
||||
"source: /volume1/docker/nodedc-platform/secrets/foundry-edp-managed-provisioner/private-key.pem",
|
||||
"target: /run/nodedc-secrets/foundry-edp-managed-provisioner/private-key.pem",
|
||||
"NODEDC_EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PRIVATE_KEY_FILE: /run/nodedc-secrets/foundry-edp-managed-provisioner/private-key.pem",
|
||||
"create_host_path: false",
|
||||
]) {
|
||||
if (!compose.includes(fragment)) throw new Error(`foundry_reader_grant_boundary_missing:${fragment}`);
|
||||
}
|
||||
const provisioner = await readFile(resolve(foundryRoot, "server/foundry-reader-grant-provisioner.mjs"), "utf8");
|
||||
for (const marker of [
|
||||
'"/internal/data-plane/v1/consumer-reader-bindings/plan"',
|
||||
"consumer-reader-bindings/by-key/${encodeURIComponent(bindingKey)}",
|
||||
"capabilityDigest: createHash(\"sha256\").update(token",
|
||||
"sourceScope: \"resolved-server-side\"",
|
||||
"O_NOFOLLOW",
|
||||
]) {
|
||||
if (!provisioner.includes(marker)) throw new Error(`foundry_reader_grant_boundary_missing:${marker}`);
|
||||
}
|
||||
if (/providerId|tenantId|connectionId/.test(provisioner)) {
|
||||
throw new Error("foundry_reader_grant_source_scope_boundary_violation");
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalTarScript() {
|
||||
return [
|
||||
"import gzip,io,pathlib,sys,tarfile",
|
||||
@@ -88,6 +115,7 @@ async function copySafe(source, destination) {
|
||||
|
||||
await mkdir(destination, { recursive: true });
|
||||
for (const entry of await readdir(source, { withFileTypes: true })) {
|
||||
if (entry.name === "private-key.pem") throw new Error(`private_key_source_rejected:${relative(foundryRoot, join(source, entry.name))}`);
|
||||
if (ignoredBasenames.has(entry.name) || entry.name.startsWith(".env") || entry.name.endsWith(".tsbuildinfo")) continue;
|
||||
const childSource = join(source, entry.name);
|
||||
const childDestination = join(destination, entry.name);
|
||||
|
||||
@@ -63,6 +63,10 @@ ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "engine-edp
|
||||
ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem"
|
||||
ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/engine-managed-provisioner")
|
||||
ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem"
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-edp-managed-provisioner"
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem"
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/foundry-managed-provisioner")
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem"
|
||||
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-reader-grants"
|
||||
FOUNDRY_BINDING_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-binding-grants"
|
||||
N8N_PRIVATE_EXTENSION_RELEASES_ROOT = Path("/volume1/docker/nodedc-platform/n8n-private-extensions")
|
||||
@@ -1159,6 +1163,168 @@ def ensure_engine_edp_managed_provisioner_keypair():
|
||||
if temporary_public.exists():
|
||||
temporary_public.unlink()
|
||||
return "created"
|
||||
return "created"
|
||||
|
||||
|
||||
def ensure_foundry_edp_managed_provisioner_keypair():
|
||||
# Foundry receives its own signing identity. It must never reuse or read
|
||||
# the Engine private key; EDP trusts the matching public key separately.
|
||||
openssl = shutil.which("openssl")
|
||||
if not openssl:
|
||||
die("openssl is required to manage the Foundry EDP signing key")
|
||||
|
||||
try:
|
||||
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
||||
except FileNotFoundError:
|
||||
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
|
||||
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
||||
if stat.S_ISLNK(secret_parent_stat.st_mode) or not stat.S_ISDIR(secret_parent_stat.st_mode):
|
||||
die("Foundry EDP signing key parent directory is unsafe")
|
||||
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
|
||||
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
|
||||
|
||||
try:
|
||||
private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
|
||||
except FileNotFoundError:
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False)
|
||||
private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
|
||||
if stat.S_ISLNK(private_dir_stat.st_mode) or not stat.S_ISDIR(private_dir_stat.st_mode):
|
||||
die("Foundry EDP signing key directory is unsafe")
|
||||
os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR, 0, 0)
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.chmod(0o700)
|
||||
|
||||
trust_parent = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.parent
|
||||
try:
|
||||
trust_parent_stat = trust_parent.lstat()
|
||||
except FileNotFoundError:
|
||||
trust_parent.mkdir(parents=True, exist_ok=False)
|
||||
trust_parent_stat = trust_parent.lstat()
|
||||
if stat.S_ISLNK(trust_parent_stat.st_mode) or not stat.S_ISDIR(trust_parent_stat.st_mode):
|
||||
die("Foundry EDP trust parent directory is unsafe")
|
||||
os.chown(trust_parent, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
||||
trust_parent.chmod(0o710)
|
||||
|
||||
try:
|
||||
trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
|
||||
except FileNotFoundError:
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.mkdir(parents=False, exist_ok=False)
|
||||
trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
|
||||
if stat.S_ISLNK(trust_dir_stat.st_mode) or not stat.S_ISDIR(trust_dir_stat.st_mode):
|
||||
die("Foundry EDP trust directory is unsafe")
|
||||
os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.chmod(0o550)
|
||||
|
||||
try:
|
||||
private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
|
||||
except FileNotFoundError:
|
||||
private_stat = None
|
||||
try:
|
||||
public_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.lstat()
|
||||
except FileNotFoundError:
|
||||
public_stat = None
|
||||
if private_stat is None and public_stat is not None:
|
||||
die("Foundry EDP public key exists without its private key")
|
||||
|
||||
private_created = False
|
||||
if private_stat is None:
|
||||
temporary_private = (
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR
|
||||
/ f".private-key.{os.getpid()}.{time.time_ns()}.tmp"
|
||||
)
|
||||
try:
|
||||
subprocess.run(
|
||||
[openssl, "genpkey", "-algorithm", "ED25519", "-out", str(temporary_private)],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
temporary_stat = temporary_private.lstat()
|
||||
if (stat.S_ISLNK(temporary_stat.st_mode) or not stat.S_ISREG(temporary_stat.st_mode)
|
||||
or temporary_stat.st_size < 80 or temporary_stat.st_size > 8192):
|
||||
die("generated Foundry EDP private key is invalid")
|
||||
os.chown(temporary_private, 0, 0)
|
||||
temporary_private.chmod(0o400)
|
||||
with temporary_private.open("rb") as handle:
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary_private, FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE)
|
||||
fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR)
|
||||
private_created = True
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
die(f"failed to generate Foundry EDP signing key: {type(error).__name__}")
|
||||
finally:
|
||||
if temporary_private.exists():
|
||||
temporary_private.unlink()
|
||||
private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
|
||||
|
||||
if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode)
|
||||
or private_stat.st_uid != 0 or private_stat.st_gid != 0
|
||||
or stat.S_IMODE(private_stat.st_mode) != 0o400
|
||||
or private_stat.st_size < 80 or private_stat.st_size > 8192):
|
||||
die("Foundry EDP private key has unsafe ownership, mode, or size")
|
||||
|
||||
try:
|
||||
derived = subprocess.run(
|
||||
[openssl, "pkey", "-in", str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout"],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
).stdout
|
||||
derived_der = subprocess.run(
|
||||
[
|
||||
openssl,
|
||||
"pkey",
|
||||
"-in",
|
||||
str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE),
|
||||
"-pubout",
|
||||
"-outform",
|
||||
"DER",
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
).stdout
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
die(f"Foundry EDP private key validation failed: {type(error).__name__}")
|
||||
if (not derived.startswith(b"-----BEGIN PUBLIC KEY-----\n")
|
||||
or not derived.rstrip().endswith(b"-----END PUBLIC KEY-----")
|
||||
or len(derived) > 8192
|
||||
or len(derived_der) != 44
|
||||
or not derived_der.startswith(bytes.fromhex("302a300506032b6570032100"))):
|
||||
die("Foundry EDP signing key must be Ed25519")
|
||||
|
||||
if public_stat is not None:
|
||||
if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode)
|
||||
or public_stat.st_uid != 0
|
||||
or public_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID
|
||||
or stat.S_IMODE(public_stat.st_mode) != 0o440
|
||||
or public_stat.st_size < 80 or public_stat.st_size > 8192):
|
||||
die("Foundry EDP public key has unsafe ownership, mode, or size")
|
||||
if FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.read_bytes() != derived:
|
||||
die("Foundry EDP public key does not match the installed private key")
|
||||
return "created" if private_created else "reused"
|
||||
|
||||
temporary_public = (
|
||||
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR
|
||||
/ f".public-key.{os.getpid()}.{time.time_ns()}.tmp"
|
||||
)
|
||||
descriptor = None
|
||||
try:
|
||||
descriptor = os.open(str(temporary_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440)
|
||||
os.write(descriptor, derived)
|
||||
os.fsync(descriptor)
|
||||
os.fchown(descriptor, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
||||
os.fchmod(descriptor, 0o440)
|
||||
os.close(descriptor)
|
||||
descriptor = None
|
||||
os.replace(temporary_public, FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE)
|
||||
fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR)
|
||||
finally:
|
||||
if descriptor is not None:
|
||||
os.close(descriptor)
|
||||
if temporary_public.exists():
|
||||
temporary_public.unlink()
|
||||
return "created"
|
||||
|
||||
|
||||
def ensure_engine_data_product_grant_private_state(include_reader=False):
|
||||
@@ -6394,9 +6560,12 @@ def plan_artifact(artifact):
|
||||
if component == "module-foundry":
|
||||
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
|
||||
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 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}")
|
||||
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
|
||||
if touches_engine_credential_sink(component, entries):
|
||||
print(f"runtime_identity_key_id={ENGINE_CREDENTIAL_PROVISIONER_KEY_ID}")
|
||||
print(f"runtime_private_key=runner-managed:{ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE}")
|
||||
@@ -7136,6 +7305,7 @@ def prepare_component_runtime(component, entries=None):
|
||||
|
||||
if component == "module-foundry":
|
||||
ensure_map_gateway_admin_secret()
|
||||
ensure_foundry_edp_managed_provisioner_keypair()
|
||||
ensure_root_owned_grant_directory(
|
||||
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
|
||||
"external data plane reader grants",
|
||||
@@ -7195,6 +7365,7 @@ def prepare_component_runtime(component, entries=None):
|
||||
if touches_external_data_plane:
|
||||
ensure_external_data_plane_provisioner_secret()
|
||||
ensure_engine_edp_managed_provisioner_keypair()
|
||||
ensure_foundry_edp_managed_provisioner_keypair()
|
||||
ensure_root_owned_grant_directory(
|
||||
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
|
||||
"external data plane reader grants",
|
||||
@@ -7292,12 +7463,37 @@ def external_data_plane_healthcheck(require_managed=True):
|
||||
expected_json["managedWriterBindingLifetime"] = "explicit-revoke"
|
||||
if 'managedReaderBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"' in server_text:
|
||||
expected_json["managedReaderBindingLifetime"] = "explicit-revoke"
|
||||
if 'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"' in server_text:
|
||||
expected_json["foundryReaderBindingProvisioning"] = "digest+server-resolved-source"
|
||||
expected_json["foundryReaderBindingLifetime"] = "explicit-revoke"
|
||||
return {
|
||||
"url": "http://127.0.0.1:18106/healthz",
|
||||
"expected_json": expected_json,
|
||||
}
|
||||
|
||||
|
||||
def module_foundry_healthcheck():
|
||||
expected_json = {
|
||||
"status": "ok",
|
||||
"service": "nodedc-module-foundry",
|
||||
}
|
||||
server_source = component_root("module-foundry") / "server/catalog-server.mjs"
|
||||
try:
|
||||
server_text = server_source.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
server_text = ""
|
||||
if "dataProductConsumerProvisioner" in server_text:
|
||||
expected_json["dataProductConsumerProvisioner"] = {
|
||||
"configured": True,
|
||||
"auth": "dedicated-ed25519-service-identity",
|
||||
"sourceScope": "external-data-plane-resolved",
|
||||
}
|
||||
return {
|
||||
"url": "http://172.22.0.222:9920/healthz",
|
||||
"expected_json": expected_json,
|
||||
}
|
||||
|
||||
|
||||
def component_healthchecks(component, entries=None, services=None):
|
||||
if is_engine_n8n_transition(component, entries):
|
||||
# The transition does not restart the Engine UI/backend generation.
|
||||
@@ -7311,6 +7507,8 @@ def component_healthchecks(component, entries=None, services=None):
|
||||
return ("http://127.0.0.1:3001/health",)
|
||||
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
|
||||
return ()
|
||||
if component == "module-foundry":
|
||||
return (module_foundry_healthcheck(),)
|
||||
if (
|
||||
(
|
||||
is_engine_data_product_publish_grant_slice(component, entries)
|
||||
|
||||
@@ -81,6 +81,14 @@ class ExternalDataPlaneArtifactTest(unittest.TestCase):
|
||||
"source: /volume1/docker/nodedc-platform/trust/engine-managed-provisioner",
|
||||
compose,
|
||||
)
|
||||
self.assertIn(
|
||||
"source: /volume1/docker/nodedc-platform/trust/foundry-managed-provisioner",
|
||||
compose,
|
||||
)
|
||||
self.assertIn(
|
||||
"EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/foundry-managed-provisioner/public-key.pem",
|
||||
compose,
|
||||
)
|
||||
self.assertNotIn("private-key.pem", compose)
|
||||
self.assertNotIn(b"-----BEGIN PRIVATE KEY-----", b"\n".join(regular_payloads))
|
||||
|
||||
|
||||
@@ -293,6 +293,48 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
|
||||
keypair.assert_called_once_with()
|
||||
private_state.assert_called_once_with()
|
||||
|
||||
def test_module_foundry_runtime_prepares_separate_edp_identity_and_grant_roots(self):
|
||||
with (
|
||||
mock.patch.object(RUNNER, "ensure_map_gateway_admin_secret") as map_secret,
|
||||
mock.patch.object(RUNNER, "ensure_foundry_edp_managed_provisioner_keypair") as foundry_keypair,
|
||||
mock.patch.object(RUNNER, "ensure_root_owned_grant_directory") as grant_directory,
|
||||
):
|
||||
RUNNER.prepare_component_runtime("module-foundry", ("server/catalog-server.mjs",))
|
||||
|
||||
map_secret.assert_called_once_with()
|
||||
foundry_keypair.assert_called_once_with()
|
||||
self.assertEqual(
|
||||
grant_directory.call_args_list,
|
||||
[
|
||||
mock.call(
|
||||
RUNNER.EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
|
||||
"external data plane reader grants",
|
||||
),
|
||||
mock.call(
|
||||
RUNNER.FOUNDRY_BINDING_GRANTS_DIR,
|
||||
"foundry binding grants",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def test_edp_runtime_prepares_engine_and_foundry_public_trust_independently(self):
|
||||
entries = ("platform/services/external-data-plane/src/server.mjs",)
|
||||
with (
|
||||
mock.patch.object(RUNNER, "ensure_external_data_plane_provisioner_secret") as legacy_secret,
|
||||
mock.patch.object(RUNNER, "ensure_engine_edp_managed_provisioner_keypair") as engine_keypair,
|
||||
mock.patch.object(RUNNER, "ensure_foundry_edp_managed_provisioner_keypair") as foundry_keypair,
|
||||
mock.patch.object(RUNNER, "ensure_root_owned_grant_directory") as grant_directory,
|
||||
):
|
||||
RUNNER.prepare_component_runtime("platform", entries)
|
||||
|
||||
legacy_secret.assert_called_once_with()
|
||||
engine_keypair.assert_called_once_with()
|
||||
foundry_keypair.assert_called_once_with()
|
||||
grant_directory.assert_called_once_with(
|
||||
RUNNER.EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
|
||||
"external data plane reader grants",
|
||||
)
|
||||
|
||||
def test_partial_publish_path_has_no_publish_runtime_side_effects(self):
|
||||
entries = ("nodedc-source/server/dataProductPublishGrant/store.js",)
|
||||
self.assertTrue(RUNNER.touches_engine_data_product_publish_grant(entries))
|
||||
@@ -441,6 +483,46 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
|
||||
(RUNNER.external_data_plane_healthcheck(),),
|
||||
)
|
||||
|
||||
def test_edp_healthcheck_requires_foundry_provisioner_for_the_new_source(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-edp-foundry-health-") as directory:
|
||||
root = Path(directory)
|
||||
source = root / "platform/services/external-data-plane/src/server.mjs"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text(
|
||||
'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
original_root = RUNNER.COMPONENTS["platform"]["payload_root"]
|
||||
RUNNER.COMPONENTS["platform"]["payload_root"] = root
|
||||
try:
|
||||
expected = RUNNER.external_data_plane_healthcheck()["expected_json"]
|
||||
finally:
|
||||
RUNNER.COMPONENTS["platform"]["payload_root"] = original_root
|
||||
|
||||
self.assertEqual(
|
||||
expected["foundryReaderBindingProvisioning"],
|
||||
"digest+server-resolved-source",
|
||||
)
|
||||
self.assertEqual(expected["foundryReaderBindingLifetime"], "explicit-revoke")
|
||||
|
||||
def test_module_foundry_healthcheck_tracks_candidate_and_rollback_source(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-foundry-health-") as directory:
|
||||
root = Path(directory)
|
||||
source = root / "server/catalog-server.mjs"
|
||||
source.parent.mkdir(parents=True)
|
||||
original_root = RUNNER.COMPONENTS["module-foundry"]["payload_root"]
|
||||
RUNNER.COMPONENTS["module-foundry"]["payload_root"] = root
|
||||
try:
|
||||
source.write_text("const dataProductConsumerProvisioner = true;\n", encoding="utf-8")
|
||||
candidate = RUNNER.module_foundry_healthcheck()["expected_json"]
|
||||
source.write_text("const previous = true;\n", encoding="utf-8")
|
||||
rollback = RUNNER.module_foundry_healthcheck()["expected_json"]
|
||||
finally:
|
||||
RUNNER.COMPONENTS["module-foundry"]["payload_root"] = original_root
|
||||
|
||||
self.assertEqual(candidate["dataProductConsumerProvisioner"]["configured"], True)
|
||||
self.assertNotIn("dataProductConsumerProvisioner", rollback)
|
||||
|
||||
def test_edp_runtime_never_probes_unselected_platform_services(self):
|
||||
entries = ("platform/services/external-data-plane/src/server.mjs",)
|
||||
services = ("external-data-plane",)
|
||||
|
||||
Reference in New Issue
Block a user