diff --git a/scripts/src/platform-db.ts b/scripts/src/platform-db.ts index 7e632d92..99bed44f 100644 --- a/scripts/src/platform-db.ts +++ b/scripts/src/platform-db.ts @@ -1,4 +1,4 @@ -import { createHash, randomBytes } from "node:crypto"; +import { randomBytes } from "node:crypto"; import { spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; @@ -6,6 +6,7 @@ import type { UniDeskConfig } from "./config"; import { repoRoot, rootPath } from "./config"; import { startJob } from "./jobs"; import { runSshCommandCapture, type SshCaptureResult } from "./ssh"; +import { compactText, fingerprintEnvValues as fingerprintValues, parseEnvFile, parseJsonOutput } from "./platform-infra-ops-library"; const defaultConfigPath = "config/platform-db/postgres-pk01.yaml"; const managedHbaStart = "# BEGIN unidesk managed pk01-platform-postgres"; @@ -1114,27 +1115,6 @@ function secretRoot(pg: PostgresHostConfig): string { return isAbsolute(pg.secrets.root) ? pg.secrets.root : rootPath(pg.secrets.root); } -function parseEnvFile(text: string): Record { - const result: Record = {}; - for (const rawLine of text.split(/\r?\n/u)) { - const line = rawLine.trim(); - if (line.length === 0 || line.startsWith("#")) continue; - const eq = line.indexOf("="); - if (eq <= 0) continue; - const key = line.slice(0, eq).trim(); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue; - result[key] = unquoteEnvValue(line.slice(eq + 1).trim()); - } - return result; -} - -function unquoteEnvValue(value: string): string { - if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith("\"") && value.endsWith("\""))) { - return value.slice(1, -1); - } - return value; -} - function writeEnvFile(path: string, values: Record): void { mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); const lines = Object.keys(values) @@ -1149,19 +1129,6 @@ function quoteEnv(value: string): string { return `'${value.replaceAll("'", "'\"'\"'")}'`; } -function compactText(value: string): string { - return value.replace(/\s+/gu, " ").trim().slice(0, 500); -} - -function fingerprintValues(values: Record, keys: string[]): string { - const material = keys - .slice() - .sort() - .map((key) => `${key}=${values[key] ?? ""}`) - .join("\n"); - return `sha256:${createHash("sha256").update(material).digest("hex")}`; -} - function secretSummary(secrets: SecretInspection): Record { return { ok: secrets.ok, @@ -2008,20 +1975,6 @@ function sqlStringList(values: string[]): string { return values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", "); } -function parseJsonOutput(stdout: string): Record | null { - const trimmed = stdout.trim(); - if (trimmed.length === 0) return null; - const start = trimmed.indexOf("{"); - const end = trimmed.lastIndexOf("}"); - if (start === -1 || end === -1 || end <= start) return null; - try { - const parsed = JSON.parse(trimmed.slice(start, end + 1)) as unknown; - return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record : null; - } catch { - return null; - } -} - function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record { const full = options.full ?? false; return { diff --git a/scripts/src/platform-infra-ops-library.ts b/scripts/src/platform-infra-ops-library.ts index ec5defb6..2cea0822 100644 --- a/scripts/src/platform-infra-ops-library.ts +++ b/scripts/src/platform-infra-ops-library.ts @@ -72,6 +72,38 @@ export function fingerprintValues(values: Record, keys: string[] return `sha256:${hash.digest("hex")}`; } +export function fingerprintEnvValues(values: Record, keys: string[]): string { + const material = keys + .slice() + .sort() + .map((key) => `${key}=${values[key] ?? ""}`) + .join("\n"); + return `sha256:${createHash("sha256").update(material).digest("hex")}`; +} + +export function parseEnvFile(text: string): Record { + const result: Record = {}; + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (line.length === 0 || line.startsWith("#")) continue; + const eq = line.indexOf("="); + if (eq <= 0) continue; + const key = line.slice(0, eq).trim(); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue; + result[key] = unquoteEnvValue(line.slice(eq + 1).trim()); + } + return result; +} + +function unquoteEnvValue(value: string): string { + if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith("\"") && value.endsWith("\""))) return value.slice(1, -1); + return value; +} + +export function compactText(value: string, maxChars = 500): string { + return value.replace(/\s+/gu, " ").trim().slice(0, maxChars); +} + export function shQuote(value: string): string { return `'${value.replaceAll("'", "'\"'\"'")}'`; } diff --git a/scripts/src/platform-infra-wechat-archive.ts b/scripts/src/platform-infra-wechat-archive.ts index bc1ce1bc..d36b57ac 100644 --- a/scripts/src/platform-infra-wechat-archive.ts +++ b/scripts/src/platform-infra-wechat-archive.ts @@ -18,6 +18,7 @@ import { normalizeRemotePath, numberField, optionalStringField, + parseEnvFile, parseOpsApplyOptions, parseOpsCommonOptions, readYamlRecord, @@ -2467,22 +2468,3 @@ function readArchiveCallbackToken(config: WechatArchiveConfig): { sourceRef: str fingerprint: fingerprintValues({ [config.archiveCallback.tokenKey]: value }, [config.archiveCallback.tokenKey]), }; } - -function parseEnvFile(text: string): Record { - const result: Record = {}; - for (const rawLine of text.split(/\r?\n/u)) { - const line = rawLine.trim(); - if (line.length === 0 || line.startsWith("#")) continue; - const eq = line.indexOf("="); - if (eq <= 0) continue; - const key = line.slice(0, eq).trim(); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue; - result[key] = unquoteEnvValue(line.slice(eq + 1).trim()); - } - return result; -} - -function unquoteEnvValue(value: string): string { - if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith("\"") && value.endsWith("\""))) return value.slice(1, -1); - return value; -} diff --git a/scripts/src/secrets.ts b/scripts/src/secrets.ts index 6e793443..e7d91254 100644 --- a/scripts/src/secrets.ts +++ b/scripts/src/secrets.ts @@ -12,11 +12,14 @@ import { yamlIntegerField, yamlKubernetesNameField, yamlObjectField, + parseEnvFile, yamlRecord, yamlStringArrayField, yamlStringField, } from "./platform-infra-ops-library"; +export { parseEnvFile } from "./platform-infra-ops-library"; + const defaultConfigPath = "config/secrets-distribution.yaml"; const fieldManager = "unidesk-secret-distribution"; @@ -727,25 +730,6 @@ export function readEnvSourceFile(params: { root: string; sourceRef: string; mis }; } -export function parseEnvFile(text: string): Record { - const result: Record = {}; - for (const rawLine of text.split(/\r?\n/u)) { - const line = rawLine.trim(); - if (line.length === 0 || line.startsWith("#")) continue; - const eq = line.indexOf("="); - if (eq <= 0) continue; - const key = line.slice(0, eq).trim(); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue; - result[key] = unquoteEnvValue(line.slice(eq + 1).trim()); - } - return result; -} - -function unquoteEnvValue(value: string): string { - if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith("\"") && value.endsWith("\""))) return value.slice(1, -1); - return value; -} - export function requiredEnvValue(values: Record, key: string, sourceRef: string): string { const value = values[key]; if (value === undefined || value.length === 0) throw new Error(`${sourceRef} is missing required key ${key}`);