Skip to content

Commit 3594c69

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 2831857 commit 3594c69

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
@@ -87,6 +87,9 @@ spec:
8787
{}
8888
{{- end }}
8989
volumeMounts:
90+
{{- if .Values.manager.extraVolumeMounts }}
91+
{{- toYaml .Values.manager.extraVolumeMounts | nindent 10 }}
92+
{{- end }}
9093
{{- if and .Values.certManager.enable .Values.metrics.enable }}
9194
- mountPath: /tmp/k8s-metrics-server/metrics-certs
9295
name: metrics-certs
@@ -106,6 +109,9 @@ spec:
106109
serviceAccountName: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }}
107110
terminationGracePeriodSeconds: 10
108111
volumes:
112+
{{- if .Values.manager.extraVolumes }}
113+
{{- toYaml .Values.manager.extraVolumes | nindent 8 }}
114+
{{- end }}
109115
{{- if and .Values.certManager.enable .Values.metrics.enable }}
110116
- name: metrics-certs
111117
secret:

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 10 }}
83+
{{- else }}
84+
[]
85+
{{- end }}
8186
securityContext:
8287
{{- if .Values.manager.podSecurityContext }}
8388
{{- toYaml .Values.manager.podSecurityContext | nindent 8 }}
@@ -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 8 }}
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
@@ -87,6 +87,9 @@ spec:
8787
{}
8888
{{- end }}
8989
volumeMounts:
90+
{{- if .Values.manager.extraVolumeMounts }}
91+
{{- toYaml .Values.manager.extraVolumeMounts | nindent 10 }}
92+
{{- end }}
9093
{{- if and .Values.certManager.enable .Values.metrics.enable }}
9194
- mountPath: /tmp/k8s-metrics-server/metrics-certs
9295
name: metrics-certs
@@ -106,6 +109,9 @@ spec:
106109
serviceAccountName: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }}
107110
terminationGracePeriodSeconds: 10
108111
volumes:
112+
{{- if .Values.manager.extraVolumes }}
113+
{{- toYaml .Values.manager.extraVolumes | nindent 8 }}
114+
{{- end }}
109115
{{- if and .Values.certManager.enable .Values.metrics.enable }}
110116
- name: metrics-certs
111117
secret:

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
@@ -737,17 +737,78 @@ func (t *HelmTemplater) templateSecurityContexts(yamlContent string) string {
737737
return yamlContent
738738
}
739739

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

