Skip to content

Commit ae3c3be

Browse files
committed
reconcile ControlPlane version
1 parent 3f434bb commit ae3c3be

File tree

6 files changed

+178
-9
lines changed

6 files changed

+178
-9
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ spec:
270270
supportRoleARN:
271271
type: string
272272
version:
273-
description: Openshift version, for example "openshift-v4.14.5".
273+
description: Openshift version, for example "4.14.5".
274+
pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
274275
type: string
275276
workerRoleARN:
276277
type: string

controlplane/rosa/api/v1beta2/conditions_consts.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ import clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2121
const (
2222
// ROSAControlPlaneReadyCondition condition reports on the successful reconciliation of ROSAControlPlane.
2323
ROSAControlPlaneReadyCondition clusterv1.ConditionType = "ROSAControlPlaneReady"
24+
25+
// ROSAControlPlaneUpgradingCondition condition reports whether ROSAControlPlane is upgrading or not.
26+
ROSAControlPlaneUpgradingCondition clusterv1.ConditionType = "ROSAControlPlaneUpgrading"
2427
)

controlplane/rosa/api/v1beta2/rosacontrolplane_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ type RosaControlPlaneSpec struct { //nolint: maligned
4848
// The AWS Region the cluster lives in.
4949
Region *string `json:"region"`
5050

51-
// Openshift version, for example "openshift-v4.14.5".
52-
Version *string `json:"version"`
51+
// Openshift version, for example "4.14.5".
52+
//
53+
// +kubebuilder:validation:Pattern:=`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`
54+
Version string `json:"version"`
5355

5456
// ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
5557
// +optional

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

Lines changed: 0 additions & 5 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: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"time"
2828

2929
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
30+
corev1 "k8s.io/api/core/v1"
3031
apierrors "k8s.io/apimachinery/pkg/api/errors"
3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3233
"k8s.io/apimachinery/pkg/types"
@@ -188,6 +189,15 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
188189
}
189190
defer rosaClient.Close()
190191

