Skip to content

Commit 3231700

Browse files
committed
Add the Channel field to the ROSA ControlPlane.
1 parent 1ee022e commit 3231700

6 files changed

Lines changed: 225 additions & 72 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ spec:
8282
rule: self == oldSelf
8383
- message: billingAccount must be a valid AWS account ID
8484
rule: self.matches('^[0-9]{12}$')
85+
channel:
86+
description: |-
87+
Channel is the Y-stream OpenShift channel to use for this cluster, for example "stable-4.16" or "eus-4.16".
88+
This determines which Y-stream versions are available for upgrades.
89+
When not specified, defaults to "stable-{versionY}" where versionY is derived from the Version field.
90+
pattern: ^(stable|eus|fast|candidate|nightly)-[0-9]+\.[0-9]+$
91+
type: string
8592
clusterRegistryConfig:
8693
description: ClusterRegistryConfig represents registry config used
8794
with the cluster.
@@ -813,6 +820,10 @@ spec:
813820
items:
814821
type: string
815822
type: array
823+
channel:
824+
description: Channel is the current Y-stream OpenShift channel of
825+
this cluster, for example "stable-4.16" or "eus-4.16".
826+
type: string
816827
conditions:
817828
description: Conditions specifies the conditions for the managed control
818829
plane

controlplane/rosa/api/v1beta2/rosacontrolplane_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ type RosaControlPlaneSpec struct { //nolint: maligned
9191
// OpenShift semantic version, for example "4.14.5".
9292
Version string `json:"version"`
9393

94+
// Channel is the Y-stream OpenShift channel to use for this cluster, for example "stable-4.16" or "eus-4.16".
95+
// This determines which Y-stream versions are available for upgrades.
96+
// When not specified, defaults to "stable-{versionY}" where versionY is derived from the Version field.
97+
//
98+
// +optional
99+
// +kubebuilder:validation:Pattern:=`^(stable|eus|fast|candidate|nightly)-[0-9]+\.[0-9]+$`
100+
Channel string `json:"channel,omitempty"`
101+
94102
// VersionGate requires acknowledgment when upgrading ROSA-HCP y-stream versions (e.g., from 4.15 to 4.16).
95103
// Default is WaitForAcknowledge.
96104
// WaitForAcknowledge: If acknowledgment is required, the upgrade will not proceed until VersionGate is set to Acknowledge or AlwaysAcknowledge.
@@ -724,6 +732,10 @@ type RosaControlPlaneStatus struct {
724732

725733
// Available upgrades for the ROSA hosted control plane.
726734
AvailableUpgrades []string `json:"availableUpgrades,omitempty"`
735+
736+
// Channel is the current Y-stream OpenShift channel of this cluster, for example "stable-4.16" or "eus-4.16".
737+
// +optional
738+
Channel string `json:"channel,omitempty"`
727739
}
728740

729741
// +kubebuilder:object:root=true

controlplane/rosa/controllers/rosacontrolplane_controller.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,17 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
242242
rosaScope.ControlPlane.Status.OIDCEndpointURL = cluster.AWS().STS().OIDCEndpointURL()
243243
rosaScope.ControlPlane.Status.Ready = false
244244

245+
// Populate channel from OCM cluster
246+
if cluster.Version() != nil && cluster.Version().ChannelGroup() != "" {
247+
// Reconstruct Y-stream channel from channel_group and version
248+
// e.g., channel_group="stable" + version="4.16.5" -> "stable-4.16"
249+
versionID := cluster.Version().ID()
250+
versionParts := strings.Split(versionID, ".")
251+
if len(versionParts) >= 2 {
252+
rosaScope.ControlPlane.Status.Channel = fmt.Sprintf("%s-%s.%s", cluster.Version().ChannelGroup(), versionParts[0], versionParts[1])
253+
}
254+
}
255+
245256
switch cluster.Status().State() {
246257
case cmv1.ClusterStateReady:
247258
conditions.MarkTrue(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneReadyCondition)
@@ -872,7 +883,10 @@ func (r *ROSAControlPlaneReconciler) reconcileClusterAdminPassword(ctx context.C
872883

873884
func validateControlPlaneSpec(ocmClient *ocm.Client, rosaScope *scope.ROSAControlPlaneScope) (string, error) {
874885
version := rosaScope.ControlPlane.Spec.Version
875-
valid, err := ocmClient.ValidateHypershiftVersion(version, ocm.DefaultChannelGroup)
886+
channel := getEffectiveChannel(rosaScope.ControlPlane.Spec)
887+
channelGroup := getChannelGroupFromChannel(channel)
888+
889+
valid, err := ocmClient.ValidateHypershiftVersion(version, channelGroup)
876890
if err != nil {
877891
return "", fmt.Errorf("failed to check if version is valid: %w", err)
878892
}
@@ -884,20 +898,52 @@ func validateControlPlaneSpec(ocmClient *ocm.Client, rosaScope *scope.ROSAContro
884898
return "", nil
885899
}
886900

901+
// getEffectiveChannel returns the channel to use for the cluster.
902+
// If channel is specified, it returns that. Otherwise, it derives it from the version.
903+
func getEffectiveChannel(spec rosacontrolplanev1.RosaControlPlaneSpec) string {
904+
if spec.Channel != "" {
905+
return spec.Channel
906+
}
907+
908+
// Default to stable-{Y} where Y is derived from version
909+
// Parse version to extract Y-stream (e.g., "4.16.5" -> "4.16")
910+
versionParts := strings.Split(spec.Version, ".")
911+
if len(versionParts) >= 2 {
912+
return fmt.Sprintf("stable-%s.%s", versionParts[0], versionParts[1])
913+
}
914+
915+
// Fallback to stable if we can't parse the version
916+
return "stable"
917+
}
918+
919+
// getChannelGroupFromChannel extracts the channel group from a Y-stream channel.
920+
// For example: "stable-4.16" -> "stable", "eus-4.16" -> "eus"
921+
func getChannelGroupFromChannel(channel string) string {
922+
parts := strings.Split(channel, "-")
923+
if len(parts) >= 1 && parts[0] != "" {
924+
return parts[0]
925+
}
926+
return ocm.DefaultChannelGroup
927+
}
928+
887929
func buildOCMClusterSpec(controlPlaneSpec rosacontrolplanev1.RosaControlPlaneSpec, creator *rosaaws.Creator) (ocm.Spec, error) {
888930
billingAccount := controlPlaneSpec.BillingAccount
889931
if billingAccount == "" {
890932
billingAccount = creator.AccountID
891933
}
892934

935+
channel := getEffectiveChannel(controlPlaneSpec)
936+
channelGroup := getChannelGroupFromChannel(channel)
937+
893938
ocmClusterSpec := ocm.Spec{
894939
DryRun: ptr.To(false),
895940
Name: controlPlaneSpec.RosaClusterName,
896941
DomainPrefix: controlPlaneSpec.DomainPrefix,
897942
Region: controlPlaneSpec.Region,
898943
MultiAZ: true,
899-
Version: ocm.CreateVersionID(controlPlaneSpec.Version, ocm.DefaultChannelGroup),
900-
ChannelGroup: ocm.DefaultChannelGroup,
944+
Version: ocm.CreateVersionID(controlPlaneSpec.Version, channelGroup),
945+
ChannelGroup: channelGroup,
946+
Channel: channel,
901947
DisableWorkloadMonitoring: ptr.To(true),
902948
DefaultIngress: ocm.NewDefaultIngressSpec(), // n.b. this is a no-op when it's set to the default value
903949
ComputeMachineType: controlPlaneSpec.DefaultMachinePoolSpec.InstanceType,

controlplane/rosa/controllers/rosacontrolplane_controller_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,83 @@ func TestUpdateOCMClusterSpec(t *testing.T) {
150150
g.Expect(ocmSpec).To(Equal(expectedOCMSpec))
151151
})
152152
}
153+
154+
func TestGetEffectiveChannel(t *testing.T) {
155+
g := NewWithT(t)
156+
157+
t.Run("Returns channel when specified", func(t *testing.T) {
158+
spec := rosacontrolplanev1.RosaControlPlaneSpec{
159+
Channel: "stable-4.16",
160+
Version: "4.16.5",
161+
}
162+
result := getEffectiveChannel(spec)
163+
g.Expect(result).To(Equal("stable-4.16"))
164+
})
165+
166+
t.Run("Returns EUS channel when specified", func(t *testing.T) {
167+
spec := rosacontrolplanev1.RosaControlPlaneSpec{
168+
Channel: "eus-4.16",
169+
Version: "4.16.5",
170+
}
171+
result := getEffectiveChannel(spec)
172+
g.Expect(result).To(Equal("eus-4.16"))
173+
})
174+
175+
t.Run("Derives channel from version when not specified", func(t *testing.T) {
176+
spec := rosacontrolplanev1.RosaControlPlaneSpec{
177+
Version: "4.16.5",
178+
}
179+
result := getEffectiveChannel(spec)
180+
g.Expect(result).To(Equal("stable-4.16"))
181+
})
182+
183+
t.Run("Derives channel for different version", func(t *testing.T) {
184+
spec := rosacontrolplanev1.RosaControlPlaneSpec{
185+
Version: "4.15.10",
186+
}
187+
result := getEffectiveChannel(spec)
188+
g.Expect(result).To(Equal("stable-4.15"))
189+
})
190+
191+
t.Run("Fallback to stable for invalid version format", func(t *testing.T) {
192+
spec := rosacontrolplanev1.RosaControlPlaneSpec{
193+
Version: "invalid",
194+
}
195+
result := getEffectiveChannel(spec)
196+
g.Expect(result).To(Equal("stable"))
197+
})
198+
}
199+
200+
func TestGetChannelGroupFromChannel(t *testing.T) {
201+
g := NewWithT(t)
202+
203+
t.Run("Extracts stable from stable-4.16", func(t *testing.T) {
204+
result := getChannelGroupFromChannel("stable-4.16")
205+
g.Expect(result).To(Equal("stable"))
206+
})
207+
208+
t.Run("Extracts eus from eus-4.16", func(t *testing.T) {
209+
result := getChannelGroupFromChannel("eus-4.16")
210+
g.Expect(result).To(Equal("eus"))
211+
})
212+
213+
t.Run("Extracts fast from fast-4.16", func(t *testing.T) {
214+
result := getChannelGroupFromChannel("fast-4.16")
215+
g.Expect(result).To(Equal("fast"))
216+
})
217+
218+
t.Run("Extracts candidate from candidate-4.16", func(t *testing.T) {
219+
result := getChannelGroupFromChannel("candidate-4.16")
220+
g.Expect(result).To(Equal("candidate"))
221+
})
222+
223+
t.Run("Extracts nightly from nightly-4.16", func(t *testing.T) {
224+
result := getChannelGroupFromChannel("nightly-4.16")
225+
g.Expect(result).To(Equal("nightly"))
226+
})
227+
228+
t.Run("Returns default for invalid channel format", func(t *testing.T) {
229+
result := getChannelGroupFromChannel("")
230+
g.Expect(result).To(Equal(ocm.DefaultChannelGroup))
231+
})
232+
}

go.mod

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module sigs.k8s.io/cluster-api-provider-aws/v2
22

3-
go 1.22.0
4-
5-
toolchain go1.22.5
3+
go 1.24.0
64

75
replace (
86
// TODO: remove when component-base updates its prometheus deps (https://github.com/prometheus/client_golang/releases/tag/v1.19.0)
@@ -26,23 +24,23 @@ require (
2624
github.com/go-logr/logr v1.4.2
2725
github.com/gofrs/flock v0.8.1
2826
github.com/golang/mock v1.6.0
29-
github.com/google/go-cmp v0.6.0
27+
github.com/google/go-cmp v0.7.0
3028
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f
3129
github.com/google/gofuzz v1.2.0
3230
github.com/onsi/ginkgo/v2 v2.19.1
3331
github.com/onsi/gomega v1.34.0
34-
github.com/openshift-online/ocm-common v0.0.11
35-
github.com/openshift-online/ocm-sdk-go v0.1.440
36-
github.com/openshift/rosa v1.2.46-rc1.0.20241003145806-a4af6ae81a7c
32+
github.com/openshift-online/ocm-common v0.0.32
33+
github.com/openshift-online/ocm-sdk-go v0.1.497
34+
github.com/openshift/rosa v1.99.9-testing.0.20260304182407-775ca373425a
3735
github.com/pkg/errors v0.9.1
38-
github.com/prometheus/client_golang v1.19.0
36+
github.com/prometheus/client_golang v1.23.2
3937
github.com/sergi/go-diff v1.3.1
4038
github.com/sirupsen/logrus v1.9.3
4139
github.com/spf13/cobra v1.8.1
4240
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace
4341
github.com/zgalor/weberr v0.8.2
44-
golang.org/x/crypto v0.25.0
45-
golang.org/x/text v0.16.0
42+
golang.org/x/crypto v0.46.0
43+
golang.org/x/text v0.32.0
4644
gopkg.in/yaml.v2 v2.4.0
4745
k8s.io/api v0.30.5
4846
k8s.io/apiextensions-apiserver v0.30.5
@@ -119,7 +117,7 @@ require (
119117
github.com/docker/go-connections v0.5.0 // indirect
120118
github.com/docker/go-units v0.5.0 // indirect
121119
github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect
122-
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
120+
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
123121
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
124122
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
125123
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
@@ -140,8 +138,8 @@ require (
140138
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
141139
github.com/godbus/dbus/v5 v5.1.0 // indirect
142140
github.com/gogo/protobuf v1.3.2 // indirect
143-
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
144-
github.com/golang/glog v1.2.1 // indirect
141+
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
142+
github.com/golang/glog v1.2.5 // indirect
145143
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
146144
github.com/golang/protobuf v1.5.4 // indirect
147145
github.com/google/btree v1.0.1 // indirect
@@ -175,7 +173,7 @@ require (
175173
github.com/mattn/go-colorable v0.1.13 // indirect
176174
github.com/mattn/go-isatty v0.0.20 // indirect
177175
github.com/mattn/go-runewidth v0.0.14 // indirect
178-
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
176+
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
179177
github.com/mitchellh/copystructure v1.2.0 // indirect
180178
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
181179
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -193,12 +191,14 @@ require (
193191
github.com/onsi/ginkgo v1.16.5 // indirect
194192
github.com/opencontainers/go-digest v1.0.0 // indirect
195193
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
194+
github.com/openshift-online/ocm-api-model/clientapi v0.0.452 // indirect
195+
github.com/openshift-online/ocm-api-model/model v0.0.452 // indirect
196196
github.com/pelletier/go-toml v1.9.5 // indirect
197197
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
198198
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
199-
github.com/prometheus/client_model v0.6.1 // indirect
200-
github.com/prometheus/common v0.52.2 // indirect
201-
github.com/prometheus/procfs v0.13.0 // indirect
199+
github.com/prometheus/client_model v0.6.2 // indirect
200+
github.com/prometheus/common v0.67.4 // indirect
201+
github.com/prometheus/procfs v0.19.2 // indirect
202202
github.com/rivo/uniseg v0.4.2 // indirect
203203
github.com/russross/blackfriday/v2 v2.1.0 // indirect
204204
github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -227,22 +227,22 @@ require (
227227
go.opentelemetry.io/otel/trace v1.24.0 // indirect
228228
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
229229
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
230-
go.uber.org/mock v0.3.0 // indirect
230+
go.uber.org/mock v0.5.2 // indirect
231231
go.uber.org/multierr v1.11.0 // indirect
232232
go.uber.org/zap v1.27.0 // indirect
233233
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
234-
golang.org/x/net v0.27.0 // indirect
235-
golang.org/x/oauth2 v0.21.0 // indirect
236-
golang.org/x/sync v0.7.0 // indirect
237-
golang.org/x/sys v0.22.0 // indirect
238-
golang.org/x/term v0.22.0 // indirect
234+
golang.org/x/net v0.48.0 // indirect
235+
golang.org/x/oauth2 v0.34.0 // indirect
236+
golang.org/x/sync v0.19.0 // indirect
237+
golang.org/x/sys v0.39.0 // indirect
238+
golang.org/x/term v0.38.0 // indirect
239239
golang.org/x/time v0.5.0 // indirect
240-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
240+
golang.org/x/tools v0.39.0 // indirect
241241
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
242242
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect
243243
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
244244
google.golang.org/grpc v1.62.2 // indirect
245-
google.golang.org/protobuf v1.34.1 // indirect
245+
google.golang.org/protobuf v1.36.10 // indirect
246246
gopkg.in/inf.v0 v0.9.1 // indirect
247247
gopkg.in/ini.v1 v1.67.0 // indirect
248248
gopkg.in/yaml.v3 v3.0.1 // indirect

0 commit comments

Comments
 (0)