Skip to content

Commit b438e57

Browse files
authored
feat(manager/kubernetes): extract image volume references from manifests (renovatebot#42038)
* feat(manager/kubernetes): extract image volume references from manifests * fix coverage * review comments * simplify
1 parent 81e6582 commit b438e57

File tree

3 files changed

+236
-2
lines changed

3 files changed

+236
-2
lines changed

lib/modules/manager/kubernetes/extract.spec.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { codeBlock } from 'common-tags';
12
import { Fixtures } from '~test/fixtures.ts';
23
import { extractPackageFile } from './index.ts';
34

@@ -217,5 +218,167 @@ kind: ConfigMap
217218
],
218219
});
219220
});
221+
222+
describe('image volume references', () => {
223+
it.each`
224+
kind | apiVersion
225+
${'DaemonSet'} | ${'apps/v1'}
226+
${'Deployment'} | ${'apps/v1'}
227+
${'Job'} | ${'batch/v1'}
228+
${'ReplicaSet'} | ${'apps/v1'}
229+
${'ReplicationController'} | ${'v1'}
230+
${'StatefulSet'} | ${'apps/v1'}
231+
`(
232+
'extracts image volumes from $kind',
233+
({ kind, apiVersion }: { kind: string; apiVersion: string }) => {
234+
const res = extractPackageFile(
235+
codeBlock`
236+
apiVersion: ${apiVersion}
237+
kind: ${kind}
238+
metadata:
239+
name: test
240+
spec:
241+
template:
242+
spec:
243+
volumes:
244+
- name: vol
245+
image:
246+
reference: quay.io/test/image:v1.0.0
247+
`,
248+
'file.yaml',
249+
{},
250+
);
251+
252+
expect(res?.deps).toContainEqual({
253+
autoReplaceStringTemplate:
254+
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
255+
currentDigest: undefined,
256+
currentValue: 'v1.0.0',
257+
datasource: 'docker',
258+
depName: 'quay.io/test/image',
259+
packageName: 'quay.io/test/image',
260+
replaceString: 'quay.io/test/image:v1.0.0',
261+
});
262+
},
263+
);
264+
265+
it('extracts image volumes from Pod and CronJob', () => {
266+
const res = extractPackageFile(
267+
codeBlock`
268+
apiVersion: v1
269+
kind: Pod
270+
metadata:
271+
name: pod-test
272+
spec:
273+
volumes:
274+
- name: vol
275+
image:
276+
reference: quay.io/test/pod-image:v1.0.0
277+
---
278+
apiVersion: batch/v1
279+
kind: CronJob
280+
metadata:
281+
name: cronjob-test
282+
spec:
283+
jobTemplate:
284+
spec:
285+
template:
286+
spec:
287+
volumes:
288+
- name: vol
289+
image:
290+
reference: quay.io/test/cronjob-image:v2.0.0
291+
`,
292+
'file.yaml',
293+
{},
294+
);
295+
296+
expect(res?.deps).toStrictEqual([
297+
{
298+
autoReplaceStringTemplate:
299+
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
300+
currentDigest: undefined,
301+
currentValue: 'v1.0.0',
302+
datasource: 'docker',
303+
depName: 'quay.io/test/pod-image',
304+
packageName: 'quay.io/test/pod-image',
305+
replaceString: 'quay.io/test/pod-image:v1.0.0',
306+
},
307+
{
308+
autoReplaceStringTemplate:
309+
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
310+
currentDigest: undefined,
311+
currentValue: 'v2.0.0',
312+
datasource: 'docker',
313+
depName: 'quay.io/test/cronjob-image',
314+
packageName: 'quay.io/test/cronjob-image',
315+
replaceString: 'quay.io/test/cronjob-image:v2.0.0',
316+
},
317+
{
318+
currentValue: 'batch/v1',
319+
datasource: 'kubernetes-api',
320+
depName: 'CronJob',
321+
versioning: 'kubernetes-api',
322+
},
323+
]);
324+
});
325+
326+
it('does not extract image volumes for unsupported kind', () => {
327+
const res = extractPackageFile(
328+
codeBlock`
329+
apiVersion: extensions/v1beta1
330+
kind: NetworkPolicy
331+
metadata:
332+
name: test-network-policy
333+
spec:
334+
podSelector: {}
335+
`,
336+
'file.yaml',
337+
{},
338+
);
339+
expect(res?.deps).toStrictEqual([
340+
{
341+
currentValue: 'extensions/v1beta1',
342+
datasource: 'kubernetes-api',
343+
depName: 'NetworkPolicy',
344+
versioning: 'kubernetes-api',
345+
},
346+
]);
347+
});
348+
349+
it('skips malformed volume entries and extracts valid ones', () => {
350+
const res = extractPackageFile(
351+
codeBlock`
352+
apiVersion: v1
353+
kind: Pod
354+
metadata:
355+
name: pod-test
356+
spec:
357+
volumes:
358+
- name: bad-vol
359+
image:
360+
notReference: invalid
361+
- name: good-vol
362+
image:
363+
reference: quay.io/test/image:v1.0.0
364+
`,
365+
'file.yaml',
366+
{},
367+
);
368+
369+
expect(res?.deps).toStrictEqual([
370+
{
371+
autoReplaceStringTemplate:
372+
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
373+
currentDigest: undefined,
374+
currentValue: 'v1.0.0',
375+
datasource: 'docker',
376+
depName: 'quay.io/test/image',
377+
packageName: 'quay.io/test/image',
378+
replaceString: 'quay.io/test/image:v1.0.0',
379+
},
380+
]);
381+
});
382+
});
220383
});
221384
});

