Skip to content

Commit 0bff54f

Browse files
authored
fix: cilium webhook with templated values (#1437)
**What problem does this PR solve?**: Its possible that users may want to use a templated values with their custom Cilium ConfigMap to enable kube-proxy replacement feature. This fixes the webhook to support that. **Which issue(s) this PR fixes**: Fixes # **How Has This Been Tested?**: <!-- Please describe the tests that you ran to verify your changes. Provide output from the tests and any manual steps needed to replicate the tests. --> **Special notes for your reviewer**: <!-- Use this to provide any additional information to the reviewers. This may include: - Best way to review the PR. - Where the author wants the most review attention on. - etc. -->
1 parent 6a05524 commit 0bff54f

File tree

2 files changed

+202
-2
lines changed

2 files changed

+202
-2
lines changed

pkg/webhook/cluster/cilium_configuration_validator.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
package cluster
55

66
import (
7+
"bytes"
78
"context"
89
"fmt"
910
"net/http"
11+
"strings"
12+
"text/template"
1013

1114
v1 "k8s.io/api/admission/v1"
1215
corev1 "k8s.io/api/core/v1"
@@ -117,6 +120,33 @@ func hasSkipAnnotation(cluster *clusterv1.Cluster) bool {
117120
return ok && val == "true"
118121
}
119122

123+
// templateValues applies Go template expansion to the values string using only ControlPlaneEndpoint.
124+
func templateValues(cluster *clusterv1.Cluster, text string) (string, error) {
125+
funcMap := template.FuncMap{
126+
"trimPrefix": strings.TrimPrefix,
127+
}
128+
valuesTemplate, err := template.New("").Funcs(funcMap).Parse(text)
129+
if err != nil {
130+
return "", fmt.Errorf("failed to parse template: %w", err)
131+
}
132+
133+
type input struct {
134+
ControlPlaneEndpoint clusterv1.APIEndpoint
135+
}
136+
137+
templateInput := input{
138+
ControlPlaneEndpoint: cluster.Spec.ControlPlaneEndpoint,
139+
}
140+
141+
var b bytes.Buffer
142+
err = valuesTemplate.Execute(&b, templateInput)
143+
if err != nil {
144+
return "", fmt.Errorf("failed templating values: %w", err)
145+
}
146+
147+
return b.String(), nil
148+
}
149+
120150
type ciliumValues struct {
121151
KubeProxyReplacement bool `json:"kubeProxyReplacement"`
122152
}
@@ -156,9 +186,20 @@ func getCiliumValues(
156186
return nil, nil
157187
}
158188

159-
// Unmarshal the YAML
189+
// Apply templating to the values
190+
templatedValues, err := templateValues(cluster, valuesYAML)
191+
if err != nil {
192+
return nil, fmt.Errorf(
193+
"failed to template Cilium values from ConfigMap %s/%s: %w",
194+
configMapNamespace,
195+
configMapName,
196+
err,
197+
)
198+
}
199+
200+
// Unmarshal the templated YAML
160201
values := &ciliumValues{}
161-
if err := yaml.Unmarshal([]byte(valuesYAML), values); err != nil {
202+
if err := yaml.Unmarshal([]byte(templatedValues), values); err != nil {
162203
return nil, fmt.Errorf(
163204
"failed to unmarshal Cilium values from ConfigMap %s/%s: %w",
164205
configMapNamespace,

pkg/webhook/cluster/cilium_configuration_validator_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,150 @@ kubeProxyReplacement: true
420420
resp := validator.validate(context.Background(), req)
421421
Expect(resp.Allowed).To(BeTrue())
422422
})
423+
424+
It("should allow when ConfigMap uses templated values with ControlPlaneEndpoint", func() {
425+
cni := &v1alpha1.CNI{
426+
Provider: v1alpha1.CNIProviderCilium,
427+
AddonConfig: v1alpha1.AddonConfig{
428+
Values: &v1alpha1.AddonValues{
429+
SourceRef: &v1alpha1.ValuesReference{
430+
Kind: "ConfigMap",
431+
Name: "cilium-values",
432+
},
433+
},
434+
},
435+
}
436+
cluster := createTestClusterWithControlPlaneEndpoint(
437+
"test-cluster",
438+
"test-namespace",
439+
v1alpha1.KubeProxyModeDisabled,
440+
cni,
441+
"192.168.1.100",
442+
6443,
443+
)
444+
req := createAdmissionRequest(cluster)
445+
446+
// Create ConfigMap with templated values where kubeProxyReplacement depends on templating
447+
// Without templating, this would fail because {{ true }} is not a valid boolean
448+
configMap := &corev1.ConfigMap{
449+
ObjectMeta: metav1.ObjectMeta{
450+
Name: "cilium-values",
451+
Namespace: "test-namespace",
452+
},
453+
Data: map[string]string{
454+
"values.yaml": `
455+
ipam:
456+
mode: kubernetes
457+
# This will only work if templating is applied
458+
kubeProxyReplacement: true
459+
k8sServiceHost: "{{ trimPrefix .ControlPlaneEndpoint.Host "https://" }}"
460+
k8sServicePort: "{{ .ControlPlaneEndpoint.Port }}"
461+
`,
462+
},
463+
}
464+
465+
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build()
466+
validator = NewAdvancedCiliumConfigurationValidator(client, decoder)
467+
468+
resp := validator.validate(context.Background(), req)
469+
Expect(resp.Allowed).To(BeTrue())
470+
})
471+
472+
It("should deny when ConfigMap uses templated values that would be false after templating", func() {
473+
cni := &v1alpha1.CNI{
474+
Provider: v1alpha1.CNIProviderCilium,
475+
AddonConfig: v1alpha1.AddonConfig{
476+
Values: &v1alpha1.AddonValues{
477+
SourceRef: &v1alpha1.ValuesReference{
478+
Kind: "ConfigMap",
479+
Name: "cilium-values",
480+
},
481+
},
482+
},
483+
}
484+
cluster := createTestClusterWithControlPlaneEndpoint(
485+
"test-cluster",
486+
"test-namespace",
487+
v1alpha1.KubeProxyModeDisabled,
488+
cni,
489+
"192.168.1.100",
490+
6443,
491+
)
492+
req := createAdmissionRequest(cluster)
493+
494+
// Create ConfigMap with templated values that evaluate to false
495+
configMap := &corev1.ConfigMap{
496+
ObjectMeta: metav1.ObjectMeta{
497+
Name: "cilium-values",
498+
Namespace: "test-namespace",
499+
},
500+
Data: map[string]string{
501+
"values.yaml": `
502+
ipam:
503+
mode: kubernetes
504+
kubeProxyReplacement: {{ false }}
505+
k8sServiceHost: "{{ trimPrefix .ControlPlaneEndpoint.Host "https://" }}"
506+
k8sServicePort: "{{ .ControlPlaneEndpoint.Port }}"
507+
`,
508+
},
509+
}
510+
511+
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build()
512+
validator = NewAdvancedCiliumConfigurationValidator(client, decoder)
513+
514+
resp := validator.validate(context.Background(), req)
515+
Expect(resp.Allowed).To(BeFalse())
516+
expectedMessage := "kube-proxy is disabled, but Cilium ConfigMap test-namespace/cilium-values does not have 'kubeProxyReplacement' enabled"
517+
Expect(resp.Result.Message).To(Equal(expectedMessage))
518+
})
519+
520+
It("should error out when ConfigMap uses unknown templated values", func() {
521+
cni := &v1alpha1.CNI{
522+
Provider: v1alpha1.CNIProviderCilium,
523+
AddonConfig: v1alpha1.AddonConfig{
524+
Values: &v1alpha1.AddonValues{
525+
SourceRef: &v1alpha1.ValuesReference{
526+
Kind: "ConfigMap",
527+
Name: "cilium-values",
528+
},
529+
},
530+
},
531+
}
532+
cluster := createTestClusterWithControlPlaneEndpoint(
533+
"test-cluster",
534+
"test-namespace",
535+
v1alpha1.KubeProxyModeDisabled,
536+
cni,
537+
"192.168.1.100",
538+
6443,
539+
)
540+
req := createAdmissionRequest(cluster)
541+
542+
// Create ConfigMap with unknown template field
543+
configMap := &corev1.ConfigMap{
544+
ObjectMeta: metav1.ObjectMeta{
545+
Name: "cilium-values",
546+
Namespace: "test-namespace",
547+
},
548+
Data: map[string]string{
549+
"values.yaml": `
550+
ipam:
551+
mode: kubernetes
552+
kubeProxyReplacement: true
553+
k8sServiceHost: "{{ .UnknownField.Host }}"
554+
k8sServicePort: "{{ .ControlPlaneEndpoint.Port }}"
555+
`,
556+
},
557+
}
558+
559+
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build()
560+
validator = NewAdvancedCiliumConfigurationValidator(client, decoder)
561+
562+
resp := validator.validate(context.Background(), req)
563+
Expect(resp.Allowed).To(BeFalse())
564+
Expect(resp.Result.Message).To(ContainSubstring("failed templating values"))
565+
Expect(resp.Result.Message).To(ContainSubstring("can't evaluate field UnknownField"))
566+
})
423567
})
424568
})
425569

@@ -467,6 +611,21 @@ func createTestCluster(
467611
}
468612
}
469613

614+
func createTestClusterWithControlPlaneEndpoint(
615+
name, namespace string,
616+
kubeProxyMode v1alpha1.KubeProxyMode,
617+
cni *v1alpha1.CNI,
618+
host string,
619+
port int32,
620+
) *clusterv1.Cluster {
621+
cluster := createTestCluster(name, namespace, kubeProxyMode, cni)
622+
cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
623+
Host: host,
624+
Port: port,
625+
}
626+
return cluster
627+
}
628+
470629
func createAdmissionRequest(cluster *clusterv1.Cluster) admission.Request {
471630
objRaw, err := json.Marshal(cluster)
472631
Expect(err).NotTo(HaveOccurred())

0 commit comments

Comments
 (0)