Skip to content

Commit 213faa0

Browse files
authored
Add ROSA-HCP AutoNode for karpenter auto scale (#5686)
Signed-off-by: serngawy <serngawy@gmail.com>
1 parent 77ef862 commit 213faa0

File tree

7 files changed

+260
-71
lines changed

7 files changed

+260
-71
lines changed

config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@ spec:
6363
AuditLogRoleARN defines the role that is used to forward audit logs to AWS CloudWatch.
6464
If not set, audit log forwarding is disabled.
6565
type: string
66+
autoNode:
67+
description: autoNode set the autoNode mode and roleARN.
68+
properties:
69+
mode:
70+
default: Disabled
71+
description: mode specifies the mode for the AutoNode. Setting
72+
Enable/Disable mode will allows/disallow karpenter AutoNode
73+
scaling.
74+
enum:
75+
- Enabled
76+
- Disabled
77+
type: string
78+
roleARN:
79+
description: |-
80+
roleARN sets the autoNode role ARN, which includes the IAM policy and cluster-specific role that grant the necessary permissions to the Karpenter controller.
81+
The role must be attached with the same OIDC-ID that is used with the ROSA-HCP cluster.
82+
maxLength: 2048
83+
type: string
84+
type: object
6685
availabilityZones:
6786
description: |-
6887
AvailabilityZones describe AWS AvailabilityZones of the worker nodes.

controlplane/rosa/api/v1beta2/rosacontrolplane_types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ const (
7171
Nightly ChannelGroupType = "nightly"
7272
)
7373

74+
// AutoNodeMode specifies the AutoNode mode for the ROSA Control Plane.
75+
type AutoNodeMode string
76+
77+
const (
78+
// AutoNodeModeEnabled enable AutoNode
79+
AutoNodeModeEnabled AutoNodeMode = "Enabled"
80+
81+
// AutoNodeModeDisabled Disabled AutoNode
82+
AutoNodeModeDisabled AutoNodeMode = "Disabled"
83+
)
84+
7485
// RosaControlPlaneSpec defines the desired state of ROSAControlPlane.
7586
type RosaControlPlaneSpec struct { //nolint: maligned
7687
// Cluster name must be valid DNS-1035 label, so it must consist of lower case alphanumeric
@@ -249,6 +260,25 @@ type RosaControlPlaneSpec struct { //nolint: maligned
249260
// ClusterRegistryConfig represents registry config used with the cluster.
250261
// +optional
251262
ClusterRegistryConfig *RegistryConfig `json:"clusterRegistryConfig,omitempty"`
263+
264+
// autoNode set the autoNode mode and roleARN.
265+
// +optional
266+
AutoNode *AutoNode `json:"autoNode,omitempty"`
267+
}
268+
269+
// AutoNode set the AutoNode mode and AutoNode role ARN.
270+
type AutoNode struct {
271+
// mode specifies the mode for the AutoNode. Setting Enable/Disable mode will allows/disallow karpenter AutoNode scaling.
272+
// +kubebuilder:validation:Enum=Enabled;Disabled
273+
// +kubebuilder:default=Disabled
274+
// +optional
275+
Mode AutoNodeMode `json:"mode,omitempty"`
276+
277+
// roleARN sets the autoNode role ARN, which includes the IAM policy and cluster-specific role that grant the necessary permissions to the Karpenter controller.
278+
// The role must be attached with the same OIDC-ID that is used with the ROSA-HCP cluster.
279+
// +kubebuilder:validation:MaxLength:=2048
280+
// +optional
281+
RoleARN string `json:"roleARN,omitempty"`
252282
}
253283

254284
// RegistryConfig for ROSA-HCP cluster

controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controlplane/rosa/controllers/rosacontrolplane_controller.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
234234
return ctrl.Result{}, err
235235
}
236236

237-
validationMessage, err := validateControlPlaneSpec(ocmClient, rosaScope)
237+
validationMessage, err := validateControlPlaneSpec(ocmClient, rosaScope.ControlPlane)
238238
if err != nil {
239239
return ctrl.Result{}, fmt.Errorf("failed to validate ROSAControlPlane.spec: %w", err)
240240
}
@@ -627,6 +627,18 @@ func (r *ROSAControlPlaneReconciler) updateOCMClusterSpec(rosaControlPlane *rosa
627627
updated = true
628628
}
629629

630+
if rosaControlPlane.Spec.AutoNode != nil {
631+
if !strings.EqualFold(ocmClusterSpec.AutoNodeMode, string(rosaControlPlane.Spec.AutoNode.Mode)) {
632+
ocmClusterSpec.AutoNodeMode = strings.ToLower(string(rosaControlPlane.Spec.AutoNode.Mode))
633+
updated = true
634+
}
635+
636+
if ocmClusterSpec.AutoNodeRoleARN != rosaControlPlane.Spec.AutoNode.RoleARN {
637+
ocmClusterSpec.AutoNodeRoleARN = rosaControlPlane.Spec.AutoNode.RoleARN
638+
updated = true
639+
}
640+
}
641+
630642
return ocmClusterSpec, updated
631643
}
632644