lib/modules/manager/kubernetes/extract.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function extractPackageFile(
3434

3535
const deps: PackageDependency[] = [
3636
...extractImages(content, config),
37+
...extractImageVolumes(manifests, config),
3738
...extractApis(manifests),
3839
];
3940

@@ -74,6 +75,30 @@ function extractImages(
7475
return deps;
7576
}
7677

78+
function extractImageVolumes(
79+
manifests: KubernetesManifest[],
80+
config: ExtractConfig,
81+
): PackageDependency[] {
82+
const deps: PackageDependency[] = [];
83+
84+
for (const manifest of manifests) {
85+
for (const currentFrom of manifest.imageVolumeReferences) {
86+
const dep = getDep(currentFrom, true, config.registryAliases);
87+
logger.debug(
88+
{
89+
depName: dep.depName,
90+
currentValue: dep.currentValue,
91+
currentDigest: dep.currentDigest,
92+
},
93+
'Kubernetes image volume',
94+
);
95+
deps.push(dep);
96+
}
97+
}
98+
99+
return deps;
100+
}
101+
77102
function extractApis(manifests: KubernetesManifest[]): PackageDependency[] {
78103
return manifests
79104
.filter((m) => supportedApis.has(m.kind))
Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,43 @@
11
import { z } from 'zod/v3';
22
import { LooseArray, multidocYaml } from '../../../util/schema-utils/index.ts';
33

4+
const PodSpecVolumes = z.object({
5+
volumes: LooseArray(z.object({ image: z.object({ reference: z.string() }) }))
6+
.transform((volumes) => volumes.map((v) => v.image.reference))
7+
.catch([]),
8+
});
9+
10+
const TemplateSpecVolumes = z.object({
11+
template: z.object({ spec: PodSpecVolumes }),
12+
});
13+
14+
const ImageVolumeReferences = z
15+
.discriminatedUnion('kind', [
16+
z.object({
17+
kind: z.literal('Pod'),
18+
spec: PodSpecVolumes.transform((s) => s.volumes),
19+
}),
20+
z.object({
21+
kind: z.enum([
22+
'DaemonSet',
23+
'Deployment',
24+
'Job',
25+
'ReplicaSet',
26+
'ReplicationController',
27+
'StatefulSet',
28+
]),
29+
spec: TemplateSpecVolumes.transform((s) => s.template.spec.volumes),
30+
}),
31+
z.object({
32+
kind: z.literal('CronJob'),
33+
spec: z
34+
.object({ jobTemplate: z.object({ spec: TemplateSpecVolumes }) })
35+
.transform((s) => s.jobTemplate.spec.template.spec.volumes),
36+
}),
37+
])
38+
.transform(({ spec }) => spec)
39+
.catch([]);
40+
441
export const KubernetesResource = z.object({
542
apiVersion: z.string().trim().min(1),
643
kind: z.string().trim().min(1),
@@ -10,8 +47,17 @@ export const KubernetesResource = z.object({
1047
}),
1148
});
1249

13-
export type KubernetesManifest = z.infer<typeof KubernetesResource>;
50+
export const KubernetesManifest = KubernetesResource.extend({
51+
spec: z.unknown(),
52+
}).transform(({ spec, ...resource }) => ({
53+
...resource,
54+
imageVolumeReferences: ImageVolumeReferences.parse({
55+
kind: resource.kind,
56+
spec,
57+
}),
58+
}));
59+
export type KubernetesManifest = z.infer<typeof KubernetesManifest>;
1460

1561
export const KubernetesManifests = multidocYaml({
1662
removeTemplates: true,
17-
}).pipe(LooseArray(KubernetesResource));
63+
}).pipe(LooseArray(KubernetesManifest));

0 commit comments

Comments
 (0)