mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ql3): preserve materialization in live fixtures
This commit is contained in:
@@ -442,13 +442,19 @@ function imageTag(reference) {
|
|||||||
return tag;
|
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(
|
assert.equal(
|
||||||
rendered.split(APP_IMAGE_PLACEHOLDER).length - 1,
|
rendered.split(APP_IMAGE_PLACEHOLDER).length - 1,
|
||||||
1,
|
expectedPlaceholderCount,
|
||||||
'rendered application manifest must contain exactly one fail-closed image placeholder',
|
`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(
|
assert.ok(
|
||||||
!local.includes(`@sha256:${'0'.repeat(64)}`),
|
!local.includes(`@sha256:${'0'.repeat(64)}`),
|
||||||
'rendered application manifest retained a fail-closed image placeholder',
|
'rendered application manifest retained a fail-closed image placeholder',
|
||||||
@@ -859,6 +865,7 @@ async function main() {
|
|||||||
['kustomize', 'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg'],
|
['kustomize', 'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg'],
|
||||||
{ capture: true, quiet: true },
|
{ capture: true, quiet: true },
|
||||||
),
|
),
|
||||||
|
2,
|
||||||
);
|
);
|
||||||
kubectl(['apply', '-f', '-'], { input: runtimeManifest });
|
kubectl(['apply', '-f', '-'], { input: runtimeManifest });
|
||||||
kubectl([
|
kubectl([
|
||||||
|
|||||||
@@ -1450,6 +1450,122 @@ function recoveryRbacEvidence() {
|
|||||||
return Object.freeze(decisions);
|
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) {
|
function applyRuntimeAfterRecovery(recoveryJob, migrationJobValue, secrets) {
|
||||||
const recoveryComplete = recoveryJob.status.conditions.find(
|
const recoveryComplete = recoveryJob.status.conditions.find(
|
||||||
(condition) => condition.type === 'Complete' && condition.status === 'True',
|
(condition) => condition.type === 'Complete' && condition.status === 'True',
|
||||||
@@ -1495,83 +1611,7 @@ function applyRuntimeAfterRecovery(recoveryJob, migrationJobValue, secrets) {
|
|||||||
},
|
},
|
||||||
'create runtime-only credential after recovery success',
|
'create runtime-only credential after recovery success',
|
||||||
);
|
);
|
||||||
const names = [
|
const resources = runtimeDeploymentResources(annotation);
|
||||||
'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',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
apply(resource, `deployment controller apply ${resource.kind}`);
|
apply(resource, `deployment controller apply ${resource.kind}`);
|
||||||
}
|
}
|
||||||
@@ -2060,5 +2100,7 @@ module.exports = {
|
|||||||
REPORT_SCHEMA,
|
REPORT_SCHEMA,
|
||||||
buildOrderingEvidence,
|
buildOrderingEvidence,
|
||||||
privateReportPath,
|
privateReportPath,
|
||||||
|
runtimeDeploymentResources,
|
||||||
|
runtimeKeyringMaterializationSource,
|
||||||
writePrivateReport,
|
writePrivateReport,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ test('derives a normal tagged preload reference from a reviewed image', () => {
|
|||||||
assert.throws(() => imageTag('registry.example/operand:18.4'));
|
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(
|
const placeholder = `registry.example.com/qinglong/qinglong3-cluster-control@sha256:${'0'.repeat(
|
||||||
64,
|
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('kind: Deployment\n'));
|
||||||
assert.throws(() => localApplicationManifest(`${rendered}---\n${rendered}`));
|
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', () => {
|
test('accepts uniform runtime reporting of the reviewed index or platform digest', () => {
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ const yaml = require('js-yaml');
|
|||||||
const {
|
const {
|
||||||
createFixture,
|
createFixture,
|
||||||
} = require('../../scripts/ql3-plugin-package-recovery-e2e-fixture.cjs');
|
} = require('../../scripts/ql3-plugin-package-recovery-e2e-fixture.cjs');
|
||||||
|
const {
|
||||||
|
runtimeDeploymentResources,
|
||||||
|
runtimeKeyringMaterializationSource,
|
||||||
|
} = require('../../scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs');
|
||||||
const {
|
const {
|
||||||
CONTRACT_VERSION,
|
CONTRACT_VERSION,
|
||||||
MIGRATION_COUNT,
|
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', () => {
|
test('recovery Job keeps exact ConfigMap-only RBAC and runtime cannot read install authority', () => {
|
||||||
assert.match(
|
assert.match(
|
||||||
live,
|
live,
|
||||||
|
|||||||
Reference in New Issue
Block a user