feat: 支持 provider profile auth.json 写入
This commit is contained in:
@@ -167,10 +167,9 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
|||||||
const profile = validateBackendProfile(profileValue);
|
const profile = validateBackendProfile(profileValue);
|
||||||
const spec = requiredSpec(profile);
|
const spec = requiredSpec(profile);
|
||||||
const record = asRecord(body ?? {}, "providerProfileCredential");
|
const record = asRecord(body ?? {}, "providerProfileCredential");
|
||||||
const apiKey = stringField(record, "apiKey");
|
const credential = credentialAuthJson(record);
|
||||||
if (apiKey.length < 8) throw new AgentRunError("secret-unavailable", "apiKey is too short", { httpStatus: 400 });
|
|
||||||
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
||||||
const rendered = renderCredential(apiKey, await renderedConfigForWrite(profile, record, options));
|
const renderedConfig = await renderedConfigForWrite(profile, record, options);
|
||||||
const namespace = profileNamespace(options);
|
const namespace = profileNamespace(options);
|
||||||
const secretManifest: JsonRecord = {
|
const secretManifest: JsonRecord = {
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
@@ -184,16 +183,16 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
|||||||
},
|
},
|
||||||
annotations: {
|
annotations: {
|
||||||
[`${credentialAnnotationPrefix}-profile`]: profile,
|
[`${credentialAnnotationPrefix}-profile`]: profile,
|
||||||
[`${credentialAnnotationPrefix}-credential-hash-suffix`]: shortHash(rendered.authJson),
|
[`${credentialAnnotationPrefix}-credential-hash-suffix`]: shortHash(credential.authJson),
|
||||||
[`${credentialAnnotationPrefix}-config-hash-suffix`]: shortHash(rendered.config.configToml),
|
[`${credentialAnnotationPrefix}-config-hash-suffix`]: shortHash(renderedConfig.configToml),
|
||||||
[`${credentialAnnotationPrefix}-updated-at`]: new Date().toISOString(),
|
[`${credentialAnnotationPrefix}-updated-at`]: new Date().toISOString(),
|
||||||
...(delegatedBy ? { [`${credentialAnnotationPrefix}-delegated-system`]: delegatedBy.system, [`${credentialAnnotationPrefix}-delegated-request-id`]: delegatedBy.requestId ?? "" } : {}),
|
...(delegatedBy ? { [`${credentialAnnotationPrefix}-delegated-system`]: delegatedBy.system, [`${credentialAnnotationPrefix}-delegated-request-id`]: delegatedBy.requestId ?? "" } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: "Opaque",
|
type: "Opaque",
|
||||||
data: {
|
data: {
|
||||||
"auth.json": base64Data(rendered.authJson),
|
"auth.json": base64Data(credential.authJson),
|
||||||
"config.toml": base64Data(rendered.config.configToml),
|
"config.toml": base64Data(renderedConfig.configToml),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
||||||
@@ -204,12 +203,13 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
|||||||
configured: true,
|
configured: true,
|
||||||
secretRef: secretRefSummary(profile, namespace),
|
secretRef: secretRefSummary(profile, namespace),
|
||||||
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
||||||
credentialHashSuffix: shortHash(rendered.authJson),
|
credentialHashSuffix: shortHash(credential.authJson),
|
||||||
configHashSuffix: shortHash(rendered.config.configToml),
|
configHashSuffix: shortHash(renderedConfig.configToml),
|
||||||
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
|
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
|
||||||
configSummary: rendered.config.configSummary,
|
credentialSource: credential.source,
|
||||||
|
configSummary: renderedConfig.configSummary,
|
||||||
delegatedBy,
|
delegatedBy,
|
||||||
requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, rendered.config.configToml),
|
requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, renderedConfig.configToml),
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
pollCommands: {
|
pollCommands: {
|
||||||
show: `./scripts/agentrun provider-profiles show ${profile}`,
|
show: `./scripts/agentrun provider-profiles show ${profile}`,
|
||||||
@@ -357,20 +357,32 @@ function profileFromSecretName(name: string | null): string | null {
|
|||||||
return profile.length > 0 ? profile : null;
|
return profile.length > 0 ? profile : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCredential(apiKey: string, config: RenderedConfig): { authJson: string; config: RenderedConfig } {
|
|
||||||
const authJson = `${JSON.stringify(authPayload(apiKey))}\n`;
|
|
||||||
return {
|
|
||||||
authJson,
|
|
||||||
config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerProfileSecretData(input: { authJsonData?: string | null; configToml: string }): JsonRecord {
|
function providerProfileSecretData(input: { authJsonData?: string | null; configToml: string }): JsonRecord {
|
||||||
const data: JsonRecord = { "config.toml": base64Data(input.configToml) };
|
const data: JsonRecord = { "config.toml": base64Data(input.configToml) };
|
||||||
if (input.authJsonData) data["auth.json"] = input.authJsonData;
|
if (input.authJsonData) data["auth.json"] = input.authJsonData;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function credentialAuthJson(record: JsonRecord): { authJson: string; source: "auth-json" | "api-key" } {
|
||||||
|
const authJson = record.authJson;
|
||||||
|
if (typeof authJson === "string" && authJson.trim().length > 0) return { authJson: authJsonField(authJson), source: "auth-json" };
|
||||||
|
const apiKey = stringField(record, "apiKey");
|
||||||
|
if (apiKey.length < 8) throw new AgentRunError("secret-unavailable", "apiKey is too short", { httpStatus: 400 });
|
||||||
|
return { authJson: `${JSON.stringify(authPayload(apiKey))}\n`, source: "api-key" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function authJsonField(value: string): string {
|
||||||
|
if (!value.trim()) throw new AgentRunError("schema-invalid", "authJson is required", { httpStatus: 400 });
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
throw new AgentRunError("schema-invalid", "authJson must be valid JSON", { httpStatus: 400 });
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AgentRunError("schema-invalid", "authJson must be a JSON object", { httpStatus: 400 });
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function authPayload(apiKey: string): JsonRecord {
|
function authPayload(apiKey: string): JsonRecord {
|
||||||
return { OPENAI_API_KEY: apiKey };
|
return { OPENAI_API_KEY: apiKey };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ const selfTest: SelfTestCase = async (context) => {
|
|||||||
const createdJobPath = path.join(context.tmp, "provider-validation-job.json");
|
const createdJobPath = path.join(context.tmp, "provider-validation-job.json");
|
||||||
const secretStateDir = path.join(context.tmp, "provider-secret-state");
|
const secretStateDir = path.join(context.tmp, "provider-secret-state");
|
||||||
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
||||||
import { rmSync } from "node:fs";
|
import { mkdirSync, rmSync } from "node:fs";
|
||||||
const args = Bun.argv.slice(2);
|
const args = Bun.argv.slice(2);
|
||||||
const secretStateDir = ${JSON.stringify(secretStateDir)};
|
const secretStateDir = ${JSON.stringify(secretStateDir)};
|
||||||
const secretStatePath = (name) => secretStateDir + "/" + name + ".json";
|
const secretStatePath = (name) => secretStateDir + "/" + name + ".json";
|
||||||
const deletedMarkerPath = (name) => secretStateDir + "/" + name + ".deleted";
|
const deletedMarkerPath = (name) => secretStateDir + "/" + name + ".deleted";
|
||||||
|
mkdirSync(secretStateDir, { recursive: true });
|
||||||
const fixtureSecret = (name) => ({ apiVersion: "v1", kind: "Secret", metadata: { name, namespace: "agentrun-v01", resourceVersion: "rv-selftest", creationTimestamp: "2026-06-05T00:00:00.000Z" }, data: { "auth.json": Buffer.from(JSON.stringify({ token: "redacted-fixture" })).toString("base64"), "config.toml": Buffer.from("model = \\\"fixture\\\"\\n").toString("base64") } });
|
const fixtureSecret = (name) => ({ apiVersion: "v1", kind: "Secret", metadata: { name, namespace: "agentrun-v01", resourceVersion: "rv-selftest", creationTimestamp: "2026-06-05T00:00:00.000Z" }, data: { "auth.json": Buffer.from(JSON.stringify({ token: "redacted-fixture" })).toString("base64"), "config.toml": Buffer.from("model = \\\"fixture\\\"\\n").toString("base64") } });
|
||||||
const readStdin = async () => {
|
const readStdin = async () => {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -119,7 +120,7 @@ if (args[0] === "create") {
|
|||||||
if (args[0] === "delete" && args[1] === "secret") {
|
if (args[0] === "delete" && args[1] === "secret") {
|
||||||
const name = args[2];
|
const name = args[2];
|
||||||
try { rmSync(secretStatePath(name)); } catch {}
|
try { rmSync(secretStatePath(name)); } catch {}
|
||||||
await Bun.write(deletedMarkerPath(name), "deleted\n");
|
await Bun.write(deletedMarkerPath(name), "deleted\\n");
|
||||||
console.log(JSON.stringify({ kind: "Status", status: "Success", details: { name } }));
|
console.log(JSON.stringify({ kind: "Status", status: "Success", details: { name } }));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -265,6 +266,24 @@ process.exit(1);
|
|||||||
const dynamicReplaceData = dynamicReplaceManifest.data as JsonRecord;
|
const dynamicReplaceData = dynamicReplaceManifest.data as JsonRecord;
|
||||||
assert.equal(Buffer.from(String(dynamicReplaceData["auth.json"]), "base64").toString("utf8").includes(secretText), true);
|
assert.equal(Buffer.from(String(dynamicReplaceData["auth.json"]), "base64").toString("utf8").includes(secretText), true);
|
||||||
assert.equal(Buffer.from(String(dynamicReplaceData["config.toml"]), "base64").toString("utf8"), dynamicConfigToml);
|
assert.equal(Buffer.from(String(dynamicReplaceData["config.toml"]), "base64").toString("utf8"), dynamicConfigToml);
|
||||||
|
|
||||||
|
const directAuthJsonSecret = "sk-selftest-auth-json-direct";
|
||||||
|
const directAuthJson = `${JSON.stringify({ OPENAI_API_KEY: directAuthJsonSecret })}\n`;
|
||||||
|
const dynamicAuthJsonCredential = await client.put(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}/credential`, {
|
||||||
|
authJson: directAuthJson,
|
||||||
|
delegatedBy: { system: "hwlab-v02", userId: "u4", username: "tester4", requestId: "req-dynamic-auth-json-selftest" },
|
||||||
|
reason: "self-test-dynamic-auth-json",
|
||||||
|
}) as JsonRecord;
|
||||||
|
assert.equal(dynamicAuthJsonCredential.profile, dynamicProfile);
|
||||||
|
assert.equal(dynamicAuthJsonCredential.credentialSource, "auth-json");
|
||||||
|
assert.equal(JSON.stringify(dynamicAuthJsonCredential).includes(directAuthJsonSecret), false);
|
||||||
|
assertNoSecretLeak(dynamicAuthJsonCredential);
|
||||||
|
const dynamicAuthJsonReplaceRecord = JSON.parse(await readFile(replacedSecretPath, "utf8")) as JsonRecord;
|
||||||
|
const dynamicAuthJsonReplaceManifest = dynamicAuthJsonReplaceRecord.manifest as JsonRecord;
|
||||||
|
const dynamicAuthJsonData = dynamicAuthJsonReplaceManifest.data as JsonRecord;
|
||||||
|
assert.equal(Buffer.from(String(dynamicAuthJsonData["auth.json"]), "base64").toString("utf8"), directAuthJson);
|
||||||
|
assert.equal(Buffer.from(String(dynamicAuthJsonData["config.toml"]), "base64").toString("utf8"), dynamicConfigToml);
|
||||||
|
|
||||||
const dynamicShown = await client.get(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}`) as JsonRecord;
|
const dynamicShown = await client.get(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}`) as JsonRecord;
|
||||||
assert.equal(dynamicShown.configured, true);
|
assert.equal(dynamicShown.configured, true);
|
||||||
assert.equal(dynamicShown.failureKind, null);
|
assert.equal(dynamicShown.failureKind, null);
|
||||||
@@ -322,7 +341,7 @@ process.exit(1);
|
|||||||
assert.equal(finalValidation.status, "completed");
|
assert.equal(finalValidation.status, "completed");
|
||||||
assert.equal(JSON.stringify(finalValidation).includes(secretText), false);
|
assert.equal(JSON.stringify(finalValidation).includes(secretText), false);
|
||||||
assertNoSecretLeak(finalValidation);
|
assertNoSecretLeak(finalValidation);
|
||||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||||
} finally {
|
} finally {
|
||||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user