Skip to content

Commit c5d400e

Browse files
robinlioretRobin LIORET
authored andcommitted
feat: helm v2-alpha: add nodeSelector, toleration and affinity to the chart
1 parent b5a66b0 commit c5d400e

File tree

5 files changed

+274
-33
lines changed

5 files changed

+274
-33
lines changed

pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/manager/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ spec:
9696
# operator: In
9797
# values:
9898
# - linux
99+
# TODO(user): Uncomment the following code to configure the nodeSelector.
100+
# nodeSelector:
101+
# kubernetes.io/os: linux
102+
# TODO(user): Uncomment the following code to configure the tolerations.
103+
# tolerations:
104+
# - key: "key1"
105+
# operator: "Exists"
106+
# effect: "NoSchedule"
99107
securityContext:
100108
# Projects are configured by default to adhere to the "restricted" Pod Security Standards.
101109
# This ensures that deployments meet the highest security requirements for Kubernetes.

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ func (c *ChartConverter) ExtractDeploymentConfig() map[string]interface{} {
105105
}
106106

107107
extractPodSecurityContext(specMap, config)
108+
extractPodNodeSelector(specMap, config)
109+
extractPodTolerations(specMap, config)
110+
extractPodAffinity(specMap, config)
108111

109112
container := firstManagerContainer(specMap)
110113
if container == nil {
@@ -149,6 +152,48 @@ func extractPodSecurityContext(specMap map[string]interface{}, config map[string
149152
config["podSecurityContext"] = podSecurityContext
150153
}
151154

155+
func extractPodNodeSelector(specMap map[string]interface{}, config map[string]interface{}) {
156+
raw, found, err := unstructured.NestedFieldNoCopy(specMap, "nodeSelector")
157+
if !found || err != nil {
158+
return
159+
}
160+
161+
result, ok := raw.(map[string]interface{})
162+
if !ok || len(result) == 0 {
163+
return
164+
}
165+
166+
config["podNodeSelector"] = result
167+
}
168+
169+
func extractPodTolerations(specMap map[string]interface{}, config map[string]interface{}) {
170+
raw, found, err := unstructured.NestedFieldNoCopy(specMap, "tolerations")
171+
if !found || err != nil {
172+
return
173+
}
174+
175+
result, ok := raw.([]interface{})
176+
if !ok || len(result) == 0 {
177+
return
178+
}
179+
180+
config["podTolerations"] = result
181+
}
182+
183+
func extractPodAffinity(specMap map[string]interface{}, config map[string]interface{}) {
184+
raw, found, err := unstructured.NestedFieldNoCopy(specMap, "affinity")
185+
if !found || err != nil {
186+
return
187+
}
188+
189+
result, ok := raw.(map[string]interface{})
190+
if !ok || len(result) == 0 {
191+
return
192+
}
193+
194+
config["podAffinity"] = result
195+
}
196+
152197
func firstManagerContainer(specMap map[string]interface{}) map[string]interface{} {
153198
containers, found, err := unstructured.NestedFieldNoCopy(specMap, "containers")
154199
if !found || err != nil {

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,24 @@ func (t *HelmTemplater) templateDeploymentFields(yamlContent string) string {
190190
yamlContent = t.templateVolumeMounts(yamlContent)
191191
yamlContent = t.templateVolumes(yamlContent)
192192
yamlContent = t.templateControllerManagerArgs(yamlContent)
193+
yamlContent = t.templateBasicWithStatement(
194+
yamlContent,
195+
"nodeSelector",
196+
"spec.template.spec",
197+
".Values.manager.nodeSelector",
198+
)
199+
yamlContent = t.templateBasicWithStatement(
200+
yamlContent,
201+
"affinity",
202+
"spec.template.spec",
203+
".Values.manager.affinity",
204+
)
205+
yamlContent = t.templateBasicWithStatement(
206+
yamlContent,
207+
"tolerations",
208+
"spec.template.spec",
209+
".Values.manager.tolerations",
210+
)
193211

194212
return yamlContent
195213
}
@@ -620,6 +638,88 @@ func (t *HelmTemplater) templateImageReference(yamlContent string) string {
620638
return yamlContent
621639
}
622640

641+
func (t *HelmTemplater) templateBasicWithStatement(
642+
yamlContent string,
643+
key string,
644+
parentKey string,
645+
valuePath string,
646+
) string {
647+
lines := strings.Split(yamlContent, "\n")
648+
yamlKey := fmt.Sprintf("%s:", key)
649+
650+
var start, end int
651+
var indentLen int
652+
if !strings.Contains(yamlContent, yamlKey) {
653+
// Find parent block start if the key is missing
654+
pKeyParts := strings.Split(parentKey, ".")
655+
pKeyIdx := 0
656+
pKeyInit := false
657+
currIndent := 0
658+
for i := 0; i < len(lines); i++ {
659+
_, lineIndent := leadingWhitespace(lines[i])
660+
if pKeyInit && lineIndent <= currIndent {
661+
return yamlContent
662+
}
663+
if !strings.HasPrefix(strings.TrimSpace(lines[i]), pKeyParts[pKeyIdx]) {
664+
continue
665+
}
666+
667+
// Parent key part found
668+
pKeyIdx++
669+
pKeyInit = true
670+
if pKeyIdx >= len(pKeyParts) {
671+
start = i + 1
672+
end = start
673+
break
674+
}
675+
}
676+
_, indentLen = leadingWhitespace(lines[start])
677+
} else {
678+
// Find the existing block
679+
for i := 0; i < len(lines); i++ {
680+
if !strings.HasPrefix(strings.TrimSpace(lines[i]), key) {
681+
continue
682+
}
683+
start = i
684+
end = i + 1
685+
trimmed := strings.TrimSpace(lines[i])
686+
if len(trimmed) == len(yamlKey) {
687+
_, indentLenSearch := leadingWhitespace(lines[i])
688+
for j := end; j < len(lines); j++ {
689+
_, indentLenLine := leadingWhitespace(lines[j])
690+
if indentLenLine <= indentLenSearch {
691+
end = j
692+
break
693+
}
694+
}
695+
}
696+
}
697+
_, indentLen = leadingWhitespace(lines[start])
698+
}
699+
700+
indentStr := strings.Repeat(" ", indentLen)
701+
702+
var builder strings.Builder
703+
builder.WriteString(indentStr)
704+
builder.WriteString("{{- with ")
705+
builder.WriteString(valuePath)
706+
builder.WriteString(" }}\n")
707+
builder.WriteString(indentStr)
708+
builder.WriteString(yamlKey)
709+
builder.WriteString(" {{ toYaml . | nindent ")
710+
builder.WriteString(strconv.Itoa(indentLen + 4))
711+
builder.WriteString(" }}\n")
712+
builder.WriteString(indentStr)
713+
builder.WriteString("{{- end }}\n")
714+
715+
newBlock := strings.TrimRight(builder.String(), "\n")
716+
717+
newLines := append([]string{}, lines[:start]...)
718+
newLines = append(newLines, strings.Split(newBlock, "\n")...)
719+
newLines = append(newLines, lines[end:]...)
720+
return strings.Join(newLines, "\n")
721+
}
722+
623723
// makeWebhookAnnotationsConditional makes only cert-manager annotations conditional, not the entire webhook
624724
func (t *HelmTemplater) makeWebhookAnnotationsConditional(yamlContent string) string {
625725
// Find cert-manager.io/inject-ca-from annotation and make it conditional

pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic.go

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,7 @@ func (f *HelmValuesBasic) addDeploymentConfig(buf *bytes.Buffer) {
210210
buf.WriteString(" # Environment variables\n")
211211
buf.WriteString(" env:\n")
212212
if envYaml, err := yaml.Marshal(env); err == nil {
213-
// Indent the YAML properly
214-
lines := bytes.Split(envYaml, []byte("\n"))
215-
for _, line := range lines {
216-
if len(line) > 0 {
217-
buf.WriteString(" ")
218-
buf.Write(line)
219-
buf.WriteString("\n")
220-
}
221-
}
213+
f.IndentYamlProperly(buf, envYaml)
222214
} else {
223215
buf.WriteString(" []\n")
224216
}
@@ -233,14 +225,7 @@ func (f *HelmValuesBasic) addDeploymentConfig(buf *bytes.Buffer) {
233225
buf.WriteString(" # Pod-level security settings\n")
234226
buf.WriteString(" podSecurityContext:\n")
235227
if secYaml, err := yaml.Marshal(podSecCtx); err == nil {
236-
lines := bytes.Split(secYaml, []byte("\n"))
237-
for _, line := range lines {
238-
if len(line) > 0 {
239-
buf.WriteString(" ")
240-
buf.Write(line)
241-
buf.WriteString("\n")
242-
}
243-
}
228+
f.IndentYamlProperly(buf, secYaml)
244229
}
245230
buf.WriteString("\n")
246231
} else {
@@ -252,14 +237,7 @@ func (f *HelmValuesBasic) addDeploymentConfig(buf *bytes.Buffer) {
252237
buf.WriteString(" # Container-level security settings\n")
253238
buf.WriteString(" securityContext:\n")
254239
if secYaml, err := yaml.Marshal(secCtx); err == nil {
255-
lines := bytes.Split(secYaml, []byte("\n"))
256-
for _, line := range lines {
257-
if len(line) > 0 {
258-
buf.WriteString(" ")
259-
buf.Write(line)
260-
buf.WriteString("\n")
261-
}
262-
}
240+
f.IndentYamlProperly(buf, secYaml)
263241
}
264242
buf.WriteString("\n")
265243
} else {
@@ -271,19 +249,59 @@ func (f *HelmValuesBasic) addDeploymentConfig(buf *bytes.Buffer) {
271249
buf.WriteString(" # Resource limits and requests\n")
272250
buf.WriteString(" resources:\n")
273251
if resYaml, err := yaml.Marshal(resources); err == nil {
274-
lines := bytes.Split(resYaml, []byte("\n"))
275-
for _, line := range lines {
276-
if len(line) > 0 {
277-
buf.WriteString(" ")
278-
buf.Write(line)
279-
buf.WriteString("\n")
280-
}
281-
}
252+
f.IndentYamlProperly(buf, resYaml)
282253
}
283254
buf.WriteString("\n")
284255
} else {
285256
f.addDefaultResources(buf)
286257
}
258+
259+
buf.WriteString(" # Pod's affinity\n")
260+
if affinity, exists := f.DeploymentConfig["podAffinity"]; exists && affinity != nil {
261+
buf.WriteString(" affinity:\n")
262+
if affYaml, err := yaml.Marshal(affinity); err == nil {
263+
f.IndentYamlProperly(buf, affYaml)
264+
}
265+
buf.WriteString("\n")
266+
} else {
267+
buf.WriteString(" affinity: {}\n")
268+
buf.WriteString("\n")
269+
}
270+
271+
buf.WriteString(" # Pod's node selector\n")
272+
if nodeSelector, exists := f.DeploymentConfig["podNodeSelector"]; exists && nodeSelector != nil {
273+
buf.WriteString(" nodeSelector:\n")
274+
if nodYaml, err := yaml.Marshal(nodeSelector); err == nil {
275+
f.IndentYamlProperly(buf, nodYaml)
276+
}
277+
buf.WriteString("\n")
278+
} else {
279+
buf.WriteString(" nodeSelector: {}\n")
280+
buf.WriteString("\n")
281+
}
282+
283+
buf.WriteString(" # Pod's tolerations\n")
284+
if tolerations, exists := f.DeploymentConfig["podTolerations"]; exists && tolerations != nil {
285+
buf.WriteString(" tolerations:\n")
286+
if tolYaml, err := yaml.Marshal(tolerations); err == nil {
287+
f.IndentYamlProperly(buf, tolYaml)
288+
}
289+
buf.WriteString("\n")
290+
} else {
291+
buf.WriteString(" tolerations: []\n")
292+
buf.WriteString("\n")
293+
}
294+
}
295+
296+
func (f *HelmValuesBasic) IndentYamlProperly(buf *bytes.Buffer, envYaml []byte) {
297+
lines := bytes.Split(envYaml, []byte("\n"))
298+
for _, line := range lines {
299+
if len(line) > 0 {
300+
buf.WriteString(" ")
301+
buf.Write(line)
302+
buf.WriteString("\n")
303+
}
304+
}
287305
}
288306

289307
// addDefaultDeploymentSections adds default sections when no deployment config is available

pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,76 @@ var _ = Describe("HelmValuesBasic", func() {
159159
Expect(content).To(ContainSubstring("resources:"))
160160
Expect(content).To(ContainSubstring("cpu: 100m"))
161161
Expect(content).To(ContainSubstring("memory: 128Mi"))
162+
Expect(content).To(ContainSubstring("affinity: {}"))
163+
Expect(content).To(ContainSubstring("nodeSelector: {}"))
164+
Expect(content).To(ContainSubstring("tolerations: []"))
165+
})
166+
})
167+
168+
Context("with nodeSelector, affinity and tolerations configuration", func() {
169+
BeforeEach(func() {
170+
deploymentConfig := map[string]interface{}{
171+
"podNodeSelector": map[string]string{
172+
"kubernetes.io/os": "linux",
173+
},
174+
"podTolerations": []map[string]string{
175+
{
176+
"key": "key1",
177+
"operator": "Equal",
178+
"effect": "NoSchedule",
179+
},
180+
},
181+
"podAffinity": map[string]interface{}{
182+
"nodeAffinity": map[string]interface{}{
183+
"requiredDuringSchedulingIgnoredDuringExecution": map[string]interface{}{
184+
"nodeSelectorTerms": []interface{}{
185+
map[string]interface{}{
186+
"matchExpressions": []interface{}{
187+
map[string]interface{}{
188+
"key": "topology.kubernetes.io/zone",
189+
"operator": "In",
190+
"values": []string{"antarctica-east1", "antarctica-east2"},
191+
},
192+
},
193+
},
194+
},
195+
},
196+
},
197+
},
198+
}
199+
200+
valuesTemplate = &HelmValuesBasic{
201+
HasWebhooks: false,
202+
DeploymentConfig: deploymentConfig,
203+
}
204+
valuesTemplate.InjectProjectName("test-project")
205+
err := valuesTemplate.SetTemplateDefaults()
206+
Expect(err).NotTo(HaveOccurred())
207+
})
208+
209+
It("should include default values", func() {
210+
content := valuesTemplate.GetBody()
211+
Expect(content).To(ContainSubstring(` # Pod's node selector
212+
nodeSelector:
213+
kubernetes.io/os: linux`))
214+
215+
Expect(content).To(ContainSubstring(` # Pod's tolerations
216+
tolerations:
217+
- effect: NoSchedule
218+
key: key1
219+
operator: Equal`))
220+
221+
Expect(content).To(ContainSubstring(` # Pod's affinity
222+
affinity:
223+
nodeAffinity:
224+
requiredDuringSchedulingIgnoredDuringExecution:
225+
nodeSelectorTerms:
226+
- matchExpressions:
227+
- key: topology.kubernetes.io/zone
228+
operator: In
229+
values:
230+
- antarctica-east1
231+
- antarctica-east2`))
162232
})
163233
})
164234

0 commit comments

Comments
 (0)