Skip to content

Commit 8940a33

Browse files
committed
feat: ensure Cilium kube-proxy is enabled when needed
1 parent 53680cf commit 8940a33

File tree

3 files changed

+529
-0
lines changed

3 files changed

+529
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables)
60+
if err != nil {
61+
return admission.Denied(
62+
fmt.Errorf("failed to unmarshal cluster topology variable %q: %w",
63+
v1alpha1.ClusterConfigVariableName,
64+
err).Error(),
65+
)
66+
}
67+
68+
if clusterConfig == nil {
69+
return admission.Allowed("")
70+
}
71+
// Skip validation if kube-proxy is not disabled.
72+
if clusterConfig.KubeProxy == nil || clusterConfig.KubeProxy.Mode != v1alpha1.KubeProxyModeDisabled {
73+
return admission.Allowed("")
74+
}
75+
// Skip validation if not using Cilium as CNI provider.
76+
if clusterConfig.Addons == nil || clusterConfig.Addons.CNI == nil ||
77+
clusterConfig.Addons.CNI.Provider != v1alpha1.CNIProviderCilium {
78+
return admission.Allowed("")
79+
}
80+
// Skip validation if no custom values are specified.
81+
if clusterConfig.Addons.CNI.Values == nil || clusterConfig.Addons.CNI.Values.SourceRef == nil {
82+
return admission.Allowed("")
83+
}
84+
85+
// Get the Cilium values from the ConfigMap.
86+
ciliumValues, err := getCiliumValues(ctx, a.client, cluster, clusterConfig.Addons.CNI)
87+
if err != nil {
88+
return admission.Denied(err.Error())
89+
}
90+
// Skip validation if no values found.
91+
if ciliumValues == nil {
92+
return admission.Allowed("")
93+
}
94+
95+
// Validate that kubeProxyReplacement is enabled
96+
if err := validateCiliumKubeProxyReplacement(ciliumValues, cluster.Namespace, clusterConfig.Addons.CNI.Values.SourceRef.Name); err != nil {
97+
return admission.Denied(err.Error())
98+
}
99+
100+
return admission.Allowed("")
101+
}
102+
103+
type ciliumValues struct {
104+
KubeProxyReplacement bool `json:"kubeProxyReplacement"`
105+
}
106+
107+
// getCiliumValues retrieves and parses the Cilium values from a ConfigMap.
108+
// Returns nil if ConfigMap doesn't exist or values.yaml key is missing.
109+
// Returns error only for actual failures (permission errors, invalid YAML, etc.).
110+
func getCiliumValues(
111+
ctx context.Context,
112+
client ctrlclient.Client,
113+
cluster *clusterv1.Cluster,
114+
cni *v1alpha1.CNI,
115+
) (*ciliumValues, error) {
116+
configMapName := cni.Values.SourceRef.Name
117+
configMapNamespace := cluster.Namespace
118+
119+
configMap := &corev1.ConfigMap{
120+
ObjectMeta: metav1.ObjectMeta{
121+
Namespace: configMapNamespace,
122+
Name: configMapName,
123+
},
124+
}
125+
126+
err := client.Get(ctx, ctrlclient.ObjectKeyFromObject(configMap), configMap)
127+
if err != nil {
128+
// ConfigMap doesn't exist - this is OK, return nil
129+
if err = ctrlclient.IgnoreNotFound(err); err != nil {
130+
return nil, fmt.Errorf("failed to get ConfigMap %s/%s: %w", configMapNamespace, configMapName, err)
131+
}
132+
return nil, nil
133+
}
134+
135+
// Look for values.yaml key in the ConfigMap
136+
valuesYAML, ok := configMap.Data["values.yaml"]
137+
if !ok {
138+
// values.yaml key doesn't exist - this is OK, return nil
139+
return nil, nil
140+
}
141+
142+
// Unmarshal the YAML
143+
values := &ciliumValues{}
144+
if err := yaml.Unmarshal([]byte(valuesYAML), values); err != nil {
145+
return nil, fmt.Errorf(
146+
"failed to unmarshal Cilium values from ConfigMap %s/%s: %w",
147+
configMapNamespace,
148+
configMapName,
149+
err,
150+
)
151+
}
152+
153+
return values, nil
154+
}
155+
156+
func validateCiliumKubeProxyReplacement(values *ciliumValues, namespace, configMapName string) error {
157+
if !values.KubeProxyReplacement {
158+
return fmt.Errorf(
159+
"kube-proxy is disabled, but Cilium ConfigMap %s/%s does not have 'kubeProxyReplacement' enabled",
160+
namespace,
161+
configMapName,
162+
)
163+
}
164+
165+
return nil
166+
}

0 commit comments

Comments
 (0)