feat(n8n): add rotating provider access credential

This commit is contained in:
Codex
2026-07-19 10:11:56 +03:00
parent 3c5d8f6cef
commit 0611a88971
10 changed files with 408 additions and 64 deletions
@@ -16,17 +16,24 @@ from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
ENGINE_ROOT = PLATFORM_ROOT.parent / "NODEDC_ENGINE_INFRA"
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
RUNNER_PATH = Path(
os.environ.get("NODEDC_DEPLOY_RUNNER_UNDER_TEST", SCRIPT_DIR / "nodedc-deploy")
).resolve()
BUILDER_PATH = SCRIPT_DIR / "build-engine-n8n-private-extension-artifact.mjs"
STAGE_ARTIFACT = (
PLATFORM_ROOT
/ "infra/deploy-artifacts"
/ "nodedc-n8n-private-extension-n8n-nodes-ndc-history-read-20260717-004.tgz"
/ "nodedc-n8n-private-extension-n8n-nodes-ndc-provider-rotating-access-20260718-006.tgz"
)
TRANSITION_ID = "20260717-005"
SEALED_RELEASE_ID = "0.1.3-3354149b5245e39a"
SEALED_PACKAGE_SHA256 = "3354149b5245e39a10f6f03a4b43796602d7e57ea1f91dcc6cb6774ead97501d"
PREDECESSOR_RELEASE_ID = "0.1.2-05e4b38b14b4a019"
PREDECESSOR_ARTIFACT = (
PLATFORM_ROOT
/ "infra/deploy-artifacts"
/ "nodedc-engine-n8n-private-extension-20260717-005.tgz"
)
TRANSITION_ID = "20260718-008"
SEALED_RELEASE_ID = "0.1.4-59dc9f7882721d6a"
SEALED_PACKAGE_SHA256 = "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f"
PREDECESSOR_RELEASE_ID = "0.1.3-3354149b5245e39a"
BUILDER_ENGINE_PATHS = (
"nodedc-source/server/assets/n8n/schema/v2.3.2/nodes.catalog.json",
"nodedc-source/server/assets/n8n/schema/v2.3.2/credentials.catalog.json",
@@ -45,14 +52,16 @@ EXPECTED_CREDENTIALS = [
"ndcDataProductWriterApi",
"ndcDataProductReaderApi",
"ndcFoundryBindingApi",
"ndcProviderRotatingAccessApi",
]
PREDECESSOR_CREDENTIALS = EXPECTED_CREDENTIALS[:3]
PRIVATE_EXTENSION_CANON_FUNCTION_SHA256 = {
"expected_engine_n8n_compose_override": "8ee93f3e7c407f5557c711119236449a440d73a7fcac1bbfa50a09e6a13c1cff",
"engine_n8n_package_loader_probe_script": "2b3b3c96fad6e5e48ebea74426b21bc7eea6b6fe0e786b537da3366d18aedd9b",
"engine_n8n_private_loader_catalog": "a2ae389b4ef215900cf967b863d01056bd2a8eec55554566d0f6005e4e0bcbac",
"validate_engine_n8n_base_compose_source": "adacc5b20e52684cbc427362ddba5a6d2e13ef2091a89859e2b8f7bbf2e20458",
"preflight_engine_n8n_transition": "3d574acb081fb752e45cc0c3286d3ae4596dcee83a85e2c3fee81ac65ec796e0",
"accept_engine_n8n_runtime": "1a2614a465161e3833e84aca402817682da1a538bdb2aecc4af66b665497338d",
"preflight_engine_n8n_transition": "25bf33b3be0916a9dd82f064a710060eaf6bd7d87b3614ee0065af07ddbb446e",
"accept_engine_n8n_runtime": "bc49275adb2d5ab4fd98abd07b9059a197d023e03b40145b6fc910b70b2f738b",
}
@@ -107,6 +116,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
env.pop("NODEDC_N8N_TRANSITION_ID", None)
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(output)
env["NODEDC_N8N_EXTENSION_STAGE_ARTIFACT"] = str(STAGE_ARTIFACT)
env["NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT"] = str(PREDECESSOR_ARTIFACT)
command = ["node", str(BUILDER_PATH)]
if index == 0:
command.append(TRANSITION_ID)
@@ -165,8 +175,8 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
def test_transition_id_rejects_missing_invalid_and_previously_issued_values(self):
cases = (
([], "transition_id_required"),
(["20260717-004"], "transition_id_already_issued"),
(["engine-n8n-private-extension-20260717-005"], "transition_id_invalid"),
(["20260718-007"], "transition_id_already_issued"),
(["engine-n8n-private-extension-20260718-008"], "transition_id_invalid"),
(["20260230-004"], "transition_id_invalid"),
(["20260717-000"], "transition_id_invalid"),
([TRANSITION_ID, "unexpected-second-id"], "transition_id_argument_count_invalid"),
@@ -191,6 +201,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
env.pop("NODEDC_N8N_TRANSITION_ID", None)
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(self.artifact(0, "activation").parent)
env["NODEDC_N8N_EXTENSION_STAGE_ARTIFACT"] = str(STAGE_ARTIFACT)
env["NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT"] = str(PREDECESSOR_ARTIFACT)
result = subprocess.run(
["node", str(BUILDER_PATH), TRANSITION_ID],
cwd=PLATFORM_ROOT,
@@ -233,7 +244,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
self.assertEqual([item["name"] for item in private_nodes], EXPECTED_NODES)
self.assertEqual([item["name"] for item in private_credentials], EXPECTED_CREDENTIALS)
self.assertTrue(all("usableAsTool" not in item for item in private_nodes))
self.assertEqual((len(nodes), len(credentials)), (437, 388))
self.assertEqual((len(nodes), len(credentials)), (437, 389))
def test_rollback_catalog_restores_verified_predecessor_release(self):
with tempfile.TemporaryDirectory() as directory:
@@ -253,8 +264,8 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
EXPECTED_NODES,
)
self.assertEqual(
[item.get("name") for item in credentials if item.get("name") in EXPECTED_CREDENTIALS],
EXPECTED_CREDENTIALS,
[item.get("name") for item in credentials if item.get("name") in PREDECESSOR_CREDENTIALS],
PREDECESSOR_CREDENTIALS,
)
self.assertEqual((len(nodes), len(credentials)), (437, 388))
@@ -271,7 +282,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
self.assertEqual(override, RUNNER.expected_engine_n8n_compose_override(descriptor))
self.assertEqual(
hashlib.sha256(override.encode("utf-8")).hexdigest(),
"256142c14ae295bf9be4d74fdc8dcd6e134e4ded8a264f15e66b38e85503b589",
"d6d1249e93b5bed5e697715b6b773c7efa4ac234f12eab57d868407e6bb5390e",
)
self.assertIn("pull_policy: never", override)
self.assertIn("N8N_USER_FOLDER: /home/node", override)
@@ -563,7 +574,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
def test_sealed_release_exact_set_includes_implicit_directories(self):
release_relative = Path(
"payload/releases/n8n-nodes-ndc/0.1.3-3354149b5245e39a"
"payload/releases/n8n-nodes-ndc/0.1.4-59dc9f7882721d6a"
)
with tempfile.TemporaryDirectory() as directory:
work = Path(directory)