@@ -958,9 +970,9 @@ func (r *ROSAControlPlaneReconciler) reconcileClusterAdminPassword(ctx context.C
958970
return password, nil
959971
}
960972

961-
func validateControlPlaneSpec(ocmClient rosa.OCMClient, rosaScope *scope.ROSAControlPlaneScope) (string, error) {
962-
version := rosaScope.ControlPlane.Spec.Version
963-
channelGroup := string(rosaScope.ControlPlane.Spec.ChannelGroup)
973+
func validateControlPlaneSpec(ocmClient rosa.OCMClient, rosaControlPlane *rosacontrolplanev1.ROSAControlPlane) (string, error) {
974+
version := rosaControlPlane.Spec.Version
975+
channelGroup := string(rosaControlPlane.Spec.ChannelGroup)
964976
valid, err := ocmClient.ValidateHypershiftVersion(version, channelGroup)
965977
if err != nil {
966978
return "", fmt.Errorf("error validating version in this channelGroup : %w", err)
@@ -969,6 +981,12 @@ func validateControlPlaneSpec(ocmClient rosa.OCMClient, rosaScope *scope.ROSACon
969981
return fmt.Sprintf("this version %s is not supported in this channelGroup", version), nil
970982
}
971983

984+
if rosaControlPlane.Spec.AutoNode != nil {
985+
if rosaControlPlane.Spec.AutoNode.Mode == rosacontrolplanev1.AutoNodeModeEnabled && rosaControlPlane.Spec.AutoNode.RoleARN == "" {
986+
return "", fmt.Errorf("error ROSAControlPlane autoNode.roleARN, must be set when autoNode mode is enabled")
987+
}
988+
}
989+
972990
// TODO: add more input validations
973991
return "", nil
974992
}
@@ -1088,6 +1106,12 @@ func buildOCMClusterSpec(controlPlaneSpec rosacontrolplanev1.RosaControlPlaneSpe
10881106
}
10891107
}
10901108

1109+
// Set auto node karpenter config
1110+
if controlPlaneSpec.AutoNode != nil {
1111+
ocmClusterSpec.AutoNodeMode = strings.ToLower(string(controlPlaneSpec.AutoNode.Mode))
1112+
ocmClusterSpec.AutoNodeRoleARN = controlPlaneSpec.AutoNode.RoleARN
1113+
}
1114+
10911115
return ocmClusterSpec, nil
10921116
}
10931117

controlplane/rosa/controllers/rosacontrolplane_controller_test.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"net/http"
2626
"net/http/httptest"
2727
"net/url"
28+
"strings"
2829
"testing"
2930
"time"
3031

@@ -181,7 +182,7 @@ func TestUpdateOCMClusterSpec(t *testing.T) {
181182
})
182183

