mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
fix(ci): align recovery gate with durable admission
This commit is contained in:
@@ -340,6 +340,172 @@ function signedPackageRoutes(material, lock, privateKey) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createInstallAuthority(actionInput, phase, proposedAtMs, eventBase) {
|
||||||
|
const {
|
||||||
|
consumeApprovalRequest,
|
||||||
|
createApprovalRequest,
|
||||||
|
decideApprovalRequest,
|
||||||
|
} = ql3Require('@qinglong/runtime-core/approved-action');
|
||||||
|
const {
|
||||||
|
createPluginPackageInstallProposal,
|
||||||
|
resolvePluginPackageInstallProposal,
|
||||||
|
} = ql3Require('@qinglong/runtime-core/plugin-package-proposal');
|
||||||
|
const requester = { type: 'user', id: 'plugin-recovery-e2e-owner' };
|
||||||
|
const reviewer = { type: 'user', id: 'plugin-recovery-e2e-reviewer' };
|
||||||
|
const consumer = { type: 'system', id: 'plugin-recovery-e2e-dispatcher' };
|
||||||
|
const fence = { projectVersion: 1, bindingVersion: 1 };
|
||||||
|
const requestedAtMs = proposedAtMs + 1;
|
||||||
|
const decidedAtMs = proposedAtMs + 2;
|
||||||
|
const consumedAtMs = proposedAtMs + 3;
|
||||||
|
const lockCreatedAtMs = proposedAtMs + 4;
|
||||||
|
const expiresAtMs = proposedAtMs + 60 * 60 * 1000;
|
||||||
|
const actionRef = `plugin-package:e2e-monitor:${phase}`;
|
||||||
|
const proposal = createPluginPackageInstallProposal({
|
||||||
|
actionRef,
|
||||||
|
actionInput,
|
||||||
|
proposedBy: requester,
|
||||||
|
proposalFence: fence,
|
||||||
|
createdAtMs: proposedAtMs,
|
||||||
|
});
|
||||||
|
const action = {
|
||||||
|
actionRef: proposal.actionRef,
|
||||||
|
actionType: proposal.actionType,
|
||||||
|
permission: proposal.permission,
|
||||||
|
actionDigest: proposal.actionDigest,
|
||||||
|
previewDigest: proposal.previewDigest,
|
||||||
|
};
|
||||||
|
const request = createApprovalRequest({
|
||||||
|
id: `approval-plugin-recovery-e2e-${phase}`,
|
||||||
|
projectId: proposal.projectId,
|
||||||
|
action,
|
||||||
|
risk: 'high',
|
||||||
|
decisionMode: 'separation_of_duty',
|
||||||
|
requestedBy: requester,
|
||||||
|
requestedAtMs,
|
||||||
|
expiresAtMs,
|
||||||
|
requestFence: fence,
|
||||||
|
});
|
||||||
|
const decision = {
|
||||||
|
expectedVersion: 1,
|
||||||
|
decisionId: `decision-plugin-recovery-e2e-${phase}`,
|
||||||
|
decision: 'approved',
|
||||||
|
reasonCode: 'reviewed',
|
||||||
|
principal: {
|
||||||
|
subject: reviewer,
|
||||||
|
authenticationId: `auth-plugin-recovery-e2e-reviewer-${phase}`,
|
||||||
|
authenticatedAtMs: proposedAtMs,
|
||||||
|
expiresAtMs,
|
||||||
|
assurance: 'multi_factor',
|
||||||
|
},
|
||||||
|
decidedAtMs,
|
||||||
|
authorizationFence: fence,
|
||||||
|
};
|
||||||
|
const decided = decideApprovalRequest(request, decision);
|
||||||
|
const consumption = {
|
||||||
|
expectedVersion: 2,
|
||||||
|
consumptionId: `consume-plugin-recovery-e2e-${phase}`,
|
||||||
|
dispatchId: `dispatch-plugin-recovery-e2e-${phase}`,
|
||||||
|
action,
|
||||||
|
requestedBy: requester,
|
||||||
|
consumedBy: consumer,
|
||||||
|
consumedAtMs,
|
||||||
|
authorizationFence: fence,
|
||||||
|
};
|
||||||
|
const consumed = consumeApprovalRequest(decided, consumption);
|
||||||
|
const audit = (
|
||||||
|
offset,
|
||||||
|
requestId,
|
||||||
|
operationId,
|
||||||
|
subject,
|
||||||
|
authenticationId,
|
||||||
|
outcome,
|
||||||
|
reasons,
|
||||||
|
occurredAtMs,
|
||||||
|
) => ({
|
||||||
|
eventId: `10000000-0000-4000-8000-${String(
|
||||||
|
eventBase + offset,
|
||||||
|
).padStart(12, '0')}`,
|
||||||
|
requestId,
|
||||||
|
operationId,
|
||||||
|
projectId: proposal.projectId,
|
||||||
|
subject,
|
||||||
|
authenticationId,
|
||||||
|
outcome,
|
||||||
|
reasons,
|
||||||
|
fence,
|
||||||
|
occurredAtMs,
|
||||||
|
});
|
||||||
|
return Object.freeze({
|
||||||
|
requester,
|
||||||
|
reviewer,
|
||||||
|
consumer,
|
||||||
|
fence,
|
||||||
|
proposalCommand: {
|
||||||
|
proposal,
|
||||||
|
audit: audit(
|
||||||
|
0,
|
||||||
|
proposal.actionRef,
|
||||||
|
'plugin_package.propose',
|
||||||
|
requester,
|
||||||
|
`auth-plugin-recovery-e2e-owner-${phase}`,
|
||||||
|
'allowed',
|
||||||
|
['package_proposal'],
|
||||||
|
proposedAtMs,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
requestCommand: {
|
||||||
|
request,
|
||||||
|
audit: audit(
|
||||||
|
1,
|
||||||
|
`request-plugin-recovery-e2e-${phase}`,
|
||||||
|
'approval.request',
|
||||||
|
requester,
|
||||||
|
`auth-plugin-recovery-e2e-owner-${phase}`,
|
||||||
|
'approval_required',
|
||||||
|
['package_review'],
|
||||||
|
requestedAtMs,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
decisionCommand: {
|
||||||
|
requestId: request.id,
|
||||||
|
...decision,
|
||||||
|
audit: audit(
|
||||||
|
2,
|
||||||
|
`decision-plugin-recovery-e2e-${phase}`,
|
||||||
|
'approval.decide',
|
||||||
|
reviewer,
|
||||||
|
decision.principal.authenticationId,
|
||||||
|
'allowed',
|
||||||
|
['role_grant'],
|
||||||
|
decidedAtMs,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
consumptionCommand: {
|
||||||
|
requestId: request.id,
|
||||||
|
...consumption,
|
||||||
|
audit: audit(
|
||||||
|
3,
|
||||||
|
`consume-plugin-recovery-e2e-${phase}`,
|
||||||
|
'approval.consume',
|
||||||
|
consumer,
|
||||||
|
`auth-plugin-recovery-e2e-dispatcher-${phase}`,
|
||||||
|
'allowed',
|
||||||
|
['role_grant'],
|
||||||
|
consumedAtMs,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
dispatch: consumed.dispatch,
|
||||||
|
lock: resolvePluginPackageInstallProposal(
|
||||||
|
proposal,
|
||||||
|
consumed.dispatch,
|
||||||
|
lockCreatedAtMs,
|
||||||
|
),
|
||||||
|
admissionAuditEventId: `10000000-0000-4000-8000-${String(
|
||||||
|
eventBase + 4,
|
||||||
|
).padStart(12, '0')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
||||||
if (
|
if (
|
||||||
typeof registry !== 'string' ||
|
typeof registry !== 'string' ||
|
||||||
@@ -356,11 +522,6 @@ function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
|||||||
const { planPluginPackageInstall } = ql3Require(
|
const { planPluginPackageInstall } = ql3Require(
|
||||||
'@qinglong/runtime-core/plugin-package',
|
'@qinglong/runtime-core/plugin-package',
|
||||||
);
|
);
|
||||||
const {
|
|
||||||
createPluginPackageLock,
|
|
||||||
pluginPackageInstallActionDigest,
|
|
||||||
pluginPackageInstallPlanDigest,
|
|
||||||
} = ql3Require('@qinglong/runtime-core/plugin-package-install');
|
|
||||||
const { createPluginPackageResourceGenerationFromReferences } = ql3Require(
|
const { createPluginPackageResourceGenerationFromReferences } = ql3Require(
|
||||||
'@qinglong/runtime-core/plugin-package-resource-generation',
|
'@qinglong/runtime-core/plugin-package-resource-generation',
|
||||||
);
|
);
|
||||||
@@ -386,22 +547,14 @@ function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
|||||||
deploymentProfile: 'cluster-control',
|
deploymentProfile: 'cluster-control',
|
||||||
targetGeneration: 1,
|
targetGeneration: 1,
|
||||||
};
|
};
|
||||||
const initialLock = createPluginPackageLock({
|
const initialAuthority = createInstallAuthority(
|
||||||
...initialAction,
|
initialAction,
|
||||||
approval: {
|
'initial',
|
||||||
requestId: 'approval-plugin-recovery-e2e-initial',
|
|
||||||
requestVersion: 1,
|
|
||||||
dispatchId: 'dispatch-plugin-recovery-e2e-initial',
|
|
||||||
actionDigest: pluginPackageInstallActionDigest(initialAction),
|
|
||||||
previewDigest: pluginPackageInstallPlanDigest(initialPlan),
|
|
||||||
approvedBy: { type: 'user', id: 'e2e-owner' },
|
|
||||||
approvedAtMs: createdAtMs - 1,
|
|
||||||
expiresAtMs: createdAtMs + 60 * 60 * 1000,
|
|
||||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
||||||
},
|
|
||||||
createdAtMs,
|
createdAtMs,
|
||||||
});
|
1,
|
||||||
const upgradeCreatedAtMs = createdAtMs + 10;
|
);
|
||||||
|
const initialLock = initialAuthority.lock;
|
||||||
|
const upgradeCreatedAtMs = initialLock.createdAtMs + 10;
|
||||||
const upgradeManifest = pluginManifest(architecture, '2.0.0', true);
|
const upgradeManifest = pluginManifest(architecture, '2.0.0', true);
|
||||||
const upgradeMaterial = packageMaterial(
|
const upgradeMaterial = packageMaterial(
|
||||||
registry,
|
registry,
|
||||||
@@ -426,21 +579,13 @@ function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
|||||||
targetGeneration: 2,
|
targetGeneration: 2,
|
||||||
previousLockDigest: initialLock.lockDigest,
|
previousLockDigest: initialLock.lockDigest,
|
||||||
};
|
};
|
||||||
const upgradeLock = createPluginPackageLock({
|
const upgradeAuthority = createInstallAuthority(
|
||||||
...upgradeAction,
|
upgradeAction,
|
||||||
approval: {
|
'upgrade',
|
||||||
requestId: 'approval-plugin-recovery-e2e-upgrade',
|
upgradeCreatedAtMs,
|
||||||
requestVersion: 1,
|
11,
|
||||||
dispatchId: 'dispatch-plugin-recovery-e2e-upgrade',
|
);
|
||||||
actionDigest: pluginPackageInstallActionDigest(upgradeAction),
|
const upgradeLock = upgradeAuthority.lock;
|
||||||
previewDigest: pluginPackageInstallPlanDigest(upgradePlan),
|
|
||||||
approvedBy: { type: 'user', id: 'e2e-owner' },
|
|
||||||
approvedAtMs: upgradeCreatedAtMs - 1,
|
|
||||||
expiresAtMs: upgradeCreatedAtMs + 60 * 60 * 1000,
|
|
||||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
||||||
},
|
|
||||||
createdAtMs: upgradeCreatedAtMs,
|
|
||||||
});
|
|
||||||
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
||||||
const publicKeyPem = publicKey.export({ format: 'pem', type: 'spki' });
|
const publicKeyPem = publicKey.export({ format: 'pem', type: 'spki' });
|
||||||
const trust = {
|
const trust = {
|
||||||
@@ -467,6 +612,7 @@ function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
|||||||
privateKey,
|
privateKey,
|
||||||
);
|
);
|
||||||
const initial = Object.freeze({
|
const initial = Object.freeze({
|
||||||
|
authority: initialAuthority,
|
||||||
installationId: 'install-plugin-recovery-e2e-initial',
|
installationId: 'install-plugin-recovery-e2e-initial',
|
||||||
manifest: initialManifest,
|
manifest: initialManifest,
|
||||||
lock: initialLock,
|
lock: initialLock,
|
||||||
@@ -483,6 +629,7 @@ function createFixture({ registry, architecture, createdAtMs = Date.now() }) {
|
|||||||
routes: initialRoutes,
|
routes: initialRoutes,
|
||||||
});
|
});
|
||||||
const upgrade = Object.freeze({
|
const upgrade = Object.freeze({
|
||||||
|
authority: upgradeAuthority,
|
||||||
installationId: 'install-plugin-recovery-e2e-upgrade',
|
installationId: 'install-plugin-recovery-e2e-upgrade',
|
||||||
manifest: upgradeManifest,
|
manifest: upgradeManifest,
|
||||||
lock: upgradeLock,
|
lock: upgradeLock,
|
||||||
@@ -518,8 +665,12 @@ function readFixture(filePath) {
|
|||||||
value.schema !== FIXTURE_SCHEMA ||
|
value.schema !== FIXTURE_SCHEMA ||
|
||||||
!Array.isArray(value.routes) ||
|
!Array.isArray(value.routes) ||
|
||||||
!value.initial?.lock ||
|
!value.initial?.lock ||
|
||||||
|
!value.initial?.authority?.proposalCommand?.proposal ||
|
||||||
|
!value.initial?.authority?.dispatch ||
|
||||||
!value.initial?.generation ||
|
!value.initial?.generation ||
|
||||||
!value.upgrade?.lock ||
|
!value.upgrade?.lock ||
|
||||||
|
!value.upgrade?.authority?.proposalCommand?.proposal ||
|
||||||
|
!value.upgrade?.authority?.dispatch ||
|
||||||
!value.upgrade?.generation ||
|
!value.upgrade?.generation ||
|
||||||
!value.trust
|
!value.trust
|
||||||
) {
|
) {
|
||||||
@@ -624,32 +775,31 @@ async function runSeed() {
|
|||||||
assertPostgresPackageExecutorSchemaReady,
|
assertPostgresPackageExecutorSchemaReady,
|
||||||
createPostgresDatabaseOpener,
|
createPostgresDatabaseOpener,
|
||||||
loadPostgresConnectionEnvironment,
|
loadPostgresConnectionEnvironment,
|
||||||
PostgresPluginPackageSecretBindingTransitionRepository,
|
PostgresApprovedActionExecutionRepository,
|
||||||
} = ql3Require('@qinglong/cluster-postgres/package-executor');
|
} = ql3Require('@qinglong/cluster-postgres/package-executor');
|
||||||
const {
|
const {
|
||||||
assertPostgresPackageManagerSchemaReady,
|
assertPostgresPackageManagerSchemaReady,
|
||||||
|
PostgresApprovalRequestRepository,
|
||||||
PostgresPluginPackagePublisherTrustAuthorityRepository,
|
PostgresPluginPackagePublisherTrustAuthorityRepository,
|
||||||
} = ql3Require('@qinglong/cluster-postgres/package-manager');
|
} = ql3Require('@qinglong/cluster-postgres/package-manager');
|
||||||
|
const { PostgresProjectPolicyRepository } = ql3Require(
|
||||||
|
'@qinglong/cluster-postgres/project-policy',
|
||||||
|
);
|
||||||
const { PostgresPluginPackageInstallRepository } = ql3Require(
|
const { PostgresPluginPackageInstallRepository } = ql3Require(
|
||||||
'@qinglong/cluster-postgres/plugin-package-install',
|
'@qinglong/cluster-postgres/plugin-package-install',
|
||||||
);
|
);
|
||||||
const {
|
const { PostgresPluginPackageInstallProposalRepository } = ql3Require(
|
||||||
createPluginPackageInstall,
|
'@qinglong/cluster-postgres/plugin-package-proposal',
|
||||||
normalizePluginPackageLock,
|
|
||||||
pluginPackageInstallCreate,
|
|
||||||
} = ql3Require('@qinglong/runtime-core/plugin-package-install');
|
|
||||||
const { createPluginPackageSecretBindingTarget } = ql3Require(
|
|
||||||
'@qinglong/runtime-core/plugin-package-secret-binding',
|
|
||||||
);
|
);
|
||||||
const { createPluginPackageSecretBindingTransitionPlan } = ql3Require(
|
const { normalizePluginPackageLock } = ql3Require(
|
||||||
'@qinglong/runtime-core/plugin-package-secret-binding-transition-plan',
|
'@qinglong/runtime-core/plugin-package-install',
|
||||||
);
|
);
|
||||||
const { createPluginPackagePublisherTrustSnapshot } = ql3Require(
|
const { createPluginPackagePublisherTrustSnapshot } = ql3Require(
|
||||||
'@qinglong/runtime-core/plugin-package-publisher-trust',
|
'@qinglong/runtime-core/plugin-package-publisher-trust',
|
||||||
);
|
);
|
||||||
const fixture = readFixture(process.env.QL3_E2E_FIXTURE_FILE);
|
const fixture = readFixture(process.env.QL3_E2E_FIXTURE_FILE);
|
||||||
const mode = process.env.QL3_E2E_MODE;
|
const mode = process.env.QL3_E2E_MODE;
|
||||||
if (!['seed-initial', 'seed-upgrade', 'commit-transition'].includes(mode)) {
|
if (!['seed-initial', 'seed-upgrade'].includes(mode)) {
|
||||||
throw new Error('Plugin Package E2E seed mode is invalid');
|
throw new Error('Plugin Package E2E seed mode is invalid');
|
||||||
}
|
}
|
||||||
const connection = loadPostgresConnectionEnvironment(process.env, {
|
const connection = loadPostgresConnectionEnvironment(process.env, {
|
||||||
@@ -671,45 +821,54 @@ async function runSeed() {
|
|||||||
})();
|
})();
|
||||||
try {
|
try {
|
||||||
await assertPostgresPackageExecutorSchemaReady(database.pool);
|
await assertPostgresPackageExecutorSchemaReady(database.pool);
|
||||||
if (mode === 'commit-transition') {
|
|
||||||
const plannedAtMs = Date.now();
|
|
||||||
const transitionPlan = createPluginPackageSecretBindingTransitionPlan({
|
|
||||||
previousTarget: createPluginPackageSecretBindingTarget(
|
|
||||||
fixture.initial.generation,
|
|
||||||
fixture.initial.manifest,
|
|
||||||
),
|
|
||||||
previousBinding: null,
|
|
||||||
previousAttemptGeneration: 1,
|
|
||||||
nextGeneration: fixture.upgrade.generation,
|
|
||||||
nextManifest: fixture.upgrade.manifest,
|
|
||||||
assignments: [],
|
|
||||||
plannedAtMs,
|
|
||||||
});
|
|
||||||
const result =
|
|
||||||
await new PostgresPluginPackageSecretBindingTransitionRepository(
|
|
||||||
database.pool,
|
|
||||||
).apply({
|
|
||||||
transitionPlan,
|
|
||||||
evidenceDigest: transitionPlan.transitionDigest,
|
|
||||||
committedAtMs: plannedAtMs + 1,
|
|
||||||
});
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify({
|
|
||||||
schema: 'qinglong/plugin-package-recovery-e2e-transition-result@v1',
|
|
||||||
event: 'transition_completed',
|
|
||||||
status: result.status,
|
|
||||||
generationDigest: transitionPlan.nextTarget.generationDigest,
|
|
||||||
transitionDigest: transitionPlan.transitionDigest,
|
|
||||||
bindingDigest: result.receipt.bindingDigest,
|
|
||||||
receiptDigest: result.receipt.receiptDigest,
|
|
||||||
})}\n`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selected =
|
const selected =
|
||||||
mode === 'seed-initial' ? fixture.initial : fixture.upgrade;
|
mode === 'seed-initial' ? fixture.initial : fixture.upgrade;
|
||||||
|
const runtimeConnection = loadPostgresConnectionEnvironment(process.env, {
|
||||||
|
host: 'QL3_E2E_POSTGRES_HOST',
|
||||||
|
port: 'QL3_E2E_POSTGRES_PORT',
|
||||||
|
database: 'QL3_E2E_POSTGRES_DATABASE',
|
||||||
|
user: 'QL3_E2E_POSTGRES_RUNTIME_USER',
|
||||||
|
password: 'QL3_E2E_POSTGRES_RUNTIME_PASSWORD',
|
||||||
|
});
|
||||||
|
const runtimeDatabase = await createPostgresDatabaseOpener({
|
||||||
|
role: 'runtime',
|
||||||
|
connection: { ...runtimeConnection, tls: { mode: 'disable' } },
|
||||||
|
pool: {
|
||||||
|
applicationName: 'qinglong3-plugin-package-e2e-policy-seed',
|
||||||
|
maxConnections: 1,
|
||||||
|
connectionTimeoutMs: 15_000,
|
||||||
|
},
|
||||||
|
onPoolError() {},
|
||||||
|
})();
|
||||||
|
try {
|
||||||
|
const policies = new PostgresProjectPolicyRepository(
|
||||||
|
runtimeDatabase.pool,
|
||||||
|
);
|
||||||
|
for (const [subject, role] of [
|
||||||
|
[selected.authority.requester, 'owner'],
|
||||||
|
[selected.authority.reviewer, 'admin'],
|
||||||
|
]) {
|
||||||
|
const policy = await policies.append({
|
||||||
|
expectedCurrentVersion: 0,
|
||||||
|
binding: {
|
||||||
|
projectId: selected.lock.projectId,
|
||||||
|
subject,
|
||||||
|
version: 1,
|
||||||
|
state: 'active',
|
||||||
|
role,
|
||||||
|
mutationId: `plugin-recovery-e2e-grant-${role}`,
|
||||||
|
changedBy: { type: 'system', id: 'plugin-recovery-e2e-bootstrap' },
|
||||||
|
createdAtMs: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!['inserted', 'existing'].includes(policy.status)) {
|
||||||
|
throw new Error('Plugin Package E2E policy seed was not durable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await runtimeDatabase.close();
|
||||||
|
}
|
||||||
let publisherTrustStatus = null;
|
let publisherTrustStatus = null;
|
||||||
if (mode === 'seed-initial') {
|
|
||||||
const packageManagerConnection = loadPostgresConnectionEnvironment(
|
const packageManagerConnection = loadPostgresConnectionEnvironment(
|
||||||
process.env,
|
process.env,
|
||||||
{
|
{
|
||||||
@@ -727,7 +886,7 @@ async function runSeed() {
|
|||||||
tls: { mode: 'disable' },
|
tls: { mode: 'disable' },
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
applicationName: 'qinglong3-plugin-package-e2e-trust-seed',
|
applicationName: 'qinglong3-plugin-package-e2e-management-seed',
|
||||||
maxConnections: 1,
|
maxConnections: 1,
|
||||||
connectionTimeoutMs: 15_000,
|
connectionTimeoutMs: 15_000,
|
||||||
},
|
},
|
||||||
@@ -737,6 +896,7 @@ async function runSeed() {
|
|||||||
await assertPostgresPackageManagerSchemaReady(
|
await assertPostgresPackageManagerSchemaReady(
|
||||||
packageManagerDatabase.pool,
|
packageManagerDatabase.pool,
|
||||||
);
|
);
|
||||||
|
if (mode === 'seed-initial') {
|
||||||
const observed =
|
const observed =
|
||||||
await new PostgresPluginPackagePublisherTrustAuthorityRepository(
|
await new PostgresPluginPackagePublisherTrustAuthorityRepository(
|
||||||
packageManagerDatabase.pool,
|
packageManagerDatabase.pool,
|
||||||
@@ -754,10 +914,18 @@ async function runSeed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
publisherTrustStatus = observed.status;
|
publisherTrustStatus = observed.status;
|
||||||
|
}
|
||||||
|
await new PostgresPluginPackageInstallProposalRepository(
|
||||||
|
packageManagerDatabase.pool,
|
||||||
|
).createProposal(selected.authority.proposalCommand);
|
||||||
|
const approvals = new PostgresApprovalRequestRepository(
|
||||||
|
packageManagerDatabase.pool,
|
||||||
|
);
|
||||||
|
await approvals.create(selected.authority.requestCommand);
|
||||||
|
await approvals.decide(selected.authority.decisionCommand);
|
||||||
} finally {
|
} finally {
|
||||||
await packageManagerDatabase.close();
|
await packageManagerDatabase.close();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const lock = normalizePluginPackageLock(selected.lock);
|
const lock = normalizePluginPackageLock(selected.lock);
|
||||||
const repository = new PostgresPluginPackageInstallRepository(
|
const repository = new PostgresPluginPackageInstallRepository(
|
||||||
database.pool,
|
database.pool,
|
||||||
@@ -772,16 +940,68 @@ async function runSeed() {
|
|||||||
) {
|
) {
|
||||||
throw new Error('Plugin Package E2E previous install head is invalid');
|
throw new Error('Plugin Package E2E previous install head is invalid');
|
||||||
}
|
}
|
||||||
const record = createPluginPackageInstall(lock, {
|
const consumed = await new PostgresApprovalRequestRepository(
|
||||||
|
database.pool,
|
||||||
|
).consume(selected.authority.consumptionCommand);
|
||||||
|
if (
|
||||||
|
JSON.stringify(consumed.dispatch) !==
|
||||||
|
JSON.stringify(selected.authority.dispatch)
|
||||||
|
) {
|
||||||
|
throw new Error('Plugin Package E2E durable dispatch drifted');
|
||||||
|
}
|
||||||
|
const executions = new PostgresApprovedActionExecutionRepository(
|
||||||
|
database.pool,
|
||||||
|
);
|
||||||
|
const executionAtMs = Date.now();
|
||||||
|
const owner = 'plugin_recovery_e2e_dispatcher';
|
||||||
|
const leaseToken = `lease-plugin-recovery-e2e-${
|
||||||
|
mode === 'seed-initial' ? 'initial' : 'upgrade'
|
||||||
|
}`;
|
||||||
|
const claimed = await executions.claimExecution({
|
||||||
|
dispatchId: consumed.dispatch.id,
|
||||||
|
owner,
|
||||||
|
leaseToken,
|
||||||
|
nowMs: executionAtMs,
|
||||||
|
leaseDurationMs: 60_000,
|
||||||
|
});
|
||||||
|
if (claimed.status !== 'claimed') {
|
||||||
|
throw new Error('Plugin Package E2E execution was not claimable');
|
||||||
|
}
|
||||||
|
const started = await executions.startExecution({
|
||||||
|
dispatchId: consumed.dispatch.id,
|
||||||
|
approvalRequestId: consumed.dispatch.approvalRequestId,
|
||||||
|
actionDigest: consumed.dispatch.action.actionDigest,
|
||||||
|
owner,
|
||||||
|
leaseToken,
|
||||||
|
expectedVersion: claimed.snapshot.execution.version,
|
||||||
|
startedAtMs: executionAtMs + 1,
|
||||||
|
});
|
||||||
|
const admittedAtMs = executionAtMs + 2;
|
||||||
|
const result = await repository.admit({
|
||||||
|
lock,
|
||||||
|
proposalDigest:
|
||||||
|
selected.authority.proposalCommand.proposal.proposalDigest,
|
||||||
|
execution: started.execution,
|
||||||
installationId: selected.installationId,
|
installationId: selected.installationId,
|
||||||
mutationId: `mutation-plugin-recovery-e2e-${
|
mutationId: `mutation-plugin-recovery-e2e-${
|
||||||
mode === 'seed-initial' ? 'initial' : 'upgrade'
|
mode === 'seed-initial' ? 'initial' : 'upgrade'
|
||||||
}-create`,
|
}-admit`,
|
||||||
occurredAtMs: lock.createdAtMs + 1,
|
admittedAtMs,
|
||||||
|
audit: {
|
||||||
|
eventId: selected.authority.admissionAuditEventId,
|
||||||
|
requestId: consumed.dispatch.id,
|
||||||
|
operationId: 'plugin_package.admit',
|
||||||
|
projectId: lock.projectId,
|
||||||
|
subject: selected.authority.consumer,
|
||||||
|
authenticationId: `auth-plugin-recovery-e2e-dispatcher-${
|
||||||
|
mode === 'seed-initial' ? 'initial' : 'upgrade'
|
||||||
|
}`,
|
||||||
|
outcome: 'allowed',
|
||||||
|
reasons: ['approved_action'],
|
||||||
|
fence: selected.authority.fence,
|
||||||
|
occurredAtMs: admittedAtMs,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const result = await repository.create(
|
|
||||||
pluginPackageInstallCreate(lock, record, previous),
|
|
||||||
);
|
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`${JSON.stringify({
|
`${JSON.stringify({
|
||||||
schema: 'qinglong/plugin-package-recovery-e2e-seed-result@v1',
|
schema: 'qinglong/plugin-package-recovery-e2e-seed-result@v1',
|
||||||
@@ -806,15 +1026,13 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
['seed-initial', 'seed-upgrade', 'commit-transition'].includes(
|
['seed-initial', 'seed-upgrade'].includes(process.env.QL3_E2E_MODE)
|
||||||
process.env.QL3_E2E_MODE,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
await runSeed();
|
await runSeed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'QL3_E2E_MODE must be registry, seed-initial, seed-upgrade or commit-transition',
|
'QL3_E2E_MODE must be registry, seed-initial or seed-upgrade',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const FIXTURE = 'qinglong/plugin-package-recovery-e2e-live-contract@v2';
|
const FIXTURE = 'qinglong/plugin-package-recovery-e2e-live-contract@v3';
|
||||||
const LIMITATIONS = Object.freeze([
|
const LIMITATIONS = Object.freeze([
|
||||||
'isolated PostgreSQL uses explicit TLS disable; production manifests remain verify-full',
|
'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 authenticated HTTPS OCI Distribution fixture implements the immutable GET/referrers surface used by the resolver, not a production registry storage implementation',
|
||||||
@@ -13,7 +13,6 @@ const LIMITATIONS = Object.freeze([
|
|||||||
]);
|
]);
|
||||||
const GATE_KEYS = Object.freeze([
|
const GATE_KEYS = Object.freeze([
|
||||||
'healthyInitialActivation',
|
'healthyInitialActivation',
|
||||||
'missingTransitionFailedClosed',
|
|
||||||
'invalidUpgradeRejectedBeforeActivation',
|
'invalidUpgradeRejectedBeforeActivation',
|
||||||
'activePointerUidUnchanged',
|
'activePointerUidUnchanged',
|
||||||
'activePointerResourceVersionUnchanged',
|
'activePointerResourceVersionUnchanged',
|
||||||
@@ -119,8 +118,8 @@ function validRuntime(report) {
|
|||||||
]) &&
|
]) &&
|
||||||
runtime.replicas === 2 &&
|
runtime.replicas === 2 &&
|
||||||
runtime.creationTimestamp === ordering?.runtimeCreatedAt &&
|
runtime.creationTimestamp === ordering?.runtimeCreatedAt &&
|
||||||
runtime.recoveryJobUid === ordering?.rejectionRecoveryJobUid &&
|
runtime.recoveryJobUid === ordering?.upgradeRecoveryJobUid &&
|
||||||
runtime.recoveryCompletedAt === ordering?.rejectionRecoveryCompletedAt &&
|
runtime.recoveryCompletedAt === ordering?.upgradeRecoveryCompletedAt &&
|
||||||
Array.isArray(runtime.nodes) &&
|
Array.isArray(runtime.nodes) &&
|
||||||
runtime.nodes.length === 2 &&
|
runtime.nodes.length === 2 &&
|
||||||
new Set(runtime.nodes).size === 2 &&
|
new Set(runtime.nodes).size === 2 &&
|
||||||
@@ -179,8 +178,7 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
'postgresRepositoryDigest',
|
'postgresRepositoryDigest',
|
||||||
'migrationImageId',
|
'migrationImageId',
|
||||||
'initialRecoveryImageId',
|
'initialRecoveryImageId',
|
||||||
'stageRecoveryImageId',
|
'upgradeRecoveryImageId',
|
||||||
'rejectionRecoveryImageId',
|
|
||||||
'postgresImageId',
|
'postgresImageId',
|
||||||
]) ||
|
]) ||
|
||||||
!SHA256_ID.test(images?.adminBuildId ?? '') ||
|
!SHA256_ID.test(images?.adminBuildId ?? '') ||
|
||||||
@@ -192,9 +190,7 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
!imageDigest(images?.migrationImageId) ||
|
!imageDigest(images?.migrationImageId) ||
|
||||||
imageDigest(images?.initialRecoveryImageId) !==
|
imageDigest(images?.initialRecoveryImageId) !==
|
||||||
imageDigest(images?.migrationImageId) ||
|
imageDigest(images?.migrationImageId) ||
|
||||||
imageDigest(images?.stageRecoveryImageId) !==
|
imageDigest(images?.upgradeRecoveryImageId) !==
|
||||||
imageDigest(images?.migrationImageId) ||
|
|
||||||
imageDigest(images?.rejectionRecoveryImageId) !==
|
|
||||||
imageDigest(images?.migrationImageId) ||
|
imageDigest(images?.migrationImageId) ||
|
||||||
!imageDigest(images?.postgresImageId)
|
!imageDigest(images?.postgresImageId)
|
||||||
) {
|
) {
|
||||||
@@ -207,17 +203,13 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
const timeKeys = [
|
const timeKeys = [
|
||||||
'migrationCompletedAt',
|
'migrationCompletedAt',
|
||||||
'initialRecoveryCompletedAt',
|
'initialRecoveryCompletedAt',
|
||||||
'upgradeStageFailedAt',
|
'upgradeRecoveryCompletedAt',
|
||||||
'transitionCompletedAt',
|
|
||||||
'rejectionRecoveryCompletedAt',
|
|
||||||
'runtimeCreatedAt',
|
'runtimeCreatedAt',
|
||||||
];
|
];
|
||||||
const uidKeys = [
|
const uidKeys = [
|
||||||
'migrationJobUid',
|
'migrationJobUid',
|
||||||
'initialRecoveryJobUid',
|
'initialRecoveryJobUid',
|
||||||
'upgradeStageJobUid',
|
'upgradeRecoveryJobUid',
|
||||||
'transitionJobUid',
|
|
||||||
'rejectionRecoveryJobUid',
|
|
||||||
'runtimeBoundRecoveryJobUid',
|
'runtimeBoundRecoveryJobUid',
|
||||||
];
|
];
|
||||||
const times = timeKeys.map((key) => Date.parse(ordering?.[key]));
|
const times = timeKeys.map((key) => Date.parse(ordering?.[key]));
|
||||||
@@ -225,7 +217,7 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
!exactKeys(ordering, [...uidKeys, ...timeKeys]) ||
|
!exactKeys(ordering, [...uidKeys, ...timeKeys]) ||
|
||||||
uidKeys.some((key) => !UUID.test(ordering?.[key] ?? '')) ||
|
uidKeys.some((key) => !UUID.test(ordering?.[key] ?? '')) ||
|
||||||
ordering?.runtimeBoundRecoveryJobUid !==
|
ordering?.runtimeBoundRecoveryJobUid !==
|
||||||
ordering?.rejectionRecoveryJobUid ||
|
ordering?.upgradeRecoveryJobUid ||
|
||||||
timeKeys.some((key) => !validIso(ordering?.[key])) ||
|
timeKeys.some((key) => !validIso(ordering?.[key])) ||
|
||||||
times.some((value, index) => index > 0 && value < times[index - 1])
|
times.some((value, index) => index > 0 && value < times[index - 1])
|
||||||
) {
|
) {
|
||||||
@@ -240,18 +232,12 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
const failed = report?.failedUpgrade;
|
const failed = report?.failedUpgrade;
|
||||||
if (
|
if (
|
||||||
!exactKeys(failed, [
|
!exactKeys(failed, [
|
||||||
'stageFailure',
|
'recoveryJobUid',
|
||||||
'transitionReceiptDigest',
|
|
||||||
'rejectionReason',
|
'rejectionReason',
|
||||||
'candidateRevisionCount',
|
'candidateRevisionCount',
|
||||||
'activePointerUnchanged',
|
'activePointerUnchanged',
|
||||||
]) ||
|
]) ||
|
||||||
!exactKeys(failed?.stageFailure, ['jobUid', 'reason', 'durableState']) ||
|
failed?.recoveryJobUid !== ordering?.upgradeRecoveryJobUid ||
|
||||||
failed?.stageFailure?.jobUid !== ordering?.upgradeStageJobUid ||
|
|
||||||
failed?.stageFailure?.reason !==
|
|
||||||
'ClusterPluginPackageRecoveryRequiredError' ||
|
|
||||||
failed?.stageFailure?.durableState !== 'staged' ||
|
|
||||||
!SHA256.test(failed?.transitionReceiptDigest ?? '') ||
|
|
||||||
failed?.rejectionReason !== 'activation_fact_conflict' ||
|
failed?.rejectionReason !== 'activation_fact_conflict' ||
|
||||||
failed?.candidateRevisionCount !== 0 ||
|
failed?.candidateRevisionCount !== 0 ||
|
||||||
failed?.activePointerUnchanged !== true
|
failed?.activePointerUnchanged !== true
|
||||||
@@ -277,7 +263,6 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
'initialMutationCount',
|
'initialMutationCount',
|
||||||
'upgradeMutationCount',
|
'upgradeMutationCount',
|
||||||
'headInstallationId',
|
'headInstallationId',
|
||||||
'transitionReceiptCount',
|
|
||||||
'initialRevisionCount',
|
'initialRevisionCount',
|
||||||
'upgradeRevisionCount',
|
'upgradeRevisionCount',
|
||||||
'recoverableCount',
|
'recoverableCount',
|
||||||
@@ -296,7 +281,6 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
database?.initialMutationCount !== 4 ||
|
database?.initialMutationCount !== 4 ||
|
||||||
database?.upgradeMutationCount !== 3 ||
|
database?.upgradeMutationCount !== 3 ||
|
||||||
database?.headInstallationId !== 'install-plugin-recovery-e2e-upgrade' ||
|
database?.headInstallationId !== 'install-plugin-recovery-e2e-upgrade' ||
|
||||||
database?.transitionReceiptCount !== 1 ||
|
|
||||||
database?.initialRevisionCount !== 1 ||
|
database?.initialRevisionCount !== 1 ||
|
||||||
database?.upgradeRevisionCount !== 0 ||
|
database?.upgradeRevisionCount !== 0 ||
|
||||||
database?.recoverableCount !== 0
|
database?.recoverableCount !== 0
|
||||||
@@ -323,11 +307,11 @@ function validatePluginPackageRecoveryE2ELiveReport(report) {
|
|||||||
]) ||
|
]) ||
|
||||||
oci?.https !== true ||
|
oci?.https !== true ||
|
||||||
oci?.authentication !== 'exact-registry-basic' ||
|
oci?.authentication !== 'exact-registry-basic' ||
|
||||||
oci?.authenticatedRequestCount !== 18 ||
|
oci?.authenticatedRequestCount !== 12 ||
|
||||||
oci?.requestCount !== 18 ||
|
oci?.requestCount !== 12 ||
|
||||||
oci?.uniquePaths !== 12 ||
|
oci?.uniquePaths !== 12 ||
|
||||||
oci?.initialRequestCount !== 6 ||
|
oci?.initialRequestCount !== 6 ||
|
||||||
oci?.upgradeRequestCount !== 12 ||
|
oci?.upgradeRequestCount !== 6 ||
|
||||||
oci?.redirects !== 0
|
oci?.redirects !== 0
|
||||||
) {
|
) {
|
||||||
findings.push(
|
findings.push(
|
||||||
|
|||||||
@@ -38,13 +38,11 @@ const POSTGRES_RUNTIME_IMAGE =
|
|||||||
const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e';
|
const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e';
|
||||||
const DEFAULT_CONTROL_IMAGE =
|
const DEFAULT_CONTROL_IMAGE =
|
||||||
'qinglong3-cluster-control:ql3-plugin-recovery-e2e';
|
'qinglong3-cluster-control:ql3-plugin-recovery-e2e';
|
||||||
const REPORT_SCHEMA = 'qinglong/plugin-package-recovery-e2e-live-contract@v2';
|
const REPORT_SCHEMA = 'qinglong/plugin-package-recovery-e2e-live-contract@v3';
|
||||||
const INITIAL_SEED_JOB = 'ql3-plugin-package-e2e-seed-initial';
|
const INITIAL_SEED_JOB = 'ql3-plugin-package-e2e-seed-initial';
|
||||||
const INITIAL_RECOVERY_JOB = 'ql3-plugin-package-recovery-initial';
|
const INITIAL_RECOVERY_JOB = 'ql3-plugin-package-recovery-initial';
|
||||||
const UPGRADE_SEED_JOB = 'ql3-plugin-package-e2e-seed-upgrade';
|
const UPGRADE_SEED_JOB = 'ql3-plugin-package-e2e-seed-upgrade';
|
||||||
const UPGRADE_STAGE_JOB = 'ql3-plugin-package-recovery-stage-upgrade';
|
const UPGRADE_RECOVERY_JOB = 'ql3-plugin-package-recovery-reject-upgrade';
|
||||||
const TRANSITION_JOB = 'ql3-plugin-package-e2e-transition';
|
|
||||||
const UPGRADE_REJECTION_JOB = 'ql3-plugin-package-recovery-reject-upgrade';
|
|
||||||
const SAFE_CLUSTER =
|
const SAFE_CLUSTER =
|
||||||
/^ql3-plugin-recovery-e2e(?:-[a-z0-9](?:[-a-z0-9]{0,24}[a-z0-9])?)?$/;
|
/^ql3-plugin-recovery-e2e(?:-[a-z0-9](?:[-a-z0-9]{0,24}[a-z0-9])?)?$/;
|
||||||
|
|
||||||
@@ -879,6 +877,19 @@ function seedJob(name, mode) {
|
|||||||
{ name: 'QL3_E2E_POSTGRES_HOST', value: POSTGRES_NAME },
|
{ name: 'QL3_E2E_POSTGRES_HOST', value: POSTGRES_NAME },
|
||||||
{ name: 'QL3_E2E_POSTGRES_PORT', value: '5432' },
|
{ name: 'QL3_E2E_POSTGRES_PORT', value: '5432' },
|
||||||
{ name: 'QL3_E2E_POSTGRES_DATABASE', value: 'qinglong' },
|
{ 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',
|
name: 'QL3_E2E_POSTGRES_PACKAGE_MANAGER_USER',
|
||||||
value: 'ql3_package_manager',
|
value: 'ql3_package_manager',
|
||||||
@@ -1179,67 +1190,6 @@ function canI(verb, resource) {
|
|||||||
return result.stdout === 'yes';
|
return result.stdout === 'yes';
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeStageEvidence(fixture, transitionReceiptCount) {
|
|
||||||
const generationDigest = fixture.upgrade.generation.generationDigest;
|
|
||||||
assert.match(generationDigest, /^[0-9a-f]{64}$/);
|
|
||||||
const sql = `
|
|
||||||
SELECT json_build_object(
|
|
||||||
'state', (
|
|
||||||
SELECT state FROM ql3.plugin_package_installs
|
|
||||||
WHERE installation_id = '${fixture.upgrade.installationId}'
|
|
||||||
),
|
|
||||||
'previousActiveLockDigest', (
|
|
||||||
SELECT previous_active_lock_digest FROM ql3.plugin_package_installs
|
|
||||||
WHERE installation_id = '${fixture.upgrade.installationId}'
|
|
||||||
),
|
|
||||||
'activeLockDigest', (
|
|
||||||
SELECT active_lock_digest FROM ql3.plugin_package_installs
|
|
||||||
WHERE installation_id = '${fixture.upgrade.installationId}'
|
|
||||||
),
|
|
||||||
'mutationCount', (
|
|
||||||
SELECT count(*) FROM ql3.plugin_package_install_mutations
|
|
||||||
WHERE installation_id = '${fixture.upgrade.installationId}'
|
|
||||||
),
|
|
||||||
'transitionReceiptCount', (
|
|
||||||
SELECT count(*)
|
|
||||||
FROM ql3.plugin_package_secret_binding_transition_receipts
|
|
||||||
WHERE generation_digest = '${generationDigest}'
|
|
||||||
),
|
|
||||||
'candidateRevisionCount', (
|
|
||||||
SELECT count(*) FROM ql3.plugin_package_materialized_revisions
|
|
||||||
WHERE generation_digest = '${generationDigest}'
|
|
||||||
)
|
|
||||||
)::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.state, 'staged');
|
|
||||||
assert.equal(value.previousActiveLockDigest, fixture.initial.lock.lockDigest);
|
|
||||||
assert.equal(value.activeLockDigest, fixture.initial.lock.lockDigest);
|
|
||||||
assert.equal(value.mutationCount, 2);
|
|
||||||
assert.equal(value.transitionReceiptCount, transitionReceiptCount);
|
|
||||||
assert.equal(value.candidateRevisionCount, 0);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function databaseEvidence(fixture) {
|
function databaseEvidence(fixture) {
|
||||||
const initialGenerationDigest = fixture.initial.generation.generationDigest;
|
const initialGenerationDigest = fixture.initial.generation.generationDigest;
|
||||||
const upgradeGenerationDigest = fixture.upgrade.generation.generationDigest;
|
const upgradeGenerationDigest = fixture.upgrade.generation.generationDigest;
|
||||||
@@ -1298,11 +1248,6 @@ SELECT json_build_object(
|
|||||||
FROM ql3.plugin_package_install_heads
|
FROM ql3.plugin_package_install_heads
|
||||||
WHERE project_id = 'default' AND package_name = 'e2e-monitor'
|
WHERE project_id = 'default' AND package_name = 'e2e-monitor'
|
||||||
),
|
),
|
||||||
'transitionReceiptCount', (
|
|
||||||
SELECT count(*)
|
|
||||||
FROM ql3.plugin_package_secret_binding_transition_receipts
|
|
||||||
WHERE generation_digest = '${upgradeGenerationDigest}'
|
|
||||||
),
|
|
||||||
'initialRevisionCount', (
|
'initialRevisionCount', (
|
||||||
SELECT count(*) FROM ql3.plugin_package_materialized_revisions
|
SELECT count(*) FROM ql3.plugin_package_materialized_revisions
|
||||||
WHERE generation_digest = '${initialGenerationDigest}'
|
WHERE generation_digest = '${initialGenerationDigest}'
|
||||||
@@ -1352,7 +1297,6 @@ SELECT json_build_object(
|
|||||||
assert.equal(value.initialMutationCount, 4);
|
assert.equal(value.initialMutationCount, 4);
|
||||||
assert.equal(value.upgradeMutationCount, 3);
|
assert.equal(value.upgradeMutationCount, 3);
|
||||||
assert.equal(value.headInstallationId, fixture.upgrade.installationId);
|
assert.equal(value.headInstallationId, fixture.upgrade.installationId);
|
||||||
assert.equal(value.transitionReceiptCount, 1);
|
|
||||||
assert.equal(value.initialRevisionCount, 1);
|
assert.equal(value.initialRevisionCount, 1);
|
||||||
assert.equal(value.upgradeRevisionCount, 0);
|
assert.equal(value.upgradeRevisionCount, 0);
|
||||||
assert.equal(value.recoverableCount, 0);
|
assert.equal(value.recoverableCount, 0);
|
||||||
@@ -1411,10 +1355,7 @@ function registryEvidence(fixture) {
|
|||||||
const packageRequests = events.filter((event) => event.path !== '/v2/');
|
const packageRequests = events.filter((event) => event.path !== '/v2/');
|
||||||
const expectedPaths = [
|
const expectedPaths = [
|
||||||
...fixture.initial.routes.map((routeValue) => routeValue.path),
|
...fixture.initial.routes.map((routeValue) => routeValue.path),
|
||||||
...fixture.upgrade.routes.flatMap((routeValue) => [
|
...fixture.upgrade.routes.map((routeValue) => routeValue.path),
|
||||||
routeValue.path,
|
|
||||||
routeValue.path,
|
|
||||||
]),
|
|
||||||
].sort();
|
].sort();
|
||||||
assert.equal(packageRequests.length, expectedPaths.length);
|
assert.equal(packageRequests.length, expectedPaths.length);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -1430,7 +1371,7 @@ function registryEvidence(fixture) {
|
|||||||
requestCount: packageRequests.length,
|
requestCount: packageRequests.length,
|
||||||
uniquePaths: new Set(packageRequests.map((event) => event.path)).size,
|
uniquePaths: new Set(packageRequests.map((event) => event.path)).size,
|
||||||
initialRequestCount: fixture.initial.routes.length,
|
initialRequestCount: fixture.initial.routes.length,
|
||||||
upgradeRequestCount: fixture.upgrade.routes.length * 2,
|
upgradeRequestCount: fixture.upgrade.routes.length,
|
||||||
redirects: 0,
|
redirects: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1874,7 +1815,7 @@ async function main(argv = process.argv.slice(2)) {
|
|||||||
(value) => value.event === 'seed_completed',
|
(value) => value.event === 'seed_completed',
|
||||||
);
|
);
|
||||||
assert.equal(initialSeed.phase, 'initial');
|
assert.equal(initialSeed.phase, 'initial');
|
||||||
assert.equal(initialSeed.status, 'created');
|
assert.equal(initialSeed.status, 'admitted');
|
||||||
assert.equal(initialSeed.state, 'queued');
|
assert.equal(initialSeed.state, 'queued');
|
||||||
assert.equal(initialSeed.lockDigest, fixture.initial.lock.lockDigest);
|
assert.equal(initialSeed.lockDigest, fixture.initial.lock.lockDigest);
|
||||||
|
|
||||||
@@ -1901,52 +1842,16 @@ async function main(argv = process.argv.slice(2)) {
|
|||||||
(value) => value.event === 'seed_completed',
|
(value) => value.event === 'seed_completed',
|
||||||
);
|
);
|
||||||
assert.equal(upgradeSeed.phase, 'upgrade');
|
assert.equal(upgradeSeed.phase, 'upgrade');
|
||||||
assert.equal(upgradeSeed.status, 'created');
|
assert.equal(upgradeSeed.status, 'admitted');
|
||||||
assert.equal(upgradeSeed.state, 'queued');
|
assert.equal(upgradeSeed.state, 'queued');
|
||||||
assert.equal(upgradeSeed.lockDigest, fixture.upgrade.lock.lockDigest);
|
assert.equal(upgradeSeed.lockDigest, fixture.upgrade.lock.lockDigest);
|
||||||
|
|
||||||
for (const resource of recoveryResources(fixture, UPGRADE_STAGE_JOB)) {
|
for (const resource of recoveryResources(fixture, UPGRADE_RECOVERY_JOB)) {
|
||||||
apply(resource, `apply upgrade staging recovery ${resource.kind}`);
|
|
||||||
}
|
|
||||||
const stagedUpgrade = waitForJob(UPGRADE_STAGE_JOB, 'failed');
|
|
||||||
const stagedFailureCondition = stagedUpgrade.status.conditions.find(
|
|
||||||
(condition) => condition.type === 'Failed' && condition.status === 'True',
|
|
||||||
);
|
|
||||||
assert.ok(stagedFailureCondition?.lastTransitionTime);
|
|
||||||
const stageFailure = lastJsonLine(
|
|
||||||
jobLog(UPGRADE_STAGE_JOB),
|
|
||||||
(value) => value.event === 'recovery_failed',
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
stageFailure.name,
|
|
||||||
'ClusterPluginPackageRecoveryRequiredError',
|
|
||||||
);
|
|
||||||
const stagedDatabase = upgradeStageEvidence(fixture, 0);
|
|
||||||
assert.deepEqual(activePointerEvidence(fixture), pointerBeforeUpgrade);
|
|
||||||
|
|
||||||
apply(
|
|
||||||
seedJob(TRANSITION_JOB, 'commit-transition'),
|
|
||||||
'commit durable no-secret binding transition receipt',
|
|
||||||
);
|
|
||||||
const committedTransition = waitForJob(TRANSITION_JOB);
|
|
||||||
const transition = lastJsonLine(
|
|
||||||
jobLog(TRANSITION_JOB),
|
|
||||||
(value) => value.event === 'transition_completed',
|
|
||||||
);
|
|
||||||
assert.equal(transition.status, 'created');
|
|
||||||
assert.equal(
|
|
||||||
transition.generationDigest,
|
|
||||||
fixture.upgrade.generation.generationDigest,
|
|
||||||
);
|
|
||||||
assert.equal(transition.bindingDigest, null);
|
|
||||||
upgradeStageEvidence(fixture, 1);
|
|
||||||
|
|
||||||
for (const resource of recoveryResources(fixture, UPGRADE_REJECTION_JOB)) {
|
|
||||||
apply(resource, `apply upgrade rejection recovery ${resource.kind}`);
|
apply(resource, `apply upgrade rejection recovery ${resource.kind}`);
|
||||||
}
|
}
|
||||||
const rejectedUpgrade = waitForJob(UPGRADE_REJECTION_JOB);
|
const rejectedUpgrade = waitForJob(UPGRADE_RECOVERY_JOB);
|
||||||
const rejectionCompleted = lastJsonLine(
|
const rejectionCompleted = lastJsonLine(
|
||||||
jobLog(UPGRADE_REJECTION_JOB),
|
jobLog(UPGRADE_RECOVERY_JOB),
|
||||||
(value) => value.event === 'recovery_completed',
|
(value) => value.event === 'recovery_completed',
|
||||||
);
|
);
|
||||||
assert.equal(rejectionCompleted.recovery.safeToAdmit, true);
|
assert.equal(rejectionCompleted.recovery.safeToAdmit, true);
|
||||||
@@ -1965,8 +1870,7 @@ async function main(argv = process.argv.slice(2)) {
|
|||||||
secrets,
|
secrets,
|
||||||
);
|
);
|
||||||
const initialRecoveryImageId = jobImageId(INITIAL_RECOVERY_JOB);
|
const initialRecoveryImageId = jobImageId(INITIAL_RECOVERY_JOB);
|
||||||
const stageRecoveryImageId = jobImageId(UPGRADE_STAGE_JOB);
|
const upgradeRecoveryImageId = jobImageId(UPGRADE_RECOVERY_JOB);
|
||||||
const rejectionRecoveryImageId = jobImageId(UPGRADE_REJECTION_JOB);
|
|
||||||
const migrationImageId = jobImageId('ql3-cluster-migration');
|
const migrationImageId = jobImageId('ql3-cluster-migration');
|
||||||
const postgresPod = kubectlJson([
|
const postgresPod = kubectlJson([
|
||||||
'-n',
|
'-n',
|
||||||
@@ -1991,8 +1895,7 @@ async function main(argv = process.argv.slice(2)) {
|
|||||||
postgresRepositoryDigest: POSTGRES_REPOSITORY_DIGEST,
|
postgresRepositoryDigest: POSTGRES_REPOSITORY_DIGEST,
|
||||||
migrationImageId,
|
migrationImageId,
|
||||||
initialRecoveryImageId,
|
initialRecoveryImageId,
|
||||||
stageRecoveryImageId,
|
upgradeRecoveryImageId,
|
||||||
rejectionRecoveryImageId,
|
|
||||||
postgresImageId: postgresPod.status.containerStatuses[0].imageID,
|
postgresImageId: postgresPod.status.containerStatuses[0].imageID,
|
||||||
}),
|
}),
|
||||||
ordering: Object.freeze({
|
ordering: Object.freeze({
|
||||||
@@ -2000,22 +1903,13 @@ async function main(argv = process.argv.slice(2)) {
|
|||||||
migrationCompletedAt: migrated.status.completionTime,
|
migrationCompletedAt: migrated.status.completionTime,
|
||||||
initialRecoveryJobUid: initialRecovered.metadata.uid,
|
initialRecoveryJobUid: initialRecovered.metadata.uid,
|
||||||
initialRecoveryCompletedAt: initialRecovered.status.completionTime,
|
initialRecoveryCompletedAt: initialRecovered.status.completionTime,
|
||||||
upgradeStageJobUid: stagedUpgrade.metadata.uid,
|
upgradeRecoveryJobUid: rejectedUpgrade.metadata.uid,
|
||||||
upgradeStageFailedAt: stagedFailureCondition.lastTransitionTime,
|
upgradeRecoveryCompletedAt: rejectedUpgrade.status.completionTime,
|
||||||
transitionJobUid: committedTransition.metadata.uid,
|
|
||||||
transitionCompletedAt: committedTransition.status.completionTime,
|
|
||||||
rejectionRecoveryJobUid: rejectedUpgrade.metadata.uid,
|
|
||||||
rejectionRecoveryCompletedAt: rejectedUpgrade.status.completionTime,
|
|
||||||
runtimeCreatedAt: runtime.creationTimestamp,
|
runtimeCreatedAt: runtime.creationTimestamp,
|
||||||
runtimeBoundRecoveryJobUid: runtime.recoveryJobUid,
|
runtimeBoundRecoveryJobUid: runtime.recoveryJobUid,
|
||||||
}),
|
}),
|
||||||
failedUpgrade: Object.freeze({
|
failedUpgrade: Object.freeze({
|
||||||
stageFailure: Object.freeze({
|
recoveryJobUid: rejectedUpgrade.metadata.uid,
|
||||||
jobUid: stagedUpgrade.metadata.uid,
|
|
||||||
reason: stageFailure.name,
|
|
||||||
durableState: stagedDatabase.state,
|
|
||||||
}),
|
|
||||||
transitionReceiptDigest: transition.receiptDigest,
|
|
||||||
rejectionReason: database.upgradeFailureReason,
|
rejectionReason: database.upgradeFailureReason,
|
||||||
candidateRevisionCount: database.upgradeRevisionCount,
|
candidateRevisionCount: database.upgradeRevisionCount,
|
||||||
activePointerUnchanged: true,
|
activePointerUnchanged: true,
|
||||||
@@ -2029,7 +1923,6 @@ async function main(argv = process.argv.slice(2)) {
|
|||||||
runtime,
|
runtime,
|
||||||
gates: Object.freeze({
|
gates: Object.freeze({
|
||||||
healthyInitialActivation: true,
|
healthyInitialActivation: true,
|
||||||
missingTransitionFailedClosed: true,
|
|
||||||
invalidUpgradeRejectedBeforeActivation: true,
|
invalidUpgradeRejectedBeforeActivation: true,
|
||||||
activePointerUidUnchanged: true,
|
activePointerUidUnchanged: true,
|
||||||
activePointerResourceVersionUnchanged: true,
|
activePointerResourceVersionUnchanged: true,
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ function validReport() {
|
|||||||
'postgres@sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296',
|
'postgres@sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296',
|
||||||
migrationImageId: `docker://${admin}`,
|
migrationImageId: `docker://${admin}`,
|
||||||
initialRecoveryImageId: `docker://${admin}`,
|
initialRecoveryImageId: `docker://${admin}`,
|
||||||
stageRecoveryImageId: `docker://${admin}`,
|
upgradeRecoveryImageId: `docker://${admin}`,
|
||||||
rejectionRecoveryImageId: `docker://${admin}`,
|
|
||||||
postgresImageId: `containerd://sha256:${'e'.repeat(64)}`,
|
postgresImageId: `containerd://sha256:${'e'.repeat(64)}`,
|
||||||
},
|
},
|
||||||
ordering: {
|
ordering: {
|
||||||
@@ -51,22 +50,13 @@ function validReport() {
|
|||||||
migrationCompletedAt: '2026-08-14T08:00:01.000Z',
|
migrationCompletedAt: '2026-08-14T08:00:01.000Z',
|
||||||
initialRecoveryJobUid: '00000000-0000-4000-8000-000000000002',
|
initialRecoveryJobUid: '00000000-0000-4000-8000-000000000002',
|
||||||
initialRecoveryCompletedAt: '2026-08-14T08:00:02.000Z',
|
initialRecoveryCompletedAt: '2026-08-14T08:00:02.000Z',
|
||||||
upgradeStageJobUid: '00000000-0000-4000-8000-000000000003',
|
upgradeRecoveryJobUid: '00000000-0000-4000-8000-000000000003',
|
||||||
upgradeStageFailedAt: '2026-08-14T08:00:03.000Z',
|
upgradeRecoveryCompletedAt: '2026-08-14T08:00:03.000Z',
|
||||||
transitionJobUid: '00000000-0000-4000-8000-000000000004',
|
runtimeCreatedAt: '2026-08-14T08:00:04.000Z',
|
||||||
transitionCompletedAt: '2026-08-14T08:00:04.000Z',
|
runtimeBoundRecoveryJobUid: '00000000-0000-4000-8000-000000000003',
|
||||||
rejectionRecoveryJobUid: '00000000-0000-4000-8000-000000000005',
|
|
||||||
rejectionRecoveryCompletedAt: '2026-08-14T08:00:05.000Z',
|
|
||||||
runtimeCreatedAt: '2026-08-14T08:00:06.000Z',
|
|
||||||
runtimeBoundRecoveryJobUid: '00000000-0000-4000-8000-000000000005',
|
|
||||||
},
|
},
|
||||||
failedUpgrade: {
|
failedUpgrade: {
|
||||||
stageFailure: {
|
recoveryJobUid: '00000000-0000-4000-8000-000000000003',
|
||||||
jobUid: '00000000-0000-4000-8000-000000000003',
|
|
||||||
reason: 'ClusterPluginPackageRecoveryRequiredError',
|
|
||||||
durableState: 'staged',
|
|
||||||
},
|
|
||||||
transitionReceiptDigest: 'f'.repeat(64),
|
|
||||||
rejectionReason: 'activation_fact_conflict',
|
rejectionReason: 'activation_fact_conflict',
|
||||||
candidateRevisionCount: 0,
|
candidateRevisionCount: 0,
|
||||||
activePointerUnchanged: true,
|
activePointerUnchanged: true,
|
||||||
@@ -83,7 +73,6 @@ function validReport() {
|
|||||||
initialMutationCount: 4,
|
initialMutationCount: 4,
|
||||||
upgradeMutationCount: 3,
|
upgradeMutationCount: 3,
|
||||||
headInstallationId: 'install-plugin-recovery-e2e-upgrade',
|
headInstallationId: 'install-plugin-recovery-e2e-upgrade',
|
||||||
transitionReceiptCount: 1,
|
|
||||||
initialRevisionCount: 1,
|
initialRevisionCount: 1,
|
||||||
upgradeRevisionCount: 0,
|
upgradeRevisionCount: 0,
|
||||||
recoverableCount: 0,
|
recoverableCount: 0,
|
||||||
@@ -91,11 +80,11 @@ function validReport() {
|
|||||||
oci: {
|
oci: {
|
||||||
https: true,
|
https: true,
|
||||||
authentication: 'exact-registry-basic',
|
authentication: 'exact-registry-basic',
|
||||||
authenticatedRequestCount: 18,
|
authenticatedRequestCount: 12,
|
||||||
requestCount: 18,
|
requestCount: 12,
|
||||||
uniquePaths: 12,
|
uniquePaths: 12,
|
||||||
initialRequestCount: 6,
|
initialRequestCount: 6,
|
||||||
upgradeRequestCount: 12,
|
upgradeRequestCount: 6,
|
||||||
redirects: 0,
|
redirects: 0,
|
||||||
},
|
},
|
||||||
kubernetes: {
|
kubernetes: {
|
||||||
@@ -118,9 +107,9 @@ function validReport() {
|
|||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
replicas: 2,
|
replicas: 2,
|
||||||
creationTimestamp: '2026-08-14T08:00:06.000Z',
|
creationTimestamp: '2026-08-14T08:00:04.000Z',
|
||||||
recoveryJobUid: '00000000-0000-4000-8000-000000000005',
|
recoveryJobUid: '00000000-0000-4000-8000-000000000003',
|
||||||
recoveryCompletedAt: '2026-08-14T08:00:05.000Z',
|
recoveryCompletedAt: '2026-08-14T08:00:03.000Z',
|
||||||
nodes: ['worker-a', 'worker-b'],
|
nodes: ['worker-a', 'worker-b'],
|
||||||
imageIds: [`docker://${control}`],
|
imageIds: [`docker://${control}`],
|
||||||
},
|
},
|
||||||
@@ -142,7 +131,8 @@ test('offline audit accepts one exact low-sensitive recovery report', () => {
|
|||||||
test('offline audit rejects broken upgrade, ordering and image relationships', () => {
|
test('offline audit rejects broken upgrade, ordering and image relationships', () => {
|
||||||
const report = validReport();
|
const report = validReport();
|
||||||
report.database.upgradeRevisionCount = 1;
|
report.database.upgradeRevisionCount = 1;
|
||||||
report.ordering.transitionCompletedAt = '2026-08-14T07:59:59.000Z';
|
report.ordering.upgradeRecoveryCompletedAt =
|
||||||
|
'2026-08-14T07:59:59.000Z';
|
||||||
report.runtime.imageIds = [`docker://sha256:${'9'.repeat(64)}`];
|
report.runtime.imageIds = [`docker://sha256:${'9'.repeat(64)}`];
|
||||||
report.gates.activePointerJsonUnchanged = false;
|
report.gates.activePointerJsonUnchanged = false;
|
||||||
const codes = validatePluginPackageRecoveryE2ELiveReport(report).findings.map(
|
const codes = validatePluginPackageRecoveryE2ELiveReport(report).findings.map(
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ const fs = require('node:fs');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { test } = require('node:test');
|
const { test } = require('node:test');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
|
const {
|
||||||
|
createFixture,
|
||||||
|
} = require('../../scripts/ql3-plugin-package-recovery-e2e-fixture.cjs');
|
||||||
|
|
||||||
const root = path.resolve(__dirname, '../..');
|
const root = path.resolve(__dirname, '../..');
|
||||||
const livePath = path.join(
|
const livePath = path.join(
|
||||||
@@ -53,6 +56,27 @@ test('fixture uses a real HTTPS and content-addressed OCI Distribution surface',
|
|||||||
assert.match(live, /requestCount: packageRequests\.length/);
|
assert.match(live, /requestCount: packageRequests\.length/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fixture locks are bound to durable version-three approval dispatches', () => {
|
||||||
|
const value = createFixture({
|
||||||
|
registry: 'registry.fixture.test',
|
||||||
|
architecture: 'amd64',
|
||||||
|
createdAtMs: 1_000,
|
||||||
|
});
|
||||||
|
for (const selected of [value.initial, value.upgrade]) {
|
||||||
|
const proposal = selected.authority.proposalCommand.proposal;
|
||||||
|
const dispatch = selected.authority.dispatch;
|
||||||
|
assert.equal(selected.lock.approval.requestVersion, 3);
|
||||||
|
assert.equal(selected.lock.approval.dispatchId, dispatch.id);
|
||||||
|
assert.equal(selected.lock.actionDigest, proposal.actionDigest);
|
||||||
|
assert.equal(selected.lock.planDigest, proposal.previewDigest);
|
||||||
|
assert.equal(dispatch.action.actionRef, proposal.actionRef);
|
||||||
|
assert.equal(
|
||||||
|
selected.authority.consumptionCommand.dispatchId,
|
||||||
|
dispatch.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('gate runs migration, healthy activation and a durable rejected upgrade', () => {
|
test('gate runs migration, healthy activation and a durable rejected upgrade', () => {
|
||||||
assert.match(live, /operations\/base\/migrate-job\.yaml/);
|
assert.match(live, /operations\/base\/migrate-job\.yaml/);
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -69,6 +93,14 @@ test('gate runs migration, healthy activation and a durable rejected upgrade', (
|
|||||||
);
|
);
|
||||||
assert.match(live, /plugin-package-recovery\/base\/recover-job\.yaml/);
|
assert.match(live, /plugin-package-recovery\/base\/recover-job\.yaml/);
|
||||||
assert.match(fixture, /PostgresPluginPackageInstallRepository/);
|
assert.match(fixture, /PostgresPluginPackageInstallRepository/);
|
||||||
|
assert.match(fixture, /PostgresPluginPackageInstallProposalRepository/);
|
||||||
|
assert.match(fixture, /PostgresApprovalRequestRepository/);
|
||||||
|
assert.match(fixture, /PostgresApprovedActionExecutionRepository/);
|
||||||
|
assert.match(fixture, /PostgresProjectPolicyRepository/);
|
||||||
|
assert.match(fixture, /\.createProposal\(selected\.authority\.proposalCommand\)/);
|
||||||
|
assert.match(fixture, /\.consume\(selected\.authority\.consumptionCommand\)/);
|
||||||
|
assert.match(fixture, /repository\.admit\(/);
|
||||||
|
assert.doesNotMatch(fixture, /pluginPackageInstallCreate/);
|
||||||
assert.match(
|
assert.match(
|
||||||
fixture,
|
fixture,
|
||||||
/PostgresPluginPackagePublisherTrustAuthorityRepository/,
|
/PostgresPluginPackagePublisherTrustAuthorityRepository/,
|
||||||
@@ -79,12 +111,9 @@ test('gate runs migration, healthy activation and a durable rejected upgrade', (
|
|||||||
assert.match(fixture, /publisherTrustStatus/);
|
assert.match(fixture, /publisherTrustStatus/);
|
||||||
assert.match(live, /QL3_E2E_POSTGRES_PACKAGE_MANAGER_USER/);
|
assert.match(live, /QL3_E2E_POSTGRES_PACKAGE_MANAGER_USER/);
|
||||||
assert.match(live, /key: 'package-manager-password'/);
|
assert.match(live, /key: 'package-manager-password'/);
|
||||||
assert.match(
|
assert.match(live, /QL3_E2E_POSTGRES_RUNTIME_USER/);
|
||||||
fixture,
|
assert.match(live, /key: 'runtime-password'/);
|
||||||
/PostgresPluginPackageSecretBindingTransitionRepository/,
|
assert.doesNotMatch(fixture, /commit-transition/);
|
||||||
);
|
|
||||||
assert.match(fixture, /createPluginPackageInstall/);
|
|
||||||
assert.match(fixture, /pluginPackageInstallCreate/);
|
|
||||||
assert.match(live, /initialSeed\.state, 'queued'/);
|
assert.match(live, /initialSeed\.state, 'queued'/);
|
||||||
assert.match(live, /upgradeSeed\.state, 'queued'/);
|
assert.match(live, /upgradeSeed\.state, 'queued'/);
|
||||||
assert.match(live, /value\.migrationCount, 65/);
|
assert.match(live, /value\.migrationCount, 65/);
|
||||||
@@ -99,27 +128,18 @@ test('gate runs migration, healthy activation and a durable rejected upgrade', (
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('deployment controller rejects the upgrade before creating runtime', () => {
|
test('deployment controller rejects the upgrade before creating runtime', () => {
|
||||||
const failedStageWait = live.indexOf(
|
const rejectionWait = live.indexOf('waitForJob(UPGRADE_RECOVERY_JOB)');
|
||||||
"waitForJob(UPGRADE_STAGE_JOB, 'failed')",
|
|
||||||
);
|
|
||||||
const transitionWait = live.indexOf('waitForJob(TRANSITION_JOB)');
|
|
||||||
const rejectionWait = live.indexOf('waitForJob(UPGRADE_REJECTION_JOB)');
|
|
||||||
const pointerProof = live.indexOf(
|
const pointerProof = live.indexOf(
|
||||||
'assert.deepEqual(pointerAfterRejection, pointerBeforeUpgrade)',
|
'assert.deepEqual(pointerAfterRejection, pointerBeforeUpgrade)',
|
||||||
);
|
);
|
||||||
const runtimeApply = live.indexOf(
|
const runtimeApply = live.indexOf(
|
||||||
'const runtime = applyRuntimeAfterRecovery(',
|
'const runtime = applyRuntimeAfterRecovery(',
|
||||||
);
|
);
|
||||||
assert.ok(failedStageWait > 0);
|
assert.ok(rejectionWait > 0);
|
||||||
assert.ok(transitionWait > failedStageWait);
|
|
||||||
assert.ok(rejectionWait > transitionWait);
|
|
||||||
assert.ok(pointerProof > rejectionWait);
|
assert.ok(pointerProof > rejectionWait);
|
||||||
assert.ok(runtimeApply > pointerProof);
|
assert.ok(runtimeApply > pointerProof);
|
||||||
assert.match(live, /activePointerUnchanged: true/);
|
assert.match(live, /activePointerUnchanged: true/);
|
||||||
assert.match(
|
assert.doesNotMatch(live, /missingTransitionFailedClosed/);
|
||||||
live,
|
|
||||||
/stageFailure\.name,[\s\S]*'ClusterPluginPackageRecoveryRequiredError'/,
|
|
||||||
);
|
|
||||||
assert.match(live, /qinglong\.io\/plugin-recovery-job-uid/);
|
assert.match(live, /qinglong\.io\/plugin-recovery-job-uid/);
|
||||||
assert.match(live, /qinglong\.io\/plugin-recovery-completed-at/);
|
assert.match(live, /qinglong\.io\/plugin-recovery-completed-at/);
|
||||||
assert.match(live, /rollout[\s\S]*status/);
|
assert.match(live, /rollout[\s\S]*status/);
|
||||||
|
|||||||
Reference in New Issue
Block a user