@@ -20,12 +20,19 @@ import (
2020 "context"
2121 "errors"
2222 "fmt"
23+ "net"
24+ "net/url"
25+ "strconv"
2326 "strings"
2427 "time"
2528
2629 cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
2730 apierrors "k8s.io/apimachinery/pkg/api/errors"
31+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2832 "k8s.io/apimachinery/pkg/types"
33+ restclient "k8s.io/client-go/rest"
34+ "k8s.io/client-go/tools/clientcmd"
35+ "k8s.io/client-go/tools/clientcmd/api"
2936 ctrl "sigs.k8s.io/controller-runtime"
3037 "sigs.k8s.io/controller-runtime/pkg/client"
3138 "sigs.k8s.io/controller-runtime/pkg/controller"
@@ -42,7 +49,9 @@ import (
4249 "sigs.k8s.io/cluster-api/util"
4350 capiannotations "sigs.k8s.io/cluster-api/util/annotations"
4451 "sigs.k8s.io/cluster-api/util/conditions"
52+ "sigs.k8s.io/cluster-api/util/kubeconfig"
4553 "sigs.k8s.io/cluster-api/util/predicates"
54+ "sigs.k8s.io/cluster-api/util/secret"
4655)
4756
4857const (
@@ -182,11 +191,19 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
182191
183192 if clusterID := cluster .ID (); clusterID != "" {
184193 rosaScope .ControlPlane .Status .ID = & clusterID
185- if cluster .Status ().State () == "ready" {
194+ if cluster .Status ().State () == cmv1 . ClusterStateReady {
186195 conditions .MarkTrue (rosaScope .ControlPlane , rosacontrolplanev1 .ROSAControlPlaneReadyCondition )
187196 rosaScope .ControlPlane .Status .Ready = true
188- // TODO: distinguish when controlPlane is ready vs initialized
189- rosaScope .ControlPlane .Status .Initialized = true
197+
198+ apiEndpoint , err := buildAPIEndpoint (cluster )
199+ if err != nil {
200+ return ctrl.Result {}, err
201+ }
202+ rosaScope .ControlPlane .Spec .ControlPlaneEndpoint = * apiEndpoint
203+
204+ if err := r .reconcileKubeconfig (ctx , rosaScope , rosaClient , cluster ); err != nil {
205+ return ctrl.Result {}, fmt .Errorf ("failed to reconcile kubeconfig: %w" , err )
206+ }
190207
191208 return ctrl.Result {}, nil
192209 }
@@ -352,6 +369,122 @@ func (r *ROSAControlPlaneReconciler) reconcileDelete(ctx context.Context, rosaSc
352369 return ctrl.Result {}, nil
353370}
354371
372+ func (r * ROSAControlPlaneReconciler ) reconcileKubeconfig (ctx context.Context , rosaScope * scope.ROSAControlPlaneScope , rosaClient * rosa.RosaClient , cluster * cmv1.Cluster ) error {
373+ rosaScope .Debug ("Reconciling ROSA kubeconfig for cluster" , "cluster-name" , rosaScope .RosaClusterName ())
374+
375+ clusterRef := client .ObjectKeyFromObject (rosaScope .Cluster )
376+ kubeconfigSecret , err := secret .GetFromNamespacedName (ctx , r .Client , clusterRef , secret .Kubeconfig )
377+ if err != nil {
378+ if ! apierrors .IsNotFound (err ) {
379+ return fmt .Errorf ("failed to get kubeconfig secret: %w" , err )
380+ }
381+ }
382+
383+ // generate a new password for the cluster admin user, or retrieve an existing one.
384+ password , err := r .reconcileClusterAdminPassword (ctx , rosaScope )
385+ if err != nil {
386+ return fmt .Errorf ("failed to reconcile cluster admin password secret: %w" , err )
387+ }
388+
389+ clusterName := rosaScope .RosaClusterName ()
390+ userName := fmt .Sprintf ("%s-capi-admin" , clusterName )
391+ apiServerURL := cluster .API ().URL ()
392+
393+ // create new user with admin privileges in the ROSA cluster if 'userName' doesn't already exist.
394+ err = rosaClient .CreateAdminUserIfNotExist (cluster .ID (), userName , password )
395+ if err != nil {
396+ return err
397+ }
398+
399+ clientConfig := & restclient.Config {
400+ Host : apiServerURL ,
401+ Username : userName ,
402+ }
403+ // request an acccess token using the credentials of the cluster admin user created earlier.
404+ // this token is used in the kubeconfig to authenticate with the API server.
405+ token , err := rosa .RequestToken (ctx , apiServerURL , userName , password , clientConfig )
406+ if err != nil {
407+ return fmt .Errorf ("failed to request token: %w" , err )
408+ }
409+
410+ // create the kubeconfig spec.
411+ contextName := fmt .Sprintf ("%s@%s" , userName , clusterName )
412+ cfg := & api.Config {
413+ APIVersion : api .SchemeGroupVersion .Version ,
414+ Clusters : map [string ]* api.Cluster {
415+ clusterName : {
416+ Server : apiServerURL ,
417+ },
418+ },
419+ Contexts : map [string ]* api.Context {
420+ contextName : {
421+ Cluster : clusterName ,
422+ AuthInfo : userName ,
423+ },
424+ },
425+ CurrentContext : contextName ,
426+ AuthInfos : map [string ]* api.AuthInfo {
427+ userName : {
428+ Token : token .AccessToken ,
429+ },
430+ },
431+ }
432+ out , err := clientcmd .Write (* cfg )
433+ if err != nil {
434+ return fmt .Errorf ("failed to serialize config to yaml: %w" , err )
435+ }
436+
437+ if kubeconfigSecret != nil {
438+ // update existing kubeconfig secret.
439+ kubeconfigSecret .Data [secret .KubeconfigDataName ] = out
440+ if err := r .Client .Update (ctx , kubeconfigSecret ); err != nil {
441+ return fmt .Errorf ("failed to update kubeconfig secret: %w" , err )
442+ }
443+ } else {
444+ // create new kubeconfig secret.
445+ controllerOwnerRef := * metav1 .NewControllerRef (rosaScope .ControlPlane , rosacontrolplanev1 .GroupVersion .WithKind ("ROSAControlPlane" ))
446+ kubeconfigSecret = kubeconfig .GenerateSecretWithOwner (clusterRef , out , controllerOwnerRef )
447+ if err := r .Client .Create (ctx , kubeconfigSecret ); err != nil {
448+ return fmt .Errorf ("failed to create kubeconfig secret: %w" , err )
449+ }
450+ }
451+
452+ rosaScope .ControlPlane .Status .Initialized = true
453+ return nil
454+ }
455+
456+ // reconcileClusterAdminPassword generates and store the password of the cluster admin user in a secret which is used to request a token for kubeconfig auth.
457+ // Since it is not possible to retrieve a user's password through the ocm API once created,
458+ // we have to store the password in a secret as it is needed later to refresh the token.
459+ func (r * ROSAControlPlaneReconciler ) reconcileClusterAdminPassword (ctx context.Context , rosaScope * scope.ROSAControlPlaneScope ) (string , error ) {
460+ passwordSecret := rosaScope .ClusterAdminPasswordSecret ()
461+ err := r .Client .Get (ctx , client .ObjectKeyFromObject (passwordSecret ), passwordSecret )
462+ if err == nil {
463+ password := string (passwordSecret .Data ["value" ])
464+ return password , nil
465+ } else if ! apierrors .IsNotFound (err ) {
466+ return "" , fmt .Errorf ("failed to get cluster admin password secret: %w" , err )
467+ }
468+ // Generate a new password and create the secret
469+ password , err := rosa .GenerateRandomPassword ()
470+ if err != nil {
471+ return "" , err
472+ }
473+
474+ controllerOwnerRef := * metav1 .NewControllerRef (rosaScope .ControlPlane , rosacontrolplanev1 .GroupVersion .WithKind ("ROSAControlPlane" ))
475+ passwordSecret .Data = map [string ][]byte {
476+ "value" : []byte (password ),
477+ }
478+ passwordSecret .OwnerReferences = []metav1.OwnerReference {
479+ controllerOwnerRef ,
480+ }
481+ if err := r .Client .Create (ctx , passwordSecret ); err != nil {
482+ return "" , err
483+ }
484+
485+ return password , nil
486+ }
487+
355488func (r * ROSAControlPlaneReconciler ) rosaClusterToROSAControlPlane (log * logger.Logger ) handler.MapFunc {
356489 return func (ctx context.Context , o client.Object ) []ctrl.Request {
357490 rosaCluster , ok := o .(* expinfrav1.ROSACluster )
@@ -391,3 +524,24 @@ func (r *ROSAControlPlaneReconciler) rosaClusterToROSAControlPlane(log *logger.L
391524 }
392525 }
393526}
527+
528+ func buildAPIEndpoint (cluster * cmv1.Cluster ) (* clusterv1.APIEndpoint , error ) {
529+ parsedURL , err := url .ParseRequestURI (cluster .API ().URL ())
530+ if err != nil {
531+ return nil , err
532+ }
533+ host , portStr , err := net .SplitHostPort (parsedURL .Host )
534+ if err != nil {
535+ return nil , err
536+ }
537+
538+ port , err := strconv .Atoi (portStr )
539+ if err != nil {
540+ return nil , err
541+ }
542+
543+ return & clusterv1.APIEndpoint {
544+ Host : host ,
545+ Port : int32 (port ), // #nosec G109
546+ }, nil
547+ }
0 commit comments