From 8940a3365fd6ae3b01de8bb7d056389dc1b75aba Mon Sep 17 00:00:00 2001 From: Dimitri Koshkin Date: Fri, 31 Oct 2025 13:47:28 -0700 Subject: [PATCH 1/4] feat: ensure Cilium kube-proxy is enabled when needed --- .../cluster/cilium_configuration_validator.go | 166 ++++++++ .../cilium_configuration_validator_test.go | 362 ++++++++++++++++++ pkg/webhook/cluster/validator.go | 1 + 3 files changed, 529 insertions(+) create mode 100644 pkg/webhook/cluster/cilium_configuration_validator.go create mode 100644 pkg/webhook/cluster/cilium_configuration_validator_test.go diff --git a/pkg/webhook/cluster/cilium_configuration_validator.go b/pkg/webhook/cluster/cilium_configuration_validator.go new file mode 100644 index 000000000..4f5e6c5ee --- /dev/null +++ b/pkg/webhook/cluster/cilium_configuration_validator.go @@ -0,0 +1,166 @@ +// Copyright 2025 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "context" + "fmt" + "net/http" + + v1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/yaml" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables" +) + +type advancedCiliumConfigurationValidator struct { + client ctrlclient.Client + decoder admission.Decoder +} + +func NewAdvancedCiliumConfigurationValidator( + client ctrlclient.Client, decoder admission.Decoder, +) *advancedCiliumConfigurationValidator { + return &advancedCiliumConfigurationValidator{ + client: client, + decoder: decoder, + } +} + +func (a *advancedCiliumConfigurationValidator) Validator() admission.HandlerFunc { + return a.validate +} + +func (a *advancedCiliumConfigurationValidator) validate( + ctx context.Context, + req admission.Request, +) admission.Response { + if req.Operation == v1.Delete { + return admission.Allowed("") + } + + cluster := &clusterv1.Cluster{} + err := a.decoder.Decode(req, cluster) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if cluster.Spec.Topology == nil { + return admission.Allowed("") + } + + clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables) + if err != nil { + return admission.Denied( + fmt.Errorf("failed to unmarshal cluster topology variable %q: %w", + v1alpha1.ClusterConfigVariableName, + err).Error(), + ) + } + + if clusterConfig == nil { + return admission.Allowed("") + } + // Skip validation if kube-proxy is not disabled. + if clusterConfig.KubeProxy == nil || clusterConfig.KubeProxy.Mode != v1alpha1.KubeProxyModeDisabled { + return admission.Allowed("") + } + // Skip validation if not using Cilium as CNI provider. + if clusterConfig.Addons == nil || clusterConfig.Addons.CNI == nil || + clusterConfig.Addons.CNI.Provider != v1alpha1.CNIProviderCilium { + return admission.Allowed("") + } + // Skip validation if no custom values are specified. + if clusterConfig.Addons.CNI.Values == nil || clusterConfig.Addons.CNI.Values.SourceRef == nil { + return admission.Allowed("") + } + + // Get the Cilium values from the ConfigMap. + ciliumValues, err := getCiliumValues(ctx, a.client, cluster, clusterConfig.Addons.CNI) + if err != nil { + return admission.Denied(err.Error()) + } + // Skip validation if no values found. + if ciliumValues == nil { + return admission.Allowed("") + } + + // Validate that kubeProxyReplacement is enabled + if err := validateCiliumKubeProxyReplacement(ciliumValues, cluster.Namespace, clusterConfig.Addons.CNI.Values.SourceRef.Name); err != nil { + return admission.Denied(err.Error()) + } + + return admission.Allowed("") +} + +type ciliumValues struct { + KubeProxyReplacement bool `json:"kubeProxyReplacement"` +} + +// getCiliumValues retrieves and parses the Cilium values from a ConfigMap. +// Returns nil if ConfigMap doesn't exist or values.yaml key is missing. +// Returns error only for actual failures (permission errors, invalid YAML, etc.). +func getCiliumValues( + ctx context.Context, + client ctrlclient.Client, + cluster *clusterv1.Cluster, + cni *v1alpha1.CNI, +) (*ciliumValues, error) { + configMapName := cni.Values.SourceRef.Name + configMapNamespace := cluster.Namespace + + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: configMapNamespace, + Name: configMapName, + }, + } + + err := client.Get(ctx, ctrlclient.ObjectKeyFromObject(configMap), configMap) + if err != nil { + // ConfigMap doesn't exist - this is OK, return nil + if err = ctrlclient.IgnoreNotFound(err); err != nil { + return nil, fmt.Errorf("failed to get ConfigMap %s/%s: %w", configMapNamespace, configMapName, err) + } + return nil, nil + } + + // Look for values.yaml key in the ConfigMap + valuesYAML, ok := configMap.Data["values.yaml"] + if !ok { + // values.yaml key doesn't exist - this is OK, return nil + return nil, nil + } + + // Unmarshal the YAML + values := &ciliumValues{} + if err := yaml.Unmarshal([]byte(valuesYAML), values); err != nil { + return nil, fmt.Errorf( + "failed to unmarshal Cilium values from ConfigMap %s/%s: %w", + configMapNamespace, + configMapName, + err, + ) + } + + return values, nil +} + +func validateCiliumKubeProxyReplacement(values *ciliumValues, namespace, configMapName string) error { + if !values.KubeProxyReplacement { + return fmt.Errorf( + "kube-proxy is disabled, but Cilium ConfigMap %s/%s does not have 'kubeProxyReplacement' enabled", + namespace, + configMapName, + ) + } + + return nil +} diff --git a/pkg/webhook/cluster/cilium_configuration_validator_test.go b/pkg/webhook/cluster/cilium_configuration_validator_test.go new file mode 100644 index 000000000..c018d29c4 --- /dev/null +++ b/pkg/webhook/cluster/cilium_configuration_validator_test.go @@ -0,0 +1,362 @@ +// Copyright 2025 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "context" + "encoding/json" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables" +) + +func TestCiliumValidator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cilium Validator Suite") +} + +var _ = Describe("AdvancedCiliumConfigurationValidator", func() { + var ( + validator *advancedCiliumConfigurationValidator + decoder admission.Decoder + scheme *runtime.Scheme + ) + + BeforeEach(func() { + scheme = runtime.NewScheme() + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) + Expect(v1alpha1.AddToScheme(scheme)).To(Succeed()) + + decoder = admission.NewDecoder(scheme) + }) + + Context("when operation is DELETE", func() { + It("should allow deletion", func() { + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, nil) + req := createAdmissionRequest(cluster) + req.Operation = admissionv1.Delete + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + }) + + Context("when cluster has no topology", func() { + It("should allow the cluster", func() { + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "test-namespace", + }, + } + req := createAdmissionRequest(cluster) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + }) + + Context("when kube-proxy is not disabled", func() { + It("should allow the cluster", func() { + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeIPTables, nil) + req := createAdmissionRequest(cluster) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + }) + + Context("when kube-proxy is disabled", func() { + It("should allow when CNI is not configured", func() { + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, nil) + req := createAdmissionRequest(cluster) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + + It("should allow when CNI provider is not Cilium", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCalico, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + + It("should allow when Cilium is configured with default values", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + + It("should allow when ConfigMap does not exist", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + + It("should allow when ConfigMap does not have values.yaml key", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + // Create ConfigMap without values.yaml key + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "some-key": "some-value", + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + + It("should deny when ConfigMap does not have kubeProxyReplacement enabled", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + // Create ConfigMap without kubeProxyReplacement + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "values.yaml": ` +ipam: + mode: kubernetes +`, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeFalse()) + Expect(resp.Result.Message).To(ContainSubstring("does not have 'kubeProxyReplacement' enabled")) + }) + + It("should deny when kubeProxyReplacement is set to false", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + // Create ConfigMap with kubeProxyReplacement set to false + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "values.yaml": ` +ipam: + mode: kubernetes +kubeProxyReplacement: false +`, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeFalse()) + Expect(resp.Result.Message).To(ContainSubstring("does not have 'kubeProxyReplacement' enabled")) + }) + + It("should allow when kubeProxyReplacement is set to true", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + // Create ConfigMap with kubeProxyReplacement set to true + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "values.yaml": ` +ipam: + mode: kubernetes +kubeProxyReplacement: true +`, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + }) +}) + +func createTestCluster( + name, namespace string, + kubeProxyMode v1alpha1.KubeProxyMode, + cni *v1alpha1.CNI, +) *clusterv1.Cluster { + clusterConfig := &variables.ClusterConfigSpec{ + KubeProxy: &v1alpha1.KubeProxy{ + Mode: kubeProxyMode, + }, + } + + if cni != nil { + clusterConfig.Addons = &variables.Addons{ + GenericAddons: v1alpha1.GenericAddons{ + CNI: cni, + }, + } + } + + clusterConfigRaw, err := json.Marshal(clusterConfig) + Expect(err).NotTo(HaveOccurred()) + + return &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: clusterv1.ClusterSpec{ + Topology: &clusterv1.Topology{ + Class: "test-class", + Version: "v1.30.0", + Variables: []clusterv1.ClusterVariable{ + { + Name: v1alpha1.ClusterConfigVariableName, + Value: apiextensionsv1.JSON{ + Raw: clusterConfigRaw, + }, + }, + }, + }, + }, + } +} + +func createAdmissionRequest(cluster *clusterv1.Cluster) admission.Request { + objRaw, err := json.Marshal(cluster) + Expect(err).NotTo(HaveOccurred()) + + return admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: objRaw, + }, + RequestKind: &metav1.GroupVersionKind{ + Group: clusterv1.GroupVersion.Group, + Version: clusterv1.GroupVersion.Version, + Kind: "Cluster", + }, + }, + } +} diff --git a/pkg/webhook/cluster/validator.go b/pkg/webhook/cluster/validator.go index adbddae27..1d6708725 100644 --- a/pkg/webhook/cluster/validator.go +++ b/pkg/webhook/cluster/validator.go @@ -12,5 +12,6 @@ func NewValidator(client ctrlclient.Client, decoder admission.Decoder) admission return admission.MultiValidatingHandler( NewClusterUUIDLabeler(client, decoder).Validator(), NewNutanixValidator(client, decoder).Validator(), + NewAdvancedCiliumConfigurationValidator(client, decoder).Validator(), ) } From 2831cb7b351f7d7f8ca008009fd9a2ef0ec6b2b2 Mon Sep 17 00:00:00 2001 From: Dimitri Koshkin Date: Fri, 31 Oct 2025 13:52:15 -0700 Subject: [PATCH 2/4] feat: add skip annotation for cilium kube-proxy validator --- api/v1alpha1/constants.go | 4 + .../cluster/cilium_configuration_validator.go | 13 ++ .../cilium_configuration_validator_test.go | 125 ++++++++++++++++++ 3 files changed, 142 insertions(+) diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index 1093bd273..cec6aa281 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -58,4 +58,8 @@ const ( // PreflightChecksSkipAllAnnotationValue is the value used in the cluster's annotations to indicate // that all checks are skipped. PreflightChecksSkipAllAnnotationValue = "all" + + // SkipCiliumKubeProxyReplacementValidation is the key of the annotation on the Cluster + // used to skip Cilium kube-proxy replacement validation. + SkipCiliumKubeProxyReplacementValidation = APIGroup + "/skip-cilium-kube-proxy-replacement-validation" ) diff --git a/pkg/webhook/cluster/cilium_configuration_validator.go b/pkg/webhook/cluster/cilium_configuration_validator.go index 4f5e6c5ee..b5164c21b 100644 --- a/pkg/webhook/cluster/cilium_configuration_validator.go +++ b/pkg/webhook/cluster/cilium_configuration_validator.go @@ -56,6 +56,11 @@ func (a *advancedCiliumConfigurationValidator) validate( return admission.Allowed("") } + // Skip validation if the skip annotation is present + if hasSkipAnnotation(cluster) { + return admission.Allowed("") + } + clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables) if err != nil { return admission.Denied( @@ -100,6 +105,14 @@ func (a *advancedCiliumConfigurationValidator) validate( return admission.Allowed("") } +func hasSkipAnnotation(cluster *clusterv1.Cluster) bool { + if cluster.Annotations == nil { + return false + } + val, ok := cluster.Annotations[v1alpha1.SkipCiliumKubeProxyReplacementValidation] + return ok && val == "true" +} + type ciliumValues struct { KubeProxyReplacement bool `json:"kubeProxyReplacement"` } diff --git a/pkg/webhook/cluster/cilium_configuration_validator_test.go b/pkg/webhook/cluster/cilium_configuration_validator_test.go index c018d29c4..44b742b95 100644 --- a/pkg/webhook/cluster/cilium_configuration_validator_test.go +++ b/pkg/webhook/cluster/cilium_configuration_validator_test.go @@ -76,6 +76,131 @@ var _ = Describe("AdvancedCiliumConfigurationValidator", func() { }) }) + Context("when skip annotation is present", func() { + It("should skip validation when annotation is true", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + + // Add skip annotation + cluster.Annotations = map[string]string{ + v1alpha1.SkipCiliumKubeProxyReplacementValidation: "true", + } + + req := createAdmissionRequest(cluster) + + // Create ConfigMap with kubeProxyReplacement set to false - should still allow due to annotation + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "values.yaml": ` +ipam: + mode: kubernetes +kubeProxyReplacement: false +`, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeTrue()) + }) + + It("should not skip validation when annotation is false", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + + // Add skip annotation with false value + cluster.Annotations = map[string]string{ + v1alpha1.SkipCiliumKubeProxyReplacementValidation: "false", + } + + req := createAdmissionRequest(cluster) + + // Create ConfigMap with kubeProxyReplacement set to false - should deny + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "values.yaml": ` +ipam: + mode: kubernetes +kubeProxyReplacement: false +`, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeFalse()) + }) + + It("should not skip validation when annotation is missing", func() { + cni := &v1alpha1.CNI{ + Provider: v1alpha1.CNIProviderCilium, + AddonConfig: v1alpha1.AddonConfig{ + Values: &v1alpha1.AddonValues{ + SourceRef: &v1alpha1.ValuesReference{ + Kind: "ConfigMap", + Name: "cilium-values", + }, + }, + }, + } + cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeDisabled, cni) + req := createAdmissionRequest(cluster) + + // Create ConfigMap with kubeProxyReplacement set to false - should deny + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cilium-values", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "values.yaml": ` +ipam: + mode: kubernetes +kubeProxyReplacement: false +`, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(configMap).Build() + validator = NewAdvancedCiliumConfigurationValidator(client, decoder) + + resp := validator.validate(context.Background(), req) + Expect(resp.Allowed).To(BeFalse()) + }) + }) + Context("when kube-proxy is not disabled", func() { It("should allow the cluster", func() { cluster := createTestCluster("test-cluster", "test-namespace", v1alpha1.KubeProxyModeIPTables, nil) From 194ba182008f9516e0a1963d00e5ac602620aaef Mon Sep 17 00:00:00 2001 From: Dimitri Koshkin Date: Fri, 31 Oct 2025 13:52:40 -0700 Subject: [PATCH 3/4] refactor: move existing test file --- .../cluster/{webhook_test.go => clusteruuidlabeler_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/webhook/cluster/{webhook_test.go => clusteruuidlabeler_test.go} (100%) diff --git a/pkg/webhook/cluster/webhook_test.go b/pkg/webhook/cluster/clusteruuidlabeler_test.go similarity index 100% rename from pkg/webhook/cluster/webhook_test.go rename to pkg/webhook/cluster/clusteruuidlabeler_test.go From 9248b5ccb0f17567a814639254be4c454e83f0e2 Mon Sep 17 00:00:00 2001 From: Dimitri Koshkin Date: Fri, 31 Oct 2025 14:03:52 -0700 Subject: [PATCH 4/4] fixup! feat: ensure Cilium kube-proxy is enabled when needed --- pkg/webhook/cluster/cilium_configuration_validator.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/webhook/cluster/cilium_configuration_validator.go b/pkg/webhook/cluster/cilium_configuration_validator.go index b5164c21b..d7ea90888 100644 --- a/pkg/webhook/cluster/cilium_configuration_validator.go +++ b/pkg/webhook/cluster/cilium_configuration_validator.go @@ -98,7 +98,11 @@ func (a *advancedCiliumConfigurationValidator) validate( } // Validate that kubeProxyReplacement is enabled - if err := validateCiliumKubeProxyReplacement(ciliumValues, cluster.Namespace, clusterConfig.Addons.CNI.Values.SourceRef.Name); err != nil { + if err := validateCiliumKubeProxyReplacement( + ciliumValues, + cluster.Namespace, + clusterConfig.Addons.CNI.Values.SourceRef.Name, + ); err != nil { return admission.Denied(err.Error()) }