Skip to content

Commit 4755eab

Browse files
Add support for ScaledJob (#436)
* Added ScaledJob support * Fixed getReplicaCount error * Fixed file length error in fileUtils.test.ts * Adjust scaledJob spec path * Made updateImagesInK8sObj more concise
1 parent 7a954ab commit 4755eab

File tree

8 files changed

+261
-109
lines changed

8 files changed

+261
-109
lines changed

src/types/kubernetesTypes.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('Kubernetes types', () => {
2121
expect(KubernetesWorkload.DAEMON_SET).toBe('DaemonSet')
2222
expect(KubernetesWorkload.JOB).toBe('job')
2323
expect(KubernetesWorkload.CRON_JOB).toBe('cronjob')
24+
expect(KubernetesWorkload.SCALED_JOB).toBe('scaledjob')
2425
})
2526

2627
it('contains discovery and load balancer resources', () => {
@@ -53,7 +54,8 @@ describe('Kubernetes types', () => {
5354
'pod',
5455
'statefulset',
5556
'job',
56-
'cronjob'
57+
'cronjob',
58+
'scaledjob'
5759
]
5860
expect(expected.every((val) => WORKLOAD_TYPES.includes(val))).toBe(true)
5961
})

src/types/kubernetesTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export class KubernetesWorkload {
66
public static DAEMON_SET: string = 'DaemonSet'
77
public static JOB: string = 'job'
88
public static CRON_JOB: string = 'cronjob'
9+
public static SCALED_JOB: string = 'scaledjob'
910
}
1011

1112
export class DiscoveryAndLoadBalancerResource {
@@ -34,7 +35,8 @@ export const WORKLOAD_TYPES: string[] = [
3435
'pod',
3536
'statefulset',
3637
'job',
37-
'cronjob'
38+
'cronjob',
39+
'scaledjob'
3840
]
3941

4042
export const WORKLOAD_TYPES_WITH_ROLLOUT_STATUS: string[] = [

src/utilities/fileUtils.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('File utils', () => {
4949
'test/unit/manifests/basic-test.yml'
5050
]
5151

52-
expect(testSearch).toHaveLength(9)
52+
expect(testSearch).toHaveLength(10)
5353
expectedManifests.forEach((fileName) => {
5454
if (fileName.startsWith('test/unit')) {
5555
expect(testSearch).toContain(fileName)
@@ -95,7 +95,7 @@ describe('File utils', () => {
9595
fileAtOuter,
9696
innerPath
9797
])
98-
).toHaveLength(8)
98+
).toHaveLength(9)
9999
})
100100

101101
it('throws an error for an invalid URL', async () => {
Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,58 @@
11
import {KubernetesWorkload} from '../types/kubernetesTypes'
22

33
export function getImagePullSecrets(inputObject: any) {
4-
if (!inputObject?.spec) return null
4+
const kind = inputObject?.kind?.toLowerCase()
5+
const spec = inputObject?.spec
56

6-
if (
7-
inputObject.kind.toLowerCase() ===
8-
KubernetesWorkload.CRON_JOB.toLowerCase()
9-
)
10-
return inputObject?.spec?.jobTemplate?.spec?.template?.spec
11-
?.imagePullSecrets
7+
if (!spec || !kind) return null
128

13-
if (inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase())
14-
return inputObject.spec.imagePullSecrets
9+
switch (kind) {
10+
case KubernetesWorkload.CRON_JOB.toLowerCase():
11+
return spec.jobTemplate?.spec?.template?.spec?.imagePullSecrets
1512

16-
if (inputObject?.spec?.template?.spec) {
17-
return inputObject.spec.template.spec.imagePullSecrets
13+
case KubernetesWorkload.SCALED_JOB.toLowerCase():
14+
return spec.jobTargetRef?.template?.spec?.imagePullSecrets
15+
16+
case KubernetesWorkload.POD.toLowerCase():
17+
return spec.imagePullSecrets
18+
19+
default:
20+
return spec.template?.spec?.imagePullSecrets || null
1821
}
1922
}
2023

2124
export function setImagePullSecrets(
2225
inputObject: any,
2326
newImagePullSecrets: any
2427
) {
25-
if (!inputObject || !inputObject.spec || !newImagePullSecrets) return
26-
27-
if (
28-
inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase()
29-
) {
30-
inputObject.spec.imagePullSecrets = newImagePullSecrets
31-
return
32-
}
33-
34-
if (
35-
inputObject.kind.toLowerCase() ===
36-
KubernetesWorkload.CRON_JOB.toLowerCase()
37-
) {
38-
if (inputObject?.spec?.jobTemplate?.spec?.template?.spec)
39-
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets =
40-
newImagePullSecrets
41-
return
42-
}
43-
44-
if (inputObject?.spec?.template?.spec) {
45-
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets
46-
return
28+
const kind = inputObject?.kind?.toLowerCase()
29+
const spec = inputObject?.spec
30+
31+
if (!inputObject || !spec || !newImagePullSecrets || !kind) return
32+
33+
switch (kind) {
34+
case KubernetesWorkload.POD.toLowerCase():
35+
spec.imagePullSecrets = newImagePullSecrets
36+
break
37+
38+
case KubernetesWorkload.CRON_JOB.toLowerCase():
39+
if (spec.jobTemplate?.spec?.template?.spec) {
40+
spec.jobTemplate.spec.template.spec.imagePullSecrets =
41+
newImagePullSecrets
42+
}
43+
break
44+
45+
case KubernetesWorkload.SCALED_JOB.toLowerCase():
46+
if (spec.jobTargetRef?.template?.spec) {
47+
spec.jobTargetRef.template.spec.imagePullSecrets =
48+
newImagePullSecrets
49+
}
50+
break
51+
52+
default:
53+
if (spec.template?.spec) {
54+
spec.template.spec.imagePullSecrets = newImagePullSecrets
55+
}
56+
break
4757
}
4858
}

src/utilities/manifestSpecLabelUtils.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,54 @@ export function updateSpecLabels(
3030
}
3131

3232
function getSpecLabels(inputObject: any) {
33-
if (!inputObject) return null
33+
const kind = inputObject?.kind?.toLowerCase()
34+
const spec = inputObject?.spec
3435

35-
if (inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase())
36-
return inputObject.metadata.labels
36+
if (!inputObject || !kind) return null
3737

38-
if (inputObject?.spec?.template?.metadata)
39-
return inputObject.spec.template.metadata.labels
38+
switch (kind) {
39+
case KubernetesWorkload.POD.toLowerCase():
40+
return inputObject.metadata.labels
4041

41-
return null
42+
case KubernetesWorkload.CRON_JOB.toLowerCase():
43+
return spec?.jobTemplate?.spec?.template?.metadata?.labels
44+
45+
case KubernetesWorkload.SCALED_JOB.toLowerCase():
46+
return spec?.jobTargetRef?.template?.metadata?.labels
47+
48+
default:
49+
return spec?.template?.metadata?.labels || null
50+
}
4251
}
4352

4453
function setSpecLabels(inputObject: any, newLabels: any) {
45-
if (!inputObject || !newLabels) return null
54+
const kind = inputObject?.kind?.toLowerCase()
55+
const spec = inputObject?.spec
4656

47-
if (
48-
inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase()
49-
) {
50-
inputObject.metadata.labels = newLabels
51-
return
52-
}
57+
if (!inputObject || !newLabels || !kind) return null
58+
59+
switch (kind) {
60+
case KubernetesWorkload.POD.toLowerCase():
61+
inputObject.metadata.labels = newLabels
62+
break
63+
64+
case KubernetesWorkload.CRON_JOB.toLowerCase():
65+
if (spec?.jobTemplate?.spec?.template?.metadata) {
66+
spec.jobTemplate.spec.template.metadata.labels = newLabels
67+
}
68+
break
69+
70+
case KubernetesWorkload.SCALED_JOB.toLowerCase():
71+
if (spec?.jobTargetRef?.template?.metadata) {
72+
spec.jobTargetRef.template.metadata.labels = newLabels
73+
}
74+
break
5375

54-
if (inputObject?.spec?.template?.metatada) {
55-
inputObject.spec.template.metatada.labels = newLabels
56-
return
76+
default:
77+
if (spec?.template?.metadata) {
78+
spec.template.metadata.labels = newLabels
79+
}
80+
break
5781
}
5882
}
5983

src/utilities/manifestUpdateUtils.ts

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -79,70 +79,81 @@ function updateContainerImagesInManifestFiles(
7979
filePaths: string[],
8080
containers: string[]
8181
): string[] {
82-
if (filePaths?.length <= 0) return filePaths
82+
if (!filePaths?.length) return filePaths
8383

84-
// update container images
8584
filePaths.forEach((filePath: string) => {
86-
let contents = fs.readFileSync(filePath).toString()
87-
containers.forEach((container: string) => {
88-
let [imageName] = container.split(':')
89-
if (imageName.indexOf('@') > 0) {
90-
imageName = imageName.split('@')[0]
91-
}
85+
const fileContents = fs.readFileSync(filePath, 'utf8')
86+
const inputObjects = yaml.loadAll(fileContents) as K8sObject[]
9287

93-
if (contents.indexOf(imageName) > 0)
94-
contents = substituteImageNameInSpecFile(
95-
contents,
96-
imageName,
97-
container
98-
)
99-
})
88+
const updatedObjects = inputObjects.map((obj) => {
89+
if (!isWorkloadEntity(obj.kind)) return obj
10090

101-
// write updated files
102-
fs.writeFileSync(path.join(filePath), contents)
103-
})
91+
containers.forEach((container: string) => {
92+
let [imageName] = container.split(':')
93+
if (imageName.includes('@')) {
94+
imageName = imageName.split('@')[0]
95+
}
96+
updateImagesInK8sObject(obj, imageName, container)
97+
})
10498

99+
return obj
100+
})
101+
const newYaml = updatedObjects.map((o) => yaml.dump(o)).join('---\n')
102+
fs.writeFileSync(path.join(filePath), newYaml)
103+
})
105104
return filePaths
106105
}
107106

108-
/*
109-
Example:
107+
const SPECIAL_CONTAINER_SPEC_PATHS: Record<string, string> = {
108+
[KubernetesWorkload.POD.toLowerCase()]: 'spec',
109+
[KubernetesWorkload.CRON_JOB.toLowerCase()]:
110+
'spec.jobTemplate.spec.template.spec',
111+
[KubernetesWorkload.SCALED_JOB.toLowerCase()]:
112+
'spec.jobTargetRef.template.spec'
113+
}
110114

111-
Input of
112-
currentString: `image: "example/example-image"`
113-
imageName: `example/example-image`
114-
imageNameWithNewTag: `example/example-image:identifiertag`
115+
const DEFAULT_CONTAINER_SPEC_PATH = 'spec.template.spec'
115116

116-
would return
117-
`image: "example/example-image:identifiertag"`
118-
*/
119-
export function substituteImageNameInSpecFile(
120-
spec: string,
117+
export function updateImagesInK8sObject(
118+
obj: any,
121119
imageName: string,
122-
imageNameWithNewTag: string
120+
newImage: string
123121
) {
124-
if (spec.indexOf(imageName) < 0) return spec
125-
126-
return spec.split('\n').reduce((acc, line) => {
127-
const imageKeyword = line.match(/^ *-? *image:/)
128-
if (imageKeyword) {
129-
let [currentImageName] = line
130-
.substring(imageKeyword[0].length) // consume the line from keyword onwards
131-
.trim()
132-
.replace(/[',"]/g, '') // replace allowed quotes with nothing
133-
.split(':')
134-
135-
if (currentImageName?.indexOf(' ') > 0) {
136-
currentImageName = currentImageName.split(' ')[0] // remove comments
137-
}
122+
const kind = obj?.kind?.toLowerCase()
123+
const specPath =
124+
SPECIAL_CONTAINER_SPEC_PATHS[kind] || DEFAULT_CONTAINER_SPEC_PATH
125+
126+
// Convert dot-separated path string into nested object traversal with full optional chaining
127+
// Example: 'spec.jobTargetRef.template.spec' becomes obj?.spec?.jobTargetRef?.template?.spec
128+
// The reduce function walks through each key safely with optional chaining at every step
129+
const path = specPath
130+
.split('.')
131+
.reduce((current, key) => current?.[key], obj)
132+
133+
if (path?.containers) {
134+
updateImageInContainerArray(path.containers, imageName, newImage)
135+
}
136+
if (path?.initContainers) {
137+
updateImageInContainerArray(path.initContainers, imageName, newImage)
138+
}
139+
}
138140

139-
if (currentImageName === imageName) {
140-
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`
141-
}
141+
function updateImageInContainerArray(
142+
containers: any[],
143+
imageName: string,
144+
newImage: string
145+
) {
146+
if (!Array.isArray(containers)) return
147+
containers.forEach((container) => {
148+
if (
149+
container.image &&
150+
(container.image === imageName ||
151+
container.image.startsWith(imageName + ':') ||
152+
container.image.startsWith(imageName + '@'))
153+
) {
154+
container.image = newImage
142155
}
143-
144-
return acc + line + '\n'
145-
}, '')
156+
})
146157
}
147158

148159
export function getReplicaCount(inputObject: any): any {
@@ -152,12 +163,17 @@ export function getReplicaCount(inputObject: any): any {
152163
throw InputObjectKindNotDefinedError
153164
}
154165

155-
const {kind} = inputObject
156-
if (
157-
kind.toLowerCase() !== KubernetesWorkload.POD.toLowerCase() &&
158-
kind.toLowerCase() !== KubernetesWorkload.DAEMON_SET.toLowerCase()
159-
)
160-
return inputObject.spec.replicas
166+
const kind = inputObject.kind.toLowerCase()
167+
168+
const workloadsWithReplicas = new Set([
169+
KubernetesWorkload.DEPLOYMENT.toLowerCase(),
170+
KubernetesWorkload.REPLICASET.toLowerCase(),
171+
KubernetesWorkload.STATEFUL_SET.toLowerCase()
172+
])
173+
174+
if (workloadsWithReplicas.has(kind)) {
175+
return inputObject.spec?.replicas
176+
}
161177

162178
return 0
163179
}

0 commit comments

Comments
 (0)