192+
isValid, err := validateControlPlaneSpec(rosaClient, rosaScope)
193+
if err != nil {
194+
return ctrl.Result{}, fmt.Errorf("failed to validate ROSAControlPlane.spec: %w", err)
195+
}
196+
if !isValid {
197+
// dont' requeue because input is invalid and manual intervention is needed.
198+
return ctrl.Result{}, nil
199+
}
200+
191201
cluster, err := rosaClient.GetCluster()
192202
if err != nil {
193203
return ctrl.Result{}, err
@@ -213,6 +223,9 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
213223
if err := r.reconcileKubeconfig(ctx, rosaScope, rosaClient, cluster); err != nil {
214224
return ctrl.Result{}, fmt.Errorf("failed to reconcile kubeconfig: %w", err)
215225
}
226+
if err := r.reconcileClusterVersion(rosaScope, rosaClient, cluster); err != nil {
227+
return ctrl.Result{}, err
228+
}
216229
return ctrl.Result{}, nil
217230
case cmv1.ClusterStateError:
218231
errorMessage := cluster.Status().ProvisionErrorMessage()
@@ -255,7 +268,7 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
255268
DisableUserWorkloadMonitoring(true).
256269
Version(
257270
cmv1.NewVersion().
258-
ID(*rosaScope.ControlPlane.Spec.Version).
271+
ID(fmt.Sprintf("openshift-v%s", rosaScope.ControlPlane.Spec.Version)).
259272
ChannelGroup("stable"),
260273
).
261274
ExpirationTimestamp(time.Now().Add(1 * time.Hour)).
@@ -394,6 +407,41 @@ func (r *ROSAControlPlaneReconciler) reconcileDelete(ctx context.Context, rosaSc
394407
return ctrl.Result{RequeueAfter: time.Second * 60}, nil
395408
}
396409

410+
func (r *ROSAControlPlaneReconciler) reconcileClusterVersion(rosaScope *scope.ROSAControlPlaneScope, rosaClient *rosa.RosaClient, cluster *cmv1.Cluster) error {
411+
version := rosaScope.ControlPlane.Spec.Version
412+
if version == cluster.Version().RawID() {
413+
conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneUpgradingCondition, "upgraded", clusterv1.ConditionSeverityInfo, "")
414+
return nil
415+
}
416+
417+
scheduledUpgrade, err := rosaClient.CheckExistingScheduledUpgrade(cluster)
418+
if err != nil {
419+
return fmt.Errorf("failed to get existing scheduled upgrades: %w", err)
420+
}
421+
422+
if scheduledUpgrade == nil {
423+
scheduledUpgrade, err = rosaClient.ScheduleControlPlaneUpgrade(cluster, version, time.Now())
424+
if err != nil {
425+
return fmt.Errorf("failed to schedule control plane upgrade to version %s: %w", version, err)
426+
}
427+
}
428+
429+
condition := &clusterv1.Condition{
430+
Type: rosacontrolplanev1.ROSAControlPlaneUpgradingCondition,
431+
Status: corev1.ConditionTrue,
432+
Reason: string(scheduledUpgrade.State().Value()),
433+
Message: fmt.Sprintf("Upgrading to version %s", scheduledUpgrade.Version()),
434+
}
435+
conditions.Set(rosaScope.ControlPlane, condition)
436+
437+
// if cluster is already upgrading to another version we need to wait until the current upgrade is finished, return an error to requeue and try later.
438+
if scheduledUpgrade.Version() != version {
439+
return fmt.Errorf("there is already a %s upgrade to version %s", scheduledUpgrade.State().Value(), scheduledUpgrade.Version())
440+
}
441+
442+
return nil
443+
}
444+
397445
func (r *ROSAControlPlaneReconciler) reconcileKubeconfig(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope, rosaClient *rosa.RosaClient, cluster *cmv1.Cluster) error {
398446
rosaScope.Debug("Reconciling ROSA kubeconfig for cluster", "cluster-name", rosaScope.RosaClusterName())
399447

@@ -510,6 +558,26 @@ func (r *ROSAControlPlaneReconciler) reconcileClusterAdminPassword(ctx context.C
510558
return password, nil
511559
}
512560

561+
func validateControlPlaneSpec(rosaClient *rosa.RosaClient, rosaScope *scope.ROSAControlPlaneScope) (bool, error) {
562+
// reset previous message.
563+
rosaScope.ControlPlane.Status.FailureMessage = nil
564+
565+
version := rosaScope.ControlPlane.Spec.Version
566+
isSupported, err := rosaClient.IsVersionSupported(version)
567+
if err != nil {
568+
return false, err
569+
}
570+
571+
if !isSupported {
572+
message := fmt.Sprintf("version %s is not supported", version)
573+
rosaScope.ControlPlane.Status.FailureMessage = &message
574+
return false, nil
575+
}
576+
577+
// TODO: add more input validations
578+
return true, nil
579+
}
580+
513581
func (r *ROSAControlPlaneReconciler) rosaClusterToROSAControlPlane(log *logger.Logger) handler.MapFunc {
514582
return func(ctx context.Context, o client.Object) []ctrl.Request {
515583
rosaCluster, ok := o.(*expinfrav1.ROSACluster)

pkg/rosa/versions.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package rosa
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
8+
)
9+
10+
// IsVersionSupported checks whether the input version is supported for ROSA clusters.
11+
func (c *RosaClient) IsVersionSupported(versionID string) (bool, error) {
12+
filter := fmt.Sprintf("raw_id='%s' AND channel_group = '%s'", versionID, "stable")
13+
response, err := c.ocm.ClustersMgmt().V1().
14+
Versions().
15+
List().
16+
Search(filter).
17+
Page(1).Size(1).
18+
Parameter("product", "hcp").
19+
Send()
20+
if err != nil {
21+
return false, handleErr(response.Error(), err)
22+
}
23+
if response.Total() == 0 {
24+
return false, nil
25+
}
26+
27+
version := response.Items().Get(0)
28+
return version.ROSAEnabled() && version.HostedControlPlaneEnabled(), nil
29+
}
30+
31+
// CheckExistingScheduledUpgrade checks and returns the current upgrade schedule if any.
32+
func (c *RosaClient) CheckExistingScheduledUpgrade(cluster *cmv1.Cluster) (*cmv1.ControlPlaneUpgradePolicy, error) {
33+
upgradePolicies, err := c.getControlPlaneUpgradePolicies(cluster.ID())
34+
if err != nil {
35+
return nil, err
36+
}
37+
for _, upgradePolicy := range upgradePolicies {
38+
if upgradePolicy.UpgradeType() == cmv1.UpgradeTypeControlPlane {
39+
return upgradePolicy, nil
40+
}
41+
}
42+
return nil, nil
43+
}
44+
45+
// ScheduleControlPlaneUpgrade schedules a new control plane upgrade to the specified version at the specified time.
46+
func (c *RosaClient) ScheduleControlPlaneUpgrade(cluster *cmv1.Cluster, version string, nextRun time.Time) (*cmv1.ControlPlaneUpgradePolicy, error) {
47+
// earliestNextRun is set to at least 5 min from now by the OCM API.
48+
// we set it to 6 min here to account for latencty.
49+
earliestNextRun := time.Now().Add(time.Minute * 6)
50+
if nextRun.Before(earliestNextRun) {
51+
nextRun = earliestNextRun
52+
}
53+
54+
upgradePolicy, err := cmv1.NewControlPlaneUpgradePolicy().
55+
UpgradeType(cmv1.UpgradeTypeControlPlane).
56+
ScheduleType(cmv1.ScheduleTypeManual).
57+
Version(version).
58+
NextRun(nextRun).
59+
Build()
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
response, err := c.ocm.ClustersMgmt().V1().
65+
Clusters().Cluster(cluster.ID()).
66+
ControlPlane().
67+
UpgradePolicies().
68+
Add().Body(upgradePolicy).
69+
Send()
70+
if err != nil {
71+
return nil, handleErr(response.Error(), err)
72+
}
73+
74+
return response.Body(), nil
75+
}
76+
77+
func (c *RosaClient) getControlPlaneUpgradePolicies(clusterID string) (controlPlaneUpgradePolicies []*cmv1.ControlPlaneUpgradePolicy, err error) {
78+
collection := c.ocm.ClustersMgmt().V1().
79+
Clusters().
80+
Cluster(clusterID).
81+
ControlPlane().
82+
UpgradePolicies()
83+
page := 1
84+
size := 100
85+
for {
86+
response, err := collection.List().
87+
Page(page).
88+
Size(size).
89+
Send()
90+
if err != nil {
91+
return nil, handleErr(response.Error(), err)
92+
}
93+
controlPlaneUpgradePolicies = append(controlPlaneUpgradePolicies, response.Items().Slice()...)
94+
if response.Size() < size {
95+
break
96+
}
97+
page++
98+
}
99+
return
100+
}

0 commit comments

Comments
 (0)