feat(config-refs): introduce config reference parsing and resolution

- Added a new module for parsing and resolving configuration references, supporting `$SECRET_`, `$KV_`, and `$CONTEXT_` prefixes.
- Implemented functions to handle the resolution of these references within workflow contexts, enhancing the flexibility of configuration management.
- Updated existing workflows and scripts to utilize the new config reference resolution, improving data handling and context management.
- Introduced a smoke test to validate the functionality of the config reference system, ensuring robustness and reliability.

This update significantly enhances the ability to manage dynamic configurations within workflows, improving overall usability and maintainability.
This commit is contained in:
2026-08-16 21:36:15 +07:00
parent cf4b342e04
commit fd0865063b
16 changed files with 752 additions and 42 deletions
+185
View File
@@ -0,0 +1,185 @@
import { migrate, db } from "../db.js";
import { kvSet, kvDelete } from "../kv-store.js";
import { upsertSecret, deleteSecret } from "../secrets-store.js";
import { Secret } from "../secret-value.js";
import { parseConfigRef, resolveConfigRefs } from "../config-refs.js";
await migrate();
function assert(cond, msg) {
if (!cond) throw new Error(msg);
}
async function assertRejects(fn, match) {
try {
await fn();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (match && !message.includes(match)) {
throw new Error(`rejected with "${message}", expected to include "${match}"`);
}
return;
}
throw new Error(`expected to reject (${match ?? "any error"})`);
}
const owner = "default";
const workflowKey = "default/config-refs-smoke.yaml";
const ctx = { owner, workflowKey, context: {} };
function assertParse(value, expected) {
const got = parseConfigRef(value);
if (expected == null) {
assert(got == null, `expected null parse for ${JSON.stringify(value)}, got ${JSON.stringify(got)}`);
return;
}
assert(got != null, `expected parse for ${JSON.stringify(value)}`);
assert(got.kind === expected.kind, `kind ${got.kind} !== ${expected.kind}`);
assert(got.name === expected.name, `name ${JSON.stringify(got.name)} !== ${JSON.stringify(expected.name)}`);
}
assertParse("password123", null);
assertParse("$FOO_bar", null);
assertParse("$SECRET", null);
assertParse(" password123 ", null);
assertParse("$SECRET_zte_modem_password", { kind: "secret", name: "zte_modem_password" });
assertParse(" $SECRET_zte_modem_password ", { kind: "secret", name: "zte_modem_password" });
assertParse("Bearer $SECRET_x", null);
assertParse("$KV_modem password", { kind: "kv", name: "modem password" });
assertParse("$CONTEXT_token", { kind: "context", name: "token" });
assertParse("$SECRET_", { kind: "secret", name: "" });
assertParse("$CONTEXT_SECRET_foo", { kind: "context", name: "SECRET_foo" });
{
const literal = await resolveConfigRefs("password123", ctx);
assert(literal === "password123", "literal passthrough");
const unknown = await resolveConfigRefs("$FOO_bar", ctx);
assert(unknown === "$FOO_bar", "$FOO_bar stays literal");
const embedded = await resolveConfigRefs("Bearer $SECRET_x", ctx);
assert(embedded === "Bearer $SECRET_x", "mid-string stays literal");
const number = await resolveConfigRefs(42, ctx);
assert(number === 42, "number passthrough");
}
const secret = await upsertSecret({
owner,
name: "config_refs_smoke_token",
value: "s3cret-ok",
});
await kvSet(workflowKey, "modem_password", "kv-pass-ok");
await kvSet(workflowKey, "modem password", "kv-spaced-ok");
await kvSet(workflowKey, "obj-key", { nested: true });
try {
{
const resolved = await resolveConfigRefs("$SECRET_config_refs_smoke_token", ctx);
assert(resolved === "s3cret-ok", "secret resolve");
}
{
const resolved = await resolveConfigRefs(" $SECRET_config_refs_smoke_token ", ctx);
assert(resolved === "s3cret-ok", "secret resolve trimmed");
}
{
const resolved = await resolveConfigRefs("$KV_modem_password", ctx);
assert(resolved === "kv-pass-ok", "kv resolve");
}
{
const resolved = await resolveConfigRefs("$KV_modem password", ctx);
assert(resolved === "kv-spaced-ok", "kv spaced key");
}
{
const resolved = await resolveConfigRefs("$CONTEXT_token", {
...ctx,
context: { token: "ctx-token-ok" },
});
assert(resolved === "ctx-token-ok", "context string");
}
{
const resolved = await resolveConfigRefs("$CONTEXT_n", {
...ctx,
context: { n: 7 },
});
assert(resolved === "7", "context number stringify");
}
{
const wrapped = new Secret("wrapped-secret-ok");
const resolved = await resolveConfigRefs("$CONTEXT_tok", {
...ctx,
context: { tok: wrapped },
});
assert(resolved === "wrapped-secret-ok", "context Secret unwrap");
}
{
const nested = await resolveConfigRefs(
{
url: "http://example.test",
password: "$SECRET_config_refs_smoke_token",
headers: { Authorization: "$KV_modem_password" },
extra: ["$CONTEXT_token", "plain"],
},
{ ...ctx, context: { token: "ctx-token-ok" } },
);
assert(nested.url === "http://example.test", "nested literal");
assert(nested.password === "s3cret-ok", "nested secret");
assert(nested.headers.Authorization === "kv-pass-ok", "nested kv");
assert(nested.extra[0] === "ctx-token-ok", "nested array context");
assert(nested.extra[1] === "plain", "nested array literal");
}
const data = { password: "$SECRET_config_refs_smoke_token" };
const config = { password: "$SECRET_config_refs_smoke_token" };
const resolvedConfig = await resolveConfigRefs(config, ctx);
assert(resolvedConfig.password === "s3cret-ok", "config resolved");
assert(data.password === "$SECRET_config_refs_smoke_token", "data not walked");
assert(config.password === "$SECRET_config_refs_smoke_token", "input config not mutated");
await assertRejects(
() => resolveConfigRefs("$SECRET_does_not_exist_xyz", ctx),
'secret "does_not_exist_xyz" not found',
);
await assertRejects(
() => resolveConfigRefs("$SECRET_not valid", ctx),
"invalid secret name",
);
await assertRejects(
() => resolveConfigRefs("$SECRET_", ctx),
"invalid secret name",
);
await assertRejects(
() => resolveConfigRefs("$KV_missing-key", ctx),
'KV "missing-key" not found',
);
await assertRejects(
() => resolveConfigRefs("$KV_obj-key", ctx),
'KV "obj-key" is not a scalar',
);
await assertRejects(
() => resolveConfigRefs("$KV_", ctx),
"empty KV key",
);
await assertRejects(
() => resolveConfigRefs("$CONTEXT_missing", ctx),
'context "missing" not found',
);
await assertRejects(
() =>
resolveConfigRefs("$CONTEXT_obj", {
...ctx,
context: { obj: { a: 1 } },
}),
'context "obj" is not a scalar',
);
await assertRejects(
() => resolveConfigRefs("$CONTEXT_", ctx),
"empty context key",
);
} finally {
await deleteSecret(secret.id);
await kvDelete(workflowKey, "modem_password");
await kvDelete(workflowKey, "modem password");
await kvDelete(workflowKey, "obj-key");
}
console.log("config-refs smoke test passed");
await db.destroy();