183184
// Test case 6: channel group update
184-
t.Run("Update AllowedRegistriesForImport", func(t *testing.T) {
185+
t.Run("Update channel group", func(t *testing.T) {
185186
rosaControlPlane := &rosacontrolplanev1.ROSAControlPlane{
186187
Spec: rosacontrolplanev1.RosaControlPlaneSpec{
187188
ChannelGroup: rosacontrolplanev1.Candidate,
@@ -203,6 +204,99 @@ func TestUpdateOCMClusterSpec(t *testing.T) {
203204
g.Expect(updated).To(BeTrue())
204205
g.Expect(ocmSpec).To(Equal(expectedOCMSpec))
205206
})
207+
208+
// Test case 7: AutoNode update
209+
t.Run("Update Auto Node", func(t *testing.T) {
210+
rosaControlPlane := &rosacontrolplanev1.ROSAControlPlane{
211+
Spec: rosacontrolplanev1.RosaControlPlaneSpec{
212+
AutoNode: &rosacontrolplanev1.AutoNode{
213+
Mode: rosacontrolplanev1.AutoNodeModeEnabled,
214+
RoleARN: "autoNodeARN",
215+
},
216+
},
217+
}
218+
219+
mockCluster, _ := v1.NewCluster().
220+
AutoNode(v1.NewClusterAutoNode().Mode("disabled")).
221+
AWS(v1.NewAWS().AutoNode(v1.NewAwsAutoNode().RoleArn("anyARN"))).
222+
Build()
223+
224+
expectedOCMSpec := ocm.Spec{
225+
AutoNodeMode: "enabled",
226+
AutoNodeRoleARN: "autoNodeARN",
227+
}
228+
229+
reconciler := &ROSAControlPlaneReconciler{}
230+
ocmSpec, updated := reconciler.updateOCMClusterSpec(rosaControlPlane, mockCluster)
231+
232+
g.Expect(updated).To(BeTrue())
233+
g.Expect(ocmSpec).To(Equal(expectedOCMSpec))
234+
})
235+
}
236+
237+
func TestValidateControlPlaneSpec(t *testing.T) {
238+
g := NewWithT(t)
239+
240+
mockCtrl := gomock.NewController(t)
241+
ocmMock := mocks.NewMockOCMClient(mockCtrl)
242+
expect := func(m *mocks.MockOCMClientMockRecorder) {
243+
m.ValidateHypershiftVersion(gomock.Any(), gomock.Any()).DoAndReturn(func(versionRawID string, channelGroup string) (bool, error) {
244+
return true, nil
245+
}).AnyTimes()
246+
}
247+
expect(ocmMock.EXPECT())
248+
249+
// Test case 1: AutoNode and Version are set valid
250+
t.Run("AutoNode is valid.", func(t *testing.T) {
251+
rosaControlPlane := &rosacontrolplanev1.ROSAControlPlane{
252+
Spec: rosacontrolplanev1.RosaControlPlaneSpec{
253+
AutoNode: &rosacontrolplanev1.AutoNode{
254+
Mode: rosacontrolplanev1.AutoNodeModeEnabled,
255+
RoleARN: "autoNodeARN",
256+
},
257+
Version: "4.19.0",
258+
ChannelGroup: rosacontrolplanev1.Stable,
259+
},
260+
}
261+
str, err := validateControlPlaneSpec(ocmMock, rosaControlPlane)
262+
g.Expect(err).NotTo(HaveOccurred())
263+
g.Expect(str).To(Equal(""))
264+
})
265+
266+
// Test case 2: AutoNode is enabled and AutoNode ARN is empty.
267+
t.Run("AutoNode is enabled and AutoNode ARN is empty", func(t *testing.T) {
268+
rosaControlPlane := &rosacontrolplanev1.ROSAControlPlane{
269+
Spec: rosacontrolplanev1.RosaControlPlaneSpec{
270+
AutoNode: &rosacontrolplanev1.AutoNode{
271+
Mode: rosacontrolplanev1.AutoNodeModeEnabled,
272+
RoleARN: "",
273+
},
274+
Version: "4.19.0",
275+
ChannelGroup: rosacontrolplanev1.Stable,
276+
},
277+
}
278+
str, err := validateControlPlaneSpec(ocmMock, rosaControlPlane)
279+
g.Expect(err).To(HaveOccurred())
280+
g.Expect(strings.Contains(err.Error(), "autoNode.roleARN, must be set when autoNode mode is enabled")).To(BeTrue())
281+
g.Expect(str).To(Equal(""))
282+
})
283+
284+
// Test case 3: AutoNode is disabled and AutoNode ARN is empty.
285+
t.Run("AutoNode is disabled and AutoNode ARN is empty.", func(t *testing.T) {
286+
rosaControlPlane := &rosacontrolplanev1.ROSAControlPlane{
287+
Spec: rosacontrolplanev1.RosaControlPlaneSpec{
288+
AutoNode: &rosacontrolplanev1.AutoNode{
289+
Mode: rosacontrolplanev1.AutoNodeModeDisabled,
290+
RoleARN: "",
291+
},
292+
Version: "4.19.0",
293+
ChannelGroup: rosacontrolplanev1.Stable,
294+
},
295+
}
296+
str, err := validateControlPlaneSpec(ocmMock, rosaControlPlane)
297+
g.Expect(err).NotTo(HaveOccurred())
298+
g.Expect(str).To(Equal(""))
299+
})
206300
}
207301

208302
func TestRosaControlPlaneReconcileStatusVersion(t *testing.T) {

0 commit comments

Comments
 (0)