Skip to content

Commit e4f8876

Browse files
authored
feat: Add k8s version logic for external cloud-provider flag (#1134)
This flag has been removed from kube-apiserver in k8s v1.33 so need to use version specific logic to add it for pre-1.33 clusters.
1 parent 45667ab commit e4f8876

File tree

10 files changed

+326
-38
lines changed

10 files changed

+326
-38
lines changed

charts/cluster-api-runtime-extensions-nutanix/defaultclusterclasses/aws-cluster-class.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ spec:
8484
clusterConfiguration:
8585
apiServer:
8686
extraArgs:
87-
cloud-provider: external
8887
profiling: "false"
8988
controllerManager:
9089
extraArgs:

charts/cluster-api-runtime-extensions-nutanix/defaultclusterclasses/nutanix-cluster-class.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ spec:
123123
clusterConfiguration:
124124
apiServer:
125125
extraArgs:
126-
cloud-provider: external
127126
profiling: "false"
128127
tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
129128
controllerManager:

common/pkg/capi/clustertopology/variables/variable.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ func IsNotFoundError(err error) bool {
3333
return topologymutation.IsNotFoundError(err) || errors.As(err, &fieldNotFoundError{})
3434
}
3535

36+
func IsFieldNotFoundError(err error) bool {
37+
return errors.As(err, &fieldNotFoundError{})
38+
}
39+
3640
// Get finds and parses variable to given type.
3741
func Get[T any](
3842
variables map[string]apiextensionsv1.JSON,

common/pkg/testutils/capitest/patches.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,16 @@ func AssertGeneratePatches[T mutation.GeneratePatches](
6363
}
6464
resp := &runtimehooksv1.GeneratePatchesResponse{}
6565
h.GeneratePatches(context.Background(), req, resp)
66-
expectedStatus := runtimehooksv1.ResponseStatusSuccess
6766
if tt.ExpectedFailure {
68-
expectedStatus = runtimehooksv1.ResponseStatusFailure
69-
}
70-
g.Expect(resp.Status).
71-
To(gomega.Equal(expectedStatus), fmt.Sprintf("Message: %s", resp.Message))
72-
73-
if len(tt.ExpectedPatchMatchers) == 0 {
67+
g.Expect(resp.Status).
68+
To(gomega.Equal(runtimehooksv1.ResponseStatusFailure), fmt.Sprintf("Message: %s", resp.Message))
7469
g.Expect(resp.Items).To(gomega.BeEmpty())
7570
return
7671
}
72+
73+
g.Expect(resp.Status).
74+
To(gomega.Equal(runtimehooksv1.ResponseStatusSuccess), fmt.Sprintf("Message: %s", resp.Message))
75+
7776
g.Expect(resp.Items).To(containPatches(&tt.RequestItem, tt.ExpectedPatchMatchers...))
7877

7978
if len(tt.UnexpectedPatchMatchers) > 0 {
@@ -111,6 +110,17 @@ func containPatches(
111110
requestItem *runtimehooksv1.GeneratePatchesRequestItem,
112111
jsonMatchers ...JSONPatchMatcher,
113112
) gomega.OmegaMatcher {
113+
if len(jsonMatchers) == 0 {
114+
return gomega.SatisfyAny(
115+
gomega.BeEmpty(),
116+
gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
117+
"UID": gomega.Equal(requestItem.UID),
118+
"PatchType": gomega.Equal(runtimehooksv1.JSONPatchType),
119+
"Patch": gomega.Equal([]byte("[]")),
120+
})),
121+
)
122+
}
123+
114124
patchMatchers := make([]interface{}, 0, len(jsonMatchers))
115125
for patchIdx := range jsonMatchers {
116126
unexpectedPatch := jsonMatchers[patchIdx]

common/pkg/testutils/capitest/request/items.go

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
package request
55

66
import (
7+
"encoding/json"
8+
"maps"
9+
710
corev1 "k8s.io/api/core/v1"
11+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
812
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
913
"k8s.io/apimachinery/pkg/runtime"
1014
"k8s.io/apimachinery/pkg/types"
1115
"k8s.io/apimachinery/pkg/util/uuid"
16+
"k8s.io/utils/ptr"
1217
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1318
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
1419
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
@@ -88,7 +93,9 @@ func NewKubeadmConfigTemplateRequest(
8893
}
8994

9095
type KubeadmControlPlaneTemplateRequestItemBuilder struct {
91-
files []bootstrapv1.File
96+
files []bootstrapv1.File
97+
version *string
98+
apiServerExtraArgs map[string]string
9299
}
93100

94101
func (b *KubeadmControlPlaneTemplateRequestItemBuilder) WithFiles(
@@ -98,43 +105,67 @@ func (b *KubeadmControlPlaneTemplateRequestItemBuilder) WithFiles(
98105
return b
99106
}
100107

108+
func (b *KubeadmControlPlaneTemplateRequestItemBuilder) WithKubernetesVersion(
109+
version string,
110+
) *KubeadmControlPlaneTemplateRequestItemBuilder {
111+
b.version = ptr.To(version)
112+
return b
113+
}
114+
115+
func (b *KubeadmControlPlaneTemplateRequestItemBuilder) WithAPIServerExtraArgs(
116+
extraArgs map[string]string,
117+
) *KubeadmControlPlaneTemplateRequestItemBuilder {
118+
b.apiServerExtraArgs = extraArgs
119+
return b
120+
}
121+
101122
func (b *KubeadmControlPlaneTemplateRequestItemBuilder) NewRequest(
102123
uid types.UID,
103124
) runtimehooksv1.GeneratePatchesRequestItem {
104-
return NewRequestItem(
105-
&controlplanev1.KubeadmControlPlaneTemplate{
106-
TypeMeta: metav1.TypeMeta{
107-
APIVersion: controlplanev1.GroupVersion.String(),
108-
Kind: "KubeadmControlPlaneTemplate",
109-
},
110-
ObjectMeta: metav1.ObjectMeta{
111-
Name: kubeadmControlPlaneTemplateRequestObjectName,
112-
Namespace: Namespace,
113-
},
114-
Spec: controlplanev1.KubeadmControlPlaneTemplateSpec{
115-
Template: controlplanev1.KubeadmControlPlaneTemplateResource{
116-
Spec: controlplanev1.KubeadmControlPlaneTemplateResourceSpec{
117-
KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
118-
InitConfiguration: &bootstrapv1.InitConfiguration{
119-
NodeRegistration: bootstrapv1.NodeRegistrationOptions{
120-
KubeletExtraArgs: map[string]string{
121-
"cloud-provider": "external",
122-
},
125+
cpTemplate := &controlplanev1.KubeadmControlPlaneTemplate{
126+
TypeMeta: metav1.TypeMeta{
127+
APIVersion: controlplanev1.GroupVersion.String(),
128+
Kind: "KubeadmControlPlaneTemplate",
129+
},
130+
ObjectMeta: metav1.ObjectMeta{
131+
Name: kubeadmControlPlaneTemplateRequestObjectName,
132+
Namespace: Namespace,
133+
},
134+
Spec: controlplanev1.KubeadmControlPlaneTemplateSpec{
135+
Template: controlplanev1.KubeadmControlPlaneTemplateResource{
136+
Spec: controlplanev1.KubeadmControlPlaneTemplateResourceSpec{
137+
KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
138+
InitConfiguration: &bootstrapv1.InitConfiguration{
139+
NodeRegistration: bootstrapv1.NodeRegistrationOptions{
140+
KubeletExtraArgs: map[string]string{
141+
"cloud-provider": "external",
123142
},
124143
},
125-
JoinConfiguration: &bootstrapv1.JoinConfiguration{
126-
NodeRegistration: bootstrapv1.NodeRegistrationOptions{
127-
KubeletExtraArgs: map[string]string{
128-
"cloud-provider": "external",
129-
},
144+
},
145+
JoinConfiguration: &bootstrapv1.JoinConfiguration{
146+
NodeRegistration: bootstrapv1.NodeRegistrationOptions{
147+
KubeletExtraArgs: map[string]string{
148+
"cloud-provider": "external",
130149
},
131150
},
132-
Files: b.files,
133151
},
152+
Files: b.files,
134153
},
135154
},
136155
},
137156
},
157+
}
158+
159+
if b.apiServerExtraArgs != nil {
160+
if cpTemplate.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration == nil {
161+
cpTemplate.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration = &bootstrapv1.ClusterConfiguration{}
162+
}
163+
clusterConfiguration := cpTemplate.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration
164+
clusterConfiguration.APIServer.ExtraArgs = maps.Clone(b.apiServerExtraArgs)
165+
}
166+
167+
requestItem := NewRequestItem(
168+
cpTemplate,
138169
&runtimehooksv1.HolderReference{
139170
APIVersion: clusterv1.GroupVersion.String(),
140171
Kind: "Cluster",
@@ -144,6 +175,22 @@ func (b *KubeadmControlPlaneTemplateRequestItemBuilder) NewRequest(
144175
},
145176
uid,
146177
)
178+
179+
if b.version != nil {
180+
marshaledBuiltin, _ := json.Marshal( //nolint:errchkjson // Marshalling is guaranteed to succeed.
181+
map[string]interface{}{
182+
"controlPlane": map[string]interface{}{
183+
"version": *b.version,
184+
},
185+
},
186+
)
187+
requestItem.Variables = append(requestItem.Variables, runtimehooksv1.Variable{
188+
Name: runtimehooksv1.BuiltinsName,
189+
Value: apiextensionsv1.JSON{Raw: marshaledBuiltin},
190+
})
191+
}
192+
193+
return requestItem
147194
}
148195

149196
func NewKubeadmControlPlaneTemplateRequestItem(

hack/examples/bases/aws/clusterclass/kustomization.yaml.tmpl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ patches:
3434
- target:
3535
kind: KubeadmControlPlaneTemplate
3636
patch: |-
37-
- op: "replace"
38-
path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/cloud-provider"
39-
value: "external"
4037
- op: "replace"
4138
path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/controllerManager/extraArgs/cloud-provider"
4239
value: "external"
@@ -52,6 +49,13 @@ patches:
5249
- op: "replace"
5350
path: "/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cloud-provider"
5451
value: "external"
52+
# Delete the API server cloud-provider flag from the template.
53+
# They will be added by the handler for k8s < 1.33.
54+
- target:
55+
kind: KubeadmControlPlaneTemplate
56+
patch: |-
57+
- op: "remove"
58+
path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/cloud-provider"
5559

5660
# Delete the cluster-specific resources.
5761
- target:

hack/examples/bases/nutanix/clusterclass/kustomization.yaml.tmpl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ patches:
4646
- op: "remove"
4747
path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs"
4848

49+
# TODO: Remove once https://github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pull/519 is
50+
# merged and released.
51+
# Delete the API server cloud-provider flag from the template.
52+
# They will be added by the handler for k8s < 1.33.
53+
- target:
54+
kind: KubeadmControlPlaneTemplate
55+
patch: |-
56+
- op: "remove"
57+
path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/cloud-provider"
58+
4959
# Template the kube-vip file.
5060
# The handler will set the variables if needed, or remove it.
5161
- target:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package externalcloudprovider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
11+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12+
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
13+
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
14+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
15+
ctrl "sigs.k8s.io/controller-runtime"
16+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
17+
18+
"github.com/blang/semver/v4"
19+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation"
20+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches"
21+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches/selectors"
22+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables"
23+
)
24+
25+
var (
26+
versionGreaterOrEqualTo133Range = semver.MustParseRange(">=1.33.0-0")
27+
)
28+
29+
type externalCloudProviderPatchHandler struct{}
30+
31+
func NewControlPlanePatch() *externalCloudProviderPatchHandler {
32+
return &externalCloudProviderPatchHandler{}
33+
}
34+
35+
func (h *externalCloudProviderPatchHandler) Mutate(
36+
ctx context.Context,
37+
obj *unstructured.Unstructured,
38+
vars map[string]apiextensionsv1.JSON,
39+
holderRef runtimehooksv1.HolderReference,
40+
_ ctrlclient.ObjectKey,
41+
_ mutation.ClusterGetter,
42+
) error {
43+
log := ctrl.LoggerFrom(ctx).WithValues(
44+
"holderRef", holderRef,
45+
)
46+
47+
cpVersion, err := variables.Get[string](vars, runtimehooksv1.BuiltinsName, "controlPlane", "version")
48+
if err != nil {
49+
// This builtin variable is guaranteed to be provided for control plane component patch requests so if it is not
50+
// found then we can safely skip this patch for this request item.
51+
if variables.IsFieldNotFoundError(err) {
52+
log.V(5).
53+
WithValues("variables", vars).
54+
Info(
55+
"skipping external cloud-provider flag to control plane because CP Kubernetes version is not found",
56+
)
57+
return nil
58+
}
59+
60+
// This is a fatal error, we can't proceed without the control plane version.
61+
log.WithValues("variables", vars).Error(err, "failed to get control plane Kubernetes version from builtin variable")
62+
return fmt.Errorf("failed to get control plane Kubernetes version from builtin variable: %w", err)
63+
}
64+
65+
kubernetesVersion, err := semver.ParseTolerant(cpVersion)
66+
if err != nil {
67+
log.WithValues(
68+
"kubernetesVersion",
69+
cpVersion,
70+
).Error(err, "failed to parse control plane Kubernetes version")
71+
return fmt.Errorf("failed to parse control plane Kubernetes version: %w", err)
72+
}
73+
74+
if versionGreaterOrEqualTo133Range(kubernetesVersion) {
75+
log.V(5).Info(
76+
"skipping external cloud-provider flag to control plane kubeadm config template because Kubernetes >= 1.33.0",
77+
)
78+
return nil
79+
}
80+
81+
if err := patches.MutateIfApplicable(
82+
obj, vars, &holderRef, selectors.ControlPlane(), log,
83+
func(obj *controlplanev1.KubeadmControlPlaneTemplate) error {
84+
if obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration == nil {
85+
obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration = &bootstrapv1.ClusterConfiguration{}
86+
}
87+
if obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer.ExtraArgs == nil {
88+
obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer.ExtraArgs = make(map[string]string, 1)
89+
}
90+
if _, ok := obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer.ExtraArgs["cloud-provider"]; !ok {
91+
obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer.ExtraArgs["cloud-provider"] = "external"
92+
}
93+
94+
return nil
95+
}); err != nil {
96+
return err
97+
}
98+
99+
return nil
100+
}

0 commit comments

Comments
 (0)