diff --git a/scripts/ql3-cloudnativepg-live-contract.cjs b/scripts/ql3-cloudnativepg-live-contract.cjs index ed922e0e..cb6a9c5b 100644 --- a/scripts/ql3-cloudnativepg-live-contract.cjs +++ b/scripts/ql3-cloudnativepg-live-contract.cjs @@ -442,13 +442,19 @@ function imageTag(reference) { return tag; } -function localApplicationManifest(rendered) { +function localApplicationManifest(rendered, expectedPlaceholderCount = 1) { + assert.ok( + Number.isInteger(expectedPlaceholderCount) && + expectedPlaceholderCount >= 1 && + expectedPlaceholderCount <= 2, + 'expected application image placeholder count must be one or two', + ); assert.equal( rendered.split(APP_IMAGE_PLACEHOLDER).length - 1, - 1, - 'rendered application manifest must contain exactly one fail-closed image placeholder', + expectedPlaceholderCount, + `rendered application manifest must contain exactly ${expectedPlaceholderCount} fail-closed image placeholder(s)`, ); - const local = rendered.replace(APP_IMAGE_PLACEHOLDER, APP_IMAGE); + const local = rendered.replaceAll(APP_IMAGE_PLACEHOLDER, APP_IMAGE); assert.ok( !local.includes(`@sha256:${'0'.repeat(64)}`), 'rendered application manifest retained a fail-closed image placeholder', @@ -859,6 +865,7 @@ async function main() { ['kustomize', 'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg'], { capture: true, quiet: true }, ), + 2, ); kubectl(['apply', '-f', '-'], { input: runtimeManifest }); kubectl([ diff --git a/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs b/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs index 775be6d9..e2aa812e 100644 --- a/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs +++ b/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs @@ -1450,6 +1450,122 @@ function recoveryRbacEvidence() { return Object.freeze(decisions); } +function runtimeKeyringMaterializationSource() { + return [ + "const fs=require('node:fs')", + "const path=require('node:path')", + "const source=fs.realpathSync('/var/run/secrets/qinglong3/api-credential-projected/..data')", + "const output='/var/run/secrets/qinglong3/runtime/keyring.json'", + "fs.copyFileSync(path.join(source,'keyring.json'),output,fs.constants.COPYFILE_EXCL)", + 'fs.chmodSync(output,0o400)', + ].join(';'); +} + +function runtimeDeploymentResources(annotation) { + 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 pod = resource.spec.template.spec; + const container = pod.containers[0]; + const materializer = pod.initContainers[0]; + container.image = CONTROL_IMAGE; + container.imagePullPolicy = 'Never'; + materializer.image = CONTROL_IMAGE; + materializer.imagePullPolicy = 'Never'; + materializer.command = [ + 'node', + '-e', + runtimeKeyringMaterializationSource(), + ]; + materializer.volumeMounts = [ + { + name: 'api-credential-keyring-projected', + mountPath: '/var/run/secrets/qinglong3/api-credential-projected', + readOnly: true, + }, + { + name: 'runtime-private', + mountPath: '/var/run/secrets/qinglong3/runtime', + }, + ]; + 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_KEYRING_FILE', + value: '/var/run/secrets/qinglong3/runtime/keyring.json', + }, + ]; + container.volumeMounts = [ + { name: 'tmp', mountPath: '/tmp' }, + { + name: 'runtime-private', + mountPath: '/var/run/secrets/qinglong3/runtime', + readOnly: true, + }, + ]; + pod.volumes = [ + { + name: 'tmp', + emptyDir: { medium: 'Memory', sizeLimit: '16Mi' }, + }, + { + name: 'api-credential-keyring-projected', + secret: { + secretName: 'ql3-cluster-control-runtime', + defaultMode: 288, + items: [{ + key: 'api-credential-pepper-keyring.json', + path: 'keyring.json', + }], + }, + }, + { + name: 'runtime-private', + emptyDir: { medium: 'Memory', sizeLimit: '1Mi' }, + }, + ]; + } + return resources; +} + function applyRuntimeAfterRecovery(recoveryJob, migrationJobValue, secrets) { const recoveryComplete = recoveryJob.status.conditions.find( (condition) => condition.type === 'Complete' && condition.status === 'True', @@ -1495,83 +1611,7 @@ function applyRuntimeAfterRecovery(recoveryJob, migrationJobValue, secrets) { }, '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_KEYRING_FILE', - value: '/var/run/secrets/qinglong3/api-credential/keyring.json', - }, - ]; - container.volumeMounts = [ - { name: 'tmp', mountPath: '/tmp' }, - { - name: 'api-credential-keyring', - mountPath: '/var/run/secrets/qinglong3/api-credential', - readOnly: true, - }, - ]; - resource.spec.template.spec.volumes = [ - { - name: 'tmp', - emptyDir: { medium: 'Memory', sizeLimit: '16Mi' }, - }, - { - name: 'api-credential-keyring', - secret: { - secretName: 'ql3-cluster-control-runtime', - defaultMode: 288, - items: [{ - key: 'api-credential-pepper-keyring.json', - path: 'keyring.json', - }], - }, - }, - ]; - } + const resources = runtimeDeploymentResources(annotation); for (const resource of resources) { apply(resource, `deployment controller apply ${resource.kind}`); } @@ -2060,5 +2100,7 @@ module.exports = { REPORT_SCHEMA, buildOrderingEvidence, privateReportPath, + runtimeDeploymentResources, + runtimeKeyringMaterializationSource, writePrivateReport, }; diff --git a/test/back/ql3CloudNativePgLiveContract.test.cjs b/test/back/ql3CloudNativePgLiveContract.test.cjs index 0f694aa9..c350acae 100644 --- a/test/back/ql3CloudNativePgLiveContract.test.cjs +++ b/test/back/ql3CloudNativePgLiveContract.test.cjs @@ -41,7 +41,7 @@ test('derives a normal tagged preload reference from a reviewed image', () => { assert.throws(() => imageTag('registry.example/operand:18.4')); }); -test('replaces exactly one fail-closed application image only in live rendering', () => { +test('replaces the exact fail-closed application image count only in live rendering', () => { const placeholder = `registry.example.com/qinglong/qinglong3-cluster-control@sha256:${'0'.repeat( 64, )}`; @@ -52,6 +52,12 @@ test('replaces exactly one fail-closed application image only in live rendering' ); assert.throws(() => localApplicationManifest('kind: Deployment\n')); assert.throws(() => localApplicationManifest(`${rendered}---\n${rendered}`)); + assert.equal( + localApplicationManifest(`${rendered}---\n${rendered}`, 2), + `kind: Deployment\nspec:\n image: registry.example.com/qinglong/qinglong3-cluster-control:${VERSION}\n---\nkind: Deployment\nspec:\n image: registry.example.com/qinglong/qinglong3-cluster-control:${VERSION}\n`, + ); + assert.throws(() => localApplicationManifest(rendered, 2)); + assert.throws(() => localApplicationManifest(rendered, 0)); }); test('accepts uniform runtime reporting of the reviewed index or platform digest', () => { diff --git a/test/back/ql3PluginPackageRecoveryE2ELiveContract.test.cjs b/test/back/ql3PluginPackageRecoveryE2ELiveContract.test.cjs index 1733d9af..b9343392 100644 --- a/test/back/ql3PluginPackageRecoveryE2ELiveContract.test.cjs +++ b/test/back/ql3PluginPackageRecoveryE2ELiveContract.test.cjs @@ -6,6 +6,10 @@ const yaml = require('js-yaml'); const { createFixture, } = require('../../scripts/ql3-plugin-package-recovery-e2e-fixture.cjs'); +const { + runtimeDeploymentResources, + runtimeKeyringMaterializationSource, +} = require('../../scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs'); const { CONTRACT_VERSION, MIGRATION_COUNT, @@ -176,6 +180,40 @@ test('deployment controller rejects the upgrade before creating runtime', () => ); }); +test('runtime deployment preserves private regular-file keyring materialization', () => { + const resources = runtimeDeploymentResources({ + 'qinglong.io/test': 'runtime-materialization', + }); + const deployment = resources.find( + (resource) => resource.kind === 'Deployment', + ); + const pod = deployment.spec.template.spec; + const materializer = pod.initContainers[0]; + const container = pod.containers[0]; + assert.equal(materializer.name, 'materialize-runtime-files'); + assert.equal(materializer.image, container.image); + assert.equal(materializer.imagePullPolicy, 'Never'); + assert.deepEqual(materializer.command.slice(0, 2), ['node', '-e']); + assert.equal(materializer.command[2], runtimeKeyringMaterializationSource()); + assert.match(materializer.command[2], /realpathSync/); + assert.match(materializer.command[2], /COPYFILE_EXCL/); + assert.match(materializer.command[2], /chmodSync\(output,0o400\)/); + assert.equal( + container.env.find( + (entry) => entry.name === 'QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE', + ).value, + '/var/run/secrets/qinglong3/runtime/keyring.json', + ); + assert.equal( + container.volumeMounts.some((mount) => mount.name.includes('projected')), + false, + ); + assert.deepEqual( + pod.volumes.map((volume) => volume.name), + ['tmp', 'api-credential-keyring-projected', 'runtime-private'], + ); +}); + test('recovery Job keeps exact ConfigMap-only RBAC and runtime cannot read install authority', () => { assert.match( live,