@@ -20,11 +20,17 @@ import (
2020 "context"
2121 "fmt"
2222
23+ cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
2324 "github.com/pkg/errors"
25+ corev1 "k8s.io/api/core/v1"
2426 apierrors "k8s.io/apimachinery/pkg/api/errors"
27+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+ "k8s.io/apimachinery/pkg/runtime"
2529 "k8s.io/apimachinery/pkg/types"
30+ kerrors "k8s.io/apimachinery/pkg/util/errors"
2631 "k8s.io/client-go/tools/record"
2732 "k8s.io/klog/v2"
33+ "k8s.io/utils/ptr"
2834 ctrl "sigs.k8s.io/controller-runtime"
2935 "sigs.k8s.io/controller-runtime/pkg/client"
3036 "sigs.k8s.io/controller-runtime/pkg/controller"
@@ -35,9 +41,15 @@ import (
3541 infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
3642 rosacontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2"
3743 expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2"
44+ "sigs.k8s.io/cluster-api-provider-aws/v2/exp/utils"
45+ "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud"
46+ "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
47+ stsservice "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/sts"
3848 "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger"
49+ "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/rosa"
3950 "sigs.k8s.io/cluster-api-provider-aws/v2/util/paused"
4051 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
52+ expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
4153 "sigs.k8s.io/cluster-api/util"
4254 "sigs.k8s.io/cluster-api/util/patch"
4355 "sigs.k8s.io/cluster-api/util/predicates"
@@ -48,16 +60,20 @@ type ROSAClusterReconciler struct {
4860 client.Client
4961 Recorder record.EventRecorder
5062 WatchFilterValue string
63+ NewStsClient func (cloud.ScopeUsage , cloud.Session , logger.Wrapper , runtime.Object ) stsservice.STSClient
64+ NewOCMClient func (ctx context.Context , rosaScope * scope.ROSAControlPlaneScope ) (rosa.OCMClient , error )
5165}
5266
5367// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaclusters,verbs=get;list;watch;update;patch;delete
5468// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaclusters/status,verbs=get;update;patch
5569// +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=rosacontrolplanes;rosacontrolplanes/status,verbs=get;list;watch
5670// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch
71+ // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools;machinepools/status,verbs=get;list;watch;create
72+ // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosamachinepools;rosamachinepools/status,verbs=get;list;watch;create
5773// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
5874
5975func (r * ROSAClusterReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (_ ctrl.Result , reterr error ) {
60- log := ctrl . LoggerFrom (ctx )
76+ log := logger . FromContext (ctx )
6177 log .Info ("Reconciling ROSACluster" )
6278
6379 // Fetch the ROSACluster instance
@@ -70,11 +86,17 @@ func (r *ROSAClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
7086 return reconcile.Result {}, err
7187 }
7288
89+ if ! rosaCluster .DeletionTimestamp .IsZero () {
90+ log .Info ("Deleting ROSACluster." )
91+ return reconcile.Result {}, nil
92+ }
93+
7394 // Fetch the Cluster.
7495 cluster , err := util .GetOwnerCluster (ctx , r .Client , rosaCluster .ObjectMeta )
7596 if err != nil {
7697 return reconcile.Result {}, err
7798 }
99+
78100 if cluster == nil {
79101 log .Info ("Cluster Controller has not yet set OwnerRef" )
80102 return reconcile.Result {}, nil
@@ -111,13 +133,78 @@ func (r *ROSAClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
111133 return reconcile.Result {}, fmt .Errorf ("failed to patch ROSACluster: %w" , err )
112134 }
113135
136+ if controlPlane .Status .Ready {
137+ rosaScope , err := scope .NewROSAControlPlaneScope (scope.ROSAControlPlaneScopeParams {
138+ Client : r .Client ,
139+ Cluster : cluster ,
140+ ControlPlane : controlPlane ,
141+ ControllerName : "" ,
142+ Logger : log ,
143+ NewStsClient : r .NewStsClient ,
144+ })
145+ if err != nil {
146+ return ctrl.Result {}, fmt .Errorf ("failed to create rosa controlplane scope: %w" , err )
147+ }
148+
149+ if r .NewOCMClient == nil {
150+ return ctrl.Result {}, fmt .Errorf ("failed to create OCM client: NewOCMClient is nil" )
151+ }
152+
153+ ocmClient , err := r .NewOCMClient (ctx , rosaScope )
154+ if err != nil || ocmClient == nil {
155+ return ctrl.Result {}, fmt .Errorf ("failed to create OCM client: %w" , err )
156+ }
157+
158+ // List the ROSA-HCP nodePools and MachinePools
159+ nodePools , err := ocmClient .GetNodePools (rosaScope .ControlPlane .Status .ID )
160+ if err != nil {
161+ return ctrl.Result {}, fmt .Errorf ("failed to get nodePools: %w" , err )
162+ }
163+
164+ rosaMPNames , err := r .getRosaMachinePoolNames (ctx , cluster )
165+ if err != nil {
166+ return ctrl.Result {}, fmt .Errorf ("failed to get Rosa machinePool names: %w" , err )
167+ }
168+
169+ // Ensure every NodePool has a MachinePool and create a corresponding MachinePool if it does not exist.
170+ var errs []error
171+ for _ , nodePool := range nodePools {
172+ // continue if nodePool is not in ready state.
173+ if ! rosa .IsNodePoolReady (nodePool ) {
174+ continue
175+ }
176+ // continue if nodePool exist
177+ if rosaMPNames [nodePool .ID ()] {
178+ continue
179+ }
180+ // create ROSAMachinePool & MachinePool
181+ rosaMachinePool , machinePool := r .buildROSAMachinePool (nodePool .ID (), cluster .Name , cluster .Namespace , nodePool )
182+
183+ log .Info (fmt .Sprintf ("Create ROSAMachinePool %s" , rosaMachinePool .Name ))
184+ if err = r .Client .Create (ctx , rosaMachinePool ); err != nil {
185+ errs = append (errs , err )
186+ }
187+
188+ log .Info (fmt .Sprintf ("Create MachinePool %s" , machinePool .Name ))
189+ if err = r .Client .Create (ctx , machinePool ); err != nil {
190+ errs = append (errs , err )
191+ }
192+ }
193+
194+ if len (errs ) > 0 {
195+ return ctrl.Result {}, kerrors .NewAggregate (errs )
196+ }
197+ }
198+
114199 log .Info ("Successfully reconciled ROSACluster" )
115200
116201 return reconcile.Result {}, nil
117202}
118203
119204func (r * ROSAClusterReconciler ) SetupWithManager (ctx context.Context , mgr ctrl.Manager , options controller.Options ) error {
120205 log := logger .FromContext (ctx )
206+ r .NewOCMClient = rosa .NewWrappedOCMClient
207+ r .NewStsClient = scope .NewSTSClient
121208
122209 rosaCluster := & expinfrav1.ROSACluster {}
123210
@@ -196,3 +283,77 @@ func (r *ROSAClusterReconciler) rosaControlPlaneToManagedCluster(log *logger.Log
196283 }
197284 }
198285}
286+
287+ // getRosMachinePools returns a map of RosaMachinePool names associatd with the cluster.
288+ func (r * ROSAClusterReconciler ) getRosaMachinePoolNames (ctx context.Context , cluster * clusterv1.Cluster ) (map [string ]bool , error ) {
289+ selectors := []client.ListOption {
290+ client .InNamespace (cluster .GetNamespace ()),
291+ client.MatchingLabels {
292+ clusterv1 .ClusterNameLabel : cluster .GetName (),
293+ },
294+ }
295+
296+ rosaMachinePoolList := & expinfrav1.ROSAMachinePoolList {}
297+ err := r .Client .List (ctx , rosaMachinePoolList , selectors ... )
298+ if err != nil {
299+ return nil , err
300+ }
301+
302+ rosaMPNames := make (map [string ]bool )
303+ for _ , rosaMP := range rosaMachinePoolList .Items {
304+ rosaMPNames [rosaMP .Spec .NodePoolName ] = true
305+ }
306+
307+ return rosaMPNames , nil
308+ }
309+
310+ // buildROSAMachinePool returns a ROSAMachinePool and its corresponding MachinePool.
311+ func (r * ROSAClusterReconciler ) buildROSAMachinePool (nodePoolName string , clusterName string , namespace string , nodePool * cmv1.NodePool ) (* expinfrav1.ROSAMachinePool , * expclusterv1.MachinePool ) {
312+ rosaMPSpec := utils .NodePoolToRosaMachinePoolSpec (nodePool )
313+ rosaMachinePool := & expinfrav1.ROSAMachinePool {
314+ TypeMeta : metav1.TypeMeta {
315+ APIVersion : expinfrav1 .GroupVersion .String (),
316+ Kind : "ROSAMachinePool" ,
317+ },
318+ ObjectMeta : metav1.ObjectMeta {
319+ Name : nodePoolName ,
320+ Namespace : namespace ,
321+ Labels : map [string ]string {
322+ clusterv1 .ClusterNameLabel : clusterName ,
323+ },
324+ },
325+ Spec : rosaMPSpec ,
326+ }
327+ machinePool := & expclusterv1.MachinePool {
328+ TypeMeta : metav1.TypeMeta {
329+ APIVersion : expclusterv1 .GroupVersion .String (),
330+ Kind : "MachinePool" ,
331+ },
332+ ObjectMeta : metav1.ObjectMeta {
333+ Name : nodePoolName ,
334+ Namespace : namespace ,
335+ Labels : map [string ]string {
336+ clusterv1 .ClusterNameLabel : clusterName ,
337+ },
338+ },
339+ Spec : expclusterv1.MachinePoolSpec {
340+ ClusterName : clusterName ,
341+ Replicas : ptr .To (int32 (1 )),
342+ Template : clusterv1.MachineTemplateSpec {
343+ Spec : clusterv1.MachineSpec {
344+ ClusterName : clusterName ,
345+ Bootstrap : clusterv1.Bootstrap {
346+ DataSecretName : ptr .To (string ("" )),
347+ },
348+ InfrastructureRef : corev1.ObjectReference {
349+ APIVersion : expinfrav1 .GroupVersion .String (),
350+ Kind : "ROSAMachinePool" ,
351+ Name : rosaMachinePool .Name ,
352+ },
353+ },
354+ },
355+ },
356+ }
357+
358+ return rosaMachinePool , machinePool
359+ }
0 commit comments