747-
// templateVolumes converts volumes sections to keep them as-is since they're webhook-specific
745+
// templateVolumes injects .Values.manager.extraVolumes (append or replace empty list).
748746
func (t *HelmTemplater) templateVolumes(yamlContent string) string {
749-
// For webhook volumes, we keep them as-is since they're required for webhook functionality
750-
// They will be conditionally included based on webhook configuration
747+
return t.appendToListFromValues(yamlContent, "volumes:", ".Values.manager.extraVolumes")
748+
}
749+
750+
// appendToListFromValues finds "key:" or "key: []", and either appends values path to an existing list
751+
// or replaces "key: []" with a template that outputs the values path when set. Idempotent if already present.
752+
func (t *HelmTemplater) appendToListFromValues(yamlContent string, keyColon string, valuesPath string) string {
753+
if !strings.Contains(yamlContent, keyColon) {
754+
return yamlContent
755+
}
756+
if strings.Contains(yamlContent, valuesPath) {
757+
return yamlContent
758+
}
759+
760+
lines := strings.Split(yamlContent, "\n")
761+
keyEmpty := keyColon + " []"
762+
763+
for i := range lines {
764+
trimmed := strings.TrimSpace(lines[i])
765+
indentStr, indentLen := leadingWhitespace(lines[i])
766+
childIndent := indentStr + " "
767+
childIndentWidth := strconv.Itoa(len(childIndent))
768+
769+
// Case 1: "key: []" (Kustomize default) — replace with template so extra* values are used
770+
if trimmed == keyEmpty {
771+
block := []string{
772+
indentStr + keyColon,
773+
childIndent + "{{- if " + valuesPath + " }}",
774+
childIndent + "{{- toYaml " + valuesPath + " | nindent " + childIndentWidth + " }}",
775+
childIndent + "{{- else }}",
776+
childIndent + "[]",
777+
childIndent + "{{- end }}",
778+
}
779+
newLines := append([]string{}, lines[:i]...)
780+
newLines = append(newLines, block...)
781+
newLines = append(newLines, lines[i+1:]...)
782+
return strings.Join(newLines, "\n")
783+
}
784+
785+
// Case 2: "key:" with multi-line list — append values path after list
786+
if trimmed != keyColon {
787+
continue
788+
}
789+
790+
end := i + 1
791+
for ; end < len(lines); end++ {
792+
tLine := strings.TrimSpace(lines[end])
793+
if tLine == "" {
794+
break
795+
}
796+
lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
797+
if lineIndent <= indentLen {
798+
break
799+
}
800+
}
801+
802+
block := []string{
803+
childIndent + "{{- if " + valuesPath + " }}",
804+
childIndent + "{{- toYaml " + valuesPath + " | nindent " + childIndentWidth + " }}",
805+
childIndent + "{{- end }}",
806+
}
807+
newLines := append([]string{}, lines[:end]...)
808+
newLines = append(newLines, block...)
809+
newLines = append(newLines, lines[end:]...)
810+
return strings.Join(newLines, "\n")
811+
}
751812
return yamlContent
752813
}
753814

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
@@ -2100,6 +2100,65 @@ spec:
21002100
Expect(result).To(ContainSubstring("name: controller-test"))
21012101
})
21022102

2103+
It("should append extraVolumes and extraVolumeMounts when lists are present", func() {
2104+
deployment := &unstructured.Unstructured{}
2105+
deployment.SetAPIVersion("apps/v1")
2106+
deployment.SetKind("Deployment")
2107+
deployment.SetName("test-project-controller-manager")
2108+
2109+
content := `apiVersion: apps/v1
2110+
kind: Deployment
2111+
metadata:
2112+
name: test-project-controller-manager
2113+
spec:
2114+
template:
2115+
spec:
2116+
volumes:
2117+
- name: webhook-certs
2118+
secret:
2119+
secretName: webhook-server-cert
2120+
containers:
2121+
- name: manager
2122+
image: controller:latest
2123+
volumeMounts:
2124+
- name: webhook-certs
2125+
mountPath: /tmp/k8s-webhook-server/serving-certs
2126+
readOnly: true
2127+
`
2128+
2129+
result := templater.ApplyHelmSubstitutions(content, deployment)
2130+
2131+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumes"))
2132+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumeMounts"))
2133+
})
2134+
2135+
It("should inject extraVolumes/extraVolumeMounts when Kustomize has volumeMounts: [] and volumes: []", func() {
2136+
deployment := &unstructured.Unstructured{}
2137+
deployment.SetAPIVersion("apps/v1")
2138+
deployment.SetKind("Deployment")
2139+
deployment.SetName("test-project-controller-manager")
2140+
2141+
// Default Kustomize output: single-line empty lists (no webhook/metrics patches)
2142+
content := `apiVersion: apps/v1
2143+
kind: Deployment
2144+
metadata:
2145+
name: test-project-controller-manager
2146+
spec:
2147+
template:
2148+
spec:
2149+
containers:
2150+
- name: manager
2151+
image: controller:latest
2152+
volumeMounts: []
2153+
volumes: []`
2154+
2155+
result := templater.ApplyHelmSubstitutions(content, deployment)
2156+
2157+
// Plugin must inject template so values work without manual manager.yaml edits
2158+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumes"))
2159+
Expect(result).To(ContainSubstring(".Values.manager.extraVolumeMounts"))
2160+
})
2161+
21032162
It("should fall back to 'manager' when default-container annotation is missing", func() {
21042163
deployment := &unstructured.Unstructured{}
21052164
deployment.SetAPIVersion("apps/v1")

0 commit comments

Comments
 (0)