Skip to content

Commit 79c2382

Browse files
feat(helm/v2-alpha): add extra volumes support
Support manager.extraVolumes and manager.extraVolumeMounts: extract from kustomize (excluding webhook-certs/metrics-certs), inject into values when present, and template in manager deployment (including when volumes: []). Document in Helm v2-alpha plugin page. Generated-by: Cursor/Claude
1 parent 66c1957 commit 79c2382

File tree

11 files changed

+367
-9
lines changed

11 files changed

+367
-9
lines changed

docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ spec:
9797
name: webhook-certs
9898
readOnly: true
9999
{{- end }}
100+
{{- if .Values.manager.extraVolumeMounts }}
101+
{{- toYaml .Values.manager.extraVolumeMounts | nindent 20 }}
102+
{{- end }}
100103
securityContext:
101104
{{- if .Values.manager.podSecurityContext }}
102105
{{- toYaml .Values.manager.podSecurityContext | nindent 14 }}
@@ -124,3 +127,6 @@ spec:
124127
secret:
125128
secretName: webhook-server-cert
126129
{{- end }}
130+
{{- if .Values.manager.extraVolumes }}
131+
{{- toYaml .Values.manager.extraVolumes | nindent 14 }}
132+
{{- end }}

docs/book/src/getting-started/testdata/project/dist/chart/templates/manager/manager.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ spec:
7777
{{- else }}
7878
{}
7979
{{- end }}
80-
volumeMounts: []
80+
volumeMounts:
81+
{{- if .Values.manager.extraVolumeMounts }}
82+
{{- toYaml .Values.manager.extraVolumeMounts | nindent 20 }}
83+
{{- else }}
84+
[]
85+
{{- end }}
8186
securityContext:
8287
{{- if .Values.manager.podSecurityContext }}
8388
{{- toYaml .Values.manager.podSecurityContext | nindent 14 }}
@@ -86,4 +91,9 @@ spec:
8691
{{- end }}
8792
serviceAccountName: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }}
8893
terminationGracePeriodSeconds: 10
89-
volumes: []
94+
volumes:
95+
{{- if .Values.manager.extraVolumes }}
96+
{{- toYaml .Values.manager.extraVolumes | nindent 14 }}
97+
{{- else }}
98+
[]
99+
{{- end }}

docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ spec:
9797
name: webhook-certs
9898
readOnly: true
9999
{{- end }}
100+
{{- if .Values.manager.extraVolumeMounts }}
101+
{{- toYaml .Values.manager.extraVolumeMounts | nindent 20 }}
102+
{{- end }}
100103
securityContext:
101104
{{- if .Values.manager.podSecurityContext }}
102105
{{- toYaml .Values.manager.podSecurityContext | nindent 14 }}
@@ -124,3 +127,6 @@ spec:
124127
secret:
125128
secretName: webhook-server-cert
126129
{{- end }}
130+
{{- if .Values.manager.extraVolumes }}
131+
{{- toYaml .Values.manager.extraVolumes | nindent 14 }}
132+
{{- end }}

