From 2005cb6ba9f060214508fd4525b349b3fb064720 Mon Sep 17 00:00:00 2001 From: whyour Date: Fri, 4 Sep 2026 04:21:17 +0800 Subject: [PATCH] fix(ci): ignore terminating CloudNativePG rollout remnants --- scripts/ql3-cloudnativepg-live-contract.cjs | 19 ++++++-- .../ql3CloudNativePgLiveContract.test.cjs | 44 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/scripts/ql3-cloudnativepg-live-contract.cjs b/scripts/ql3-cloudnativepg-live-contract.cjs index cb6a9c5b..fe1684cc 100644 --- a/scripts/ql3-cloudnativepg-live-contract.cjs +++ b/scripts/ql3-cloudnativepg-live-contract.cjs @@ -485,11 +485,21 @@ function verifyImageIds(pods, expectedDigests, description) { return [...new Set(imageIds)]; } +function activeOperatorPods(pods) { + // A successful Deployment rollout can leave the old Pod terminating with + // an empty imageID. It is not evidence for the new serving generation. + return pods.filter( + (pod) => + !pod.metadata?.deletionTimestamp && + (pod.spec?.containers || []).some((container) => + container.image.includes('cloudnative-pg'), + ), + ); +} + function operatorPods() { - return kubectlJson(['-n', 'cnpg-system', 'get', 'pods']).items.filter((pod) => - (pod.spec?.containers || []).some((container) => - container.image.includes('cloudnative-pg'), - ), + return activeOperatorPods( + kubectlJson(['-n', 'cnpg-system', 'get', 'pods']).items, ); } @@ -1034,6 +1044,7 @@ if (require.main === module) { } module.exports = { + activeOperatorPods, imageDigest, imageTag, localApplicationManifest, diff --git a/test/back/ql3CloudNativePgLiveContract.test.cjs b/test/back/ql3CloudNativePgLiveContract.test.cjs index c350acae..e8b730dd 100644 --- a/test/back/ql3CloudNativePgLiveContract.test.cjs +++ b/test/back/ql3CloudNativePgLiveContract.test.cjs @@ -5,6 +5,7 @@ const path = require('node:path'); const { test } = require('node:test'); const { + activeOperatorPods, imageDigest, imageTag, localApplicationManifest, @@ -20,6 +21,49 @@ const INDEX = `sha256:${'a'.repeat(64)}`; const PLATFORM = `sha256:${'b'.repeat(64)}`; const VERSION = readReleaseIdentity(path.resolve(__dirname, '../..')).version; +test('operator image evidence excludes terminating rollout remnants but never missing or unreviewed live pods', () => { + const operator = (imageID, deleting = false) => ({ + metadata: deleting ? { deletionTimestamp: '2026-09-03T20:00:17Z' } : {}, + spec: { + containers: [ + { image: `ghcr.io/cloudnative-pg/cloudnative-pg:1.30.0@${INDEX}` }, + ], + }, + status: { containerStatuses: [{ imageID }] }, + }); + const live = operator(`ghcr.io/cloudnative-pg/cloudnative-pg@${PLATFORM}`); + const old = operator('', true); + assert.throws(() => + verifyImageIds([live, old], [INDEX, PLATFORM], 'operator'), + ); + assert.deepEqual(activeOperatorPods([live, old]), [live]); + assert.deepEqual( + verifyImageIds( + activeOperatorPods([live, old]), + [INDEX, PLATFORM], + 'operator', + ), + [`ghcr.io/cloudnative-pg/cloudnative-pg@${PLATFORM}`], + ); + assert.throws(() => + verifyImageIds(activeOperatorPods([old]), [INDEX], 'operator'), + ); + assert.throws(() => + verifyImageIds( + activeOperatorPods([live, operator('')]), + [INDEX, PLATFORM], + 'operator', + ), + ); + assert.throws(() => + verifyImageIds( + activeOperatorPods([live, operator(`sha256:${'c'.repeat(64)}`)]), + [INDEX, PLATFORM], + 'operator', + ), + ); +}); + function pods(...imageIds) { return imageIds.map((imageID) => ({ status: { containerStatuses: [{ imageID }] },