Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/types/kubernetesTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Kubernetes types', () => {
expect(KubernetesWorkload.DAEMON_SET).toBe('DaemonSet')
expect(KubernetesWorkload.JOB).toBe('job')
expect(KubernetesWorkload.CRON_JOB).toBe('cronjob')
expect(KubernetesWorkload.SCALED_JOB).toBe('scaledjob')
})

it('contains discovery and load balancer resources', () => {
Expand Down Expand Up @@ -53,7 +54,8 @@ describe('Kubernetes types', () => {
'pod',
'statefulset',
'job',
'cronjob'
'cronjob',
'scaledjob'
]
expect(expected.every((val) => WORKLOAD_TYPES.includes(val))).toBe(true)
})
Expand Down
4 changes: 3 additions & 1 deletion src/types/kubernetesTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class KubernetesWorkload {
public static DAEMON_SET: string = 'DaemonSet'
public static JOB: string = 'job'
public static CRON_JOB: string = 'cronjob'
public static SCALED_JOB: string = 'scaledjob'
}

export class DiscoveryAndLoadBalancerResource {
Expand Down Expand Up @@ -34,7 +35,8 @@ export const WORKLOAD_TYPES: string[] = [
'pod',
'statefulset',
'job',
'cronjob'
'cronjob',
'scaledjob'
]

export const WORKLOAD_TYPES_WITH_ROLLOUT_STATUS: string[] = [
Expand Down
4 changes: 2 additions & 2 deletions src/utilities/fileUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('File utils', () => {
'test/unit/manifests/basic-test.yml'
]

expect(testSearch).toHaveLength(9)
expect(testSearch).toHaveLength(10)
expectedManifests.forEach((fileName) => {
if (fileName.startsWith('test/unit')) {
expect(testSearch).toContain(fileName)
Expand Down Expand Up @@ -95,7 +95,7 @@ describe('File utils', () => {
fileAtOuter,
innerPath
])
).toHaveLength(8)
).toHaveLength(9)
})

it('throws an error for an invalid URL', async () => {
Expand Down
76 changes: 43 additions & 33 deletions src/utilities/manifestPullSecretUtils.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,58 @@
import {KubernetesWorkload} from '../types/kubernetesTypes'

export function getImagePullSecrets(inputObject: any) {
if (!inputObject?.spec) return null
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec

if (
inputObject.kind.toLowerCase() ===
KubernetesWorkload.CRON_JOB.toLowerCase()
)
return inputObject?.spec?.jobTemplate?.spec?.template?.spec
?.imagePullSecrets
if (!spec || !kind) return null

if (inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase())
return inputObject.spec.imagePullSecrets
switch (kind) {
case KubernetesWorkload.CRON_JOB.toLowerCase():
return spec.jobTemplate?.spec?.template?.spec?.imagePullSecrets

if (inputObject?.spec?.template?.spec) {
return inputObject.spec.template.spec.imagePullSecrets
case KubernetesWorkload.SCALED_JOB.toLowerCase():
return spec.jobTargetRef?.template?.spec?.imagePullSecrets

case KubernetesWorkload.POD.toLowerCase():
return spec.imagePullSecrets

default:
return spec.template?.spec?.imagePullSecrets || null
}
}

export function setImagePullSecrets(
inputObject: any,
newImagePullSecrets: any
) {
if (!inputObject || !inputObject.spec || !newImagePullSecrets) return

if (
inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase()
) {
inputObject.spec.imagePullSecrets = newImagePullSecrets
return
}

if (
inputObject.kind.toLowerCase() ===
KubernetesWorkload.CRON_JOB.toLowerCase()
) {
if (inputObject?.spec?.jobTemplate?.spec?.template?.spec)
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets =
newImagePullSecrets
return
}

if (inputObject?.spec?.template?.spec) {
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets
return
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec

if (!inputObject || !spec || !newImagePullSecrets || !kind) return

switch (kind) {
case KubernetesWorkload.POD.toLowerCase():
spec.imagePullSecrets = newImagePullSecrets
break

case KubernetesWorkload.CRON_JOB.toLowerCase():
if (spec.jobTemplate?.spec?.template?.spec) {
spec.jobTemplate.spec.template.spec.imagePullSecrets =
newImagePullSecrets
}
break

case KubernetesWorkload.SCALED_JOB.toLowerCase():
if (spec.jobTargetRef?.template?.spec) {
spec.jobTargetRef.template.spec.imagePullSecrets =
newImagePullSecrets
}
break

default:
if (spec.template?.spec) {
spec.template.spec.imagePullSecrets = newImagePullSecrets
}
break
}
}
56 changes: 40 additions & 16 deletions src/utilities/manifestSpecLabelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,54 @@ export function updateSpecLabels(
}

function getSpecLabels(inputObject: any) {
if (!inputObject) return null
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec

if (inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase())
return inputObject.metadata.labels
if (!inputObject || !kind) return null

if (inputObject?.spec?.template?.metadata)
return inputObject.spec.template.metadata.labels
switch (kind) {
case KubernetesWorkload.POD.toLowerCase():
return inputObject.metadata.labels

return null
case KubernetesWorkload.CRON_JOB.toLowerCase():
return spec?.jobTemplate?.spec?.template?.metadata?.labels

case KubernetesWorkload.SCALED_JOB.toLowerCase():
return spec?.jobTargetRef?.template?.metadata?.labels

default:
return spec?.template?.metadata?.labels || null
}
}

function setSpecLabels(inputObject: any, newLabels: any) {
if (!inputObject || !newLabels) return null
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec

if (
inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase()
) {
inputObject.metadata.labels = newLabels
return
}
if (!inputObject || !newLabels || !kind) return null

switch (kind) {
case KubernetesWorkload.POD.toLowerCase():
inputObject.metadata.labels = newLabels
break

case KubernetesWorkload.CRON_JOB.toLowerCase():
if (spec?.jobTemplate?.spec?.template?.metadata) {
spec.jobTemplate.spec.template.metadata.labels = newLabels
}
break

case KubernetesWorkload.SCALED_JOB.toLowerCase():
if (spec?.jobTargetRef?.template?.metadata) {
spec.jobTargetRef.template.metadata.labels = newLabels
}
break

if (inputObject?.spec?.template?.metatada) {
inputObject.spec.template.metatada.labels = newLabels
return
default:
if (spec?.template?.metadata) {
spec.template.metadata.labels = newLabels
}
break
}
}

Expand Down
128 changes: 72 additions & 56 deletions src/utilities/manifestUpdateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,70 +79,81 @@ function updateContainerImagesInManifestFiles(
filePaths: string[],
containers: string[]
): string[] {
if (filePaths?.length <= 0) return filePaths
if (!filePaths?.length) 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]
}
const fileContents = fs.readFileSync(filePath, 'utf8')
const inputObjects = yaml.loadAll(fileContents) as K8sObject[]

if (contents.indexOf(imageName) > 0)
contents = substituteImageNameInSpecFile(
contents,
imageName,
container
)
})
const updatedObjects = inputObjects.map((obj) => {
if (!isWorkloadEntity(obj.kind)) return obj

// write updated files
fs.writeFileSync(path.join(filePath), contents)
})
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)
})
return filePaths
}

/*
Example:
const SPECIAL_CONTAINER_SPEC_PATHS: Record<string, string> = {
[KubernetesWorkload.POD.toLowerCase()]: 'spec',
[KubernetesWorkload.CRON_JOB.toLowerCase()]:
'spec.jobTemplate.spec.template.spec',
[KubernetesWorkload.SCALED_JOB.toLowerCase()]:
'spec.jobTargetRef.template.spec'
}

Input of
currentString: `image: "example/example-image"`
imageName: `example/example-image`
imageNameWithNewTag: `example/example-image:identifiertag`
const DEFAULT_CONTAINER_SPEC_PATH = 'spec.template.spec'

would return
`image: "example/example-image:identifiertag"`
*/
export function substituteImageNameInSpecFile(
spec: string,
export function updateImagesInK8sObject(
obj: any,
imageName: string,
imageNameWithNewTag: string
newImage: 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
}
const kind = obj?.kind?.toLowerCase()
const specPath =
SPECIAL_CONTAINER_SPEC_PATHS[kind] || DEFAULT_CONTAINER_SPEC_PATH

// Convert dot-separated path string into nested object traversal with full optional chaining
// Example: 'spec.jobTargetRef.template.spec' becomes obj?.spec?.jobTargetRef?.template?.spec
// The reduce function walks through each key safely with optional chaining at every step
const path = specPath
.split('.')
.reduce((current, key) => current?.[key], obj)

if (path?.containers) {
updateImageInContainerArray(path.containers, imageName, newImage)
}
if (path?.initContainers) {
updateImageInContainerArray(path.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 {
Expand All @@ -152,12 +163,17 @@ export function getReplicaCount(inputObject: any): any {
throw InputObjectKindNotDefinedError
}

const {kind} = inputObject
if (
kind.toLowerCase() !== KubernetesWorkload.POD.toLowerCase() &&
kind.toLowerCase() !== KubernetesWorkload.DAEMON_SET.toLowerCase()
)
return inputObject.spec.replicas
const kind = inputObject.kind.toLowerCase()

const workloadsWithReplicas = new Set([
KubernetesWorkload.DEPLOYMENT.toLowerCase(),
KubernetesWorkload.REPLICASET.toLowerCase(),
KubernetesWorkload.STATEFUL_SET.toLowerCase()
])

if (workloadsWithReplicas.has(kind)) {
return inputObject.spec?.replicas
}

return 0
}
Expand Down
Loading
Loading