#!/usr/bin/env node 'use strict'; const fs = require('node:fs'); const path = require('node:path'); const yaml = require('js-yaml'); const COMPONENT_DIRECTORY = 'deploy/kubernetes/ql3-cluster/components/barman-cloud-backup'; const RESTORE_DIRECTORY = 'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg-restore'; const PLUGIN_NAME = 'barman-cloud.cloudnative-pg.io'; const BACKUP_OBJECT_STORE = 'ql3-postgres-backup'; const RECOVERY_OBJECT_STORE = 'ql3-postgres-recovery-source'; const POSTGRES_IMAGE = 'ghcr.io/cloudnative-pg/postgresql:18.4-minimal-trixie@sha256:24d229d801663f95b584416f8ebdfad4849b1a3fa4cfcf95a7f026df7aa6e22d'; function finding(code, detail) { return Object.freeze({ code, detail }); } function readYaml(readFile, root, relativePath) { return yaml.load(readFile(path.join(root, relativePath), 'utf8')); } function exactPlugin(plugin, objectStore) { return ( plugin?.name === PLUGIN_NAME && plugin?.isWALArchiver === true && plugin?.parameters?.barmanObjectName === objectStore && Object.keys(plugin.parameters).length === 1 ); } function exactRecoveryPlugin(plugin) { return ( plugin?.name === PLUGIN_NAME && plugin?.parameters?.barmanObjectName === RECOVERY_OBJECT_STORE && plugin?.parameters?.serverName === 'ql3-postgres' && Object.keys(plugin.parameters).length === 2 ); } function assertComponent(readFile, root, findings) { const kustomization = readYaml( readFile, root, `${COMPONENT_DIRECTORY}/kustomization.yaml`, ); if ( kustomization?.apiVersion !== 'kustomize.config.k8s.io/v1alpha1' || kustomization?.kind !== 'Component' || JSON.stringify(kustomization?.resources) !== JSON.stringify(['scheduled-backup.yaml']) || JSON.stringify(kustomization?.patches) !== JSON.stringify([{ path: 'cluster-plugin-patch.yaml' }]) ) { findings.push( finding( 'QL3_CNPG_BACKUP_COMPONENT', 'the backup capability must remain an explicit cluster-only Kustomize Component', ), ); } const serialized = JSON.stringify(kustomization); if ( serialized.includes('object-store') || serialized.includes('private-overlay') ) { findings.push( finding( 'QL3_CNPG_BACKUP_PRIVATE_INPUT', 'provider ObjectStore examples and private overlays must never be applied by the shared Component', ), ); } } function assertWalAndSchedule(readFile, root, findings) { const patch = readYaml( readFile, root, `${COMPONENT_DIRECTORY}/cluster-plugin-patch.yaml`, ); if ( patch?.apiVersion !== 'postgresql.cnpg.io/v1' || patch?.kind !== 'Cluster' || patch?.metadata?.name !== 'ql3-postgres' || patch?.spec?.plugins?.length !== 1 || !exactPlugin(patch.spec.plugins[0], BACKUP_OBJECT_STORE) || patch?.spec?.backup !== undefined ) { findings.push( finding( 'QL3_CNPG_WAL_ARCHIVER', 'the source cluster must use exactly one CNPG-I Barman WAL archiver and no deprecated in-tree backup', ), ); } const schedule = readYaml( readFile, root, `${COMPONENT_DIRECTORY}/scheduled-backup.yaml`, ); if ( schedule?.apiVersion !== 'postgresql.cnpg.io/v1' || schedule?.kind !== 'ScheduledBackup' || schedule?.metadata?.name !== 'ql3-postgres-daily' || schedule?.metadata?.namespace !== 'qinglong3-system' || schedule?.spec?.schedule !== '0 0 0 * * *' || schedule?.spec?.backupOwnerReference !== 'self' || schedule?.spec?.immediate !== false || schedule?.spec?.suspend !== false || schedule?.spec?.target !== 'prefer-standby' || schedule?.spec?.cluster?.name !== 'ql3-postgres' || schedule?.spec?.method !== 'plugin' || schedule?.spec?.pluginConfiguration?.name !== PLUGIN_NAME || Object.keys(schedule?.spec?.pluginConfiguration || {}).length !== 1 ) { findings.push( finding( 'QL3_CNPG_BASE_BACKUP_SCHEDULE', 'daily standby-preferred base backups must use the CNPG-I plugin explicitly', ), ); } } function assertObjectStoreExample(readFile, root, findings) { const objectStore = readYaml( readFile, root, `${COMPONENT_DIRECTORY}/object-store.s3.example.yaml`, ); const configuration = objectStore?.spec?.configuration; if ( objectStore?.apiVersion !== 'barmancloud.cnpg.io/v1' || objectStore?.kind !== 'ObjectStore' || objectStore?.metadata?.name !== BACKUP_OBJECT_STORE || objectStore?.metadata?.namespace !== 'qinglong3-system' || objectStore?.spec?.retentionPolicy !== '30d' || !configuration?.destinationPath?.startsWith( 's3://REPLACE_WITH_VERSIONED_LOCKED_BUCKET/', ) || !configuration?.endpointURL?.startsWith('https://REPLACE_WITH_') || configuration?.s3Credentials?.accessKeyId?.name !== 'ql3-postgres-backup-object-store' || configuration?.s3Credentials?.accessKeyId?.key !== 'ACCESS_KEY_ID' || configuration?.s3Credentials?.secretAccessKey?.name !== 'ql3-postgres-backup-object-store' || configuration?.s3Credentials?.secretAccessKey?.key !== 'ACCESS_SECRET_KEY' || configuration?.wal?.compression !== 'lz4' || configuration?.wal?.encryption !== 'AES256' || configuration?.wal?.maxParallel !== 2 || configuration?.data?.compression !== 'lz4' || configuration?.data?.encryption !== 'AES256' ) { findings.push( finding( 'QL3_CNPG_OBJECT_STORE_CONTRACT', 'the private ObjectStore schema must require HTTPS, placeholders, retention, encryption and bounded compression/concurrency', ), ); } if ( JSON.stringify(objectStore).includes('REPLACE_WITH_SECRET_MANAGER_VALUE') || objectStore?.stringData || objectStore?.data ) { findings.push( finding( 'QL3_CNPG_OBJECT_STORE_SECRET_BOUNDARY', 'the ObjectStore example may reference credentials but must not contain secret material', ), ); } } function assertRestore(readFile, root, findings) { const kustomization = readYaml( readFile, root, `${RESTORE_DIRECTORY}/kustomization.yaml`, ); if ( kustomization?.kind !== 'Kustomization' || kustomization?.namespace !== 'qinglong3-system' || JSON.stringify(kustomization?.resources) !== JSON.stringify(['restore-cluster.yaml']) ) { findings.push( finding( 'QL3_CNPG_RESTORE_OPERATION', 'restore must remain a separate explicit operation with no provider ObjectStore example applied', ), ); } const cluster = readYaml( readFile, root, `${RESTORE_DIRECTORY}/restore-cluster.yaml`, ); const spec = cluster?.spec; const origin = spec?.externalClusters?.[0]; if ( cluster?.apiVersion !== 'postgresql.cnpg.io/v1' || cluster?.kind !== 'Cluster' || cluster?.metadata?.name !== 'ql3-postgres-restore' || cluster?.metadata?.annotations?.['cnpg.io/skipEmptyWalArchiveCheck'] !== undefined || spec?.instances !== 3 || spec?.imageName !== POSTGRES_IMAGE || spec?.enableSuperuserAccess !== false || spec?.bootstrap?.recovery?.source !== 'ql3-postgres-origin' || Object.keys(spec?.bootstrap?.recovery || {}).length !== 1 || spec?.externalClusters?.length !== 1 || origin?.name !== 'ql3-postgres-origin' || !exactRecoveryPlugin(origin?.plugin) || spec?.plugins !== undefined ) { findings.push( finding( 'QL3_CNPG_RESTORE_ISOLATION', 'recovery must bootstrap a distinct digest-pinned cluster from a read-only source without archive-check bypass or source-store writes', ), ); } if ( spec?.postgresql?.parameters?.synchronous_commit !== 'remote_apply' || spec?.postgresql?.synchronous?.method !== 'any' || spec?.postgresql?.synchronous?.number !== 1 || spec?.postgresql?.synchronous?.dataDurability !== 'required' || spec?.postgresql?.synchronous?.failoverQuorum !== true || spec?.affinity?.podAntiAffinityType !== 'required' || spec?.affinity?.topologyKey !== 'kubernetes.io/hostname' || JSON.stringify(spec?.managed?.services?.disabledDefaultServices) !== JSON.stringify(['r', 'ro']) ) { findings.push( finding( 'QL3_CNPG_RESTORE_HA', 'the restored cluster must re-enter the reviewed three-node synchronous HA envelope', ), ); } const sourceStore = readYaml( readFile, root, `${RESTORE_DIRECTORY}/object-store.s3.example.yaml`, ); const sourceConfiguration = sourceStore?.spec?.configuration; if ( sourceStore?.metadata?.name !== RECOVERY_OBJECT_STORE || sourceConfiguration?.s3Credentials?.accessKeyId?.name !== 'ql3-postgres-restore-object-store' || sourceConfiguration?.s3Credentials?.secretAccessKey?.name !== 'ql3-postgres-restore-object-store' || sourceConfiguration?.wal?.maxParallel !== 4 || sourceConfiguration?.serverName !== undefined || sourceStore?.spec?.retentionPolicy !== undefined || sourceConfiguration?.data !== undefined ) { findings.push( finding( 'QL3_CNPG_RECOVERY_SOURCE', 'the recovery ObjectStore must use a distinct credential authority and remain excluded from retention/write policy', ), ); } } function auditCloudNativePgBackup(options = {}) { const root = path.resolve(options.root ?? path.join(__dirname, '..')); const readFile = options.readFile ?? fs.readFileSync; const findings = []; try { assertComponent(readFile, root, findings); assertWalAndSchedule(readFile, root, findings); assertObjectStoreExample(readFile, root, findings); assertRestore(readFile, root, findings); } catch (error) { findings.push( finding( 'QL3_CNPG_BACKUP_AUDIT_UNAVAILABLE', error instanceof Error ? error.message : 'unknown audit failure', ), ); } return Object.freeze({ schemaVersion: 1, plugin: PLUGIN_NAME, sourceCluster: 'ql3-postgres', restoreCluster: 'ql3-postgres-restore', baseBackupSchedule: '0 0 0 * * *', retentionPolicy: '30d', findings: Object.freeze(findings), compatible: findings.length === 0, }); } if (require.main === module) { const report = auditCloudNativePgBackup(); process.stdout.write(`${JSON.stringify(report)}\n`); if (!report.compatible) process.exitCode = 1; } module.exports = { BACKUP_OBJECT_STORE, PLUGIN_NAME, RECOVERY_OBJECT_STORE, auditCloudNativePgBackup, };