Skip to content

Commit e63fee9

Browse files
committed
Add RosaClusterName field to ROSAControlPlane
- ensure RosaClusterName is valid using kubebuild validation - moved ocmClient to a seperate package and renamed to rosaClient - updated cluster-template-rosa.yaml - set ControlPlane.Status.Initialized - requeue ROSAControlPlane to poll cluster status until ready
1 parent 936018e commit e63fee9

File tree

9 files changed

+235
-185
lines changed

9 files changed

+235
-185
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,17 @@ spec:
237237
- nodePoolManagementARN
238238
- storageARN
239239
type: object
240+
rosaClusterName:
241+
description: Cluster name must be valid DNS-1035 label, so it must
242+
consist of lower case alphanumeric characters or '-', start with
243+
an alphabetic character, end with an alphanumeric character and
244+
have a max length of 15 characters.
245+
maxLength: 15
246+
pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$
247+
type: string
248+
x-kubernetes-validations:
249+
- message: rosaClusterName is immutable
250+
rule: self == oldSelf
240251
subnets:
241252
description: The Subnet IDs to use when installing the cluster. SubnetIDs
242253
should come in pairs; two per availability zone, one private and
@@ -260,6 +271,7 @@ spec:
260271
- oidcID
261272
- region
262273
- rolesRef
274+
- rosaClusterName
263275
- subnets
264276
- supportRoleARN
265277
- version

controlplane/rosa/api/v1beta2/rosacontrolplane_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ import (
2323
)
2424

