diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml index 071ced9241..22e808ed82 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -787,6 +787,19 @@ spec: version: description: OpenShift semantic version, for example "4.14.5". type: string + versionGate: + default: WaitForAcknowledge + description: |- + VersionGate requires acknowledgment when upgrading ROSA-HCP y-stream versions (e.g., from 4.15 to 4.16). + Default is WaitForAcknowledge. + WaitForAcknowledge: If acknowledgment is required, the upgrade will not proceed until VersionGate is set to Acknowledge or AlwaysAcknowledge. + Acknowledge: If acknowledgment is required, apply it for the upgrade. After upgrade is done set the version gate to WaitForAcknowledge. + AlwaysAcknowledge: If acknowledgment is required, apply it and proceed with the upgrade. + enum: + - Acknowledge + - WaitForAcknowledge + - AlwaysAcknowledge + type: string workerRoleARN: description: WorkerRoleARN is an AWS IAM role that will be attached to worker instances. @@ -801,11 +814,17 @@ spec: - subnets - supportRoleARN - version + - versionGate - workerRoleARN type: object status: description: RosaControlPlaneStatus defines the observed state of ROSAControlPlane. properties: + availableUpgrades: + description: Available upgrades for the ROSA hosted control plane. + items: + type: string + type: array conditions: description: Conditions specifies the conditions for the managed control plane diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go index 27f2ea83f7..f14c59f781 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go @@ -38,6 +38,20 @@ const ( Private RosaEndpointAccessType = "Private" ) +// VersionGateAckType specifies the version gate acknowledgement. +type VersionGateAckType string + +const ( + // Acknowledge if acknowledgment is required and proceed with the upgrade. + Acknowledge VersionGateAckType = "Acknowledge" + + // WaitForAcknowledge if acknowledgment is required, wait not to proceed with the upgrade. + WaitForAcknowledge VersionGateAckType = "WaitForAcknowledge" + + // AlwaysAcknowledge always acknowledg if required and proceed with the upgrade. + AlwaysAcknowledge VersionGateAckType = "AlwaysAcknowledge" +) + // RosaControlPlaneSpec defines the desired state of ROSAControlPlane. type RosaControlPlaneSpec struct { //nolint: maligned // Cluster name must be valid DNS-1035 label, so it must consist of lower case alphanumeric @@ -77,6 +91,16 @@ type RosaControlPlaneSpec struct { //nolint: maligned // OpenShift semantic version, for example "4.14.5". Version string `json:"version"` + // VersionGate requires acknowledgment when upgrading ROSA-HCP y-stream versions (e.g., from 4.15 to 4.16). + // Default is WaitForAcknowledge. + // WaitForAcknowledge: If acknowledgment is required, the upgrade will not proceed until VersionGate is set to Acknowledge or AlwaysAcknowledge. + // Acknowledge: If acknowledgment is required, apply it for the upgrade. After upgrade is done set the version gate to WaitForAcknowledge. + // AlwaysAcknowledge: If acknowledgment is required, apply it and proceed with the upgrade. + // + // +kubebuilder:validation:Enum=Acknowledge;WaitForAcknowledge;AlwaysAcknowledge + // +kubebuilder:default=WaitForAcknowledge + VersionGate VersionGateAckType `json:"versionGate"` + // AWS IAM roles used to perform credential requests by the openshift operators. RolesRef AWSRolesRef `json:"rolesRef"` @@ -697,6 +721,9 @@ type RosaControlPlaneStatus struct { ConsoleURL string `json:"consoleURL,omitempty"` // OIDCEndpointURL is the endpoint url for the managed OIDC provider. OIDCEndpointURL string `json:"oidcEndpointURL,omitempty"` + + // Available upgrades for the ROSA hosted control plane. + AvailableUpgrades []string `json:"availableUpgrades,omitempty"` } // +kubebuilder:object:root=true diff --git a/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go b/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go index e44f018e28..3e4dfdf8cf 100644 --- a/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go +++ b/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go @@ -380,6 +380,11 @@ func (in *RosaControlPlaneStatus) DeepCopyInto(out *RosaControlPlaneStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.AvailableUpgrades != nil { + in, out := &in.AvailableUpgrades, &out.AvailableUpgrades + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RosaControlPlaneStatus. diff --git a/controlplane/rosa/controllers/rosacontrolplane_controller.go b/controlplane/rosa/controllers/rosacontrolplane_controller.go index 66dc3c3c96..5a5b07a718 100644 --- a/controlplane/rosa/controllers/rosacontrolplane_controller.go +++ b/controlplane/rosa/controllers/rosacontrolplane_controller.go @@ -410,6 +410,17 @@ func (r *ROSAControlPlaneReconciler) reconcileClusterVersion(rosaScope *scope.RO version := rosaScope.ControlPlane.Spec.Version if version == rosa.RawVersionID(cluster.Version()) { conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneUpgradingCondition, "upgraded", clusterv1.ConditionSeverityInfo, "") + + if cluster.Version() != nil { + rosaScope.ControlPlane.Status.AvailableUpgrades = cluster.Version().AvailableUpgrades() + } + + // Set the version gate to WaitForAcknowledge as the previous upgrade is applied. + if rosaScope.ControlPlane.Spec.VersionGate == rosacontrolplanev1.Acknowledge { + rosaScope.ControlPlane.Spec.VersionGate = rosacontrolplanev1.WaitForAcknowledge + } + + // return as there is no upgrade to schedule. return nil } @@ -419,9 +430,18 @@ func (r *ROSAControlPlaneReconciler) reconcileClusterVersion(rosaScope *scope.RO } if scheduledUpgrade == nil { - scheduledUpgrade, err = rosa.ScheduleControlPlaneUpgrade(ocmClient, cluster, version, time.Now()) + ack := (rosaScope.ControlPlane.Spec.VersionGate == rosacontrolplanev1.Acknowledge || rosaScope.ControlPlane.Spec.VersionGate == rosacontrolplanev1.AlwaysAcknowledge) + scheduledUpgrade, err = rosa.ScheduleControlPlaneUpgrade(ocmClient, cluster, version, time.Now(), ack) if err != nil { - return fmt.Errorf("failed to schedule control plane upgrade to version %s: %w", version, err) + condition := &clusterv1.Condition{ + Type: rosacontrolplanev1.ROSAControlPlaneUpgradingCondition, + Status: corev1.ConditionFalse, + Reason: "failed", + Message: fmt.Sprintf("failed to schedule upgrade to version %s: %v", version, err), + } + conditions.Set(rosaScope.ControlPlane, condition) + + return err } } diff --git a/pkg/rosa/versions.go b/pkg/rosa/versions.go index d300adbf96..27136c8772 100644 --- a/pkg/rosa/versions.go +++ b/pkg/rosa/versions.go @@ -27,7 +27,7 @@ func CheckExistingScheduledUpgrade(client *ocm.Client, cluster *cmv1.Cluster) (* } // ScheduleControlPlaneUpgrade schedules a new control plane upgrade to the specified version at the specified time. -func ScheduleControlPlaneUpgrade(client *ocm.Client, cluster *cmv1.Cluster, version string, nextRun time.Time) (*cmv1.ControlPlaneUpgradePolicy, error) { +func ScheduleControlPlaneUpgrade(client *ocm.Client, cluster *cmv1.Cluster, version string, nextRun time.Time, ack bool) (*cmv1.ControlPlaneUpgradePolicy, error) { // earliestNextRun is set to at least 5 min from now by the OCM API. // Set our next run request to something slightly longer than 5min to make sure we account for the latency between when we send this // request and when the server processes it. @@ -41,10 +41,32 @@ func ScheduleControlPlaneUpgrade(client *ocm.Client, cluster *cmv1.Cluster, vers ScheduleType(cmv1.ScheduleTypeManual). Version(version). NextRun(nextRun). + EnableMinorVersionUpgrades(true). Build() if err != nil { return nil, err } + + versionGates, err := client.GetMissingGateAgreementsHypershift(cluster.ID(), upgradePolicy) + if err != nil { + return nil, err + } + + if !ack && len(versionGates) > 0 { + errMess := "version gate acknowledgement required" + for id := range versionGates { + errMess = fmt.Sprintf(errMess+"\nid:%s\n %s\n %s\n %s\n", versionGates[id].ID(), versionGates[id].Description(), versionGates[id].DocumentationURL(), versionGates[id].WarningMessage()) + } + + return nil, fmt.Errorf("%s", errMess) + } + + for id := range versionGates { + if err = client.AckVersionGate(cluster.ID(), versionGates[id].ID()); err != nil { + return nil, err + } + } + return client.ScheduleHypershiftControlPlaneUpgrade(cluster.ID(), upgradePolicy) } @@ -64,6 +86,7 @@ func ScheduleNodePoolUpgrade(client *ocm.Client, clusterID string, nodePool *cmv ScheduleType(cmv1.ScheduleTypeManual). Version(version). NextRun(nextRun). + EnableMinorVersionUpgrades(true). Build() if err != nil { return nil, err