mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
599 lines
19 KiB
JavaScript
599 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const yaml = require('js-yaml');
|
|
|
|
const OPERATOR_DIRECTORY =
|
|
'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg';
|
|
const RUNTIME_PATCH =
|
|
'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg/postgres-runtime-patch.yaml';
|
|
const RUNTIME_KUSTOMIZATION =
|
|
'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg/kustomization.yaml';
|
|
const MIGRATION_PATCH =
|
|
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg/migrate-job-patch.yaml';
|
|
const MIGRATION_KUSTOMIZATION =
|
|
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg/kustomization.yaml';
|
|
const PRIMARY_DNS = 'ql3-postgres-rw.qinglong3-system.svc';
|
|
const APPLICATION_IMAGE_NAME = 'qinglong3-cluster-control';
|
|
const APPLICATION_IMAGE_REPOSITORY =
|
|
'registry.example.com/qinglong/qinglong3-cluster-control';
|
|
const FAIL_CLOSED_IMAGE_DIGEST = `sha256:${'0'.repeat(64)}`;
|
|
const POSTGRES_IMAGE =
|
|
'ghcr.io/cloudnative-pg/postgresql:18.4-minimal-trixie@sha256:24d229d801663f95b584416f8ebdfad4849b1a3fa4cfcf95a7f026df7aa6e22d';
|
|
const OPERATOR_IMAGE =
|
|
'ghcr.io/cloudnative-pg/cloudnative-pg:1.30.0@sha256:a2701eb97cdd2a34b1fdb2cb51987f544b706e40bec72ae7146cd8580efefebb';
|
|
|
|
const EXPECTED_ROLES = Object.freeze({
|
|
ql3_admin: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-admin-auth',
|
|
}),
|
|
ql3_ai_maintenance: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-ai-maintenance-auth',
|
|
}),
|
|
ql3_ai_credential_manager: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-ai-credential-manager-auth',
|
|
}),
|
|
ql3_ai_credential_tester: Object.freeze({
|
|
connectionLimit: 2,
|
|
secret: 'ql3-postgres-ai-credential-tester-auth',
|
|
}),
|
|
ql3_automation_manager: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-automation-manager-auth',
|
|
}),
|
|
ql3_approval_manager: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-approval-manager-auth',
|
|
}),
|
|
ql3_migration: Object.freeze({
|
|
connectionLimit: 2,
|
|
secret: 'ql3-postgres-migration-auth',
|
|
}),
|
|
ql3_package_executor: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-package-executor-auth',
|
|
}),
|
|
ql3_package_manager: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-package-manager-auth',
|
|
}),
|
|
ql3_runtime: Object.freeze({
|
|
connectionLimit: 32,
|
|
secret: 'ql3-postgres-runtime-auth',
|
|
}),
|
|
ql3_run_manager: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-run-manager-auth',
|
|
}),
|
|
ql3_worker_credential_executor: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-worker-credential-executor-auth',
|
|
}),
|
|
ql3_worker_credential_manager: Object.freeze({
|
|
connectionLimit: 4,
|
|
secret: 'ql3-postgres-worker-credential-manager-auth',
|
|
}),
|
|
ql3_worker_ingress: Object.freeze({
|
|
connectionLimit: 32,
|
|
secret: 'ql3-postgres-worker-ingress-auth',
|
|
}),
|
|
});
|
|
|
|
function finding(code, detail) {
|
|
return Object.freeze({ code, detail });
|
|
}
|
|
|
|
function loadDocuments(source) {
|
|
const result = [];
|
|
yaml.loadAll(source, (document) => {
|
|
if (document) result.push(document);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function envByName(container) {
|
|
return new Map(
|
|
(Array.isArray(container?.env) ? container.env : []).map((entry) => [
|
|
entry?.name,
|
|
entry,
|
|
]),
|
|
);
|
|
}
|
|
|
|
function exactSecretRef(entry, secret, key) {
|
|
return (
|
|
entry?.valueFrom?.secretKeyRef?.name === secret &&
|
|
entry?.valueFrom?.secretKeyRef?.key === key
|
|
);
|
|
}
|
|
|
|
function exactFailClosedApplicationImage(kustomization) {
|
|
return (
|
|
Array.isArray(kustomization?.images) &&
|
|
kustomization.images.length === 1 &&
|
|
kustomization.images[0]?.name === APPLICATION_IMAGE_NAME &&
|
|
kustomization.images[0]?.newName === APPLICATION_IMAGE_REPOSITORY &&
|
|
kustomization.images[0]?.digest === FAIL_CLOSED_IMAGE_DIGEST &&
|
|
!Object.hasOwn(kustomization.images[0], 'newTag')
|
|
);
|
|
}
|
|
|
|
function assertOperatorLock(readFile, root, findings) {
|
|
const lock = JSON.parse(
|
|
readFile(path.join(root, OPERATOR_DIRECTORY, 'operator-lock.json'), 'utf8'),
|
|
);
|
|
if (
|
|
lock?.schemaVersion !== 1 ||
|
|
lock?.operator?.name !== 'cloudnative-pg' ||
|
|
lock?.operator?.version !== '1.30.0' ||
|
|
lock?.operator?.image !== OPERATOR_IMAGE ||
|
|
lock?.operator?.platforms?.['linux/amd64'] !==
|
|
'sha256:091d306935cfdf646debfe78010d59ebfb572150eb6eb922b0203873c0c68841' ||
|
|
lock?.operator?.platforms?.['linux/arm64'] !==
|
|
'sha256:6c7926147fd23a053dea6605d61c013d43bfd411be3532e7500ef2d2b68bb98c'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_OPERATOR_LOCK',
|
|
'CloudNativePG 1.30.0 must remain locked to its reviewed multi-platform OCI digest',
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
lock?.operator?.releaseManifest !==
|
|
'https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.30.0/cnpg-1.30.0.yaml' ||
|
|
lock?.operator?.releaseManifestSha256 !==
|
|
'sha256:f8bede43fe4ee0d478c2355b204a36876b2ae4faac60f2a9452280b293da3b88' ||
|
|
lock?.operator?.signatureBundle !==
|
|
'https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.30.0/cnpg-1.30.0.sigstore.json' ||
|
|
lock?.operand?.postgresqlVersion !== '18.4' ||
|
|
lock?.operand?.image !== POSTGRES_IMAGE ||
|
|
lock?.operand?.platforms?.['linux/amd64'] !==
|
|
'sha256:67e56bcbe50a58e60509815dd89e58effc5f331ab844f66331d945cd42131e8d' ||
|
|
lock?.operand?.platforms?.['linux/arm64'] !==
|
|
'sha256:2adbb634a1c0af8cb036e81200aaa7c8b62517bf5e501699926b337bd9f863f1'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_SUPPLY_CHAIN_LOCK',
|
|
'operator release inputs and PostgreSQL 18.4 operand must remain digest locked',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertCluster(readFile, root, findings) {
|
|
const cluster = yaml.load(
|
|
readFile(path.join(root, OPERATOR_DIRECTORY, 'cluster.yaml'), 'utf8'),
|
|
);
|
|
const spec = cluster?.spec;
|
|
const synchronous = spec?.postgresql?.synchronous;
|
|
if (
|
|
cluster?.apiVersion !== 'postgresql.cnpg.io/v1' ||
|
|
cluster?.kind !== 'Cluster' ||
|
|
cluster?.metadata?.name !== 'ql3-postgres' ||
|
|
spec?.instances !== 3 ||
|
|
spec?.imageName !== POSTGRES_IMAGE ||
|
|
spec?.enableSuperuserAccess !== false
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_CLUSTER_BASELINE',
|
|
'the production cluster must use three instances, disabled superuser access and the locked operand',
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
synchronous?.method !== 'any' ||
|
|
synchronous?.number !== 1 ||
|
|
synchronous?.dataDurability !== 'required' ||
|
|
synchronous?.failoverQuorum !== true ||
|
|
spec?.postgresql?.parameters?.synchronous_commit !== 'remote_apply'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_DURABILITY',
|
|
'the HA profile must require one remote-applied synchronous replica and failover quorum',
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
spec?.affinity?.enablePodAntiAffinity !== 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_FAILURE_DOMAIN',
|
|
'database instances must occupy distinct nodes and expose only the primary-tracking rw service',
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
spec?.resources?.requests?.cpu !== '250m' ||
|
|
spec?.resources?.requests?.memory !== '512Mi' ||
|
|
spec?.resources?.limits?.cpu !== '2' ||
|
|
spec?.resources?.limits?.memory !== '1Gi' ||
|
|
spec?.storage?.size !== '20Gi' ||
|
|
spec?.walStorage?.size !== '5Gi'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_RESOURCE_ENVELOPE',
|
|
'database CPU, memory, data and WAL envelopes must remain explicit',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertRolesAndDatabase(readFile, root, findings) {
|
|
const roles = loadDocuments(
|
|
readFile(
|
|
path.join(root, OPERATOR_DIRECTORY, 'database-roles.yaml'),
|
|
'utf8',
|
|
),
|
|
);
|
|
const seen = new Set();
|
|
for (const role of roles) {
|
|
const expected = EXPECTED_ROLES[role?.spec?.name];
|
|
const spec = role?.spec;
|
|
if (
|
|
role?.apiVersion !== 'postgresql.cnpg.io/v1' ||
|
|
role?.kind !== 'DatabaseRole' ||
|
|
!expected ||
|
|
seen.has(spec?.name) ||
|
|
spec?.cluster?.name !== 'ql3-postgres' ||
|
|
spec?.login !== true ||
|
|
spec?.superuser !== false ||
|
|
spec?.createdb !== false ||
|
|
spec?.createrole !== false ||
|
|
spec?.replication !== false ||
|
|
spec?.bypassrls !== false ||
|
|
spec?.connectionLimit !== expected?.connectionLimit ||
|
|
spec?.databaseRoleReclaimPolicy !== 'retain' ||
|
|
spec?.passwordSecret?.name !== expected?.secret
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_DATABASE_ROLE',
|
|
'DatabaseRole resources must match the fourteen fixed least-privilege identities',
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
seen.add(spec.name);
|
|
}
|
|
if (
|
|
seen.size !== Object.keys(EXPECTED_ROLES).length ||
|
|
Object.keys(EXPECTED_ROLES).some((name) => !seen.has(name))
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_DATABASE_ROLE_SET',
|
|
'exactly the fourteen reviewed migration, runtime, AI maintenance, AI credential management/testing, admin, automation, Approval, Run, package and Worker roles are required',
|
|
),
|
|
);
|
|
}
|
|
|
|
const database = yaml.load(
|
|
readFile(path.join(root, OPERATOR_DIRECTORY, 'database.yaml'), 'utf8'),
|
|
);
|
|
if (
|
|
database?.apiVersion !== 'postgresql.cnpg.io/v1' ||
|
|
database?.kind !== 'Database' ||
|
|
database?.spec?.cluster?.name !== 'ql3-postgres' ||
|
|
database?.spec?.name !== 'qinglong' ||
|
|
database?.spec?.owner !== 'ql3_migration'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_DATABASE_OWNER',
|
|
'the qinglong database must be owned only by the migration role',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertSecretBoundary(readFile, root, findings) {
|
|
const kustomization = yaml.load(
|
|
readFile(path.join(root, OPERATOR_DIRECTORY, 'kustomization.yaml'), 'utf8'),
|
|
);
|
|
const resources = kustomization?.resources;
|
|
if (
|
|
JSON.stringify(resources) !==
|
|
JSON.stringify([
|
|
'cluster.yaml',
|
|
'database-roles.yaml',
|
|
'database.yaml',
|
|
]) ||
|
|
resources?.includes('credentials.example.yaml')
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_SECRET_APPLICATION_BOUNDARY',
|
|
'credential examples must never be part of the operator kustomization',
|
|
),
|
|
);
|
|
}
|
|
const secrets = loadDocuments(
|
|
readFile(
|
|
path.join(root, OPERATOR_DIRECTORY, 'credentials.example.yaml'),
|
|
'utf8',
|
|
),
|
|
);
|
|
const expectedUsers = new Map([
|
|
['ql3-postgres-migration-auth', 'ql3_migration'],
|
|
['ql3-postgres-ai-maintenance-auth', 'ql3_ai_maintenance'],
|
|
['ql3-postgres-ai-credential-manager-auth', 'ql3_ai_credential_manager'],
|
|
['ql3-postgres-ai-credential-tester-auth', 'ql3_ai_credential_tester'],
|
|
['ql3-postgres-runtime-auth', 'ql3_runtime'],
|
|
['ql3-postgres-admin-auth', 'ql3_admin'],
|
|
['ql3-postgres-automation-manager-auth', 'ql3_automation_manager'],
|
|
['ql3-postgres-approval-manager-auth', 'ql3_approval_manager'],
|
|
['ql3-postgres-run-manager-auth', 'ql3_run_manager'],
|
|
['ql3-postgres-worker-ingress-auth', 'ql3_worker_ingress'],
|
|
['ql3-postgres-package-manager-auth', 'ql3_package_manager'],
|
|
['ql3-postgres-package-executor-auth', 'ql3_package_executor'],
|
|
[
|
|
'ql3-postgres-worker-credential-manager-auth',
|
|
'ql3_worker_credential_manager',
|
|
],
|
|
[
|
|
'ql3-postgres-worker-credential-executor-auth',
|
|
'ql3_worker_credential_executor',
|
|
],
|
|
]);
|
|
for (const secret of secrets) {
|
|
const name = secret?.metadata?.name;
|
|
if (name === 'ql3-cluster-control-runtime') {
|
|
if (
|
|
secret?.type !== 'Opaque' ||
|
|
secret?.stringData?.['api-credential-pepper-keyring.json'] !==
|
|
'{"schemaVersion":1,"activePepperKeyId":"REPLACE_WITH_ACTIVE_KEY_ID","keys":[{"pepperKeyId":"REPLACE_WITH_ACTIVE_KEY_ID","pepper":"REPLACE_WITH_CANONICAL_32_BYTE_BASE64URL"}]}\n'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_RUNTIME_SECRET_EXAMPLE',
|
|
'runtime Secret example must contain only the bounded placeholder credential pepper keyring',
|
|
),
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
if (name === 'ql3-cluster-worker-ingress') {
|
|
const data = secret?.stringData;
|
|
const keys =
|
|
data && typeof data === 'object' ? Object.keys(data).sort() : [];
|
|
if (
|
|
secret?.type !== 'Opaque' ||
|
|
JSON.stringify(keys) !==
|
|
JSON.stringify([
|
|
'artifact-s3-bucket',
|
|
'artifact-s3-encryption',
|
|
'artifact-s3-region',
|
|
'client-ca.crt',
|
|
'tls.crt',
|
|
'tls.key',
|
|
'worker-credential-pepper',
|
|
]) ||
|
|
data?.['artifact-s3-encryption'] !== 's3' ||
|
|
keys
|
|
.filter((key) => key !== 'artifact-s3-encryption')
|
|
.some((key) => !String(data[key]).includes('REPLACE_WITH_'))
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_WORKER_INGRESS_SECRET_EXAMPLE',
|
|
'Worker ingress Secret example must contain only reviewed placeholders',
|
|
),
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
if (
|
|
secret?.type !== 'kubernetes.io/basic-auth' ||
|
|
secret?.stringData?.username !== expectedUsers.get(name) ||
|
|
secret?.stringData?.password !== 'REPLACE_WITH_SECRET_MANAGER_VALUE'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_DATABASE_SECRET_EXAMPLE',
|
|
'database Secret examples must contain only fixed usernames and non-secret placeholders',
|
|
),
|
|
);
|
|
}
|
|
expectedUsers.delete(name);
|
|
}
|
|
if (
|
|
expectedUsers.size !== 0 ||
|
|
secrets.filter(
|
|
(secret) => secret?.metadata?.name === 'ql3-cluster-control-runtime',
|
|
).length !== 1 ||
|
|
secrets.filter(
|
|
(secret) => secret?.metadata?.name === 'ql3-cluster-worker-ingress',
|
|
).length !== 1
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_SECRET_EXAMPLE_SET',
|
|
'the example must describe all fourteen database credentials and the reviewed workload Secrets',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertRuntimeBinding(readFile, root, findings) {
|
|
const patch = yaml.load(readFile(path.join(root, RUNTIME_PATCH), 'utf8'));
|
|
const kustomization = yaml.load(
|
|
readFile(path.join(root, RUNTIME_KUSTOMIZATION), 'utf8'),
|
|
);
|
|
const podSpec = patch?.spec?.template?.spec;
|
|
const container = podSpec?.containers?.find(
|
|
(candidate) => candidate?.name === 'cluster-control',
|
|
);
|
|
const env = envByName(container);
|
|
const caVolume = podSpec?.volumes?.find(
|
|
(candidate) => candidate?.name === 'postgres-runtime-projected',
|
|
);
|
|
const projectedSources = caVolume?.projected?.sources;
|
|
if (
|
|
!exactFailClosedApplicationImage(kustomization) ||
|
|
env.get('QL3_POSTGRES_RUNTIME_URL')?.$patch !== 'delete' ||
|
|
env.get('QL3_POSTGRES_RUNTIME_HOST')?.value !== PRIMARY_DNS ||
|
|
env.get('QL3_POSTGRES_RUNTIME_PORT')?.value !== '5432' ||
|
|
env.get('QL3_POSTGRES_RUNTIME_DATABASE')?.value !== 'qinglong' ||
|
|
!exactSecretRef(
|
|
env.get('QL3_POSTGRES_RUNTIME_USER'),
|
|
'ql3-postgres-runtime-auth',
|
|
'username',
|
|
) ||
|
|
!exactSecretRef(
|
|
env.get('QL3_POSTGRES_RUNTIME_PASSWORD'),
|
|
'ql3-postgres-runtime-auth',
|
|
'password',
|
|
) ||
|
|
env.get('QL3_POSTGRES_TLS_SERVERNAME')?.value !== PRIMARY_DNS ||
|
|
caVolume?.secret !== null ||
|
|
caVolume?.projected?.defaultMode !== 292 ||
|
|
JSON.stringify(projectedSources) !==
|
|
JSON.stringify([
|
|
{
|
|
secret: {
|
|
name: 'ql3-postgres-ca',
|
|
items: [{ key: 'ca.crt', path: 'ca.crt' }],
|
|
},
|
|
},
|
|
{
|
|
secret: {
|
|
name: 'ql3-cluster-control-runtime',
|
|
items: [
|
|
{
|
|
key: 'api-credential-pepper-keyring.json',
|
|
path: 'api-credential-pepper-keyring.json',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
])
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_RUNTIME_BINDING',
|
|
'cluster-control must use discrete runtime credentials, the primary service DNS and operator CA',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertMigrationBinding(readFile, root, findings) {
|
|
const operations = yaml.load(
|
|
readFile(path.join(root, MIGRATION_PATCH), 'utf8'),
|
|
);
|
|
const kustomization = yaml.load(
|
|
readFile(path.join(root, MIGRATION_KUSTOMIZATION), 'utf8'),
|
|
);
|
|
const envOperation = operations?.find(
|
|
(operation) =>
|
|
operation?.op === 'replace' &&
|
|
operation?.path === '/spec/template/spec/containers/0/env',
|
|
);
|
|
const caOperation = operations?.find(
|
|
(operation) =>
|
|
operation?.op === 'replace' &&
|
|
operation?.path === '/spec/template/spec/volumes/1/secret/secretName',
|
|
);
|
|
const caKeyOperation = operations?.find(
|
|
(operation) =>
|
|
operation?.op === 'replace' &&
|
|
operation?.path === '/spec/template/spec/volumes/1/secret/items/0/key',
|
|
);
|
|
const env = new Map(
|
|
(Array.isArray(envOperation?.value) ? envOperation.value : []).map(
|
|
(entry) => [entry?.name, entry],
|
|
),
|
|
);
|
|
if (
|
|
!exactFailClosedApplicationImage(kustomization) ||
|
|
env.has('QL3_POSTGRES_MIGRATION_URL') ||
|
|
env.has('QL3_POSTGRES_RUNTIME_URL') ||
|
|
env.has('QL3_API_CREDENTIAL_PEPPER') ||
|
|
env.has('QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE') ||
|
|
env.get('QL3_POSTGRES_MIGRATION_HOST')?.value !== PRIMARY_DNS ||
|
|
env.get('QL3_POSTGRES_MIGRATION_PORT')?.value !== '5432' ||
|
|
env.get('QL3_POSTGRES_MIGRATION_DATABASE')?.value !== 'qinglong' ||
|
|
!exactSecretRef(
|
|
env.get('QL3_POSTGRES_MIGRATION_USER'),
|
|
'ql3-postgres-migration-auth',
|
|
'username',
|
|
) ||
|
|
!exactSecretRef(
|
|
env.get('QL3_POSTGRES_MIGRATION_PASSWORD'),
|
|
'ql3-postgres-migration-auth',
|
|
'password',
|
|
) ||
|
|
env.get('QL3_POSTGRES_TLS_MODE')?.value !== 'verify-full' ||
|
|
env.get('QL3_POSTGRES_TLS_SERVERNAME')?.value !== PRIMARY_DNS ||
|
|
caOperation?.value !== 'ql3-postgres-ca' ||
|
|
caKeyOperation?.value !== 'ca.crt'
|
|
) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_MIGRATION_BINDING',
|
|
'the one-shot migration must use only discrete migration credentials and operator TLS authority',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function auditCloudNativePgDeployment(options = {}) {
|
|
const root = path.resolve(options.root ?? path.join(__dirname, '..'));
|
|
const readFile = options.readFile ?? fs.readFileSync;
|
|
const findings = [];
|
|
try {
|
|
assertOperatorLock(readFile, root, findings);
|
|
assertCluster(readFile, root, findings);
|
|
assertRolesAndDatabase(readFile, root, findings);
|
|
assertSecretBoundary(readFile, root, findings);
|
|
assertRuntimeBinding(readFile, root, findings);
|
|
assertMigrationBinding(readFile, root, findings);
|
|
} catch (error) {
|
|
findings.push(
|
|
finding(
|
|
'QL3_CNPG_AUDIT_UNAVAILABLE',
|
|
error instanceof Error ? error.message : 'unknown audit failure',
|
|
),
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
operator: 'cloudnative-pg',
|
|
operatorVersion: '1.30.0',
|
|
postgresqlVersion: '18.4',
|
|
instances: 3,
|
|
primaryService: PRIMARY_DNS,
|
|
roles: Object.freeze(Object.keys(EXPECTED_ROLES).sort()),
|
|
findings: Object.freeze(findings),
|
|
compatible: findings.length === 0,
|
|
});
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const report = auditCloudNativePgDeployment();
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
if (!report.compatible) process.exitCode = 1;
|
|
}
|
|
|
|
module.exports = {
|
|
EXPECTED_ROLES,
|
|
OPERATOR_IMAGE,
|
|
POSTGRES_IMAGE,
|
|
PRIMARY_DNS,
|
|
auditCloudNativePgDeployment,
|
|
};
|