2525
type RosaControlPlaneSpec struct { //nolint: maligned
26+
// Cluster name must be valid DNS-1035 label, so it must consist of lower case alphanumeric
27+
// characters or '-', start with an alphabetic character, end with an alphanumeric character
28+
// and have a max length of 15 characters.
29+
//
30+
// +immutable
31+
// +kubebuilder:validation:XValidation:rule="self == oldSelf", message="rosaClusterName is immutable"
32+
// +kubebuilder:validation:MaxLength:=15
33+
// +kubebuilder:validation:Pattern:=`^[a-z]([-a-z0-9]*[a-z0-9])?$`
34+
RosaClusterName string `json:"rosaClusterName"`
35+
2636
// The Subnet IDs to use when installing the cluster.
2737
// SubnetIDs should come in pairs; two per availability zone, one private and one public.
2838
Subnets []string `json:"subnets"`

controlplane/rosa/controllers/rosacontrolplane_controller.go

Lines changed: 52 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import (
2424
"strings"
2525
"time"
2626

27-
sdk "github.com/openshift-online/ocm-sdk-go"
2827
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
29-
ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"
3028
apierrors "k8s.io/apimachinery/pkg/api/errors"
3129
"k8s.io/apimachinery/pkg/types"
3230
ctrl "sigs.k8s.io/controller-runtime"
@@ -40,6 +38,7 @@ import (
4038
expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2"
4139
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
4240
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger"
41+
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/rosa"
4342
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
4443
"sigs.k8s.io/cluster-api/util"
4544
capiannotations "sigs.k8s.io/cluster-api/util/annotations"
@@ -171,9 +170,47 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
171170
}
172171
}
173172

173+
// TODO: token should be read from a secret: https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues/4460
174+
token := os.Getenv("OCM_TOKEN")
175+
rosaClient, err := rosa.NewRosaClient(token)
176+
if err != nil {
177+
return ctrl.Result{}, fmt.Errorf("failed to create a rosa client: %w", err)
178+
}
179+
180+
defer func() {
181+
rosaClient.Close()
182+
}()
183+
184+
cluster, err := rosaClient.GetCluster(rosaScope.RosaClusterName(), rosaScope.ControlPlane.Spec.CreatorARN)
185+
if err != nil {
186+
return ctrl.Result{}, err
187+
}
188+
189+
if clusterID := cluster.ID(); clusterID != "" {
190+
rosaScope.ControlPlane.Status.ID = &clusterID
191+
if cluster.Status().State() == "ready" {
192+
conditions.MarkTrue(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneReadyCondition)
193+
rosaScope.ControlPlane.Status.Ready = true
194+
// TODO: distinguish when controlPlane is ready vs initialized
195+
rosaScope.ControlPlane.Status.Initialized = true
196+
197+
return ctrl.Result{}, nil
198+
}
199+
200+
conditions.MarkFalse(rosaScope.ControlPlane,
201+
rosacontrolplanev1.ROSAControlPlaneReadyCondition,
202+
string(cluster.Status().State()),
203+
clusterv1.ConditionSeverityInfo,
204+
"")
205+
206+
rosaScope.Info("waiting for cluster to become ready", "state", cluster.Status().State())
207+
// Requeue so that status.ready is set to true when the cluster is fully created.
208+
return ctrl.Result{RequeueAfter: time.Second * 60}, nil
209+
}
210+
174211
// Create the cluster:
175212
clusterBuilder := cmv1.NewCluster().
176-
Name(rosaScope.ControlPlane.Name[:15]).
213+
Name(rosaScope.RosaClusterName()).
177214
MultiAZ(true).
178215
Product(
179216
cmv1.NewProduct().
@@ -283,86 +320,46 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
283320
return ctrl.Result{}, fmt.Errorf("failed to create description of cluster: %v", err)
284321
}
285322

286-
// create OCM NodePool
287-
ocmClient, err := newOCMClient()
323+
newCluster, err := rosaClient.CreateCluster(clusterSpec)
288324
if err != nil {
289-
return ctrl.Result{}, err
290-
}
291-
defer func() {
292-
ocmClient.ocm.Close()
293-
}()
294-
295-
cluster, err := ocmClient.GetCluster(rosaScope)
296-
if err != nil {
297-
return ctrl.Result{}, err
298-
}
299-
300-
log := logger.FromContext(ctx)
301-
if cluster.ID() != "" {
302-
clusterID := cluster.ID()
303-
rosaScope.ControlPlane.Status.ID = &clusterID
304-
conditions.MarkFalse(rosaScope.ControlPlane,
305-
rosacontrolplanev1.ROSAControlPlaneReadyCondition,
306-
string(cluster.Status().State()),
307-
clusterv1.ConditionSeverityInfo,
308-
"")
309-
310-
if cluster.Status().State() == "ready" {
311-
conditions.MarkTrue(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneReadyCondition)
312-
rosaScope.ControlPlane.Status.Ready = true
313-
}
314-
315-
if err := rosaScope.PatchObject(); err != nil {
316-
return ctrl.Result{}, err
317-
}
318-
319-
log.Info("cluster exists", "state", cluster.Status().State())
320-
return ctrl.Result{}, nil
321-
}
322-
323-
newCluster, err := ocmClient.CreateCluster(clusterSpec)
324-
if err != nil {
325-
log.Info("error", "error", err)
325+
rosaScope.Info("error", "error", err)
326326
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
327327
}
328328

329-
log.Info("cluster created", "state", newCluster.Status().State())
329+
rosaScope.Info("cluster created", "state", newCluster.Status().State())
330330
clusterID := newCluster.ID()
331331
rosaScope.ControlPlane.Status.ID = &clusterID
332-
if err := rosaScope.PatchObject(); err != nil {
333-
return ctrl.Result{}, err
334-
}
335332

336333
return ctrl.Result{}, nil
337334
}
338335

