From 97bf7f2786137c0f91fbd6ae85606c3d52e379e2 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 16 Jul 2026 03:15:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9BPikaOA=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E5=89=8D=E7=BD=AE=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/platform-db/postgres-pk01.yaml | 1 - config/secrets-distribution.yaml | 6 +- scripts/src/pikaoa-attachments-backup.test.ts | 66 ++++++++++++++++++- scripts/src/pikaoa-attachments-backup.ts | 27 ++++++-- .../src/platform-db-postgres-backups.test.ts | 8 +++ 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/config/platform-db/postgres-pk01.yaml b/config/platform-db/postgres-pk01.yaml index 643f05ed..2e8d0056 100644 --- a/config/platform-db/postgres-pk01.yaml +++ b/config/platform-db/postgres-pk01.yaml @@ -728,7 +728,6 @@ backup: encryption: enabled: false futureKeyRef: platform-db/backup-age-recipient.txt - futureKeyRef: platform-db/backup-age-recipient.txt physicalWalArchive: enabled: false futureTool: pgbackrest diff --git a/config/secrets-distribution.yaml b/config/secrets-distribution.yaml index 112cf321..b741e3fb 100644 --- a/config/secrets-distribution.yaml +++ b/config/secrets-distribution.yaml @@ -106,7 +106,11 @@ sources: requiredKeys: - RESTIC_PASSWORD createIfMissing: - enabled: false + enabled: true + randomBase64Url: + RESTIC_PASSWORD: + bytes: 32 + prefix: poa_restic_ - sourceRef: platform-infra/pikaoa-test.env type: env requiredKeys: diff --git a/scripts/src/pikaoa-attachments-backup.test.ts b/scripts/src/pikaoa-attachments-backup.test.ts index a6b1d86a..f665f148 100644 --- a/scripts/src/pikaoa-attachments-backup.test.ts +++ b/scripts/src/pikaoa-attachments-backup.test.ts @@ -1,6 +1,9 @@ import { describe, expect, test } from "bun:test"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join, relative } from "node:path"; import type { UniDeskConfig } from "./config"; -import { readPikaoaAttachmentsBackupSpec, renderPikaoaAttachmentsBackupManifest, renderPikaoaAttachmentsInitJob, renderPikaoaAttachmentsRestoreSmokeJob, runPikaoaAttachmentsBackupCommand } from "./pikaoa-attachments-backup"; +import { projectPikaoaAttachmentsRepositoryPrerequisite, readPikaoaAttachmentsBackupSpec, renderPikaoaAttachmentsBackupManifest, renderPikaoaAttachmentsInitJob, renderPikaoaAttachmentsRestoreSmokeJob, runPikaoaAttachmentsBackupCommand } from "./pikaoa-attachments-backup"; +import { runSecretsCommand } from "./secrets"; describe("PikaOA attachments backup", () => { test("owning YAML renders separate read-only backup and maintenance CronJobs", () => { @@ -21,6 +24,7 @@ describe("PikaOA attachments backup", () => { expect(manifest).toContain("readOnly: true"); expect(manifest).toContain("hostPath:"); expect(manifest).toContain("path: /var/backups/unidesk/pikaoa/attachments-restic"); + expect(manifest).toContain("type: DirectoryOrCreate"); expect(manifest).toContain("mountPath: /repository"); expect(manifest).toContain("--exclude=.tmp"); expect(manifest).toContain("forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune"); @@ -30,6 +34,66 @@ describe("PikaOA attachments backup", () => { expect(manifest.split("---\n").map((document) => Bun.YAML.parse(document))).toHaveLength(4); }); + test("local directory is created by Kubernetes while missing SFTP directories remain blocking", () => { + expect(projectPikaoaAttachmentsRepositoryPrerequisite("local-directory", false)).toEqual({ + exists: false, + disposition: "will-create", + blocking: false, + }); + expect(projectPikaoaAttachmentsRepositoryPrerequisite("sftp", false)).toEqual({ + exists: false, + disposition: "waiting", + blocking: true, + }); + }); + + test("missing RESTIC_PASSWORD is pending generation instead of blocking secrets plan", async () => { + const fixtureRoot = mkdtempSync(join(process.cwd(), ".pikaoa-attachments-secret-plan-")); + try { + const sourceConfig = Bun.YAML.parse(readFileSync("config/secrets-distribution.yaml", "utf8")) as { + version: unknown; + kind: unknown; + metadata: unknown; + sources: { files: Array> }; + targets: Array>; + kubernetesSecrets: Array>; + }; + const source = sourceConfig.sources.files.find((item: Record) => item.sourceRef === "platform-infra/pikaoa-attachments-backup.env"); + const target = sourceConfig.targets.find((item: Record) => item.id === "pikaoa-nc01"); + const secret = sourceConfig.kubernetesSecrets.find((item: Record) => item.name === "pikaoa-attachments-backup"); + const fixtureConfig = { + version: sourceConfig.version, + kind: sourceConfig.kind, + metadata: sourceConfig.metadata, + sources: { root: join(fixtureRoot, "secrets"), files: [source], externalFiles: [] }, + targets: [target], + kubernetesSecrets: [secret], + }; + const fixturePath = join(fixtureRoot, "secrets-distribution.yaml"); + writeFileSync(fixturePath, Bun.YAML.stringify(fixtureConfig)); + + const planned = await runSecretsCommand({} as UniDeskConfig, ["plan", "--config", relative(process.cwd(), fixturePath), "--scope", "pikaoa", "--target", "pikaoa-nc01"]); + const localSources = planned.localSources as { entries: Array> }; + const desiredSecrets = planned.desiredSecrets as Array>; + + expect(planned.ok).toBeTrue(); + expect(localSources.entries[0]).toMatchObject({ + action: "create", + generatedKeys: ["RESTIC_PASSWORD"], + unmaterializedGeneratedKeys: ["RESTIC_PASSWORD"], + missingKeys: [], + valuesPrinted: false, + }); + expect(desiredSecrets[0]).toMatchObject({ + missingKeys: [], + pendingGeneratedKeys: ["RESTIC_PASSWORD"], + valuesPrinted: false, + }); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }); + test("restore smoke only uses a temporary emptyDir", () => { const spec = readPikaoaAttachmentsBackupSpec("config/pikaoa.yaml"); const manifest = renderPikaoaAttachmentsRestoreSmokeJob(spec, "pikaoa-attachments-restore-smoke-test"); diff --git a/scripts/src/pikaoa-attachments-backup.ts b/scripts/src/pikaoa-attachments-backup.ts index 0ef695a0..21676fc7 100644 --- a/scripts/src/pikaoa-attachments-backup.ts +++ b/scripts/src/pikaoa-attachments-backup.ts @@ -21,7 +21,19 @@ export interface PikaoaAttachmentsBackupSpec { restoreSmoke: { jobPrefix: string; emptyDirSizeLimit: string; ttlSecondsAfterFinished: number }; jobs: { initJobPrefix: string; backupCronJobName: string; checkCronJobName: string; forgetPruneCronJobName: string; serviceAccountName: string; fieldManager: string }; } -interface Prerequisites { namespace: boolean; sourcePvc: boolean; secret: boolean; destinationDirectory: boolean; ready: boolean; waiting: string[] } +interface Prerequisites { + namespace: boolean; + sourcePvc: boolean; + secret: boolean; + repositoryDirectory: PikaoaAttachmentsRepositoryPrerequisite; + ready: boolean; + waiting: string[]; +} +export interface PikaoaAttachmentsRepositoryPrerequisite { + exists: boolean; + disposition: "ready" | "will-create" | "waiting"; + blocking: boolean; +} const DEFAULT_CONFIG_PATH = rootPath("config", "pikaoa.yaml"); @@ -286,12 +298,19 @@ async function inspectPrerequisites(config: UniDeskConfig, spec: PikaoaAttachmen const sourcePvc = namespace && await remoteExists(config, spec.route, ["kubectl", "-n", spec.namespace, "get", "pvc", spec.source.claimName, "-o", "name"]); const secret = namespace && await remoteExists(config, spec.route, ["kubectl", "-n", spec.namespace, "get", "secret", spec.secret.secretName, "-o", "name"]); const destinationDirectory = await remoteExists(config, spec.destination.route, ["sh"], `test -d ${shQuote(destinationPath(spec))}\n`); + const repositoryDirectory = projectPikaoaAttachmentsRepositoryPrerequisite(spec.destination.type, destinationDirectory); const waiting: string[] = []; if (!namespace) waiting.push("production-namespace"); if (!sourcePvc) waiting.push("production-attachments-pvc"); if (!secret) waiting.push("yaml-first-secret-sync"); - if (!destinationDirectory) waiting.push("repository-directory"); - return { namespace, sourcePvc, secret, destinationDirectory, ready: waiting.length === 0, waiting }; + if (repositoryDirectory.blocking) waiting.push("repository-directory"); + return { namespace, sourcePvc, secret, repositoryDirectory, ready: waiting.length === 0, waiting }; +} + +export function projectPikaoaAttachmentsRepositoryPrerequisite(type: PikaoaAttachmentsBackupSpec["destination"]["type"], exists: boolean): PikaoaAttachmentsRepositoryPrerequisite { + if (exists) return { exists: true, disposition: "ready", blocking: false }; + if (type === "local-directory") return { exists: false, disposition: "will-create", blocking: false }; + return { exists: false, disposition: "waiting", blocking: true }; } async function remoteExists(config: UniDeskConfig, route: string, args: string[], stdin = ""): Promise { @@ -375,7 +394,7 @@ function repositoryVolumeMount(spec: PikaoaAttachmentsBackupSpec, spaces: number function repositoryVolume(spec: PikaoaAttachmentsBackupSpec, spaces: number): string { if (spec.destination.type !== "local-directory") return ""; - return `${" ".repeat(spaces)}- name: repository\n${" ".repeat(spaces + 2)}hostPath:\n${" ".repeat(spaces + 4)}path: ${spec.destination.path}\n${" ".repeat(spaces + 4)}type: Directory\n`; + return `${" ".repeat(spaces)}- name: repository\n${" ".repeat(spaces + 2)}hostPath:\n${" ".repeat(spaces + 4)}path: ${spec.destination.path}\n${" ".repeat(spaces + 4)}type: DirectoryOrCreate\n`; } function indentBlock(value: string, spaces: number): string { diff --git a/scripts/src/platform-db-postgres-backups.test.ts b/scripts/src/platform-db-postgres-backups.test.ts index 4de65ca6..3cf5de52 100644 --- a/scripts/src/platform-db-postgres-backups.test.ts +++ b/scripts/src/platform-db-postgres-backups.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; import { projectLogicalDumps } from "./platform-db-postgres-backups"; import { readPostgresLogicalDumps, renderPostgresRemoteRunnerForTest } from "./platform-db"; @@ -34,6 +35,13 @@ describe("PostgreSQL logical dumps", () => { expect(runner).not.toContain("OnCalendar=*-*-* 03:17:00"); }); + test("PK01 backup YAML has no duplicate future encryption key", () => { + const source = readFileSync("config/platform-db/postgres-pk01.yaml", "utf8"); + + expect(source.match(/^\s+futureKeyRef:/gmu)).toHaveLength(1); + expect(() => readPostgresLogicalDumps("config/platform-db/postgres-pk01.yaml")).not.toThrow(); + }); + test("reports stale or missing evidence as warnings", () => { const [declaration] = readPostgresLogicalDumps("config/platform-db/postgres-pk01.yaml"); const [projection] = projectLogicalDumps([declaration], [{