#!/usr/bin/env node 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { createHash, randomBytes } = require('node:crypto'); const { spawnSync } = require('node:child_process'); const yaml = require('js-yaml'); const { REGISTRY_EVENT_SCHEMA, createFixture, } = require('./ql3-plugin-package-recovery-e2e-fixture.cjs'); const { CONTRACT_VERSION, MIGRATION_COUNT, } = require('./ql3-plugin-package-recovery-e2e-live-audit.cjs'); const ROOT = path.resolve(__dirname, '..'); const FIXTURE_SCRIPT = path.join( __dirname, 'ql3-plugin-package-recovery-e2e-fixture.cjs', ); const NAMESPACE = 'ql3-plugin-recovery-e2e'; const CONTROL_SERVICE_ACCOUNT = 'ql3-cluster-control'; const RECOVERY_SERVICE_ACCOUNT = 'ql3-plugin-package-recovery'; const POSTGRES_NAME = 'ql3-postgres-live'; const REGISTRY_NAME = 'ql3-registry-live'; const REGISTRY_HOST = `${REGISTRY_NAME}.${NAMESPACE}.svc`; const KIND_NODE_IMAGE = 'kindest/node:v1.32.8@sha256:abd489f042d2b644e2d033f5c2d900bc707798d075e8186cb65e3f1367a9d5a1'; const POSTGRES_IMAGE = 'postgres:18.4-bookworm'; const POSTGRES_INDEX_DIGEST = 'sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296'; const POSTGRES_IMAGE_REFERENCE = `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`; const POSTGRES_REPOSITORY_DIGEST = `postgres@${POSTGRES_INDEX_DIGEST}`; const POSTGRES_RUNTIME_IMAGE = 'postgres:18.4-bookworm-ql3-plugin-recovery-e2e'; const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e'; const DEFAULT_CONTROL_IMAGE = 'qinglong3-cluster-control:ql3-plugin-recovery-e2e'; const REPORT_SCHEMA = 'qinglong/plugin-package-recovery-e2e-live-contract@v3'; const INITIAL_SEED_JOB = 'ql3-plugin-package-e2e-seed-initial'; const INITIAL_RECOVERY_JOB = 'ql3-plugin-package-recovery-initial'; const UPGRADE_SEED_JOB = 'ql3-plugin-package-e2e-seed-upgrade'; const UPGRADE_RECOVERY_JOB = 'ql3-plugin-package-recovery-reject-upgrade'; const SAFE_CLUSTER = /^ql3-plugin-recovery-e2e(?:-[a-z0-9](?:[-a-z0-9]{0,24}[a-z0-9])?)?$/; const KIND = process.env.QL3_KIND_BIN || 'kind'; const KUBECTL = process.env.QL3_KUBECTL_BIN || 'kubectl'; const DOCKER = process.env.QL3_DOCKER_BIN || 'docker'; const OPENSSL = process.env.QL3_OPENSSL_BIN || 'openssl'; const ADMIN_IMAGE = process.env.QL3_ADMIN_IMAGE || DEFAULT_ADMIN_IMAGE; const CONTROL_IMAGE = process.env.QL3_CONTROL_IMAGE || DEFAULT_CONTROL_IMAGE; function fail(message) { throw new Error(message); } function commandLabel(binary, args) { return [path.basename(binary), ...args].join(' '); } function run(binary, args, options = {}) { if (!options.quiet) { process.stderr.write(`+ ${options.label || commandLabel(binary, args)}\n`); } const capture = options.capture === true; const result = spawnSync(binary, args, { cwd: options.cwd || ROOT, env: options.env || process.env, input: options.input, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: capture ? ['pipe', 'pipe', 'pipe'] : [ options.input === undefined ? 'inherit' : 'pipe', 'inherit', 'inherit', ], }); if (result.error) throw result.error; if (result.status !== 0 && !options.allowFailure) { const detail = capture ? `\n${result.stderr || result.stdout || ''}` : ''; fail( `${path.basename(binary)} exited with status ${String( result.status, )}${detail}`, ); } return Object.freeze({ status: result.status, stdout: capture ? result.stdout.trim() : '', stderr: capture ? result.stderr.trim() : '', }); } function kind(args, options = {}) { return run(KIND, args, options); } let kubeconfig = ''; function kubectl(args, options = {}) { return run(KUBECTL, ['--kubeconfig', kubeconfig, ...args], options); } function kubectlJson(args) { const output = kubectl([...args, '-o', 'json'], { capture: true, quiet: true, }).stdout; return JSON.parse(output); } function apply(value, label) { kubectl(['apply', '-f', '-'], { input: `${JSON.stringify(value)}\n`, label, }); } function exactClusterName() { const value = process.env.QL3_KIND_CLUSTER ?? `ql3-plugin-recovery-e2e-${process.pid.toString(36)}`; if (!SAFE_CLUSTER.test(value)) { fail( 'QL3_KIND_CLUSTER must be an exact ql3-plugin-recovery-e2e[-suffix] name', ); } return value; } function kindCreateEnvironment(clusterName) { const required = [ '127.0.0.1', 'localhost', `${clusterName}-control-plane`, `${clusterName}-worker`, `${clusterName}-worker2`, '.svc', '.cluster.local', '10.96.0.0/12', '10.244.0.0/16', '172.16.0.0/12', ]; const values = [process.env.NO_PROXY, process.env.no_proxy, ...required] .flatMap((value) => (value ?? '').split(',')) .map((value) => value.trim()) .filter(Boolean); const noProxy = [...new Set(values)].join(','); return { ...process.env, NO_PROXY: noProxy, no_proxy: noProxy }; } function randomSecret() { return randomBytes(24).toString('base64url'); } function architecture() { if (process.arch === 'x64') return 'amd64'; if (process.arch === 'arm64') return 'arm64'; fail(`unsupported live-gate architecture ${process.arch}`); } function readYamlDocuments(filePath) { const documents = []; yaml.loadAll(fs.readFileSync(filePath, 'utf8'), (value) => { if (value) documents.push(value); }); return documents; } function imageExists(image) { return ( run(DOCKER, ['image', 'inspect', image], { capture: true, quiet: true, allowFailure: true, }).status === 0 ); } function sourceRevision() { const value = process.env.QL3_SOURCE_REVISION ?? ''; if (!/^[a-f0-9]{40}$/.test(value)) { fail( 'QL3_SOURCE_REVISION must be the exact lowercase 40-hex source revision', ); } return value; } function privateReportPath(argv) { if ( argv.length !== 1 || !argv[0].startsWith('--report=') || !path.isAbsolute(argv[0].slice('--report='.length)) ) { fail( 'usage: ql3-plugin-package-recovery-e2e-live-contract ' + '--report=/absolute/private-report.json', ); } const reportFile = argv[0].slice('--report='.length); if (path.resolve(reportFile) !== reportFile) { fail('Plugin Package recovery E2E report path must be canonical'); } if (fs.existsSync(reportFile)) { fail('refusing to overwrite the Plugin Package recovery E2E report'); } const parent = fs.lstatSync(path.dirname(reportFile)); if (!parent.isDirectory() || parent.isSymbolicLink()) { fail('Plugin Package recovery E2E report parent must be a real directory'); } return reportFile; } function writePrivateReport(reportFile, report) { const temporaryReport = path.join( path.dirname(reportFile), `.${path.basename(reportFile)}.${process.pid}.` + `${randomBytes(6).toString('hex')}.tmp`, ); let descriptor; try { descriptor = fs.openSync(temporaryReport, 'wx', 0o600); fs.writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = undefined; fs.linkSync(temporaryReport, reportFile); } finally { if (descriptor !== undefined) fs.closeSync(descriptor); fs.rmSync(temporaryReport, { force: true }); } } function buildImages(revision) { if (process.env.QL3_SKIP_IMAGE_BUILD === '1') { assert.equal(imageExists(ADMIN_IMAGE), true, `${ADMIN_IMAGE} is absent`); assert.equal( imageExists(CONTROL_IMAGE), true, `${CONTROL_IMAGE} is absent`, ); return; } run( DOCKER, [ 'build', '--file', 'deploy/containers/ql3-cluster-admin/Dockerfile', '--tag', ADMIN_IMAGE, '--build-arg', `SOURCE_REVISION=${revision}`, '.', ], { label: 'build current QingLong 3.0 cluster-admin image' }, ); run( DOCKER, [ 'build', '--file', 'deploy/containers/ql3-cluster-control/Dockerfile', '--tag', CONTROL_IMAGE, '--build-arg', `SOURCE_REVISION=${revision}`, '.', ], { label: 'build current QingLong 3.0 cluster-control image' }, ); } function ensurePostgresImage() { if (!imageExists(POSTGRES_IMAGE_REFERENCE)) { run(DOCKER, ['pull', POSTGRES_IMAGE_REFERENCE], { label: 'pull digest-pinned PostgreSQL 18.4 fixture image', }); } const inspection = JSON.parse( run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE_REFERENCE], { capture: true, quiet: true, }).stdout, )[0]; assert.ok( inspection.RepoDigests.includes(POSTGRES_REPOSITORY_DIGEST), `${POSTGRES_IMAGE} does not match ${POSTGRES_REPOSITORY_DIGEST}`, ); run(DOCKER, ['image', 'tag', POSTGRES_IMAGE_REFERENCE, POSTGRES_RUNTIME_IMAGE], { label: 'bind verified PostgreSQL fixture digest to its Kind-local tag', }); assert.equal(imageId(POSTGRES_RUNTIME_IMAGE), inspection.Id); } function imageId(image) { return JSON.parse( run(DOCKER, ['image', 'inspect', image], { capture: true, quiet: true, }).stdout, )[0].Id; } function imageSourceRevision(image) { return JSON.parse( run(DOCKER, ['image', 'inspect', image], { capture: true, quiet: true, }).stdout, )[0].Config?.Labels?.['org.opencontainers.image.revision']; } function createRegistryCertificate(root) { const caKey = path.join(root, 'registry-ca.key'); const caCert = path.join(root, 'registry-ca.crt'); const serverKey = path.join(root, 'registry-tls.key'); const request = path.join(root, 'registry.csr'); const serverCert = path.join(root, 'registry-tls.crt'); const extensions = path.join(root, 'registry.ext'); fs.writeFileSync( extensions, [ 'basicConstraints=critical,CA:FALSE', 'keyUsage=critical,digitalSignature,keyEncipherment', 'extendedKeyUsage=serverAuth', `subjectAltName=DNS:${REGISTRY_NAME},DNS:${REGISTRY_HOST},DNS:${REGISTRY_HOST}.cluster.local`, '', ].join('\n'), { mode: 0o600 }, ); run( OPENSSL, [ 'req', '-x509', '-newkey', 'rsa:2048', '-sha256', '-nodes', '-days', '2', '-subj', '/CN=QingLong Plugin Recovery E2E Registry CA', '-keyout', caKey, '-out', caCert, ], { quiet: true, capture: true }, ); run( OPENSSL, [ 'req', '-newkey', 'rsa:2048', '-sha256', '-nodes', '-subj', `/CN=${REGISTRY_HOST}`, '-keyout', serverKey, '-out', request, ], { quiet: true, capture: true }, ); run( OPENSSL, [ 'x509', '-req', '-sha256', '-days', '2', '-in', request, '-CA', caCert, '-CAkey', caKey, '-CAcreateserial', '-extfile', extensions, '-out', serverCert, ], { quiet: true, capture: true }, ); return Object.freeze({ ca: fs.readFileSync(caCert, 'utf8'), cert: fs.readFileSync(serverCert, 'utf8'), key: fs.readFileSync(serverKey, 'utf8'), }); } function commonSecurityContext() { return { runAsNonRoot: true, runAsUser: 10001, runAsGroup: 10001, fsGroup: 10001, seccompProfile: { type: 'RuntimeDefault' }, }; } function containerSecurityContext() { return { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: ['ALL'] }, }; } function postgresResources(secrets) { const initScript = `#!/bin/sh set -eu psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres \\ --set=migration_password="$QL3_MIGRATION_PASSWORD" \\ --set=runtime_password="$QL3_RUNTIME_PASSWORD" \\ --set=admin_password="$QL3_ADMIN_PASSWORD" \\ --set=automation_manager_password="$QL3_AUTOMATION_MANAGER_PASSWORD" \\ --set=approval_manager_password="$QL3_APPROVAL_MANAGER_PASSWORD" \\ --set=run_manager_password="$QL3_RUN_MANAGER_PASSWORD" \\ --set=package_manager_password="$QL3_PACKAGE_MANAGER_PASSWORD" \\ --set=package_executor_password="$QL3_PACKAGE_EXECUTOR_PASSWORD" \\ --set=worker_credential_manager_password="$QL3_WORKER_CREDENTIAL_MANAGER_PASSWORD" \\ --set=worker_credential_executor_password="$QL3_WORKER_CREDENTIAL_EXECUTOR_PASSWORD" \\ --set=worker_password="$QL3_WORKER_INGRESS_PASSWORD" <<'SQL' CREATE ROLE ql3_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'migration_password'; CREATE ROLE ql3_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'runtime_password'; CREATE ROLE ql3_admin LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'admin_password'; CREATE ROLE ql3_automation_manager LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'automation_manager_password'; CREATE ROLE ql3_approval_manager LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'approval_manager_password'; CREATE ROLE ql3_run_manager LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'run_manager_password'; CREATE ROLE ql3_package_manager LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'package_manager_password'; CREATE ROLE ql3_package_executor LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'package_executor_password'; CREATE ROLE ql3_worker_credential_manager LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'worker_credential_manager_password'; CREATE ROLE ql3_worker_credential_executor LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'worker_credential_executor_password'; CREATE ROLE ql3_worker_ingress LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD :'worker_password'; SQL createdb --username "$POSTGRES_USER" --owner ql3_migration qinglong `; return [ { apiVersion: 'v1', kind: 'Secret', metadata: { name: 'ql3-e2e-postgres-auth', namespace: NAMESPACE }, type: 'Opaque', stringData: { 'postgres-password': secrets.postgres, 'migration-password': secrets.migration, 'runtime-password': secrets.runtime, 'admin-password': secrets.admin, 'automation-manager-password': secrets.automationManager, 'approval-manager-password': secrets.approvalManager, 'run-manager-password': secrets.runManager, 'package-manager-password': secrets.packageManager, 'package-executor-password': secrets.packageExecutor, 'worker-credential-manager-password': secrets.workerCredentialManager, 'worker-credential-executor-password': secrets.workerCredentialExecutor, 'worker-ingress-password': secrets.worker, }, }, { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-e2e-postgres-init', namespace: NAMESPACE }, data: { '001-roles.sh': initScript }, }, { apiVersion: 'v1', kind: 'Service', metadata: { name: POSTGRES_NAME, namespace: NAMESPACE }, spec: { selector: { 'app.kubernetes.io/name': POSTGRES_NAME }, ports: [{ name: 'postgres', port: 5432, targetPort: 5432 }], }, }, { apiVersion: 'v1', kind: 'Pod', metadata: { name: POSTGRES_NAME, namespace: NAMESPACE, labels: { 'app.kubernetes.io/name': POSTGRES_NAME }, }, spec: { automountServiceAccountToken: false, restartPolicy: 'Never', securityContext: { seccompProfile: { type: 'RuntimeDefault' } }, containers: [ { name: 'postgres', image: POSTGRES_RUNTIME_IMAGE, imagePullPolicy: 'Never', env: [ { name: 'POSTGRES_USER', value: 'postgres' }, { name: 'POSTGRES_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'postgres-password', }, }, }, { name: 'POSTGRES_DB', value: 'postgres' }, { name: 'QL3_MIGRATION_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'migration-password', }, }, }, { name: 'QL3_RUNTIME_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'runtime-password', }, }, }, { name: 'QL3_ADMIN_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'admin-password', }, }, }, { name: 'QL3_AUTOMATION_MANAGER_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'automation-manager-password', }, }, }, { name: 'QL3_APPROVAL_MANAGER_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'approval-manager-password', }, }, }, { name: 'QL3_RUN_MANAGER_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'run-manager-password', }, }, }, { name: 'QL3_PACKAGE_MANAGER_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'package-manager-password', }, }, }, { name: 'QL3_PACKAGE_EXECUTOR_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'package-executor-password', }, }, }, { name: 'QL3_WORKER_CREDENTIAL_MANAGER_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'worker-credential-manager-password', }, }, }, { name: 'QL3_WORKER_CREDENTIAL_EXECUTOR_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'worker-credential-executor-password', }, }, }, { name: 'QL3_WORKER_INGRESS_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'worker-ingress-password', }, }, }, ], securityContext: { allowPrivilegeEscalation: false, }, ports: [{ name: 'postgres', containerPort: 5432 }], readinessProbe: { exec: { command: [ 'pg_isready', '--username', 'postgres', '--dbname', 'postgres', ], }, periodSeconds: 2, failureThreshold: 60, }, resources: { requests: { cpu: '100m', memory: '128Mi' }, limits: { cpu: '1', memory: '512Mi' }, }, volumeMounts: [ { name: 'data', mountPath: '/var/lib/postgresql' }, { name: 'init', mountPath: '/docker-entrypoint-initdb.d', readOnly: true, }, ], }, ], volumes: [ { name: 'data', emptyDir: { sizeLimit: '2Gi' } }, { name: 'init', configMap: { name: 'ql3-e2e-postgres-init', defaultMode: 365, }, }, ], }, }, ]; } function registryResources(fixture, certificate, registryCredential) { return [ { apiVersion: 'v1', kind: 'Secret', metadata: { name: 'ql3-e2e-registry-tls', namespace: NAMESPACE }, type: 'Opaque', stringData: { 'ca.crt': certificate.ca, 'tls.crt': certificate.cert, 'tls.key': certificate.key, }, }, { apiVersion: 'v1', kind: 'Secret', metadata: { name: 'ql3-e2e-registry-auth', namespace: NAMESPACE }, type: 'Opaque', stringData: { authorization: registryCredential.authorization, 'credentials.json': registryCredential.file, }, }, { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-e2e-fixture', namespace: NAMESPACE }, data: { 'fixture.cjs': fs.readFileSync(FIXTURE_SCRIPT, 'utf8'), 'fixture.json': JSON.stringify(fixture), }, }, { apiVersion: 'v1', kind: 'Service', metadata: { name: REGISTRY_NAME, namespace: NAMESPACE }, spec: { selector: { 'app.kubernetes.io/name': REGISTRY_NAME }, ports: [{ name: 'https', port: 443, targetPort: 8443 }], }, }, { apiVersion: 'v1', kind: 'Pod', metadata: { name: REGISTRY_NAME, namespace: NAMESPACE, labels: { 'app.kubernetes.io/name': REGISTRY_NAME }, }, spec: { automountServiceAccountToken: false, restartPolicy: 'Never', securityContext: commonSecurityContext(), containers: [ { name: 'registry', image: ADMIN_IMAGE, imagePullPolicy: 'Never', command: ['node', '/opt/ql3-e2e/fixture.cjs'], env: [ { name: 'QL3_E2E_MODE', value: 'registry' }, { name: 'QL3_E2E_FIXTURE_FILE', value: '/opt/ql3-e2e/fixture.json', }, { name: 'QL3_E2E_REGISTRY_CERT_FILE', value: '/var/run/ql3-e2e-registry/tls.crt', }, { name: 'QL3_E2E_REGISTRY_KEY_FILE', value: '/var/run/ql3-e2e-registry/tls.key', }, { name: 'QL3_E2E_REGISTRY_PORT', value: '8443' }, { name: 'QL3_E2E_REGISTRY_AUTHORIZATION', valueFrom: { secretKeyRef: { name: 'ql3-e2e-registry-auth', key: 'authorization', }, }, }, ], securityContext: containerSecurityContext(), ports: [{ name: 'https', containerPort: 8443 }], readinessProbe: { tcpSocket: { port: 'https' }, periodSeconds: 2, failureThreshold: 30, }, resources: { requests: { cpu: '25m', memory: '32Mi' }, limits: { cpu: '250m', memory: '128Mi' }, }, volumeMounts: [ { name: 'fixture', mountPath: '/opt/ql3-e2e', readOnly: true, }, { name: 'tls', mountPath: '/var/run/ql3-e2e-registry', readOnly: true, }, ], }, ], volumes: [ { name: 'fixture', configMap: { name: 'ql3-e2e-fixture', defaultMode: 292 }, }, { name: 'tls', secret: { secretName: 'ql3-e2e-registry-tls', defaultMode: 292, }, }, ], }, }, ]; } function postgresEnvironment(role, passwordKey, applicationName) { return [ { name: 'QL3_POSTGRES_TLS_MODE', value: 'disable' }, { name: 'QL3_POSTGRES_ALLOW_INSECURE', value: 'true' }, { name: 'QL3_POSTGRES_APPLICATION_NAME', value: applicationName }, { name: `QL3_POSTGRES_${role}_HOST`, value: POSTGRES_NAME }, { name: `QL3_POSTGRES_${role}_PORT`, value: '5432' }, { name: `QL3_POSTGRES_${role}_DATABASE`, value: 'qinglong' }, { name: `QL3_POSTGRES_${role}_USER`, value: `ql3_${role.toLowerCase().replaceAll('-', '_')}`, }, { name: `QL3_POSTGRES_${role}_PASSWORD`, valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: passwordKey, }, }, }, ]; } function migrationJob() { const [job] = readYamlDocuments( path.join( ROOT, 'deploy/kubernetes/ql3-cluster/operations/base/migrate-job.yaml', ), ); job.metadata.namespace = NAMESPACE; job.spec.activeDeadlineSeconds = 300; job.spec.template.spec.serviceAccountName = CONTROL_SERVICE_ACCOUNT; const container = job.spec.template.spec.containers[0]; container.image = CONTROL_IMAGE; container.imagePullPolicy = 'Never'; container.env = postgresEnvironment( 'MIGRATION', 'migration-password', 'qinglong3-plugin-e2e-migration', ); container.volumeMounts = [{ name: 'tmp', mountPath: '/tmp' }]; job.spec.template.spec.volumes = [ { name: 'tmp', emptyDir: { medium: 'Memory', sizeLimit: '16Mi' }, }, ]; return job; } function seedJob(name, mode) { return { apiVersion: 'batch/v1', kind: 'Job', metadata: { name, namespace: NAMESPACE }, spec: { backoffLimit: 0, activeDeadlineSeconds: 300, ttlSecondsAfterFinished: 600, template: { metadata: { labels: { 'app.kubernetes.io/name': 'ql3-plugin-package-e2e-seed', 'app.kubernetes.io/part-of': 'qinglong3', }, }, spec: { automountServiceAccountToken: false, restartPolicy: 'Never', securityContext: commonSecurityContext(), containers: [ { name: 'seed', image: ADMIN_IMAGE, imagePullPolicy: 'Never', command: ['node', '/opt/ql3-e2e/fixture.cjs'], env: [ { name: 'QL3_E2E_MODE', value: mode }, { name: 'QL3_E2E_FIXTURE_FILE', value: '/opt/ql3-e2e/fixture.json', }, { name: 'NODE_PATH', value: '/opt/qinglong/node_modules', }, { name: 'QL3_E2E_POSTGRES_HOST', value: POSTGRES_NAME }, { name: 'QL3_E2E_POSTGRES_PORT', value: '5432' }, { name: 'QL3_E2E_POSTGRES_DATABASE', value: 'qinglong' }, { name: 'QL3_E2E_POSTGRES_RUNTIME_USER', value: 'ql3_runtime', }, { name: 'QL3_E2E_POSTGRES_RUNTIME_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'runtime-password', }, }, }, { name: 'QL3_E2E_POSTGRES_PACKAGE_MANAGER_USER', value: 'ql3_package_manager', }, { name: 'QL3_E2E_POSTGRES_PACKAGE_MANAGER_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'package-manager-password', }, }, }, { name: 'QL3_E2E_POSTGRES_PACKAGE_EXECUTOR_USER', value: 'ql3_package_executor', }, { name: 'QL3_E2E_POSTGRES_PACKAGE_EXECUTOR_PASSWORD', valueFrom: { secretKeyRef: { name: 'ql3-e2e-postgres-auth', key: 'package-executor-password', }, }, }, ], securityContext: containerSecurityContext(), resources: { requests: { cpu: '25m', memory: '64Mi' }, limits: { cpu: '500m', memory: '256Mi' }, }, volumeMounts: [ { name: 'fixture', mountPath: '/opt/ql3-e2e', readOnly: true, }, { name: 'tmp', mountPath: '/tmp' }, ], }, ], volumes: [ { name: 'fixture', configMap: { name: 'ql3-e2e-fixture', defaultMode: 292 }, }, { name: 'tmp', emptyDir: { medium: 'Memory', sizeLimit: '16Mi' }, }, ], }, }, }, }; } function recoveryResources(fixture, jobName) { const rbac = readYamlDocuments( path.join( ROOT, 'deploy/kubernetes/ql3-cluster/operations/plugin-package-recovery/base/rbac.yaml', ), ); for (const value of rbac) { value.metadata.namespace = NAMESPACE; if (value.kind === 'RoleBinding') { for (const subject of value.subjects) subject.namespace = NAMESPACE; } } const [job] = readYamlDocuments( path.join( ROOT, 'deploy/kubernetes/ql3-cluster/operations/plugin-package-recovery/base/recover-job.yaml', ), ); job.metadata.name = jobName; job.metadata.namespace = NAMESPACE; job.spec.activeDeadlineSeconds = 300; const container = job.spec.template.spec.containers[0]; container.image = ADMIN_IMAGE; container.imagePullPolicy = 'Never'; container.env = [ { name: 'QL3_CLUSTER_IDENTITY', valueFrom: { configMapKeyRef: { name: 'ql3-plugin-package-recovery-config', key: 'cluster-identity', }, }, }, { name: 'QL3_KUBERNETES_NAMESPACE', valueFrom: { fieldRef: { apiVersion: 'v1', fieldPath: 'metadata.namespace' }, }, }, { name: 'QL3_PLUGIN_PACKAGE_OCI_REGISTRIES', valueFrom: { configMapKeyRef: { name: 'ql3-plugin-package-recovery-config', key: 'oci-registries', }, }, }, { name: 'QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE', value: '/var/run/qinglong3/plugin-package-trust/publishers.json', }, { name: 'NODE_EXTRA_CA_CERTS', value: '/var/run/secrets/ql3-e2e-registry/ca.crt', }, { name: 'QL3_PLUGIN_PACKAGE_REGISTRY_CREDENTIAL_FILE', value: '/var/run/secrets/ql3-e2e-registry-auth/credentials.json', }, { name: 'QL3_PLUGIN_PACKAGE_OCI_TIMEOUT_MS', value: '15000' }, { name: 'QL3_PLUGIN_PACKAGE_RECOVERY_PAGE_SIZE', value: '8' }, { name: 'QL3_PLUGIN_PACKAGE_RECOVERY_MAX_PAGES', value: '8' }, ...postgresEnvironment( 'PACKAGE_EXECUTOR', 'package-executor-password', 'qinglong3-plugin-package-recovery-e2e', ), ]; container.volumeMounts = [ { name: 'tmp', mountPath: '/tmp' }, { name: 'plugin-package-trust', mountPath: '/var/run/qinglong3/plugin-package-trust', readOnly: true, }, { name: 'registry-ca', mountPath: '/var/run/secrets/ql3-e2e-registry', readOnly: true, }, { name: 'registry-credentials', mountPath: '/var/run/secrets/ql3-e2e-registry-auth', readOnly: true, }, ]; job.spec.template.spec.volumes = [ { name: 'tmp', emptyDir: { medium: 'Memory', sizeLimit: '16Mi' }, }, { name: 'plugin-package-trust', configMap: { name: 'ql3-plugin-publisher-trust', defaultMode: 292, items: [{ key: 'publishers.json', path: 'publishers.json' }], }, }, { name: 'registry-ca', secret: { secretName: 'ql3-e2e-registry-tls', defaultMode: 292, items: [{ key: 'ca.crt', path: 'ca.crt' }], }, }, { name: 'registry-credentials', secret: { secretName: 'ql3-e2e-registry-auth', defaultMode: 288, items: [{ key: 'credentials.json', path: 'credentials.json' }], }, }, ]; return [ ...rbac, { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-plugin-package-recovery-config', namespace: NAMESPACE, }, data: { 'cluster-identity': 'ql3-plugin-package-recovery-e2e-cluster', 'oci-registries': fixture.registry, }, }, { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-plugin-publisher-trust', namespace: NAMESPACE, }, data: { 'publishers.json': JSON.stringify(fixture.trust) }, }, job, ]; } function waitForPostgresService(timeoutMs = 60_000) { const deadline = Date.now() + timeoutMs; let lastFact = 'probe not started'; while (Date.now() < deadline) { const probe = kubectl( [ '-n', NAMESPACE, 'exec', `pod/${POSTGRES_NAME}`, '--', 'pg_isready', '--host', POSTGRES_NAME, '--port', '5432', '--username', 'postgres', '--dbname', 'qinglong', ], { capture: true, quiet: true, allowFailure: true }, ); if (probe.status === 0) return; lastFact = (probe.stderr || probe.stdout || `status ${probe.status}`).trim(); Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1_000); } fail(`PostgreSQL Service was not ready after ${timeoutMs}ms: ${lastFact}`); } function waitForJob( name, expectedStatus = 'complete', timeoutMs = 5 * 60 * 1000, ) { if (!['complete', 'failed'].includes(expectedStatus)) { fail('expected Job status is invalid'); } const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const job = kubectlJson(['-n', NAMESPACE, 'get', 'job', name]); const complete = job.status?.conditions?.find( (condition) => condition.type === 'Complete' && condition.status === 'True', ); if (complete) { if (expectedStatus !== 'complete') { fail(`${name} completed but failure was required`); } return job; } const failed = job.status?.conditions?.find( (condition) => condition.type === 'Failed' && condition.status === 'True', ); if (failed) { if (expectedStatus === 'failed') return job; const logs = kubectl( ['-n', NAMESPACE, 'logs', `job/${name}`, '--all-containers=true'], { capture: true, quiet: true, allowFailure: true }, ); fail( `${name} failed: ${failed.reason ?? 'unknown'}\n${logs.stdout}\n${ logs.stderr }`, ); } Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1_000); } const facts = kubectl(['-n', NAMESPACE, 'get', 'job,pod', '-o', 'wide'], { capture: true, quiet: true, allowFailure: true, }); const logs = kubectl( ['-n', NAMESPACE, 'logs', `job/${name}`, '--all-containers=true'], { capture: true, quiet: true, allowFailure: true }, ); fail( `${name} timed out after ${timeoutMs}ms\n${facts.stdout}\n${logs.stdout}\n${logs.stderr}`, ); } function jobLog(name) { return kubectl( ['-n', NAMESPACE, 'logs', `job/${name}`, '--all-containers=true'], { capture: true, quiet: true }, ).stdout; } function lastJsonLine(output, predicate) { const values = output .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); const value = values.findLast(predicate); assert.ok(value, `expected JSON evidence was absent from:\n${output}`); return value; } function canI(verb, resource) { const result = kubectl( [ 'auth', 'can-i', verb, resource, '--namespace', NAMESPACE, '--as', `system:serviceaccount:${NAMESPACE}:${RECOVERY_SERVICE_ACCOUNT}`, ], { capture: true, quiet: true, allowFailure: true }, ); assert.ok(result.status === 0 || result.status === 1); assert.ok(result.stdout === 'yes' || result.stdout === 'no'); return result.stdout === 'yes'; } function databaseEvidence(fixture) { const initialGenerationDigest = fixture.initial.generation.generationDigest; const upgradeGenerationDigest = fixture.upgrade.generation.generationDigest; assert.match(initialGenerationDigest, /^[0-9a-f]{64}$/); assert.match(upgradeGenerationDigest, /^[0-9a-f]{64}$/); const sql = ` SELECT json_build_object( 'migrationCount', (SELECT count(*) FROM ql3.schema_migrations), 'capabilityVersion', ( SELECT contract_version FROM ql3.schema_capabilities WHERE contract_name = 'control-core' ), 'initialState', ( SELECT state FROM ql3.plugin_package_installs WHERE installation_id = '${fixture.initial.installationId}' ), 'initialActiveLockDigest', ( SELECT active_lock_digest FROM ql3.plugin_package_installs WHERE installation_id = '${fixture.initial.installationId}' ), 'upgradeState', ( SELECT state FROM ql3.plugin_package_installs WHERE installation_id = '${fixture.upgrade.installationId}' ), 'upgradePreviousActiveLockDigest', ( SELECT previous_active_lock_digest FROM ql3.plugin_package_installs WHERE installation_id = '${fixture.upgrade.installationId}' ), 'upgradeActiveLockDigest', ( SELECT active_lock_digest FROM ql3.plugin_package_installs WHERE installation_id = '${fixture.upgrade.installationId}' ), 'upgradeFailureReason', ( SELECT record_json #>> '{failure,reason}' FROM ql3.plugin_package_installs WHERE installation_id = '${fixture.upgrade.installationId}' ), 'initialMutationCount', ( SELECT count(*) FROM ql3.plugin_package_install_mutations WHERE installation_id = '${fixture.initial.installationId}' ), 'upgradeMutationCount', ( SELECT count(*) FROM ql3.plugin_package_install_mutations WHERE installation_id = '${fixture.upgrade.installationId}' ), 'headInstallationId', ( SELECT installation_id FROM ql3.plugin_package_install_heads WHERE project_id = 'default' AND package_name = 'e2e-monitor' ), 'initialRevisionCount', ( SELECT count(*) FROM ql3.plugin_package_materialized_revisions WHERE generation_digest = '${initialGenerationDigest}' ), 'upgradeRevisionCount', ( SELECT count(*) FROM ql3.plugin_package_materialized_revisions WHERE generation_digest = '${upgradeGenerationDigest}' ), 'recoverableCount', ( SELECT count(*) FROM ql3.plugin_package_installs WHERE state IN ('queued', 'staged', 'activating') ) )::text; `.trim(); const output = kubectl( [ '-n', NAMESPACE, 'exec', POSTGRES_NAME, '--', 'psql', '--username', 'postgres', '--dbname', 'qinglong', '--tuples-only', '--no-align', '--command', sql, ], { capture: true, quiet: true }, ).stdout; const value = JSON.parse(output); assert.equal(value.migrationCount, MIGRATION_COUNT); assert.equal(value.capabilityVersion, CONTRACT_VERSION); assert.equal(value.initialState, 'active'); assert.equal(value.initialActiveLockDigest, fixture.initial.lock.lockDigest); assert.equal(value.upgradeState, 'failed'); assert.equal( value.upgradePreviousActiveLockDigest, fixture.initial.lock.lockDigest, ); assert.equal(value.upgradeActiveLockDigest, fixture.initial.lock.lockDigest); assert.equal(value.upgradeFailureReason, 'activation_fact_conflict'); assert.equal(value.initialMutationCount, 4); assert.equal(value.upgradeMutationCount, 3); assert.equal(value.headInstallationId, fixture.upgrade.installationId); assert.equal(value.initialRevisionCount, 1); assert.equal(value.upgradeRevisionCount, 0); assert.equal(value.recoverableCount, 0); return value; } function activePointerEvidence(fixture) { const values = kubectlJson([ '-n', NAMESPACE, 'get', 'configmaps', '-l', 'qinglong.io/plugin-package-active=v2', ]); assert.equal(values.items.length, 1); const configMap = values.items[0]; const pointer = JSON.parse(configMap.data['active.json']); assert.equal(pointer.intent.installationId, fixture.initial.installationId); assert.equal(pointer.intent.lockDigest, fixture.initial.lock.lockDigest); assert.equal(pointer.receipt.generation, 1); return Object.freeze({ name: configMap.metadata.name, uid: configMap.metadata.uid, resourceVersion: configMap.metadata.resourceVersion, activeJson: configMap.data['active.json'], intentDigest: pointer.intent.intentDigest, activationRef: pointer.receipt.activationRef, }); } function reportActivePointer(pointer) { return Object.freeze({ name: pointer.name, uid: pointer.uid, resourceVersion: pointer.resourceVersion, activeJsonDigest: createHash('sha256') .update(pointer.activeJson, 'utf8') .digest('hex'), intentDigest: pointer.intentDigest, activationRef: pointer.activationRef, }); } function registryEvidence(fixture) { const output = kubectl( ['-n', NAMESPACE, 'logs', REGISTRY_NAME, '-c', 'registry'], { capture: true, quiet: true }, ).stdout; const events = output .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => JSON.parse(line)) .filter((value) => value.schema === REGISTRY_EVENT_SCHEMA); const packageRequests = events.filter((event) => event.path !== '/v2/'); const expectedPaths = [ ...fixture.initial.routes.flatMap((routeValue) => [ routeValue.path, routeValue.path, ]), ...fixture.upgrade.routes.flatMap((routeValue) => [ routeValue.path, routeValue.path, ]), ].sort(); assert.equal(packageRequests.length, expectedPaths.length); assert.deepEqual( packageRequests.map((event) => event.path).sort(), expectedPaths, ); assert.ok(packageRequests.every((event) => event.status === 200)); assert.ok(packageRequests.every((event) => event.authenticated === true)); return Object.freeze({ https: true, authentication: 'exact-registry-basic', authenticatedRequestCount: packageRequests.length, requestCount: packageRequests.length, uniquePaths: new Set(packageRequests.map((event) => event.path)).size, initialRequestCount: fixture.initial.routes.length * 2, upgradeRequestCount: fixture.upgrade.routes.length * 2, redirects: 0, }); } function recoveryRbacEvidence() { const role = kubectlJson([ '-n', NAMESPACE, 'get', 'role', RECOVERY_SERVICE_ACCOUNT, ]); assert.deepEqual(role.rules, [ { apiGroups: [''], resources: ['configmaps'], verbs: ['get', 'create', 'update'], }, ]); const decisions = { getConfigMaps: canI('get', 'configmaps'), createConfigMaps: canI('create', 'configmaps'), updateConfigMaps: canI('update', 'configmaps'), listConfigMaps: canI('list', 'configmaps'), deleteConfigMaps: canI('delete', 'configmaps'), getSecrets: canI('get', 'secrets'), }; assert.deepEqual(decisions, { getConfigMaps: true, createConfigMaps: true, updateConfigMaps: true, listConfigMaps: false, deleteConfigMaps: false, getSecrets: false, }); return Object.freeze(decisions); } function applyRuntimeAfterRecovery(recoveryJob, migrationJobValue, secrets) { const recoveryComplete = recoveryJob.status.conditions.find( (condition) => condition.type === 'Complete' && condition.status === 'True', ); const migrationComplete = migrationJobValue.status.conditions.find( (condition) => condition.type === 'Complete' && condition.status === 'True', ); assert.ok(recoveryComplete?.lastTransitionTime); assert.ok(migrationComplete?.lastTransitionTime); assert.ok( Date.parse(recoveryComplete.lastTransitionTime) >= Date.parse(migrationComplete.lastTransitionTime), ); const annotation = { 'qinglong.io/plugin-recovery-job-uid': recoveryJob.metadata.uid, 'qinglong.io/plugin-recovery-resource-version': recoveryJob.metadata.resourceVersion, 'qinglong.io/plugin-recovery-completed-at': recoveryComplete.lastTransitionTime, 'qinglong.io/migration-job-uid': migrationJobValue.metadata.uid, 'qinglong.io/migration-completed-at': migrationComplete.lastTransitionTime, }; apply( { apiVersion: 'v1', kind: 'Secret', metadata: { name: 'ql3-cluster-control-runtime', namespace: NAMESPACE, }, type: 'Opaque', stringData: { 'postgres-runtime-url': `postgresql://ql3_runtime:${secrets.runtime}@${POSTGRES_NAME}:5432/qinglong`, 'api-credential-pepper': randomBytes(32).toString('base64url'), }, }, 'create runtime-only credential after recovery success', ); const names = [ 'service-account.yaml', 'service.yaml', 'pod-disruption-budget.yaml', 'deployment.yaml', ]; const resources = names.flatMap((name) => readYamlDocuments( path.join(ROOT, 'deploy/kubernetes/ql3-cluster/base', name), ), ); for (const resource of resources) { resource.metadata.namespace = NAMESPACE; if (resource.kind !== 'Deployment') continue; resource.metadata.annotations = annotation; const container = resource.spec.template.spec.containers[0]; container.image = CONTROL_IMAGE; container.imagePullPolicy = 'Never'; container.env = [ { name: 'QL_DEPLOYMENT_PROFILE', value: 'cluster-control' }, { name: 'QL3_CLUSTER_CONTROL_ENABLED', value: 'true' }, { name: 'QL3_CLUSTER_HTTP_HOST', value: '0.0.0.0' }, { name: 'QL3_CLUSTER_HTTP_PORT', value: '5800' }, { name: 'QL3_CLUSTER_HTTP_DRAIN_TIMEOUT_MS', value: '10000' }, { name: 'QL3_POSTGRES_TLS_MODE', value: 'disable' }, { name: 'QL3_POSTGRES_ALLOW_INSECURE', value: 'true' }, { name: 'QL3_POSTGRES_MAX_CONNECTIONS', value: '4' }, { name: 'QL3_POSTGRES_APPLICATION_NAME', value: 'qinglong3-plugin-recovery-e2e-runtime', }, { name: 'QL3_CLUSTER_REPLICA_ID', valueFrom: { fieldRef: { apiVersion: 'v1', fieldPath: 'metadata.name' }, }, }, { name: 'QL3_POSTGRES_RUNTIME_URL', valueFrom: { secretKeyRef: { name: 'ql3-cluster-control-runtime', key: 'postgres-runtime-url', }, }, }, { name: 'QL3_API_CREDENTIAL_PEPPER', valueFrom: { secretKeyRef: { name: 'ql3-cluster-control-runtime', key: 'api-credential-pepper', }, }, }, ]; container.volumeMounts = [{ name: 'tmp', mountPath: '/tmp' }]; resource.spec.template.spec.volumes = [ { name: 'tmp', emptyDir: { medium: 'Memory', sizeLimit: '16Mi' }, }, ]; } for (const resource of resources) { apply(resource, `deployment controller apply ${resource.kind}`); } kubectl( [ '-n', NAMESPACE, 'rollout', 'status', 'deployment/ql3-cluster-control', '--timeout=5m', ], { capture: true, quiet: true }, ); const deployment = kubectlJson([ '-n', NAMESPACE, 'get', 'deployment', 'ql3-cluster-control', ]); assert.equal(deployment.status.availableReplicas, 2); assert.deepEqual(deployment.metadata.annotations, { ...deployment.metadata.annotations, ...annotation, }); for (const [key, value] of Object.entries(annotation)) { assert.equal(deployment.metadata.annotations[key], value); } assert.ok( Date.parse(deployment.metadata.creationTimestamp) >= Date.parse(recoveryComplete.lastTransitionTime), ); const pods = kubectlJson([ '-n', NAMESPACE, 'get', 'pods', '-l', 'app.kubernetes.io/name=ql3-cluster-control', ]); assert.equal(pods.items.length, 2); assert.equal(new Set(pods.items.map((pod) => pod.spec.nodeName)).size, 2); assert.ok( pods.items.every( (pod) => pod.status.containerStatuses?.[0]?.ready === true && pod.spec.automountServiceAccountToken === false, ), ); return Object.freeze({ replicas: deployment.status.availableReplicas, creationTimestamp: deployment.metadata.creationTimestamp, recoveryJobUid: annotation['qinglong.io/plugin-recovery-job-uid'], recoveryCompletedAt: annotation['qinglong.io/plugin-recovery-completed-at'], nodes: pods.items.map((pod) => pod.spec.nodeName).sort(), imageIds: [ ...new Set( pods.items.map((pod) => pod.status.containerStatuses[0].imageID), ), ], }); } function assertRuntimeCannotReadPluginAuthority() { const result = kubectl( [ '-n', NAMESPACE, 'exec', POSTGRES_NAME, '--', 'sh', '-c', 'PGPASSWORD="$QL3_RUNTIME_PASSWORD" psql --host=ql3-postgres-live --username=ql3_runtime --dbname=qinglong --command="SELECT count(*) FROM ql3.plugin_package_installs"', ], { capture: true, quiet: true, allowFailure: true }, ); assert.notEqual(result.status, 0); assert.match(result.stderr, /permission denied/i); } function jobImageId(name) { const pods = kubectlJson([ '-n', NAMESPACE, 'get', 'pods', '-l', `job-name=${name}`, ]); assert.equal(pods.items.length, 1); return pods.items[0].status.containerStatuses[0].imageID; } function buildOrderingEvidence( migrated, initialRecovered, rejectedUpgrade, runtime, ) { return Object.freeze({ migrationJobUid: migrated.metadata.uid, migrationCompletedAt: migrated.status.completionTime, initialRecoveryJobUid: initialRecovered.metadata.uid, initialRecoveryCompletedAt: initialRecovered.status.completionTime, upgradeRecoveryJobUid: rejectedUpgrade.metadata.uid, upgradeRecoveryCompletedAt: runtime.recoveryCompletedAt, runtimeCreatedAt: runtime.creationTimestamp, runtimeBoundRecoveryJobUid: runtime.recoveryJobUid, }); } function diagnostics() { if (!kubeconfig || !fs.existsSync(kubeconfig)) return; const snapshot = kubectl( ['-n', NAMESPACE, 'get', 'pod,job,deployment,configmap', '-o', 'wide'], { capture: true, quiet: true, allowFailure: true }, ); if (snapshot.stdout) process.stderr.write(`${snapshot.stdout}\n`); } async function main(argv = process.argv.slice(2)) { const reportFile = privateReportPath(argv); if (process.env.QL3_PLUGIN_PACKAGE_RECOVERY_E2E_LIVE !== '1') { fail( 'Refusing to create a live cluster without QL3_PLUGIN_PACKAGE_RECOVERY_E2E_LIVE=1', ); } const revision = sourceRevision(); const clusterName = exactClusterName(); const existing = kind(['get', 'clusters'], { capture: true, quiet: true, allowFailure: true, }) .stdout.split('\n') .map((value) => value.trim()) .filter(Boolean); if (existing.includes(clusterName)) { fail( `Refusing to reuse or delete pre-existing Kind cluster ${clusterName}`, ); } const root = fs.mkdtempSync( path.join(os.tmpdir(), 'ql3-plugin-recovery-e2e-'), ); kubeconfig = path.join(root, 'kubeconfig'); const kindConfig = path.join(root, 'kind.yaml'); const fixtureArchitecture = architecture(); const fixture = createFixture({ registry: REGISTRY_HOST, architecture: fixtureArchitecture, }); const certificate = createRegistryCertificate(root); const secrets = { postgres: randomSecret(), migration: randomSecret(), runtime: randomSecret(), admin: randomSecret(), automationManager: randomSecret(), approvalManager: randomSecret(), runManager: randomSecret(), packageManager: randomSecret(), packageExecutor: randomSecret(), workerCredentialManager: randomSecret(), workerCredentialExecutor: randomSecret(), worker: randomSecret(), registry: randomSecret(), }; const registryCredential = { authorization: `Basic ${Buffer.from( `ql3-e2e:${secrets.registry}`, 'utf8', ).toString('base64')}`, file: JSON.stringify({ schema: 'qinglong/plugin-package-registry-credentials@v1', credentials: [ { registry: fixture.registry, scheme: 'basic', username: 'ql3-e2e', password: secrets.registry, }, ], }), }; let created = false; const startedAt = Date.now(); try { buildImages(revision); const adminSourceRevision = imageSourceRevision(ADMIN_IMAGE); const controlSourceRevision = imageSourceRevision(CONTROL_IMAGE); assert.equal(adminSourceRevision, revision); assert.equal(controlSourceRevision, revision); const adminBuildId = imageId(ADMIN_IMAGE); const controlBuildId = imageId(CONTROL_IMAGE); ensurePostgresImage(); fs.writeFileSync( kindConfig, `${yaml.dump({ kind: 'Cluster', apiVersion: 'kind.x-k8s.io/v1alpha4', nodes: [ { role: 'control-plane' }, { role: 'worker' }, { role: 'worker' }, ], })}`, { mode: 0o600 }, ); kind( [ 'create', 'cluster', '--name', clusterName, '--image', KIND_NODE_IMAGE, '--config', kindConfig, '--kubeconfig', kubeconfig, '--wait', '5m', ], { env: kindCreateEnvironment(clusterName), label: `create isolated Kind cluster ${clusterName}`, }, ); created = true; for (const image of [ ADMIN_IMAGE, CONTROL_IMAGE, POSTGRES_RUNTIME_IMAGE, ]) { kind( [ 'load', 'docker-image', '--name', clusterName, '--nodes', `${clusterName}-control-plane,${clusterName}-worker,${clusterName}-worker2`, image, ], { label: `load ${image} into ${clusterName}` }, ); } apply( { apiVersion: 'v1', kind: 'Namespace', metadata: { name: NAMESPACE }, }, 'create Plugin Package recovery E2E namespace', ); apply( { apiVersion: 'v1', kind: 'ServiceAccount', metadata: { name: CONTROL_SERVICE_ACCOUNT, namespace: NAMESPACE, }, automountServiceAccountToken: false, }, 'create migration/runtime ServiceAccount', ); for (const resource of postgresResources(secrets)) { apply(resource, `apply PostgreSQL fixture ${resource.kind}`); } for (const resource of registryResources( fixture, certificate, registryCredential, )) { apply(resource, `apply OCI fixture ${resource.kind}`); } kubectl( [ '-n', NAMESPACE, 'wait', '--for=condition=Ready', `pod/${POSTGRES_NAME}`, '--timeout=5m', ], { capture: true, quiet: true }, ); waitForPostgresService(); kubectl( [ '-n', NAMESPACE, 'wait', '--for=condition=Ready', `pod/${REGISTRY_NAME}`, '--timeout=2m', ], { capture: true, quiet: true }, ); apply(migrationJob(), 'create reviewed migration Job'); const migrated = waitForJob('ql3-cluster-migration'); const migrationLog = jobLog('ql3-cluster-migration'); lastJsonLine( migrationLog, (value) => value.event === 'migration_completed', ); apply( seedJob(INITIAL_SEED_JOB, 'seed-initial'), 'persist initial durable queued Plugin Package installation', ); waitForJob(INITIAL_SEED_JOB); const initialSeed = lastJsonLine( jobLog(INITIAL_SEED_JOB), (value) => value.event === 'seed_completed', ); assert.equal(initialSeed.phase, 'initial'); assert.equal(initialSeed.status, 'admitted'); assert.equal(initialSeed.state, 'queued'); assert.equal(initialSeed.lockDigest, fixture.initial.lock.lockDigest); for (const resource of recoveryResources(fixture, INITIAL_RECOVERY_JOB)) { apply(resource, `apply initial recovery ${resource.kind}`); } const initialRecovered = waitForJob(INITIAL_RECOVERY_JOB); const initialCompleted = lastJsonLine( jobLog(INITIAL_RECOVERY_JOB), (value) => value.event === 'recovery_completed', ); assert.equal(initialCompleted.recovery.safeToAdmit, true); assert.equal(initialCompleted.recovery.remaining, false); assert.equal(initialCompleted.recovery.manualRequired, 0); const pointerBeforeUpgrade = activePointerEvidence(fixture); apply( seedJob(UPGRADE_SEED_JOB, 'seed-upgrade'), 'persist invalid durable queued Plugin Package upgrade', ); waitForJob(UPGRADE_SEED_JOB); const upgradeSeed = lastJsonLine( jobLog(UPGRADE_SEED_JOB), (value) => value.event === 'seed_completed', ); assert.equal(upgradeSeed.phase, 'upgrade'); assert.equal(upgradeSeed.status, 'admitted'); assert.equal(upgradeSeed.state, 'queued'); assert.equal(upgradeSeed.lockDigest, fixture.upgrade.lock.lockDigest); for (const resource of recoveryResources(fixture, UPGRADE_RECOVERY_JOB)) { apply(resource, `apply upgrade rejection recovery ${resource.kind}`); } const rejectedUpgrade = waitForJob(UPGRADE_RECOVERY_JOB); const rejectionCompleted = lastJsonLine( jobLog(UPGRADE_RECOVERY_JOB), (value) => value.event === 'recovery_completed', ); assert.equal(rejectionCompleted.recovery.safeToAdmit, true); assert.equal(rejectionCompleted.recovery.remaining, false); assert.equal(rejectionCompleted.recovery.manualRequired, 0); const database = databaseEvidence(fixture); const pointerAfterRejection = activePointerEvidence(fixture); assert.deepEqual(pointerAfterRejection, pointerBeforeUpgrade); const oci = registryEvidence(fixture); const rbac = recoveryRbacEvidence(); assertRuntimeCannotReadPluginAuthority(); const runtime = applyRuntimeAfterRecovery( rejectedUpgrade, migrated, secrets, ); const initialRecoveryImageId = jobImageId(INITIAL_RECOVERY_JOB); const upgradeRecoveryImageId = jobImageId(UPGRADE_RECOVERY_JOB); const migrationImageId = jobImageId('ql3-cluster-migration'); const postgresPod = kubectlJson([ '-n', NAMESPACE, 'get', 'pod', POSTGRES_NAME, ]); const report = Object.freeze({ schema: REPORT_SCHEMA, observedAt: new Date().toISOString(), sourceRevision: revision, passed: true, cluster: clusterName, architecture: fixtureArchitecture, elapsedMs: Date.now() - startedAt, images: Object.freeze({ adminBuildId, adminSourceRevision, controlBuildId, controlSourceRevision, postgresRepositoryDigest: POSTGRES_REPOSITORY_DIGEST, migrationImageId, initialRecoveryImageId, upgradeRecoveryImageId, postgresImageId: postgresPod.status.containerStatuses[0].imageID, }), ordering: buildOrderingEvidence( migrated, initialRecovered, rejectedUpgrade, runtime, ), failedUpgrade: Object.freeze({ recoveryJobUid: rejectedUpgrade.metadata.uid, rejectionReason: database.upgradeFailureReason, candidateRevisionCount: database.upgradeRevisionCount, activePointerUnchanged: true, }), database, oci, kubernetes: Object.freeze({ activePointer: reportActivePointer(pointerAfterRejection), rbac, }), runtime, gates: Object.freeze({ healthyInitialActivation: true, invalidUpgradeRejectedBeforeActivation: true, activePointerUidUnchanged: true, activePointerResourceVersionUnchanged: true, activePointerJsonUnchanged: true, candidateRevisionAbsent: true, exactAuthenticatedOciRequests: true, recoveryRbacLeastPrivilege: true, runtimeRolledOutAfterRecovery: true, passed: true, }), limitations: Object.freeze([ 'isolated PostgreSQL uses explicit TLS disable; production manifests remain verify-full', 'the authenticated HTTPS OCI Distribution fixture implements the immutable GET/referrers surface used by the resolver, not a production registry storage implementation', 'the disposable Kind control plane is single-replica; this gate proves workload ordering, not Kubernetes control-plane HA', ]), }); writePrivateReport(reportFile, report); process.stdout.write( `${JSON.stringify({ schemaVersion: 1, fixture: REPORT_SCHEMA, reportWritten: true, passed: true, })}\n`, ); } catch (error) { diagnostics(); throw error; } finally { for (const value of Object.keys(secrets)) secrets[value] = ''; registryCredential.authorization = ''; registryCredential.file = ''; if (created && process.env.QL3_KEEP_KIND_CLUSTER !== '1') { kind(['delete', 'cluster', '--name', clusterName], { label: `delete exact Kind cluster ${clusterName}`, }); } else if (created) { process.stderr.write( `Kept isolated Kind cluster ${clusterName} by explicit request\n`, ); } fs.rmSync(root, { recursive: true, force: true }); } } if (require.main === module) { main().catch((error) => { process.stderr.write( `${JSON.stringify({ schema: 'qinglong/plugin-package-recovery-e2e-live-failure@v1', name: error?.name ?? 'Error', message: error?.message ?? 'unknown failure', })}\n`, ); process.exitCode = 1; }); } module.exports = { REPORT_SCHEMA, buildOrderingEvidence, privateReportPath, writePrivateReport, };