From 04b61b9744ef0ec293eeb85567888eac07c869ef Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Mon, 26 Jan 2026 12:44:11 +0100 Subject: [PATCH] Resolve service name conflicts when the name is computed to the same value --- packages/k8s/src/hooks/prepare-job.ts | 37 +++++++++++++++++++------- packages/k8s/tests/prepare-job-test.ts | 25 +++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/k8s/src/hooks/prepare-job.ts b/packages/k8s/src/hooks/prepare-job.ts index 28453c1..68d3562 100644 --- a/packages/k8s/src/hooks/prepare-job.ts +++ b/packages/k8s/src/hooks/prepare-job.ts @@ -58,14 +58,30 @@ export async function prepareJob( } let services: k8s.V1Container[] = [] + let serviceNames: string[] = [] if (args.services?.length) { + const occurrences = new Map() + for (const s of args.services) { + const base = generateContainerName(s.image) + occurrences.set(base, (occurrences.get(base) || 0) + 1) + } + + const indices = new Map() services = args.services.map(service => { - return createContainerSpec( - service, - generateContainerName(service.image), - false, - extension - ) + const base = generateContainerName(service.image) + const total = occurrences.get(base) || 0 + const idx = indices.get(base) || 0 + + let name: string + if (total > 1) { + name = `${base}-${idx}` + } else { + name = base + } + + indices.set(base, idx + 1) + serviceNames.push(name) + return createContainerSpec(service, name, false, extension) }) } @@ -153,14 +169,15 @@ export async function prepareJob( throw new Error(`failed to determine if the pod is alpine: ${message}`) } core.debug(`Setting isAlpine to ${isAlpine}`) - generateResponseFile(responseFile, args, createdPod, isAlpine) + generateResponseFile(responseFile, args, createdPod, isAlpine, serviceNames) } function generateResponseFile( responseFile: string, args: PrepareJobArgs, appPod: k8s.V1Pod, - isAlpine: boolean + isAlpine: boolean, + serviceNames?: string[] ): void { if (!appPod.metadata?.name) { throw new Error('app pod must have metadata.name specified') @@ -193,7 +210,9 @@ function generateResponseFile( if (args.services?.length) { const serviceContainerNames = - args.services?.map(s => generateContainerName(s.image)) || [] + serviceNames && serviceNames.length + ? serviceNames + : args.services?.map(s => generateContainerName(s.image)) || [] response.context['services'] = appPod?.spec?.containers ?.filter(c => serviceContainerNames.includes(c.name)) diff --git a/packages/k8s/tests/prepare-job-test.ts b/packages/k8s/tests/prepare-job-test.ts index f73ee93..671f295 100644 --- a/packages/k8s/tests/prepare-job-test.ts +++ b/packages/k8s/tests/prepare-job-test.ts @@ -243,4 +243,29 @@ describe('Prepare job', () => { 'ghcr.io/actions/actions-runner:latest' ) }) + + it('should create unique service container names when images collide', async () => { + // make two services with the same image + const svc = JSON.parse(JSON.stringify(prepareJobData.args.services[0])) + prepareJobData.args.services = [svc, JSON.parse(JSON.stringify(svc))] + // ensure registries are null as TestHelper expects + prepareJobData.args.services.forEach((s: any) => (s.registry = null)) + + await expect( + prepareJob(prepareJobData.args, prepareJobOutputFilePath) + ).resolves.not.toThrow() + + const content = JSON.parse( + fs.readFileSync(prepareJobOutputFilePath).toString() + ) + + expect(content.context.services).toBeTruthy() + expect(content.context.services.length).toBe(2) + + const got = await getPodByName(content.state.jobPod) + const names = (got.spec?.containers || []).map(c => c.name) + + // when images collide, names should be suffixed with -0, -1 + expect(names).toEqual(expect.arrayContaining(['redis-0', 'redis-1'])) + }) })