docs/book/src/plugins/available/helm-v2-alpha.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,15 @@ prometheus:
318318
enable: false
319319
```
320320
321+
### Extra volumes
322+
323+
The chart supports extra volumes and volume mounts for the manager (e.g. secrets, config files) in addition to the built-in webhook and metrics cert volumes.
324+
325+
- **From kustomize**: If your deployment in `dist/install.yaml` includes volumes other than the default `webhook-certs` and `metrics-certs` (from the scaffolded patches), the plugin adds them to `values.yaml` as `manager.extraVolumes` and `manager.extraVolumeMounts`. The manager template merges them into the deployment.
326+
- **Manual**: You can add `manager.extraVolumes` and `manager.extraVolumeMounts` in `values.yaml` yourself; the template will use them. Use the same structure as in a Pod spec (list of volume and volumeMount entries). Mount names in `extraVolumeMounts` must match names in `extraVolumes`.
327+
328+
Default webhook and metrics volumes (names `webhook-certs` and `metrics-certs`) are always handled by the chart and are not part of the extra lists.
329+
321330
### Installation
322331

323332
The first time you run the plugin, it adds convenient Helm deployment targets to your `Makefile`:

pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ func (c *ChartConverter) ExtractDeploymentConfig() map[string]any {
134134
extractContainerResources(container, config)
135135
extractContainerSecurityContext(container, config)
136136

137+
// Extra volumes/mounts from input (exclude default webhook/metrics volumes; those are templated separately)
138+
extractExtraVolumes(specMap, config)
139+
extractExtraVolumeMounts(container, config)
140+
137141
return config
138142
}
139143

@@ -416,3 +420,68 @@ func extractContainerSecurityContext(container map[string]any, config map[string
416420

417421
config["securityContext"] = securityContext
418422
}
423+
424+
// Default volume names from the Kustomize scaffold (manager_webhook_patch.yaml and
425+
// cert_metrics_manager_patch.yaml). These match what main.go's --webhook-cert-path and
426+
// --metrics-cert-path are used with. Volumes with these names are never considered "extra".
427+
var defaultWebhookMetricsVolumeNames = map[string]struct{}{
428+
"webhook-certs": {}, // manager_webhook_patch.yaml
429+
"metrics-certs": {}, // cert_metrics_manager_patch.yaml
430+
}
431+
432+
// extractExtraVolumes copies pod volumes into config["extraVolumes"], excluding volumes
433+
// whose name is one of the default webhook/metrics names from the Kustomize scaffold.
434+
// Only set when there is at least one extra volume.
435+
func extractExtraVolumes(specMap map[string]any, config map[string]any) {
436+
volumes, found, err := unstructured.NestedFieldNoCopy(specMap, "volumes")
437+
if !found || err != nil {
438+
return
439+
}
440+
volumesList, ok := volumes.([]any)
441+
if !ok || len(volumesList) == 0 {
442+
return
443+
}
444+
extra := make([]any, 0, len(volumesList))
445+
for _, v := range volumesList {
446+
vm, ok := v.(map[string]any)
447+
if !ok {
448+
continue
449+
}
450+
name, _ := vm["name"].(string)
451+
if _, isDefault := defaultWebhookMetricsVolumeNames[name]; isDefault {
452+
continue
453+
}
454+
extra = append(extra, v)
455+
}
456+
if len(extra) > 0 {
457+
config["extraVolumes"] = extra
458+
}
459+
}
460+
461+
// extractExtraVolumeMounts copies container volumeMounts into config["extraVolumeMounts"],
462+
// excluding mounts that reference the default webhook/metrics volume names. Only set when there is at least one extra.
463+
func extractExtraVolumeMounts(container map[string]any, config map[string]any) {
464+
mounts, found, err := unstructured.NestedFieldNoCopy(container, "volumeMounts")
465+
if !found || err != nil {
466+
return
467+
}
468+
mountsList, ok := mounts.([]any)
469+
if !ok || len(mountsList) == 0 {
470+
return
471+
}
472+
extra := make([]any, 0, len(mountsList))
473+
for _, m := range mountsList {
474+
mm, ok := m.(map[string]any)
475+
if !ok {
476+
continue
477+
}
478+
name, _ := mm["name"].(string)
479+
if _, isDefault := defaultWebhookMetricsVolumeNames[name]; isDefault {
480+
continue
481+
}
482+
extra = append(extra, m)
483+
}
484+
if len(extra) > 0 {
485+
config["extraVolumeMounts"] = extra
486+
}
487+
}

pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,64 @@ var _ = Describe("ChartConverter", func() {
311311
config := converter.ExtractDeploymentConfig()
312312
Expect(config).To(BeEmpty())
313313
})
314+
315+
It("should extract extraVolumes and extraVolumeMounts excluding webhook and metrics", func() {
316+
volumes := []any{
317+
map[string]any{
318+
"name": "webhook-certs",
319+
"secret": map[string]any{"secretName": "webhook-server-cert"},
320+
},
321+
map[string]any{
322+
"name": "custom-volume",
323+
"secret": map[string]any{"secretName": "my-secret"},
324+
},
325+
}
326+
volumeMounts := []any{
327+
map[string]any{
328+
"name": "webhook-certs",
329+
"mountPath": "/tmp/k8s-webhook-server/serving-certs",
330+
"readOnly": true,
331+
},
332+
map[string]any{
333+
"name": "custom-volume",
334+
"mountPath": "/etc/my-secrets",
335+
"readOnly": true,
336+
},
337+
}
338+
containers := []any{
339+
map[string]any{
340+
"name": "manager",
341+
"image": "controller:latest",
342+
"volumeMounts": volumeMounts,
343+
},
344+
}
345+
err := unstructured.SetNestedSlice(
346+
resources.Deployment.Object,
347+
volumes,
348+
"spec", "template", "spec", "volumes",
349+
)
350+
Expect(err).NotTo(HaveOccurred())
351+
err = unstructured.SetNestedSlice(
352+
resources.Deployment.Object,
353+
containers,
354+
"spec", "template", "spec", "containers",
355+
)
356+
Expect(err).NotTo(HaveOccurred())
357+
358+
config := converter.ExtractDeploymentConfig()
359+
360+
Expect(config).To(HaveKey("extraVolumes"))
361+
extraVols, ok := config["extraVolumes"].([]any)
362+
Expect(ok).To(BeTrue())
363+
Expect(extraVols).To(HaveLen(1))
364+
Expect(extraVols[0]).To(HaveKeyWithValue("name", "custom-volume"))
365+
366+
Expect(config).To(HaveKey("extraVolumeMounts"))
367+
extraMounts, ok := config["extraVolumeMounts"].([]any)
368+
Expect(ok).To(BeTrue())
369+
Expect(extraMounts).To(HaveLen(1))
370+
Expect(extraMounts[0]).To(HaveKeyWithValue("mountPath", "/etc/my-secrets"))
371+
})
314372
})
315373

316374
Context("extractPortFromArg", func() {

pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater.go

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -704,17 +704,78 @@ func (t *HelmTemplater) templateSecurityContexts(yamlContent string) string {
704704
return yamlContent
705705
}
706706

707-
// templateVolumeMounts converts volumeMounts sections to keep them as-is since they're webhook-specific
707+
// templateVolumeMounts injects .Values.manager.extraVolumeMounts (append or replace empty list).
708708
func (t *HelmTemplater) templateVolumeMounts(yamlContent string) string {
709-
// For webhook volumeMounts, we keep them as-is since they're required for webhook functionality
710-
// They will be conditionally included based on webhook configuration
711-
return yamlContent
709+
return t.appendToListFromValues(yamlContent, "volumeMounts:", ".Values.manager.extraVolumeMounts")
712710
}
713711

714-
// templateVolumes converts volumes sections to keep them as-is since they're webhook-specific
712+
// templateVolumes injects .Values.manager.extraVolumes (append or replace empty list).
715713
func (t *HelmTemplater) templateVolumes(yamlContent string) string {
716-
// For webhook volumes, we keep them as-is since they're required for webhook functionality
717-
// They will be conditionally included based on webhook configuration
714+
return t.appendToListFromValues(yamlContent, "volumes:", ".Values.manager.extraVolumes")
715+
}
716+
717+
// appendToListFromValues finds "key:" or "key: []", and either appends values path to an existing list
718+
// or replaces "key: []" with a template that outputs the values path when set. Idempotent if already present.
719+
func (t *HelmTemplater) appendToListFromValues(yamlContent string, keyColon string, valuesPath string) string {
720+
if !strings.Contains(yamlContent, keyColon) {
721+
return yamlContent
722+
}
723+
if strings.Contains(yamlContent, valuesPath) {
724+
return yamlContent
725+
}
726+
727+
lines := strings.Split(yamlContent, "\n")
728+
keyEmpty := keyColon + " []"
729+
730+
for i := range lines {
731+
trimmed := strings.TrimSpace(lines[i])
732+
indentStr, indentLen := leadingWhitespace(lines[i])
733+
childIndent := indentStr + " "
734+
childIndentWidth := strconv.Itoa(len(childIndent))
735+
736+
// Case 1: "key: []" (Kustomize default) — replace with template so extra* values are used
737+
if trimmed == keyEmpty {
738+
block := []string{
739+
indentStr + keyColon,
740+
childIndent + "{{- if " + valuesPath + " }}",
741+
childIndent + "{{- toYaml " + valuesPath + " | nindent " + childIndentWidth + " }}",
742+
childIndent + "{{- else }}",
743+
childIndent + "[]",
744+
childIndent + "{{- end }}",
745+
}
746+
newLines := append([]string{}, lines[:i]...)
747+
newLines = append(newLines, block...)
748+
newLines = append(newLines, lines[i+1:]...)
749+
return strings.Join(newLines, "\n")
750+
}
751+
752+
// Case 2: "key:" with multi-line list — append values path after list
753+
if trimmed != keyColon {
754+
continue
755+
}
756+
757+
end := i + 1
758+
for ; end < len(lines); end++ {
759+
tLine := strings.TrimSpace(lines[end])
760+
if tLine == "" {
761+
break
762+
}
763+
lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
764+
if lineIndent <= indentLen {
765+
break
766+
}
767+
}
768+
769+
block := []string{
770+
childIndent + "{{- if " + valuesPath + " }}",
771+
childIndent + "{{- toYaml " + valuesPath + " | nindent " + childIndentWidth + " }}",
772+
childIndent + "{{- end }}",
773+
}
774+
newLines := append([]string{}, lines[:end]...)
775+
newLines = append(newLines, block...)
776+
newLines = append(newLines, lines[end:]...)
777+
return strings.Join(newLines, "\n")
778+
}
718779
return yamlContent
719780
}
720781

pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,6 +2058,65 @@ spec:
20582058
Expect(result).To(ContainSubstring("name: controller-test"))
20592059
})
20602060

2061+
It("should append extraVolumes and extraVolumeMounts when lists are present", func() {
2062+
deployment := &unstructured.Unstructured{}
2063+
deployment.SetAPIVersion("apps/v1")
2064+
deployment.SetKind("Deployment")
2065+
deployment.SetName("test-project-controller-manager")
2066+
2067+
content := `apiVersion: apps/v1
2068+
kind: Deployment
2069+
metadata:
2070+
name: test-project-controller-manager
2071+
spec:
2072+
template:
2073+
spec:
2074+
volumes:
2075+
- name: webhook-certs
2076+
secret:
2077+
secretName: webhook-server-cert
2078+
containers:
2079+
- name: manager
2080+
image: controller:latest
2081+
volumeMounts:
2082+
- name: webhook-certs
2083+
mountPath: /tmp/k8s-webhook-server/serving-certs
2084+
readOnly: true
2085+
`
2086+
2087+
result := templater.ApplyHelmSubstitutions(content, deployment)
2088+
2089+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumes"))
2090+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumeMounts"))
2091+
})
2092+
2093+
It("should inject extraVolumes/extraVolumeMounts when Kustomize has volumeMounts: [] and volumes: []", func() {
2094+
deployment := &unstructured.Unstructured{}
2095+
deployment.SetAPIVersion("apps/v1")
2096+
deployment.SetKind("Deployment")
2097+
deployment.SetName("test-project-controller-manager")
2098+
2099+
// Default Kustomize output: single-line empty lists (no webhook/metrics patches)
2100+
content := `apiVersion: apps/v1
2101+
kind: Deployment
2102+
metadata:
2103+
name: test-project-controller-manager
2104+
spec:
2105+
template:
2106+
spec:
2107+
containers:
2108+
- name: manager
2109+
image: controller:latest
2110+
volumeMounts: []
2111+
volumes: []`
2112+
2113+
result := templater.ApplyHelmSubstitutions(content, deployment)
2114+
2115+
// Plugin must inject template so values work without manual manager.yaml edits
2116+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumes"))
2117+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumeMounts"))
2118+
})
2119+
20612120
It("should fall back to 'manager' when default-container annotation is missing", func() {
20622121
deployment := &unstructured.Unstructured{}
20632122
deployment.SetAPIVersion("apps/v1")

0 commit comments

Comments
 (0)