mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
#!/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_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 thirteen 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 thirteen reviewed migration, runtime, AI maintenance, AI credential management/testing, admin, automation, Approval, 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-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'] !==
|
||||
'REPLACE_WITH_CANONICAL_32_BYTE_BASE64URL'
|
||||
) {
|
||||
findings.push(
|
||||
finding(
|
||||
'QL3_CNPG_RUNTIME_SECRET_EXAMPLE',
|
||||
'runtime Secret example may contain only the placeholder credential pepper',
|
||||
),
|
||||
);
|
||||
}
|
||||
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 thirteen 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-ca',
|
||||
);
|
||||
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?.secretName !== 'ql3-postgres-ca' ||
|
||||
JSON.stringify(caVolume?.secret?.items) !==
|
||||
JSON.stringify([{ key: 'ca.crt', path: 'ca.crt' }])
|
||||
) {
|
||||
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.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,
|
||||
};
|
||||
Reference in New Issue
Block a user