339336
func (r *ROSAControlPlaneReconciler) reconcileDelete(_ context.Context, rosaScope *scope.ROSAControlPlaneScope) (res ctrl.Result, reterr error) {
340337
rosaScope.Info("Reconciling ROSAControlPlane delete")
341338

342-
// create OCM NodePool
343-
ocmClient, err := newOCMClient()
339+
// Create the connection, and remember to close it:
340+
// TODO: token should be read from a secret: https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues/4460
341+
token := os.Getenv("OCM_TOKEN")
342+
rosaClient, err := rosa.NewRosaClient(token)
344343
if err != nil {
345-
return ctrl.Result{}, err
344+
return ctrl.Result{}, fmt.Errorf("failed to create a rosa client: %w", err)
346345
}
346+
347347
defer func() {
348-
ocmClient.ocm.Close()
348+
rosaClient.Close()
349349
}()
350350

351-
cluster, err := ocmClient.GetCluster(rosaScope)
351+
cluster, err := rosaClient.GetCluster(rosaScope.RosaClusterName(), rosaScope.ControlPlane.Spec.CreatorARN)
352352
if err != nil {
353353
return ctrl.Result{}, err
354354
}
355355

356356
if cluster != nil {
357-
if _, err := ocmClient.DeleteCluster(cluster.ID()); err != nil {
357+
if err := rosaClient.DeleteCluster(cluster.ID()); err != nil {
358358
return ctrl.Result{}, err
359359
}
360360
}
361361

362362
controllerutil.RemoveFinalizer(rosaScope.ControlPlane, ROSAControlPlaneFinalizer)
363-
if err := rosaScope.PatchObject(); err != nil {
364-
return ctrl.Result{}, err
365-
}
366363

367364
return ctrl.Result{}, nil
368365
}
@@ -406,119 +403,3 @@ func (r *ROSAControlPlaneReconciler) rosaClusterToROSAControlPlane(log *logger.L
406403
}
407404
}
408405
}
409-
410-
// OCMClient is a temporary helper to talk to OCM API.
411-
// TODO(alberto): vendor this from https://github.com/openshift/rosa/tree/master/pkg/ocm or build its own package here.
412-
type OCMClient struct {
413-
ocm *sdk.Connection
414-
}
415-
416-
func newOCMClient() (*OCMClient, error) {
417-
// Create the connection, and remember to close it:
418-
token := os.Getenv("OCM_TOKEN")
419-
ocmAPIUrl := os.Getenv("OCM_API_URL")
420-
if ocmAPIUrl == "" {
421-
ocmAPIUrl = "https://api.openshift.com"
422-
}
423-
424-
// Create a logger that has the debug level enabled:
425-
ocmLogger, err := sdk.NewGoLoggerBuilder().
426-
Debug(false).
427-
Build()
428-
if err != nil {
429-
return nil, fmt.Errorf("failed to build logger: %w", err)
430-
}
431-
432-
connection, err := sdk.NewConnectionBuilder().
433-
Logger(ocmLogger).
434-
Tokens(token).
435-
URL(ocmAPIUrl).
436-
Build()
437-
if err != nil {
438-
return nil, fmt.Errorf("failed to ocm client: %w", err)
439-
}
440-
ocmClient := OCMClient{ocm: connection}
441-
442-
return &ocmClient, nil
443-
}
444-
445-
func (client *OCMClient) Close() error {
446-
return client.ocm.Close()
447-
}
448-
449-
func (client *OCMClient) CreateCluster(clusterSpec *cmv1.Cluster) (*cmv1.Cluster, error) {
450-
cluster, err := client.ocm.ClustersMgmt().V1().Clusters().
451-
Add().
452-
Body(clusterSpec).
453-
Send()
454-
if err != nil {
455-
return nil, handleErr(cluster.Error(), err)
456-
}
457-
458-
clusterObject := cluster.Body()
459-
460-
return clusterObject, nil
461-
}
462-
463-
func (client *OCMClient) GetCluster(rosaScope *scope.ROSAControlPlaneScope) (*cmv1.Cluster, error) {
464-
clusterKey := rosaScope.ControlPlane.Name[:15]
465-
query := fmt.Sprintf("%s AND (id = '%s' OR name = '%s' OR external_id = '%s')",
466-
getClusterFilter(rosaScope),
467-
clusterKey, clusterKey, clusterKey,
468-
)
469-
response, err := client.ocm.ClustersMgmt().V1().Clusters().List().
470-
Search(query).
471-
Page(1).
472-
Size(1).
473-
Send()
474-
if err != nil {
475-
return nil, err
476-
}
477-
478-
switch response.Total() {
479-
case 0:
480-
return nil, nil
481-
case 1:
482-
return response.Items().Slice()[0], nil
483-
default:
484-
return nil, fmt.Errorf("there are %d clusters with identifier or name '%s'", response.Total(), clusterKey)
485-
}
486-
}
487-
488-
func (client *OCMClient) DeleteCluster(clusterID string) (*cmv1.Cluster, error) {
489-
response, err := client.ocm.ClustersMgmt().V1().Clusters().
490-
Cluster(clusterID).
491-
Delete().
492-
Send()
493-
if err != nil {
494-
return nil, handleErr(response.Error(), err)
495-
}
496-
497-
return nil, nil
498-
}
499-
500-
// Generate a query that filters clusters running on the current AWS session account.
501-
func getClusterFilter(rosaScope *scope.ROSAControlPlaneScope) string {
502-
filter := "product.id = 'rosa'"
503-
if rosaScope.ControlPlane.Spec.CreatorARN != nil {
504-
filter = fmt.Sprintf("%s AND (properties.%s = '%s')",
505-
filter,
506-
rosaCreatorArnProperty,
507-
*rosaScope.ControlPlane.Spec.CreatorARN)
508-
}
509-
return filter
510-
}
511-
512-
func handleErr(res *ocmerrors.Error, err error) error {
513-
msg := res.Reason()
514-
if msg == "" {
515-
msg = err.Error()
516-
}
517-
// Hack to always display the correct terms and conditions message
518-
if res.Code() == "CLUSTERS-MGMT-451" {
519-
msg = "You must accept the Terms and Conditions in order to continue.\n" +
520-
"Go to https://www.redhat.com/wapps/tnc/ackrequired?site=ocm&event=register\n" +
521-
"Once you accept the terms, you will need to retry the action that was blocked."
522-
}
523-
return fmt.Errorf(msg)
524-
}

