Skip to content

Commit 605bc2b

Browse files
committed
feat: Allow skipping kube-proxy on cluster creation
This commit allows users to skip kube-proxy installation when creating clusters. This is enforced via CEL to prevent users from moving between kube-proxy and non-kube-proxy deployments. Follow up work will allow Cilium configuration to enable their kube-proxy replacement (already possible via custom Helm values) and migration from kube-proxy to kube-proxy replacement for existing clusters.
1 parent a6cccf3 commit 605bc2b

File tree

10 files changed

+486
-0
lines changed

10 files changed

+486
-0
lines changed

api/v1alpha1/clusterconfig_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ type GenericClusterConfigSpec struct {
212212

213213
// +kubebuilder:validation:Optional
214214
DNS *DNS `json:"dns,omitempty"`
215+
216+
// SkipKubeProxy indicates whether to skip the installation of kube-proxy.
217+
// This is useful in scenarios where kube-proxy is not required or managed
218+
// externally.
219+
// +kubebuilder:validation:Optional
220+
// +kubebuilder:default=false
221+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value cannot be changed after cluster creation"
222+
SkipKubeProxy bool `json:"skipKubeProxy,omitempty"`
215223
}
216224

217225
type Image struct {

api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,16 @@ spec:
583583
description: HTTPS proxy value.
584584
type: string
585585
type: object
586+
skipKubeProxy:
587+
default: false
588+
description: |-
589+
SkipKubeProxy indicates whether to skip the installation of kube-proxy.
590+
This is useful in scenarios where kube-proxy is not required or managed
591+
externally.
592+
type: boolean
593+
x-kubernetes-validations:
594+
- message: Value cannot be changed after cluster creation
595+
rule: self == oldSelf
586596
users:
587597
items:
588598
description: User defines the input for a generated user in cloud-init.

api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,16 @@ spec:
520520
description: HTTPS proxy value.
521521
type: string
522522
type: object
523+
skipKubeProxy:
524+
default: false
525+
description: |-
526+
SkipKubeProxy indicates whether to skip the installation of kube-proxy.
527+
This is useful in scenarios where kube-proxy is not required or managed
528+
externally.
529+
type: boolean
530+
x-kubernetes-validations:
531+
- message: Value cannot be changed after cluster creation
532+
rule: self == oldSelf
523533
users:
524534
items:
525535
description: User defines the input for a generated user in cloud-init.

api/v1alpha1/crds/caren.nutanix.com_genericclusterconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ spec:
198198
description: HTTPS proxy value.
199199
type: string
200200
type: object
201+
skipKubeProxy:
202+
default: false
203+
description: |-
204+
SkipKubeProxy indicates whether to skip the installation of kube-proxy.
205+
This is useful in scenarios where kube-proxy is not required or managed
206+
externally.
207+
type: boolean
208+
x-kubernetes-validations:
209+
- message: Value cannot be changed after cluster creation
210+
rule: self == oldSelf
201211
users:
202212
items:
203213
description: User defines the input for a generated user in cloud-init.

api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,16 @@ spec:
793793
description: HTTPS proxy value.
794794
type: string
795795
type: object
796+
skipKubeProxy:
797+
default: false
798+
description: |-
799+
SkipKubeProxy indicates whether to skip the installation of kube-proxy.
800+
This is useful in scenarios where kube-proxy is not required or managed
801+
externally.
802+
type: boolean
803+
x-kubernetes-validations:
804+
- message: Value cannot be changed after cluster creation
805+
rule: self == oldSelf
796806
users:
797807
items:
798808
description: User defines the input for a generated user in cloud-init.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
+++
2+
title = "Skip deployment of kube-proxy"
3+
+++
4+
5+
When deploying a CNI implementation that can replace `kube-proxy`, it is necessary to disable `kube-proxy` to avoid
6+
potential conflicts. By default, `kube-proxy` is enabled.
7+
8+
## Example
9+
10+
To disable the deployment of `kube-proxy`, specify the following configuration:
11+
12+
```yaml
13+
apiVersion: cluster.x-k8s.io/v1beta1
14+
kind: Cluster
15+
metadata:
16+
name: <NAME>
17+
spec:
18+
topology:
19+
variables:
20+
- name: clusterConfig
21+
value:
22+
skipKubeProxy: true
23+
```
24+
25+
Applying this configuration will result in the following configuration being applied:
26+
27+
- `KubeadmControlPlaneTemplate`:
28+
29+
- ```yaml
30+
spec:
31+
template:
32+
spec:
33+
kubeadmConfigSpec:
34+
initConfiguration:
35+
skipPhases:
36+
- addon/kube-proxy
37+
```

pkg/handlers/generic/mutation/handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/kubernetesimagerepository"
2424
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors"
2525
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/noderegistration"
26+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/skipkubeproxy"
2627
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/taints"
2728
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users"
2829
)
@@ -44,6 +45,7 @@ func MetaMutators(mgr manager.Manager) []mutation.MetaMutator {
4445
containerdunprivilegedports.NewPatch(),
4546
encryptionatrest.NewPatch(mgr.GetClient(), encryptionatrest.RandomTokenGenerator),
4647
autorenewcerts.NewPatch(),
48+
skipkubeproxy.NewPatch(),
4749

4850
// Some patches may have changed containerd configuration.
4951
// We write the configuration changes to disk, and must run a command
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package skipkubeproxy
5+
6+
import (
7+
"context"
8+
9+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
12+
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
13+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
14+
ctrl "sigs.k8s.io/controller-runtime"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
17+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
18+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation"
19+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches"
20+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches/selectors"
21+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables"
22+
)
23+
24+
const (
25+
// VariableName is the external patch variable name.
26+
VariableName = "skipKubeProxy"
27+
)
28+
29+
type skipKubeProxy struct {
30+
variableName string
31+
variableFieldPath []string
32+
}
33+
34+
func NewPatch() *skipKubeProxy {
35+
return newSkipKubeProxy(
36+
v1alpha1.ClusterConfigVariableName,
37+
VariableName,
38+
)
39+
}
40+
41+
func newSkipKubeProxy(
42+
variableName string,
43+
variableFieldPath ...string,
44+
) *skipKubeProxy {
45+
return &skipKubeProxy{
46+
variableName: variableName,
47+
variableFieldPath: variableFieldPath,
48+
}
49+
}
50+
51+
func (h *skipKubeProxy) Mutate(
52+
ctx context.Context,
53+
obj *unstructured.Unstructured,
54+
vars map[string]apiextensionsv1.JSON,
55+
holderRef runtimehooksv1.HolderReference,
56+
_ client.ObjectKey,
57+
clusterGetter mutation.ClusterGetter,
58+
) error {
59+
log := ctrl.LoggerFrom(ctx).WithValues(
60+
"holderRef", holderRef,
61+
)
62+
63+
skipKubeProxy, err := variables.Get[bool](
64+
vars,
65+
h.variableName,
66+
h.variableFieldPath...,
67+
)
68+
if err != nil {
69+
if variables.IsNotFoundError(err) {
70+
log.V(5).Info("Control Plane skip kube proxy variable not defined")
71+
return nil
72+
}
73+
74+
return err
75+
}
76+
77+
log = log.WithValues(
78+
"variableName",
79+
h.variableName,
80+
"variableFieldPath",
81+
h.variableFieldPath,
82+
"variableValue",
83+
skipKubeProxy,
84+
)
85+
86+
if !skipKubeProxy {
87+
log.V(5).Info("Skip kube proxy variable is false, skipping mutation")
88+
return nil
89+
}
90+
91+
return patches.MutateIfApplicable(
92+
obj,
93+
vars,
94+
&holderRef,
95+
selectors.ControlPlane(),
96+
log,
97+
func(obj *controlplanev1.KubeadmControlPlaneTemplate) error {
98+
log.WithValues(
99+
"patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(),
100+
"patchedObjectName", client.ObjectKeyFromObject(obj),
101+
).Info("adding skip kube proxy addon to control plane kubeadm config spec")
102+
if obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration == nil {
103+
obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration = &bootstrapv1.InitConfiguration{}
104+
}
105+
obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.SkipPhases = append(
106+
obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.SkipPhases,
107+
"addon/kube-proxy",
108+
)
109+
110+
return nil
111+
},
112+
)
113+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package skipkubeproxy
5+
6+
import (
7+
"testing"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
"github.com/onsi/gomega"
11+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
12+
13+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
14+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation"
15+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest"
16+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest/request"
17+
)
18+
19+
func TestAutoRenewCertsPatch(t *testing.T) {
20+
gomega.RegisterFailHandler(Fail)
21+
RunSpecs(t, "Auto renew certs mutator suite")
22+
}
23+
24+
type testObj struct {
25+
patchTest capitest.PatchTestDef
26+
}
27+
28+
var _ = Describe("Generate skip kube proxy patches", func() {
29+
patchGenerator := func() mutation.GeneratePatches {
30+
return mutation.NewMetaGeneratePatchesHandler("", nil, NewPatch()).(mutation.GeneratePatches)
31+
}
32+
33+
testDefs := []testObj{
34+
{
35+
patchTest: capitest.PatchTestDef{
36+
Name: "skip kube proxy set with AWS",
37+
Vars: []runtimehooksv1.Variable{
38+
capitest.VariableWithValue(
39+
v1alpha1.ClusterConfigVariableName,
40+
v1alpha1.AWSClusterConfigSpec{
41+
GenericClusterConfigSpec: v1alpha1.GenericClusterConfigSpec{
42+
SkipKubeProxy: true,
43+
},
44+
},
45+
),
46+
},
47+
RequestItem: request.NewKubeadmControlPlaneTemplateRequestItem(""),
48+
ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{
49+
Operation: "add",
50+
Path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/skipPhases",
51+
ValueMatcher: gomega.ConsistOf([]string{
52+
"addon/kube-proxy",
53+
}),
54+
}},
55+
},
56+
},
57+
{
58+
patchTest: capitest.PatchTestDef{
59+
Name: "skip kube proxy set with Docker",
60+
Vars: []runtimehooksv1.Variable{
61+
capitest.VariableWithValue(
62+
v1alpha1.ClusterConfigVariableName,
63+
v1alpha1.DockerClusterConfigSpec{
64+
GenericClusterConfigSpec: v1alpha1.GenericClusterConfigSpec{
65+
SkipKubeProxy: true,
66+
},
67+
},
68+
),
69+
},
70+
RequestItem: request.NewKubeadmControlPlaneTemplateRequestItem(""),
71+
ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{
72+
Operation: "add",
73+
Path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/skipPhases",
74+
ValueMatcher: gomega.ConsistOf([]string{
75+
"addon/kube-proxy",
76+
}),
77+
}},
78+
},
79+
},
80+
{
81+
patchTest: capitest.PatchTestDef{
82+
Name: "skip kube proxy set with Nutanix",
83+
Vars: []runtimehooksv1.Variable{
84+
capitest.VariableWithValue(
85+
v1alpha1.ClusterConfigVariableName,
86+
v1alpha1.NutanixClusterConfigSpec{
87+
GenericClusterConfigSpec: v1alpha1.GenericClusterConfigSpec{
88+
SkipKubeProxy: true,
89+
},
90+
},
91+
),
92+
},
93+
RequestItem: request.NewKubeadmControlPlaneTemplateRequestItem(""),
94+
ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{
95+
Operation: "add",
96+
Path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/skipPhases",
97+
ValueMatcher: gomega.ConsistOf([]string{
98+
"addon/kube-proxy",
99+
}),
100+
}},
101+
},
102+
},
103+
}
104+
105+
// create test node for each case
106+
for _, tt := range testDefs {
107+
It(tt.patchTest.Name, func() {
108+
capitest.AssertGeneratePatches(GinkgoT(), patchGenerator, &tt.patchTest)
109+
})
110+
}
111+
})

0 commit comments

Comments
 (0)