Skip to content

Commit 03f7c97

Browse files
authored
feat: ensure Cilium kube-proxy is enabled when kube-proxy is disabled (#1371)
**What problem does this PR solve?**: Add a validating webhook that errors when the cluster has `kubeProxy.mode: disabled` but advanced Cilium configuration does not have `kubeProxyReplacement: false` set. **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 6b35c6d commit 03f7c97

File tree

5 files changed

+675
-0
lines changed

5 files changed

+675
-0
lines changed

api/v1alpha1/constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@ const (
6060
// PreflightChecksSkipAllAnnotationValue is the value used in the cluster's annotations to indicate
6161
// that all checks are skipped.
6262
PreflightChecksSkipAllAnnotationValue = "all"
63+
64+
// SkipCiliumKubeProxyReplacementValidation is the key of the annotation on the Cluster
65+
// used to skip Cilium kube-proxy replacement validation.
66+
SkipCiliumKubeProxyReplacementValidation = APIGroup + "/skip-cilium-kube-proxy-replacement-validation"
6367
)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cluster
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
11+
v1 "k8s.io/api/admission/v1"
12+
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/util/yaml"
15+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
16+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
17+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
18+
19+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
20+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables"
21+
)
22+
23+
type advancedCiliumConfigurationValidator struct {
24+
client ctrlclient.Client
25+
decoder admission.Decoder
26+
}
27+
28+
func NewAdvancedCiliumConfigurationValidator(
29+
client ctrlclient.Client, decoder admission.Decoder,
30+
) *advancedCiliumConfigurationValidator {
31+
return &advancedCiliumConfigurationValidator{
32+
client: client,
33+
decoder: decoder,
34+
}
35+
}
36+
37+
func (a *advancedCiliumConfigurationValidator) Validator() admission.HandlerFunc {
38+
return a.validate
39+
}
40+
41+
func (a *advancedCiliumConfigurationValidator) validate(
42+
ctx context.Context,
43+
req admission.Request,
44+
) admission.Response {
45+
if req.Operation == v1.Delete {
46+
return admission.Allowed("")
47+
}
48+
49+
cluster := &clusterv1.Cluster{}
50+
err := a.decoder.Decode(req, cluster)
51+
if err != nil {
52+
return admission.Errored(http.StatusBadRequest, err)
53+
}
54+
55+
if cluster.Spec.Topology == nil {
56+
return admission.Allowed("")
57+
}
58+
59+
// Skip validation if the skip annotation is present
60+
if hasSkipAnnotation(cluster) {
61+
return admission.Allowed("")
62+
}
63+
64+
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables)
65+
if err != nil {
66+
return admission.Denied(
67+
fmt.Errorf("failed to unmarshal cluster topology variable %q: %w",
68+
v1alpha1.ClusterConfigVariableName,
69+
err).Error(),
70+
)
71+
}
72+
73+
if clusterConfig == nil {
74+
return admission.Allowed("")
75+
}
76+
// Skip validation if kube-proxy is not disabled.
77+
if clusterConfig.KubeProxy == nil || clusterConfig.KubeProxy.Mode != v1alpha1.KubeProxyModeDisabled {
78+
return admission.Allowed("")
79+
}
80+
// Skip validation if not using Cilium as CNI provider.
81+
if clusterConfig.Addons == nil || clusterConfig.Addons.CNI == nil ||
82+
clusterConfig.Addons.CNI.Provider != v1alpha1.CNIProviderCilium {
83+
return admission.Allowed("")
84+
}
85+
// Skip validation if no custom values are specified.
86+
if clusterConfig.Addons.CNI.Values == nil || clusterConfig.Addons.CNI.Values.SourceRef == nil {
87+
return admission.Allowed("")
88+
}
89+
90+
// Get the Cilium values from the ConfigMap.
91+
ciliumValues, err := getCiliumValues(ctx, a.client, cluster, clusterConfig.Addons.CNI)
92+
if err != nil {
93+
return admission.Denied(err.Error())
94+
}
95+
// Skip validation if no values found.
96+
if ciliumValues == nil {
97+
return admission.Allowed("")
98+
}
99+
100+
// Validate that kubeProxyReplacement is enabled
101+
if err := validateCiliumKubeProxyReplacement(
102+
ciliumValues,
103+
cluster.Namespace,
104+
clusterConfig.Addons.CNI.Values.SourceRef.Name,
105+
); err != nil {
106+
return admission.Denied(err.Error())
107+
}
108+
109+
return admission.Allowed("")
110+
}
111+
112+
func hasSkipAnnotation(cluster *clusterv1.Cluster) bool {
113+
if cluster.Annotations == nil {
114+
return false
115+
}
116+
val, ok := cluster.Annotations[v1alpha1.SkipCiliumKubeProxyReplacementValidation]
117+
return ok && val == "true"
118+
}
119+
120+
type ciliumValues struct {
121+
KubeProxyReplacement bool `json:"kubeProxyReplacement"`
122+
}
123+
124+
// getCiliumValues retrieves and parses the Cilium values from a ConfigMap.
125+
// Returns nil if ConfigMap doesn't exist or values.yaml key is missing.
126+
// Returns error only for actual failures (permission errors, invalid YAML, etc.).
127+
func getCiliumValues(
128+
ctx context.Context,
129+
client ctrlclient.Client,
130+
cluster *clusterv1.Cluster,
131+
cni *v1alpha1.CNI,
132+
) (*ciliumValues, error) {
133+
configMapName := cni.Values.SourceRef.Name
134+
configMapNamespace := cluster.Namespace
135+
136+
configMap := &corev1.ConfigMap{
137+
ObjectMeta: metav1.ObjectMeta{
138+
Namespace: configMapNamespace,
139+
Name: configMapName,
140+
},
141+
}
142+
143+
err := client.Get(ctx, ctrlclient.ObjectKeyFromObject(configMap), configMap)
144+
if err != nil {
145+
// ConfigMap doesn't exist - this is OK, return nil
146+
if err = ctrlclient.IgnoreNotFound(err); err != nil {
147+
return nil, fmt.Errorf("failed to get ConfigMap %s/%s: %w", configMapNamespace, configMapName, err)
148+
}
149+
return nil, nil
150+
}
151+
152+
// Look for values.yaml key in the ConfigMap
153+
valuesYAML, ok := configMap.Data["values.yaml"]
154+
if !ok {
155+
// values.yaml key doesn't exist - this is OK, return nil
156+
return nil, nil
157+
}
158+
159+
// Unmarshal the YAML
160+
values := &ciliumValues{}
161+
if err := yaml.Unmarshal([]byte(valuesYAML), values); err != nil {
162+
return nil, fmt.Errorf(
163+
"failed to unmarshal Cilium values from ConfigMap %s/%s: %w",
164+
configMapNamespace,
165+
configMapName,
166+
err,
167+
)
168+
}
169+
170+
return values, nil
171+
}
172+
173+
func validateCiliumKubeProxyReplacement(values *ciliumValues, namespace, configMapName string) error {
174+
if !values.KubeProxyReplacement {
175+
return fmt.Errorf(
176+
"kube-proxy is disabled, but Cilium ConfigMap %s/%s does not have 'kubeProxyReplacement' enabled",
177+
namespace,
178+
configMapName,
179+
)
180+
}
181+
182+
return nil
183+
}

0 commit comments

Comments
 (0)