From c1d924ce7db6406c088e3fc5a5eec2018fe1d75c Mon Sep 17 00:00:00 2001 From: mworkman-214 Date: Tue, 8 Jul 2025 12:22:59 -0400 Subject: [PATCH 1/5] feat: add CronJob image substitution support --- src/utilities/manifestUpdateUtils.ts | 92 ++++++++++++---------------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/src/utilities/manifestUpdateUtils.ts b/src/utilities/manifestUpdateUtils.ts index 9f7718458..8d4ff609f 100644 --- a/src/utilities/manifestUpdateUtils.ts +++ b/src/utilities/manifestUpdateUtils.ts @@ -81,68 +81,54 @@ function updateContainerImagesInManifestFiles( ): string[] { if (filePaths?.length <= 0) return filePaths - // update container images filePaths.forEach((filePath: string) => { - let contents = fs.readFileSync(filePath).toString() - containers.forEach((container: string) => { - let [imageName] = container.split(':') - if (imageName.indexOf('@') > 0) { - imageName = imageName.split('@')[0] + let fileContents = fs.readFileSync(filePath).toString() + let inputObjects: K8sObject[] = yaml.loadAll(fileContents) as K8sObject[] + let updatedObjects: K8sObject[] = [] + inputObjects.forEach((obj) => { + if (isWorkloadEntity(obj.kind)) { + containers.forEach((container: string) => { + let [imageName] = container.split(':') + if (imageName.indexOf('@') > 0) { + imageName = imageName.split('@')[0] + } + updateImagesInK8sObject(obj, imageName, container) + }) } - - if (contents.indexOf(imageName) > 0) - contents = substituteImageNameInSpecFile( - contents, - imageName, - container - ) + updatedObjects.push(obj) }) - - // write updated files - fs.writeFileSync(path.join(filePath), contents) + const newYaml = updatedObjects.map((o) => yaml.dump(o)).join('---\n') + fs.writeFileSync(path.join(filePath), newYaml) }) - return filePaths } -/* - Example: - - Input of - currentString: `image: "example/example-image"` - imageName: `example/example-image` - imageNameWithNewTag: `example/example-image:identifiertag` - - would return - `image: "example/example-image:identifiertag"` -*/ -export function substituteImageNameInSpecFile( - spec: string, - imageName: string, - imageNameWithNewTag: string -) { - if (spec.indexOf(imageName) < 0) return spec - - return spec.split('\n').reduce((acc, line) => { - const imageKeyword = line.match(/^ *-? *image:/) - if (imageKeyword) { - let [currentImageName] = line - .substring(imageKeyword[0].length) // consume the line from keyword onwards - .trim() - .replace(/[',"]/g, '') // replace allowed quotes with nothing - .split(':') - - if (currentImageName?.indexOf(' ') > 0) { - currentImageName = currentImageName.split(' ')[0] // remove comments - } +// DRY helper to update images in all standard container locations +function updateImagesInK8sObject(obj: any, imageName: string, newImage: string) { + // For most workloads + if (obj?.spec?.template?.spec?.containers) { + updateImageInContainerArray(obj.spec.template.spec.containers, imageName, newImage) + } + // For CronJob + if (obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && obj?.spec?.jobTemplate?.spec?.template?.spec?.containers) { + updateImageInContainerArray(obj.spec.jobTemplate.spec.template.spec.containers, imageName, newImage) + } + // Optionally handle initContainers for both + if (obj?.spec?.template?.spec?.initContainers) { + updateImageInContainerArray(obj.spec.template.spec.initContainers, imageName, newImage) + } + if (obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && obj?.spec?.jobTemplate?.spec?.template?.spec?.initContainers) { + updateImageInContainerArray(obj.spec.jobTemplate.spec.template.spec.initContainers, imageName, newImage) + } +} - if (currentImageName === imageName) { - return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n` - } +function updateImageInContainerArray(containers: any[], imageName: string, newImage: string) { + if (!Array.isArray(containers)) return + containers.forEach((container) => { + if (container.image && (container.image === imageName || container.image.startsWith(imageName + ':') || container.image.startsWith(imageName + '@'))) { + container.image = newImage } - - return acc + line + '\n' - }, '') + }) } export function getReplicaCount(inputObject: any): any { From df5170e14e644c13caca18d688a24d56fd170150 Mon Sep 17 00:00:00 2001 From: mworkman-214 Date: Tue, 8 Jul 2025 12:31:52 -0400 Subject: [PATCH 2/5] prettier write fix --- src/utilities/manifestUpdateUtils.ts | 53 +++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/utilities/manifestUpdateUtils.ts b/src/utilities/manifestUpdateUtils.ts index 8d4ff609f..85d3e5c32 100644 --- a/src/utilities/manifestUpdateUtils.ts +++ b/src/utilities/manifestUpdateUtils.ts @@ -104,28 +104,63 @@ function updateContainerImagesInManifestFiles( } // DRY helper to update images in all standard container locations -function updateImagesInK8sObject(obj: any, imageName: string, newImage: string) { +function updateImagesInK8sObject( + obj: any, + imageName: string, + newImage: string +) { // For most workloads if (obj?.spec?.template?.spec?.containers) { - updateImageInContainerArray(obj.spec.template.spec.containers, imageName, newImage) + updateImageInContainerArray( + obj.spec.template.spec.containers, + imageName, + newImage + ) } // For CronJob - if (obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && obj?.spec?.jobTemplate?.spec?.template?.spec?.containers) { - updateImageInContainerArray(obj.spec.jobTemplate.spec.template.spec.containers, imageName, newImage) + if ( + obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && + obj?.spec?.jobTemplate?.spec?.template?.spec?.containers + ) { + updateImageInContainerArray( + obj.spec.jobTemplate.spec.template.spec.containers, + imageName, + newImage + ) } // Optionally handle initContainers for both if (obj?.spec?.template?.spec?.initContainers) { - updateImageInContainerArray(obj.spec.template.spec.initContainers, imageName, newImage) + updateImageInContainerArray( + obj.spec.template.spec.initContainers, + imageName, + newImage + ) } - if (obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && obj?.spec?.jobTemplate?.spec?.template?.spec?.initContainers) { - updateImageInContainerArray(obj.spec.jobTemplate.spec.template.spec.initContainers, imageName, newImage) + if ( + obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && + obj?.spec?.jobTemplate?.spec?.template?.spec?.initContainers + ) { + updateImageInContainerArray( + obj.spec.jobTemplate.spec.template.spec.initContainers, + imageName, + newImage + ) } } -function updateImageInContainerArray(containers: any[], imageName: string, newImage: string) { +function updateImageInContainerArray( + containers: any[], + imageName: string, + newImage: string +) { if (!Array.isArray(containers)) return containers.forEach((container) => { - if (container.image && (container.image === imageName || container.image.startsWith(imageName + ':') || container.image.startsWith(imageName + '@'))) { + if ( + container.image && + (container.image === imageName || + container.image.startsWith(imageName + ':') || + container.image.startsWith(imageName + '@')) + ) { container.image = newImage } }) From fc70206498c6aa166770825cc811a95234ccf955 Mon Sep 17 00:00:00 2001 From: mworkman-214 Date: Wed, 9 Jul 2025 18:04:30 -0400 Subject: [PATCH 3/5] simplify image update logic and improve YAML file handling --- src/utilities/manifestUpdateUtils.ts | 53 +++++++++------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/src/utilities/manifestUpdateUtils.ts b/src/utilities/manifestUpdateUtils.ts index 85d3e5c32..3ea4119d6 100644 --- a/src/utilities/manifestUpdateUtils.ts +++ b/src/utilities/manifestUpdateUtils.ts @@ -103,48 +103,27 @@ function updateContainerImagesInManifestFiles( return filePaths } -// DRY helper to update images in all standard container locations function updateImagesInK8sObject( obj: any, imageName: string, newImage: string ) { - // For most workloads - if (obj?.spec?.template?.spec?.containers) { - updateImageInContainerArray( - obj.spec.template.spec.containers, - imageName, - newImage - ) - } - // For CronJob - if ( - obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && - obj?.spec?.jobTemplate?.spec?.template?.spec?.containers - ) { - updateImageInContainerArray( - obj.spec.jobTemplate.spec.template.spec.containers, - imageName, - newImage - ) - } - // Optionally handle initContainers for both - if (obj?.spec?.template?.spec?.initContainers) { - updateImageInContainerArray( - obj.spec.template.spec.initContainers, - imageName, - newImage - ) - } - if ( - obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB && - obj?.spec?.jobTemplate?.spec?.template?.spec?.initContainers - ) { - updateImageInContainerArray( - obj.spec.jobTemplate.spec.template.spec.initContainers, - imageName, - newImage - ) + const isCronJob = obj?.kind?.toLowerCase() === KubernetesWorkload.CRON_JOB + + const containerPaths = [ + // Regular workload + obj?.spec?.template?.spec, + // CronJob workload + isCronJob ? obj?.spec?.jobTemplate?.spec?.template?.spec : null + ].filter(Boolean) // Remove any undefined/null entries + + for (const path of containerPaths) { + if (path?.containers) { + updateImageInContainerArray(path.containers, imageName, newImage) + } + if (path?.initContainers) { + updateImageInContainerArray(path.initContainers, imageName, newImage) + } } } From e6dfa0c626473fced873c47433b8588e4410c436 Mon Sep 17 00:00:00 2001 From: mworkman-214 Date: Wed, 9 Jul 2025 18:20:40 -0400 Subject: [PATCH 4/5] improve image update logic with map, includes, and utf8 read --- src/utilities/manifestUpdateUtils.ts | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/utilities/manifestUpdateUtils.ts b/src/utilities/manifestUpdateUtils.ts index 3ea4119d6..4ef42c859 100644 --- a/src/utilities/manifestUpdateUtils.ts +++ b/src/utilities/manifestUpdateUtils.ts @@ -79,23 +79,24 @@ function updateContainerImagesInManifestFiles( filePaths: string[], containers: string[] ): string[] { - if (filePaths?.length <= 0) return filePaths + if (!filePaths?.length) return filePaths filePaths.forEach((filePath: string) => { - let fileContents = fs.readFileSync(filePath).toString() - let inputObjects: K8sObject[] = yaml.loadAll(fileContents) as K8sObject[] - let updatedObjects: K8sObject[] = [] - inputObjects.forEach((obj) => { - if (isWorkloadEntity(obj.kind)) { - containers.forEach((container: string) => { - let [imageName] = container.split(':') - if (imageName.indexOf('@') > 0) { - imageName = imageName.split('@')[0] - } - updateImagesInK8sObject(obj, imageName, container) - }) - } - updatedObjects.push(obj) + const fileContents = fs.readFileSync(filePath, 'utf8') + const inputObjects = yaml.loadAll(fileContents) as K8sObject[] + + const updatedObjects = inputObjects.map((obj) => { + if (!isWorkloadEntity(obj.kind)) return obj + + containers.forEach((container: string) => { + let [imageName] = container.split(':') + if (imageName.includes('@')) { + imageName = imageName.split('@')[0] + } + updateImagesInK8sObject(obj, imageName, container) + }) + + return obj }) const newYaml = updatedObjects.map((o) => yaml.dump(o)).join('---\n') fs.writeFileSync(path.join(filePath), newYaml) From 89cbe1d521a97065e31eaee559532d0c546589f9 Mon Sep 17 00:00:00 2001 From: mworkman-214 Date: Tue, 15 Jul 2025 15:28:35 -0400 Subject: [PATCH 5/5] Added unit tests --- src/utilities/manifestUpdateUtils.test.ts | 79 +++++++++++++++++++++++ src/utilities/manifestUpdateUtils.ts | 2 +- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/utilities/manifestUpdateUtils.test.ts b/src/utilities/manifestUpdateUtils.test.ts index 26fb0eaf3..fa23c9791 100644 --- a/src/utilities/manifestUpdateUtils.test.ts +++ b/src/utilities/manifestUpdateUtils.test.ts @@ -25,4 +25,83 @@ describe('manifestUpdateUtils', () => { manifestUpdateUtils.moveFilesToTmpDir(originalFilePaths) expect(newFilePaths).toEqual(expected) }) + + describe('updateImagesInK8sObject', () => { + it('updates image in Deployment containers', () => { + const obj: any = { + kind: 'Deployment', + spec: {template: {spec: {containers: [{image: 'nginx:old'}]}}} + } + manifestUpdateUtils['updateImagesInK8sObject']( + obj, + 'nginx', + 'nginx:new' + ) + expect(obj.spec.template.spec.containers[0].image).toBe('nginx:new') + }) + + it('updates image in CronJob containers', () => { + const obj: any = { + kind: 'CronJob', + spec: { + jobTemplate: { + spec: { + template: { + spec: {containers: [{image: 'busybox:old'}]} + } + } + } + } + } + manifestUpdateUtils['updateImagesInK8sObject']( + obj, + 'busybox', + 'busybox:new' + ) + expect( + obj.spec.jobTemplate.spec.template.spec.containers[0].image + ).toBe('busybox:new') + }) + + it('does not update image if name does not match', () => { + const obj: any = { + kind: 'Deployment', + spec: {template: {spec: {containers: [{image: 'nginx:old'}]}}} + } + manifestUpdateUtils['updateImagesInK8sObject']( + obj, + 'redis', + 'redis:new' + ) + expect(obj.spec.template.spec.containers[0].image).toBe('nginx:old') + }) + + it('updates image in initContainers for Deployment', () => { + const obj: any = { + kind: 'Deployment', + spec: {template: {spec: {initContainers: [{image: 'init:old'}]}}} + } + manifestUpdateUtils['updateImagesInK8sObject'](obj, 'init', 'init:new') + expect(obj.spec.template.spec.initContainers[0].image).toBe('init:new') + }) + + it('updates image in initContainers for CronJob', () => { + const obj: any = { + kind: 'CronJob', + spec: { + jobTemplate: { + spec: { + template: { + spec: {initContainers: [{image: 'init:old'}]} + } + } + } + } + } + manifestUpdateUtils['updateImagesInK8sObject'](obj, 'init', 'init:new') + expect( + obj.spec.jobTemplate.spec.template.spec.initContainers[0].image + ).toBe('init:new') + }) + }) }) diff --git a/src/utilities/manifestUpdateUtils.ts b/src/utilities/manifestUpdateUtils.ts index 4ef42c859..42b1e148c 100644 --- a/src/utilities/manifestUpdateUtils.ts +++ b/src/utilities/manifestUpdateUtils.ts @@ -104,7 +104,7 @@ function updateContainerImagesInManifestFiles( return filePaths } -function updateImagesInK8sObject( +export function updateImagesInK8sObject( obj: any, imageName: string, newImage: string