mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
1273 lines
40 KiB
JavaScript
1273 lines
40 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const {
|
|
K3sDockerLiveFixture,
|
|
run,
|
|
waitFor,
|
|
} = require('./lib/ql3-k3s-docker-live.cjs');
|
|
const { createMutualTlsPki } = require('./lib/ql3-live-pki.cjs');
|
|
const {
|
|
clientTcpProbe,
|
|
createManagementClientExecutor,
|
|
managementHealthStatus,
|
|
patchManagementGeneration,
|
|
podReady,
|
|
podTcpProbe,
|
|
readyManagementPods,
|
|
waitForTwoPreserved,
|
|
waitManagementRollout,
|
|
} = require('./lib/ql3-management-kubernetes-live.cjs');
|
|
const {
|
|
applySecret,
|
|
currentPrimaryPod,
|
|
imageIdDigest,
|
|
localManifest,
|
|
psql,
|
|
} = require('./lib/ql3-management-kubernetes-live-platform.cjs');
|
|
const {
|
|
createManagementIdentityCeremony,
|
|
} = require('./lib/ql3-management-live-identity.cjs');
|
|
const {
|
|
durableRunManagementFacts,
|
|
retryCommand,
|
|
seedRunManagement,
|
|
stopCommand,
|
|
} = require('./lib/ql3-run-management-kubernetes-live-scenario.cjs');
|
|
const {
|
|
imageDigest,
|
|
imageTag,
|
|
reviewedOperatorManifest,
|
|
} = require('./ql3-cloudnativepg-live-contract.cjs');
|
|
const {
|
|
FIXTURE,
|
|
LIMITATIONS,
|
|
validateRunManagementKubernetesLiveReport,
|
|
} = require('./ql3-run-management-kubernetes-live-audit.cjs');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const NAMESPACE = 'qinglong3-system';
|
|
const DEPLOYMENT = 'ql3-run-management';
|
|
const SERVERNAME = `${DEPLOYMENT}.${NAMESPACE}.svc`;
|
|
const POSTGRES_CLUSTER = 'ql3-postgres';
|
|
const ZERO_DIGEST = 'sha256:' + '0'.repeat(64);
|
|
const ISSUER = 'https://identity.qinglong.test/';
|
|
const AUDIENCE = 'qinglong3-run-management';
|
|
const LOCK = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(
|
|
ROOT,
|
|
'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg/operator-lock.json',
|
|
),
|
|
'utf8',
|
|
),
|
|
);
|
|
const OPERATOR_IMAGE = LOCK.operator.image;
|
|
const POSTGRES_IMAGE = LOCK.operand.image;
|
|
const ADMIN_IMAGE_BASE = 'ql3-run-manager-live';
|
|
const CONTROL_IMAGE_BASE = 'ql3-run-migration-live';
|
|
const ROLE_NAMES = Object.freeze([
|
|
'ql3_migration',
|
|
'ql3_ai_maintenance',
|
|
'ql3_ai_credential_manager',
|
|
'ql3_ai_credential_tester',
|
|
'ql3_runtime',
|
|
'ql3_admin',
|
|
'ql3_package_manager',
|
|
'ql3_package_executor',
|
|
'ql3_automation_manager',
|
|
'ql3_approval_manager',
|
|
'ql3_run_manager',
|
|
'ql3_worker_credential_manager',
|
|
'ql3_worker_credential_executor',
|
|
'ql3_worker_ingress',
|
|
]);
|
|
const identity = createManagementIdentityCeremony({
|
|
issuer: ISSUER,
|
|
audience: AUDIENCE,
|
|
purpose: 'run-management',
|
|
tokenType: 'ql3-run-management+jwt',
|
|
subject: 'run-operator',
|
|
jtiPrefix: 'ql3-run-live',
|
|
});
|
|
|
|
function sha256(value) {
|
|
return 'sha256:' + crypto.createHash('sha256').update(value).digest('hex');
|
|
}
|
|
|
|
function randomSecret() {
|
|
return crypto.randomBytes(32).toString('base64url');
|
|
}
|
|
|
|
function managerOptions(fixture) {
|
|
return {
|
|
fixture,
|
|
namespace: NAMESPACE,
|
|
deployment: DEPLOYMENT,
|
|
description: 'two Ready Run manager Pods on distinct nodes',
|
|
};
|
|
}
|
|
|
|
function patchGeneration(fixture, generation, annotations = {}) {
|
|
patchManagementGeneration({
|
|
...managerOptions(fixture),
|
|
generation,
|
|
annotations,
|
|
});
|
|
}
|
|
|
|
function healthStatus(fixture, pod, route) {
|
|
return managementHealthStatus({
|
|
fixture,
|
|
namespace: NAMESPACE,
|
|
podName: pod.metadata.name,
|
|
port: 8448,
|
|
route,
|
|
servername: SERVERNAME,
|
|
caFile: '/var/run/secrets/qinglong3/run-management-tls/ca.crt',
|
|
});
|
|
}
|
|
|
|
function privateReportPath(argv) {
|
|
if (
|
|
argv.length !== 1 ||
|
|
!argv[0].startsWith('--report=') ||
|
|
!path.isAbsolute(argv[0].slice('--report='.length))
|
|
) {
|
|
throw new Error(
|
|
'usage: ql3-run-management-kubernetes-live-contract --report=/absolute/private-report.json',
|
|
);
|
|
}
|
|
const reportFile = argv[0].slice('--report='.length);
|
|
if (fs.existsSync(reportFile)) {
|
|
throw new Error('refusing to overwrite the Run management live report');
|
|
}
|
|
const parent = fs.lstatSync(path.dirname(reportFile));
|
|
if (!parent.isDirectory() || parent.isSymbolicLink()) {
|
|
throw new Error(
|
|
'Run management live report parent must be a real directory',
|
|
);
|
|
}
|
|
return reportFile;
|
|
}
|
|
|
|
async function main(argv = process.argv.slice(2)) {
|
|
const reportFile = privateReportPath(argv);
|
|
if (process.env.QL3_RUN_MANAGEMENT_KUBERNETES_LIVE !== '1') {
|
|
throw new Error(
|
|
'Refusing to mutate Docker/Kubernetes without QL3_RUN_MANAGEMENT_KUBERNETES_LIVE=1',
|
|
);
|
|
}
|
|
const operatorManifestFile = process.env.QL3_CNPG_OPERATOR_MANIFEST_FILE;
|
|
if (!operatorManifestFile) {
|
|
throw new Error('QL3_CNPG_OPERATOR_MANIFEST_FILE is required');
|
|
}
|
|
const reviewedManifest = reviewedOperatorManifest(operatorManifestFile);
|
|
const fixture = new K3sDockerLiveFixture({ prefix: 'ql3-run-live' });
|
|
const suffix =
|
|
process.pid.toString(36) + '-' + crypto.randomBytes(3).toString('hex');
|
|
const adminImage = `${ADMIN_IMAGE_BASE}:${suffix}`;
|
|
const controlImage = `${CONTROL_IMAGE_BASE}:${suffix}`;
|
|
let adminImageBuilt = false;
|
|
let controlImageBuilt = false;
|
|
try {
|
|
const nodes = await fixture.start();
|
|
const architecture = fixture.inspectImage(fixture.k3sImage).Architecture;
|
|
assert.ok(['amd64', 'arm64'].includes(architecture));
|
|
for (const reviewedImage of [OPERATOR_IMAGE, POSTGRES_IMAGE]) {
|
|
run(fixture.docker, ['pull', reviewedImage]);
|
|
const inspected = fixture.inspectImage(reviewedImage);
|
|
assert.ok(
|
|
inspected.RepoDigests?.some((entry) =>
|
|
entry.endsWith('@' + imageDigest(reviewedImage)),
|
|
),
|
|
);
|
|
const preloadTag = imageTag(reviewedImage);
|
|
run(fixture.docker, ['tag', reviewedImage, preloadTag]);
|
|
fixture.loadImage(preloadTag, path.basename(preloadTag) + '.tar');
|
|
}
|
|
|
|
const sourceRevision = run('git', ['rev-parse', 'HEAD'], {
|
|
capture: true,
|
|
quiet: true,
|
|
}).stdout;
|
|
for (const [dockerfile, image, archive] of [
|
|
[
|
|
'deploy/containers/ql3-cluster-admin/Dockerfile',
|
|
adminImage,
|
|
'run-admin.tar',
|
|
],
|
|
[
|
|
'deploy/containers/ql3-cluster-control/Dockerfile',
|
|
controlImage,
|
|
'run-control.tar',
|
|
],
|
|
]) {
|
|
run(fixture.docker, [
|
|
'build',
|
|
'--file',
|
|
dockerfile,
|
|
'--tag',
|
|
image,
|
|
'--build-arg',
|
|
'SOURCE_REVISION=' + sourceRevision,
|
|
'.',
|
|
]);
|
|
if (image === adminImage) adminImageBuilt = true;
|
|
else controlImageBuilt = true;
|
|
fixture.loadImage(image, archive);
|
|
}
|
|
const adminImageInfo = fixture.inspectImage(adminImage);
|
|
const postgresImageInfo = fixture.inspectImage(POSTGRES_IMAGE);
|
|
const k3sImageInfo = fixture.inspectImage(fixture.k3sImage);
|
|
|
|
fixture.kubectl(['apply', '--server-side', '-f', reviewedManifest]);
|
|
fixture.kubectl([
|
|
'-n',
|
|
'cnpg-system',
|
|
'set',
|
|
'image',
|
|
'deployment/cnpg-controller-manager',
|
|
'manager=' + imageTag(OPERATOR_IMAGE),
|
|
]);
|
|
fixture.kubectl([
|
|
'wait',
|
|
'--for=condition=Established',
|
|
'crd/clusters.postgresql.cnpg.io',
|
|
'crd/databaseroles.postgresql.cnpg.io',
|
|
'crd/databases.postgresql.cnpg.io',
|
|
'--timeout=5m',
|
|
]);
|
|
fixture.kubectl([
|
|
'-n',
|
|
'cnpg-system',
|
|
'rollout',
|
|
'status',
|
|
'deployment/cnpg-controller-manager',
|
|
'--timeout=5m',
|
|
]);
|
|
|
|
fixture.kubectl([
|
|
'apply',
|
|
'-f',
|
|
'deploy/kubernetes/ql3-cluster/base/namespace.yaml',
|
|
]);
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'apply',
|
|
'-f',
|
|
'deploy/kubernetes/ql3-cluster/base/service-account.yaml',
|
|
]);
|
|
const passwords = Object.fromEntries(
|
|
ROLE_NAMES.map((role) => [role, randomSecret()]),
|
|
);
|
|
for (const role of ROLE_NAMES) {
|
|
applySecret(
|
|
fixture,
|
|
'ql3-postgres-' +
|
|
role.replace(/^ql3_/, '').replaceAll('_', '-') +
|
|
'-auth',
|
|
'kubernetes.io/basic-auth',
|
|
{ username: role, password: passwords[role] },
|
|
);
|
|
}
|
|
const databaseManifest = fixture
|
|
.kubectl(
|
|
['kustomize', 'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg'],
|
|
{
|
|
capture: true,
|
|
quiet: true,
|
|
},
|
|
)
|
|
.stdout.replace(POSTGRES_IMAGE, imageTag(POSTGRES_IMAGE));
|
|
fixture.kubectl(['apply', '-f', '-'], { input: databaseManifest + '\n' });
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'wait',
|
|
'--for=condition=Ready',
|
|
'cluster/' + POSTGRES_CLUSTER,
|
|
'--timeout=20m',
|
|
]);
|
|
const databasePods = (
|
|
await waitFor('three ready CloudNativePG instances', 600_000, () => {
|
|
const pods = fixture
|
|
.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'pods',
|
|
'-l',
|
|
'cnpg.io/cluster=' + POSTGRES_CLUSTER,
|
|
])
|
|
.items.filter(podReady);
|
|
return pods.length === 3
|
|
? { ready: true, value: pods }
|
|
: { ready: false, fact: `${pods.length}/3 ready database Pods` };
|
|
})
|
|
).value;
|
|
|
|
const migrationManifest = localManifest(
|
|
fixture.kubectl(
|
|
[
|
|
'kustomize',
|
|
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg',
|
|
],
|
|
{ capture: true, quiet: true },
|
|
).stdout,
|
|
'registry.example.com/qinglong/qinglong3-cluster-control',
|
|
controlImage,
|
|
);
|
|
fixture.kubectl(['create', '-f', '-'], { input: migrationManifest + '\n' });
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'wait',
|
|
'--for=condition=Complete',
|
|
'job/ql3-cluster-migration',
|
|
'--timeout=10m',
|
|
]);
|
|
const migrationPrimary = currentPrimaryPod(fixture);
|
|
const migrationState = JSON.parse(
|
|
psql(
|
|
fixture,
|
|
migrationPrimary.metadata.name,
|
|
[
|
|
'SELECT json_build_object(',
|
|
' \'migrationCount\', (SELECT count(*)::integer FROM "ql3"."schema_migrations"),',
|
|
' \'controlCoreCapability\', (SELECT contract_version::integer FROM "ql3"."schema_capabilities" WHERE contract_name = \'control-core\'))',
|
|
].join('\n'),
|
|
),
|
|
);
|
|
assert.deepEqual(migrationState, {
|
|
migrationCount: 57,
|
|
controlCoreCapability: 56,
|
|
});
|
|
|
|
const values = Object.freeze({
|
|
suffix,
|
|
projectId: 'run-live-' + suffix,
|
|
operatorId: 'run-operator',
|
|
outsiderId: 'run-outsider',
|
|
taskId: 'run-live-task-' + suffix,
|
|
sourceRunId: crypto.randomUUID(),
|
|
sourceAttemptId: crypto.randomUUID(),
|
|
});
|
|
seedRunManagement(fixture, migrationPrimary.metadata.name, values, psql);
|
|
|
|
const pki = createMutualTlsPki({
|
|
directory: fixture.temporary,
|
|
servername: SERVERNAME,
|
|
label: 'QL3 Run Management Live',
|
|
run,
|
|
crypto,
|
|
});
|
|
let pkiMaterial = pki.read();
|
|
const oldKey = identity.reviewedKey('run-live-key-1');
|
|
const newKey = identity.reviewedKey('run-live-key-2');
|
|
const keysets = [
|
|
identity.keyset(1, [oldKey]),
|
|
identity.keyset(2, [oldKey, newKey]),
|
|
identity.keyset(3, [oldKey, newKey], [oldKey.kid]),
|
|
];
|
|
const applyIdentity = (document) =>
|
|
applySecret(fixture, DEPLOYMENT + '-identity', 'Opaque', {
|
|
'keyset.json': JSON.stringify(document) + '\n',
|
|
});
|
|
const applyTls = () =>
|
|
applySecret(fixture, DEPLOYMENT + '-tls', 'kubernetes.io/tls', {
|
|
'tls.crt': pkiMaterial.serverCertificate,
|
|
'tls.key': pkiMaterial.serverKey,
|
|
'ca.crt': pkiMaterial.ca,
|
|
'client.crl': pkiMaterial.clientCrl,
|
|
});
|
|
applyIdentity(keysets[0]);
|
|
applyTls();
|
|
const previousBundleSha256 = pki.bundleSha256();
|
|
let managerManifest = localManifest(
|
|
fixture.kubectl(
|
|
[
|
|
'kustomize',
|
|
'deploy/kubernetes/ql3-cluster/operations/run-management/cloudnative-pg',
|
|
],
|
|
{ capture: true, quiet: true },
|
|
).stdout,
|
|
'registry.example.com/qinglong/qinglong3-cluster-admin',
|
|
adminImage,
|
|
);
|
|
assert.equal(managerManifest.split(ZERO_DIGEST).length - 1, 2);
|
|
const productionRateWindowPattern =
|
|
/(- name: QL3_RUN_MANAGEMENT_RATE_WINDOW_MS\n\s+value: )"60000"/g;
|
|
assert.equal(
|
|
managerManifest.match(productionRateWindowPattern)?.length ?? 0,
|
|
1,
|
|
);
|
|
managerManifest = managerManifest
|
|
.replace(ZERO_DIGEST, sha256(pkiMaterial.ca))
|
|
.replace(ZERO_DIGEST, sha256(pkiMaterial.clientCrl))
|
|
.replace(productionRateWindowPattern, '$1"1000"');
|
|
fixture.kubectl(['apply', '-f', '-'], { input: managerManifest + '\n' });
|
|
waitManagementRollout(managerOptions(fixture));
|
|
let managerPods = await readyManagementPods(managerOptions(fixture));
|
|
|
|
const deployment = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'deployment',
|
|
DEPLOYMENT,
|
|
]);
|
|
assert.equal(deployment.spec.replicas, 2);
|
|
assert.equal(deployment.spec.strategy.rollingUpdate.maxUnavailable, 0);
|
|
assert.equal(
|
|
deployment.spec.template.spec.automountServiceAccountToken,
|
|
false,
|
|
);
|
|
assert.equal(
|
|
deployment.spec.template.spec.affinity.podAntiAffinity
|
|
.requiredDuringSchedulingIgnoredDuringExecution.length,
|
|
1,
|
|
);
|
|
assert.equal(
|
|
fixture.kubectlJson(['-n', NAMESPACE, 'get', 'pdb', DEPLOYMENT]).spec
|
|
.minAvailable,
|
|
1,
|
|
);
|
|
for (const pod of managerPods) {
|
|
assert.equal(pod.spec.serviceAccountName, DEPLOYMENT);
|
|
assert.equal(pod.spec.automountServiceAccountToken, false);
|
|
}
|
|
|
|
const executeClient = createManagementClientExecutor({
|
|
fixture,
|
|
namespace: NAMESPACE,
|
|
servername: SERVERNAME,
|
|
port: 8448,
|
|
managementPath: '/api/v3/runs/management',
|
|
adminImage,
|
|
ca: pkiMaterial.ca,
|
|
serviceAccount: 'ql3-run-management-client',
|
|
appName: 'ql3-run-management-client',
|
|
component: 'run-management-client',
|
|
networkPolicyLabel: 'qinglong.io/run-management-client',
|
|
clientCliPath:
|
|
'/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/run-management/runManagementClientCli.js',
|
|
retryableClientCodes: [
|
|
'QL3_PLUGIN_PACKAGE_MANAGEMENT_CLIENT_REQUEST_FAILED',
|
|
'QL3_RUN_MANAGEMENT_CLIENT_FAILED',
|
|
],
|
|
description: 'Run management',
|
|
});
|
|
const retryMutationId = crypto.randomUUID();
|
|
const retry = retryCommand(
|
|
values.projectId,
|
|
values.sourceRunId,
|
|
'run-live-retry',
|
|
retryMutationId,
|
|
1,
|
|
);
|
|
const retryAuthenticationJti = 'run-live-retry-session-' + values.suffix;
|
|
const retryBearer = identity.assertion(oldKey, retryAuthenticationJti);
|
|
const retryAccepted = await executeClient(
|
|
{
|
|
name: 'ql3-run-retry-accepted',
|
|
target: managerPods[0],
|
|
command: retry,
|
|
bearer: retryBearer,
|
|
clientCertificate: pkiMaterial.oldClientCertificate,
|
|
clientKey: pkiMaterial.oldClientKey,
|
|
},
|
|
{ statusCode: 200, resultField: 'retry', resultStatus: ['accepted'] },
|
|
);
|
|
const retriedRunId = retryAccepted.output.result.retry.runId;
|
|
const retryReplay = await executeClient(
|
|
{
|
|
name: 'ql3-run-retry-replay',
|
|
target: managerPods[1],
|
|
command: retry,
|
|
bearer: retryBearer,
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 200, resultField: 'retry', resultStatus: ['existing'] },
|
|
);
|
|
assert.equal(retryReplay.output.result.retry.runId, retriedRunId);
|
|
const weakRejected = await executeClient(
|
|
{
|
|
name: 'ql3-run-retry-weak',
|
|
target: managerPods[0],
|
|
command: retryCommand(
|
|
values.projectId,
|
|
values.sourceRunId,
|
|
'run-live-weak',
|
|
crypto.randomUUID(),
|
|
2,
|
|
),
|
|
bearer: identity.weakAssertion(oldKey),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 401, responseCode: 'authentication_required' },
|
|
);
|
|
const outsiderDenied = await executeClient(
|
|
{
|
|
name: 'ql3-run-retry-outsider',
|
|
target: managerPods[1],
|
|
command: retryCommand(
|
|
values.projectId,
|
|
values.sourceRunId,
|
|
'run-live-outsider',
|
|
crypto.randomUUID(),
|
|
3,
|
|
),
|
|
bearer: identity.assertionForSubject(oldKey, values.outsiderId),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 403, responseCode: 'forbidden' },
|
|
);
|
|
|
|
const generation1Uids = new Set(managerPods.map((pod) => pod.metadata.uid));
|
|
applyIdentity(keysets[1]);
|
|
patchGeneration(fixture, 2);
|
|
const generation2 = await waitForTwoPreserved({
|
|
...managerOptions(fixture),
|
|
excludedUids: generation1Uids,
|
|
expectedGeneration: 2,
|
|
description: 'zero-unavailable Run manager identity generation 2 rollout',
|
|
});
|
|
managerPods = generation2.pods;
|
|
const stopMutationId = crypto.randomUUID();
|
|
const stop = stopCommand(
|
|
values.projectId,
|
|
retriedRunId,
|
|
'run-live-stop',
|
|
stopMutationId,
|
|
4,
|
|
);
|
|
const stopAuthenticationJti = 'run-live-stop-session-' + values.suffix;
|
|
const overlapOld = await executeClient(
|
|
{
|
|
name: 'ql3-run-stop-overlap-old',
|
|
target: managerPods[0],
|
|
command: stop,
|
|
bearer: identity.assertion(oldKey, stopAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 200, resultField: 'stop', resultStatus: ['accepted'] },
|
|
);
|
|
const overlapNew = await executeClient(
|
|
{
|
|
name: 'ql3-run-stop-overlap-new',
|
|
target: managerPods[1],
|
|
command: stop,
|
|
bearer: identity.assertion(newKey, stopAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{
|
|
statusCode: 200,
|
|
resultField: 'stop',
|
|
resultStatus: ['already_requested'],
|
|
},
|
|
);
|
|
|
|
const generation2Uids = new Set(managerPods.map((pod) => pod.metadata.uid));
|
|
applyIdentity(keysets[2]);
|
|
patchGeneration(fixture, 3);
|
|
const generation3 = await waitForTwoPreserved({
|
|
...managerOptions(fixture),
|
|
excludedUids: generation2Uids,
|
|
expectedGeneration: 3,
|
|
description: 'zero-unavailable Run manager identity generation 3 rollout',
|
|
});
|
|
managerPods = generation3.pods;
|
|
const rejectedOldKey = await executeClient(
|
|
{
|
|
name: 'ql3-run-stop-revoked-key',
|
|
target: managerPods[0],
|
|
command: stop,
|
|
bearer: identity.assertion(oldKey, stopAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 401, responseCode: 'authentication_required' },
|
|
);
|
|
const activeNew = await executeClient(
|
|
{
|
|
name: 'ql3-run-stop-active-key',
|
|
target: managerPods[1],
|
|
command: stop,
|
|
bearer: identity.assertion(newKey, stopAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{
|
|
statusCode: 200,
|
|
resultField: 'stop',
|
|
resultStatus: ['already_requested'],
|
|
},
|
|
);
|
|
|
|
applyIdentity(keysets[1]);
|
|
patchGeneration(fixture, 'rollback-2');
|
|
const rollback = await waitFor(
|
|
'Run identity ledger rollback surge failure',
|
|
180_000,
|
|
() => {
|
|
const pods = fixture
|
|
.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'pods',
|
|
'-l',
|
|
'app.kubernetes.io/name=' + DEPLOYMENT,
|
|
])
|
|
.items.filter((pod) => pod.metadata.deletionTimestamp === undefined);
|
|
const ready = pods.filter(podReady);
|
|
const candidate = pods.find(
|
|
(pod) =>
|
|
!managerPods.some(
|
|
(current) => current.metadata.uid === pod.metadata.uid,
|
|
) &&
|
|
pod.status.containerStatuses?.[0] &&
|
|
!pod.status.containerStatuses[0].ready &&
|
|
(pod.status.containerStatuses[0].restartCount > 0 ||
|
|
pod.status.containerStatuses[0].state?.waiting?.reason ===
|
|
'CrashLoopBackOff'),
|
|
);
|
|
return ready.length === 2 && candidate
|
|
? { ready: true, value: candidate }
|
|
: {
|
|
ready: false,
|
|
fact: `${ready.length} ready Pods; rollback=${Boolean(
|
|
candidate,
|
|
)}`,
|
|
};
|
|
},
|
|
);
|
|
applyIdentity(keysets[2]);
|
|
patchGeneration(fixture, '3-rollback-recovered');
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'delete',
|
|
'pod',
|
|
rollback.value.metadata.name,
|
|
'--grace-period=0',
|
|
'--force',
|
|
'--wait=true',
|
|
]);
|
|
waitManagementRollout(managerOptions(fixture));
|
|
managerPods = await readyManagementPods(managerOptions(fixture));
|
|
|
|
const previousSerialSha256 = pki.oldSerialSha256();
|
|
pki.revokeOldClient();
|
|
pkiMaterial = pki.read();
|
|
const currentBundleSha256 = pki.bundleSha256();
|
|
const preCertificateUids = new Set(
|
|
managerPods.map((pod) => pod.metadata.uid),
|
|
);
|
|
applyTls();
|
|
patchGeneration(fixture, '3-client-crl-2', {
|
|
'qinglong.io/run-management-client-ca-sha256': sha256(pkiMaterial.ca),
|
|
'qinglong.io/run-management-client-crl-sha256': sha256(
|
|
pkiMaterial.clientCrl,
|
|
),
|
|
});
|
|
const certificateRollout = await waitForTwoPreserved({
|
|
...managerOptions(fixture),
|
|
excludedUids: preCertificateUids,
|
|
expectedGeneration: '3-client-crl-2',
|
|
description: 'zero-unavailable Run manager client certificate rollout',
|
|
});
|
|
const certificatePodsFullyReplaced = certificateRollout.pods.every(
|
|
(pod) => !preCertificateUids.has(pod.metadata.uid),
|
|
);
|
|
assert.equal(certificatePodsFullyReplaced, true);
|
|
managerPods = certificateRollout.pods;
|
|
const revokedCertificate = await executeClient(
|
|
{
|
|
name: 'ql3-run-retry-revoked-cert',
|
|
target: managerPods[0],
|
|
command: retry,
|
|
bearer: identity.assertion(newKey, retryAuthenticationJti),
|
|
clientCertificate: pkiMaterial.oldClientCertificate,
|
|
clientKey: pkiMaterial.oldClientKey,
|
|
},
|
|
{ statusCode: 401, responseCode: 'client_certificate_required' },
|
|
);
|
|
const activeCertificate = await executeClient(
|
|
{
|
|
name: 'ql3-run-retry-active-cert',
|
|
target: managerPods[1],
|
|
command: retry,
|
|
bearer: identity.assertion(newKey, retryAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 200, resultField: 'retry', resultStatus: ['existing'] },
|
|
);
|
|
|
|
const primaryBeforeFailover = currentPrimaryPod(fixture);
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'delete',
|
|
'pod',
|
|
primaryBeforeFailover.metadata.name,
|
|
'--grace-period=0',
|
|
'--force',
|
|
'--wait=false',
|
|
]);
|
|
const promoted = await waitFor(
|
|
'CloudNativePG primary promotion',
|
|
600_000,
|
|
() => {
|
|
const status = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'cluster',
|
|
POSTGRES_CLUSTER,
|
|
]).status;
|
|
return status.currentPrimary &&
|
|
status.currentPrimary !== primaryBeforeFailover.metadata.name &&
|
|
Number(status.readyInstances) >= 2
|
|
? { ready: true, value: status.currentPrimary }
|
|
: {
|
|
ready: false,
|
|
fact: `primary=${status.currentPrimary || 'none'} ready=${
|
|
status.readyInstances ?? 0
|
|
}`,
|
|
};
|
|
},
|
|
);
|
|
await waitFor('CloudNativePG recovery to three instances', 900_000, () => {
|
|
const status = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'cluster',
|
|
POSTGRES_CLUSTER,
|
|
]).status;
|
|
return Number(status.readyInstances) === 3
|
|
? { ready: true, value: status }
|
|
: {
|
|
ready: false,
|
|
fact: `${status.readyInstances ?? 0}/3 ready database instances`,
|
|
};
|
|
});
|
|
|
|
const databaseService = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'service',
|
|
POSTGRES_CLUSTER + '-rw',
|
|
]);
|
|
const databaseSelector = databaseService.spec.selector;
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'patch',
|
|
'service',
|
|
POSTGRES_CLUSTER + '-rw',
|
|
'--type=merge',
|
|
'-p',
|
|
JSON.stringify({
|
|
spec: { selector: { ...databaseSelector, 'ql3.invalid': 'true' } },
|
|
}),
|
|
]);
|
|
const unavailable = await Promise.all(
|
|
managerPods.map((pod, index) =>
|
|
executeClient(
|
|
{
|
|
name: 'ql3-run-database-unavailable-' + String(index + 1),
|
|
target: pod,
|
|
command: stop,
|
|
bearer: identity.assertion(newKey, stopAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{ statusCode: 503, responseCode: 'unavailable' },
|
|
),
|
|
),
|
|
);
|
|
assert.deepEqual(
|
|
unavailable.map((entry) => entry.statusCode),
|
|
[503, 503],
|
|
);
|
|
await waitFor('Run manager readiness withdrawal', 60_000, () => {
|
|
const current = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'deployment',
|
|
DEPLOYMENT,
|
|
]);
|
|
return Number(current.status.readyReplicas ?? 0) === 0
|
|
? { ready: true, value: current }
|
|
: {
|
|
ready: false,
|
|
fact: `${current.status.readyReplicas ?? 0} ready replicas`,
|
|
};
|
|
});
|
|
assert.deepEqual(
|
|
managerPods.map((pod) => healthStatus(fixture, pod, '/readyz')),
|
|
[503, 503],
|
|
);
|
|
assert.deepEqual(
|
|
managerPods.map((pod) => healthStatus(fixture, pod, '/livez')),
|
|
[200, 200],
|
|
);
|
|
fixture.kubectl([
|
|
'-n',
|
|
NAMESPACE,
|
|
'patch',
|
|
'service',
|
|
POSTGRES_CLUSTER + '-rw',
|
|
'--type=json',
|
|
'-p',
|
|
JSON.stringify([
|
|
{ op: 'replace', path: '/spec/selector', value: databaseSelector },
|
|
]),
|
|
]);
|
|
await waitFor('restored CloudNativePG service endpoint', 120_000, () => {
|
|
const endpoints = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'endpoints',
|
|
POSTGRES_CLUSTER + '-rw',
|
|
]);
|
|
const count = endpoints.subsets?.flatMap(
|
|
(subset) => subset.addresses ?? [],
|
|
).length;
|
|
return count >= 1
|
|
? { ready: true, value: count }
|
|
: { ready: false, fact: `${count ?? 0} service endpoints` };
|
|
});
|
|
assert.deepEqual(
|
|
managerPods.map((pod) => healthStatus(fixture, pod, '/readyz')),
|
|
[503, 503],
|
|
);
|
|
const staleUids = new Set(managerPods.map((pod) => pod.metadata.uid));
|
|
patchGeneration(fixture, '3-database-recovered');
|
|
managerPods = await readyManagementPods({
|
|
...managerOptions(fixture),
|
|
excludedUids: staleUids,
|
|
expectedGeneration: '3-database-recovered',
|
|
});
|
|
const recoveredRequests = await Promise.all(
|
|
managerPods.map((pod, index) =>
|
|
executeClient(
|
|
{
|
|
name: 'ql3-run-database-recovered-' + String(index + 1),
|
|
target: pod,
|
|
command: stop,
|
|
bearer: identity.assertion(newKey, stopAuthenticationJti),
|
|
clientCertificate: pkiMaterial.newClientCertificate,
|
|
clientKey: pkiMaterial.newClientKey,
|
|
},
|
|
{
|
|
statusCode: 200,
|
|
resultField: 'stop',
|
|
resultStatus: ['already_requested'],
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
const finalPrimary = currentPrimaryPod(fixture);
|
|
const durable = durableRunManagementFacts(
|
|
fixture,
|
|
finalPrimary.metadata.name,
|
|
values,
|
|
psql,
|
|
);
|
|
assert.deepEqual(durable, {
|
|
sourceRunStatus: 'failed',
|
|
retryRunCount: 1,
|
|
retryAttemptCount: 1,
|
|
retryEventCount: 2,
|
|
stoppedRunCount: 1,
|
|
stopEventCount: 1,
|
|
allowedAuditCount: 2,
|
|
deniedAuditCount: 1,
|
|
weakAuthenticationAuditCount: 0,
|
|
duplicateMutationCount: 0,
|
|
identityGeneration: 3,
|
|
migrationCount: 57,
|
|
controlCoreCapability: 56,
|
|
postgresVersionNumber: 180004,
|
|
});
|
|
const roleRows = JSON.parse(
|
|
psql(
|
|
fixture,
|
|
finalPrimary.metadata.name,
|
|
`SELECT json_agg(json_build_object('name', rolname, 'login', rolcanlogin, 'superuser', rolsuper, 'createDatabase', rolcreatedb, 'createRole', rolcreaterole, 'replication', rolreplication, 'bypassRls', rolbypassrls) ORDER BY rolname) FROM pg_roles WHERE rolname IN (${ROLE_NAMES.map(
|
|
(role) => "'" + role + "'",
|
|
).join(',')})`,
|
|
),
|
|
);
|
|
const rolesLeastPrivilege = roleRows.every(
|
|
(role) =>
|
|
role.login === true &&
|
|
role.superuser === false &&
|
|
role.createDatabase === false &&
|
|
role.createRole === false &&
|
|
role.replication === false &&
|
|
role.bypassRls === false,
|
|
);
|
|
assert.equal(rolesLeastPrivilege, true);
|
|
const canI = (verb, resource) => {
|
|
const result = fixture.kubectl(
|
|
[
|
|
'auth',
|
|
'can-i',
|
|
verb,
|
|
resource,
|
|
'-n',
|
|
NAMESPACE,
|
|
'--as=system:serviceaccount:' + NAMESPACE + ':' + DEPLOYMENT,
|
|
],
|
|
{ capture: true, quiet: true, allowFailure: true },
|
|
);
|
|
const decision = result.stdout.trim();
|
|
assert.ok(decision === 'yes' || decision === 'no');
|
|
assert.equal(result.status === 0, decision === 'yes');
|
|
return decision;
|
|
};
|
|
assert.equal(canI('get', 'secrets'), 'no');
|
|
assert.equal(canI('patch', 'deployments.apps'), 'no');
|
|
|
|
const managerServiceIp = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'service',
|
|
DEPLOYMENT,
|
|
]).spec.clusterIP;
|
|
const networkProbe = {
|
|
fixture,
|
|
namespace: NAMESPACE,
|
|
adminImage,
|
|
appName: 'ql3-run-network-probe',
|
|
networkPolicyLabel: 'qinglong.io/run-management-client',
|
|
};
|
|
const labelledClientAllowed = await clientTcpProbe({
|
|
...networkProbe,
|
|
name: 'ql3-run-network-labelled',
|
|
targetHost: managerServiceIp,
|
|
port: 8448,
|
|
labelled: true,
|
|
expectedConnected: true,
|
|
});
|
|
const unlabelledClientDenied = await clientTcpProbe({
|
|
...networkProbe,
|
|
name: 'ql3-run-network-unlabelled',
|
|
targetHost: managerServiceIp,
|
|
port: 8448,
|
|
labelled: false,
|
|
expectedConnected: false,
|
|
});
|
|
const wrongPortDenied = await clientTcpProbe({
|
|
...networkProbe,
|
|
name: 'ql3-run-network-wrong-port',
|
|
targetHost: managerServiceIp,
|
|
port: 8447,
|
|
labelled: true,
|
|
expectedConnected: false,
|
|
});
|
|
const kubernetesServiceIp = fixture.kubectlJson([
|
|
'get',
|
|
'service',
|
|
'kubernetes',
|
|
'-n',
|
|
'default',
|
|
]).spec.clusterIP;
|
|
const postgresServiceIp = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'service',
|
|
POSTGRES_CLUSTER + '-rw',
|
|
]).spec.clusterIP;
|
|
const podProbe = {
|
|
fixture,
|
|
namespace: NAMESPACE,
|
|
podName: managerPods[0].metadata.name,
|
|
};
|
|
const cloudNativePgEgressAllowed =
|
|
podTcpProbe({ ...podProbe, host: postgresServiceIp, port: 5432 })
|
|
.status === 0;
|
|
const kubernetesApiEgressDenied =
|
|
podTcpProbe({ ...podProbe, host: kubernetesServiceIp, port: 443 })
|
|
.status !== 0;
|
|
const publicInternetEgressDenied =
|
|
podTcpProbe({ ...podProbe, host: '1.1.1.1', port: 443 }).status !== 0;
|
|
assert.equal(cloudNativePgEgressAllowed, true);
|
|
assert.equal(kubernetesApiEgressDenied, true);
|
|
assert.equal(publicInternetEgressDenied, true);
|
|
|
|
const finalNodes = fixture.kubectlJson(['get', 'nodes']).items;
|
|
const cniReadyNodes = finalNodes.filter(
|
|
(node) =>
|
|
podReady(node) &&
|
|
Array.isArray(node.spec.podCIDRs) &&
|
|
node.spec.podCIDRs.length === 1,
|
|
);
|
|
assert.equal(cniReadyNodes.length, 3);
|
|
assert.equal(
|
|
new Set(cniReadyNodes.map((node) => node.spec.podCIDRs[0])).size,
|
|
3,
|
|
);
|
|
const serverNode = finalNodes.find(
|
|
(node) => node.metadata.name === fixture.server,
|
|
);
|
|
assert.equal(
|
|
serverNode?.metadata.annotations?.[
|
|
'flannel.alpha.coreos.com/backend-type'
|
|
],
|
|
'vxlan',
|
|
);
|
|
assert.equal(
|
|
serverNode?.metadata.annotations?.[
|
|
'flannel.alpha.coreos.com/kube-subnet-manager'
|
|
],
|
|
'true',
|
|
);
|
|
const finalCluster = fixture.kubectlJson([
|
|
'-n',
|
|
NAMESPACE,
|
|
'get',
|
|
'cluster',
|
|
POSTGRES_CLUSTER,
|
|
]);
|
|
assert.equal(Number(finalCluster.status.readyInstances), 3);
|
|
|
|
const baselineSuccesses = [
|
|
retryAccepted,
|
|
retryReplay,
|
|
overlapOld,
|
|
overlapNew,
|
|
activeNew,
|
|
activeCertificate,
|
|
];
|
|
const report = {
|
|
schemaVersion: 1,
|
|
fixture: FIXTURE,
|
|
observedAt: new Date().toISOString(),
|
|
platform: {
|
|
distribution: 'k3s',
|
|
kubernetesVersion: nodes[0].status.nodeInfo.kubeletVersion,
|
|
architecture,
|
|
kubernetesImageId: imageIdDigest(k3sImageInfo),
|
|
managementImageId: imageIdDigest(adminImageInfo),
|
|
cniName: 'flannel',
|
|
cniDistributionBinding: fixture.k3sImage,
|
|
controlPlaneNodes: 1,
|
|
workerNodes: 2,
|
|
cniReadyNodes: cniReadyNodes.length,
|
|
},
|
|
database: {
|
|
operator: 'cloudnative-pg',
|
|
operatorVersion: LOCK.operator.version,
|
|
postgresVersionNumber: durable.postgresVersionNumber,
|
|
postgresImageId: imageIdDigest(postgresImageInfo),
|
|
instances: Number(finalCluster.spec.instances),
|
|
readyInstances: Number(finalCluster.status.readyInstances),
|
|
managerRole: 'ql3_run_manager',
|
|
migrationCount: durable.migrationCount,
|
|
controlCoreCapability: durable.controlCoreCapability,
|
|
tlsVerified: true,
|
|
primaryChangedDuringFailover:
|
|
promoted.value !== primaryBeforeFailover.metadata.name,
|
|
},
|
|
deployment: {
|
|
namespace: NAMESPACE,
|
|
service: DEPLOYMENT,
|
|
port: 8448,
|
|
replicas: deployment.spec.replicas,
|
|
readyReplicas: managerPods.length,
|
|
podIdentitySha256: managerPods.map((pod) => sha256(pod.metadata.uid)),
|
|
nodeIdentitySha256: managerPods.map((pod) => sha256(pod.spec.nodeName)),
|
|
serviceAccount: DEPLOYMENT,
|
|
automountServiceAccountToken: false,
|
|
requiredPodAntiAffinity: true,
|
|
podDisruptionBudgetMinAvailable: 1,
|
|
maxUnavailable: 0,
|
|
maxConnectionsPerPod: 2,
|
|
},
|
|
client: {
|
|
binary: 'ql3-run-client',
|
|
operations: ['run.retry', 'run.stop'],
|
|
inputKind: 'Secret',
|
|
inputImmutable: true,
|
|
callerDrivenJob: true,
|
|
backoffLimit: 0,
|
|
serviceAccountTokenMounted: false,
|
|
rbacGranted: false,
|
|
transportProtocol: 'TLSv1.3',
|
|
mutualTls: true,
|
|
servernameVerified: true,
|
|
exactPodRequests: baselineSuccesses.length,
|
|
retryStatuses: [
|
|
retryAccepted.output.result.retry.status,
|
|
retryReplay.output.result.retry.status,
|
|
activeCertificate.output.result.retry.status,
|
|
],
|
|
stopStatuses: [
|
|
overlapOld.output.result.stop.status,
|
|
overlapNew.output.result.stop.status,
|
|
activeNew.output.result.stop.status,
|
|
],
|
|
responseRedacted: true,
|
|
},
|
|
identityRotation: {
|
|
overlapOldAssertionAccepted: overlapOld.statusCode === 200,
|
|
overlapNewAssertionAccepted: overlapNew.statusCode === 200,
|
|
revokedOldAssertionRejected: rejectedOldKey.statusCode === 401,
|
|
activeNewAssertionAccepted: activeNew.statusCode === 200,
|
|
rollbackSurgeFailedClosed: Boolean(rollback.value),
|
|
twoReadyReplicasPreserved:
|
|
generation2.minimumReady >= 2 && generation3.minimumReady >= 2,
|
|
durableGenerationReachedThree: durable.identityGeneration === 3,
|
|
},
|
|
certificateRotation: {
|
|
previousSerialSha256,
|
|
currentSerialSha256: pki.newSerialSha256(),
|
|
previousBundleSha256,
|
|
currentBundleSha256,
|
|
oldClientAcceptedBefore: retryAccepted.statusCode === 200,
|
|
replacementClientAcceptedBefore: retryReplay.statusCode === 200,
|
|
oldClientRejectedAfter: revokedCertificate.statusCode === 401,
|
|
replacementClientAcceptedAfter: activeCertificate.statusCode === 200,
|
|
fullPodReplacement: certificatePodsFullyReplaced,
|
|
allReplicasReadyThroughout: certificateRollout.minimumReady >= 2,
|
|
},
|
|
availability: {
|
|
databaseFailureWithdrewReadiness: true,
|
|
databaseFailurePreservedLiveness: true,
|
|
stalePodsDidNotRecoverInPlace: true,
|
|
freshPodsRecoveredAfterDatabase: managerPods.every(
|
|
(pod) => !staleUids.has(pod.metadata.uid),
|
|
),
|
|
bothReplicasServedAfterRecovery: recoveredRequests.every(
|
|
(entry) => entry.statusCode === 200,
|
|
),
|
|
},
|
|
isolation: {
|
|
labelledClientAllowed,
|
|
unlabelledClientDenied,
|
|
wrongPortDenied,
|
|
kubernetesApiEgressDenied,
|
|
publicInternetEgressDenied,
|
|
cloudNativePgEgressAllowed,
|
|
managerSecretReadDenied: canI('get', 'secrets') === 'no',
|
|
managerMutationRbacDenied: canI('patch', 'deployments.apps') === 'no',
|
|
},
|
|
durability: {
|
|
sourceRunStatus: durable.sourceRunStatus,
|
|
retryRunCount: durable.retryRunCount,
|
|
retryAttemptCount: durable.retryAttemptCount,
|
|
retryEventCount: durable.retryEventCount,
|
|
stoppedRunCount: durable.stoppedRunCount,
|
|
stopEventCount: durable.stopEventCount,
|
|
allowedAuditCount: durable.allowedAuditCount,
|
|
deniedAuditCount: durable.deniedAuditCount,
|
|
duplicateMutationCount: durable.duplicateMutationCount,
|
|
identityGeneration: durable.identityGeneration,
|
|
weakAuthenticationAuditCount: durable.weakAuthenticationAuditCount,
|
|
survivedCloudNativePgFailover: true,
|
|
},
|
|
gates: {
|
|
realThreeNodeKubernetes: nodes.length === 3,
|
|
realCniPolicy:
|
|
labelledClientAllowed &&
|
|
unlabelledClientDenied &&
|
|
wrongPortDenied &&
|
|
kubernetesApiEgressDenied &&
|
|
publicInternetEgressDenied &&
|
|
cloudNativePgEgressAllowed,
|
|
threeInstanceCloudNativePg: databasePods.length === 3,
|
|
twoManagerPodsOnDistinctNodes:
|
|
new Set(managerPods.map((pod) => pod.spec.nodeName)).size === 2,
|
|
tls13ProductClientAcrossBothPods:
|
|
new Set(baselineSuccesses.map((entry) => entry.targetPod)).size >= 2,
|
|
strongUserRetryAndStop:
|
|
weakRejected.statusCode === 401 &&
|
|
outsiderDenied.statusCode === 403 &&
|
|
retryAccepted.statusCode === 200 &&
|
|
overlapOld.statusCode === 200,
|
|
identityProjectionRotation: durable.identityGeneration === 3,
|
|
certificateRevocationRollout: revokedCertificate.statusCode === 401,
|
|
databaseReadinessFence: true,
|
|
durableFactsSurvivedFailover: true,
|
|
leastPrivilege: rolesLeastPrivilege,
|
|
passed: true,
|
|
},
|
|
limitations: [...LIMITATIONS],
|
|
};
|
|
const audit = validateRunManagementKubernetesLiveReport(report);
|
|
assert.deepEqual(audit.findings, []);
|
|
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2) + '\n', {
|
|
mode: 0o600,
|
|
flag: 'wx',
|
|
});
|
|
process.stdout.write(
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
fixture: FIXTURE,
|
|
reportWritten: true,
|
|
passed: true,
|
|
}) + '\n',
|
|
);
|
|
} finally {
|
|
await fixture.cleanup();
|
|
if (adminImageBuilt) {
|
|
run(fixture.docker, ['image', 'rm', '-f', adminImage], {
|
|
capture: true,
|
|
quiet: true,
|
|
allowFailure: true,
|
|
});
|
|
}
|
|
if (controlImageBuilt) {
|
|
run(fixture.docker, ['image', 'rm', '-f', controlImage], {
|
|
capture: true,
|
|
quiet: true,
|
|
allowFailure: true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch((error) => {
|
|
process.stderr.write(
|
|
'QL3 Run management Kubernetes live contract failed: ' +
|
|
(error instanceof Error
|
|
? error.stack || error.message
|
|
: String(error)) +
|
|
'\n',
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
identity,
|
|
retryCommand,
|
|
stopCommand,
|
|
};
|