fix: 收敛PikaOA备份前置条件
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<Record<string, unknown>> };
|
||||
targets: Array<Record<string, unknown>>;
|
||||
kubernetesSecrets: Array<Record<string, unknown>>;
|
||||
};
|
||||
const source = sourceConfig.sources.files.find((item: Record<string, unknown>) => item.sourceRef === "platform-infra/pikaoa-attachments-backup.env");
|
||||
const target = sourceConfig.targets.find((item: Record<string, unknown>) => item.id === "pikaoa-nc01");
|
||||
const secret = sourceConfig.kubernetesSecrets.find((item: Record<string, unknown>) => 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<Record<string, unknown>> };
|
||||
const desiredSecrets = planned.desiredSecrets as Array<Record<string, unknown>>;
|
||||
|
||||
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");
|
||||
|
||||
@@ -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<boolean> {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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], [{
|
||||
|
||||
Reference in New Issue
Block a user