docs/book/src/topics/rosa/creating-a-cluster.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Once Step 3 is done, you will be ready to proceed with creating a ROSA cluster u
2929

3030
1. Prepare the environment:
3131
```bash
32-
export OPENSHIFT_VERSION="openshift-v4.12.15"
32+
export OPENSHIFT_VERSION="openshift-v4.14.5"
3333
export CLUSTER_NAME="capi-rosa-quickstart"
3434
export AWS_REGION="us-west-2"
3535
export AWS_AVAILABILITY_ZONE="us-west-2a"

pkg/cloud/scope/rosacontrolplane.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ package scope
1919
import (
2020
"context"
2121

22-
amazoncni "github.com/aws/amazon-vpc-cni-k8s/pkg/apis/crd/v1alpha1"
2322
"github.com/pkg/errors"
24-
appsv1 "k8s.io/api/apps/v1"
25-
corev1 "k8s.io/api/core/v1"
26-
rbacv1 "k8s.io/api/rbac/v1"
2723
"k8s.io/klog/v2"
2824
"sigs.k8s.io/controller-runtime/pkg/client"
2925

@@ -33,13 +29,6 @@ import (
3329
"sigs.k8s.io/cluster-api/util/patch"
3430
)
3531

36-
func init() {
37-
_ = amazoncni.AddToScheme(scheme)
38-
_ = appsv1.AddToScheme(scheme)
39-
_ = corev1.AddToScheme(scheme)
40-
_ = rbacv1.AddToScheme(scheme)
41-
}
42-
4332
type ROSAControlPlaneScopeParams struct {
4433
Client client.Client
4534
Logger *logger.Logger
@@ -93,11 +82,14 @@ func (s *ROSAControlPlaneScope) Name() string {
9382
}
9483

9584
// InfraClusterName returns the AWS cluster name.
96-
9785
func (s *ROSAControlPlaneScope) InfraClusterName() string {
9886
return s.ControlPlane.Name
9987
}
10088

89+
func (s *ROSAControlPlaneScope) RosaClusterName() string {
90+
return s.ControlPlane.Spec.RosaClusterName
91+
}
92+
10193
// Namespace returns the cluster namespace.
10294
func (s *ROSAControlPlaneScope) Namespace() string {
10395
return s.Cluster.Namespace

0 commit comments

Comments
 (0)