feat: add platform secret distribution cli
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { rootPath } from "./config";
|
||||
import { startJob } from "./jobs";
|
||||
@@ -116,7 +116,7 @@ interface SecretMaterial {
|
||||
dbSourcePath: string;
|
||||
appSourceRef: string;
|
||||
appSourcePath: string;
|
||||
action: "create" | "update" | "none";
|
||||
action: "read";
|
||||
values: {
|
||||
dbUser: string;
|
||||
dbPassword: string;
|
||||
@@ -423,6 +423,7 @@ function plan(options: CommonOptions): Record<string, unknown> {
|
||||
resourcePolicy: "No Kubernetes CPU/memory requests or limits are rendered.",
|
||||
},
|
||||
next: {
|
||||
secrets: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm",
|
||||
dryRun: `bun scripts/cli.ts platform-infra langbot apply --target ${target.id} --dry-run`,
|
||||
apply: `bun scripts/cli.ts platform-infra langbot apply --target ${target.id} --confirm`,
|
||||
status: `bun scripts/cli.ts platform-infra langbot status --target ${target.id}`,
|
||||
@@ -488,6 +489,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
remote: parsed ?? compactCapture(result, { full: true }),
|
||||
pk01Caddy: caddy,
|
||||
next: {
|
||||
secrets: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm",
|
||||
status: `bun scripts/cli.ts platform-infra langbot status --target ${target.id}`,
|
||||
validate: `bun scripts/cli.ts platform-infra langbot validate --target ${target.id}`,
|
||||
bootstrapApiKey: "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm",
|
||||
@@ -989,27 +991,23 @@ function prepareSecretMaterial(langbot: LangBotConfig): SecretMaterial {
|
||||
if (dbUser !== langbot.runtime.database.user) throw new Error(`${langbot.runtime.database.sourceRef}.${langbot.runtime.database.sourceKeys.user} does not match langbot runtime.database.user`);
|
||||
if (dbName !== langbot.runtime.database.dbName) throw new Error(`${langbot.runtime.database.sourceRef}.${langbot.runtime.database.sourceKeys.dbName} does not match langbot runtime.database.dbName`);
|
||||
const appSourcePath = join(root, langbot.runtime.secrets.appSourceRef);
|
||||
const existedBefore = existsSync(appSourcePath);
|
||||
const existing = existedBefore ? parseEnvFile(readFileSync(appSourcePath, "utf8")) : {};
|
||||
const next = { ...existing };
|
||||
if (next.LANGBOT_JWT_SECRET === undefined || next.LANGBOT_JWT_SECRET.length === 0) next.LANGBOT_JWT_SECRET = randomBytes(32).toString("hex");
|
||||
if (next[langbot.apiKey.key] === undefined || next[langbot.apiKey.key].length === 0) next[langbot.apiKey.key] = `lbk_${randomBytes(32).toString("base64url")}`;
|
||||
const required = ["LANGBOT_JWT_SECRET", langbot.apiKey.key];
|
||||
const action = !existedBefore ? "create" : required.some((key) => existing[key] !== next[key]) ? "update" : "none";
|
||||
if (action !== "none") writeEnvFile(appSourcePath, next);
|
||||
if (!existsSync(appSourcePath)) throw new Error(`LangBot app secret source ${redactRepoPath(appSourcePath)} is missing; run bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm first`);
|
||||
const appValues = parseEnvFile(readFileSync(appSourcePath, "utf8"));
|
||||
const jwtSecret = requiredEnvValue(appValues, "LANGBOT_JWT_SECRET", langbot.runtime.secrets.appSourceRef);
|
||||
const apiKey = requiredEnvValue(appValues, langbot.apiKey.key, langbot.apiKey.sourceRef);
|
||||
const values = {
|
||||
dbUser,
|
||||
dbPassword,
|
||||
dbName,
|
||||
jwtSecret: next.LANGBOT_JWT_SECRET,
|
||||
apiKey: next[langbot.apiKey.key],
|
||||
jwtSecret,
|
||||
apiKey,
|
||||
};
|
||||
return {
|
||||
dbSourceRef: langbot.runtime.database.sourceRef,
|
||||
dbSourcePath: redactRepoPath(dbSourcePath),
|
||||
appSourceRef: langbot.runtime.secrets.appSourceRef,
|
||||
appSourcePath: redactRepoPath(appSourcePath),
|
||||
action,
|
||||
action: "read",
|
||||
values,
|
||||
fingerprint: fingerprintValues({ dbPassword, jwtSecret: values.jwtSecret, apiKey: values.apiKey }, ["dbPassword", "jwtSecret", "apiKey"]),
|
||||
valuesPrinted: false,
|
||||
@@ -1663,18 +1661,6 @@ function unquoteEnvValue(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function writeEnvFile(path: string, values: Record<string, string>): void {
|
||||
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
||||
const lines = Object.keys(values).sort().map((key) => `${key}=${quoteEnv(values[key])}`);
|
||||
writeFileSync(path, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
|
||||
chmodSync(path, 0o600);
|
||||
}
|
||||
|
||||
function quoteEnv(value: string): string {
|
||||
if (/^[A-Za-z0-9_./:@%?&=+-]+$/u.test(value)) return value;
|
||||
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
||||
}
|
||||
|
||||
function sqlLiteral(value: string): string {
|
||||
return `'${value.replaceAll("'", "''")}'`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user