@@ -20,12 +20,19 @@ import (
20
20
"context"
21
21
"errors"
22
22
"fmt"
23
+ "net"
24
+ "net/url"
25
+ "strconv"
23
26
"strings"
24
27
"time"
25
28
26
29
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
27
30
apierrors "k8s.io/apimachinery/pkg/api/errors"
31
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28
32
"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"
29
36
ctrl "sigs.k8s.io/controller-runtime"
30
37
"sigs.k8s.io/controller-runtime/pkg/client"
31
38
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -42,7 +49,9 @@ import (
42
49
"sigs.k8s.io/cluster-api/util"
43
50
capiannotations "sigs.k8s.io/cluster-api/util/annotations"
44
51
"sigs.k8s.io/cluster-api/util/conditions"
52
+ "sigs.k8s.io/cluster-api/util/kubeconfig"
45
53
"sigs.k8s.io/cluster-api/util/predicates"
54
+ "sigs.k8s.io/cluster-api/util/secret"
46
55
)
47
56
48
57
const (
@@ -182,11 +191,19 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc
182
191
183
192
if clusterID := cluster .ID (); clusterID != "" {
184
193
rosaScope .ControlPlane .Status .ID = & clusterID
185
- if cluster .Status ().State () == "ready" {
194
+ if cluster .Status ().State () == cmv1 . ClusterStateReady {
186
195
conditions .MarkTrue (rosaScope .ControlPlane , rosacontrolplanev1 .ROSAControlPlaneReadyCondition )
187
196
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
+ }
190
207
191
208
return ctrl.Result {}, nil
192
209
}
@@ -352,6 +369,122 @@ func (r *ROSAControlPlaneReconciler) reconcileDelete(ctx context.Context, rosaSc
352
369
return ctrl.Result {}, nil
353
370
}
354
371
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
+
355
488
func (r * ROSAControlPlaneReconciler ) rosaClusterToROSAControlPlane (log * logger.Logger ) handler.MapFunc {
356
489
return func (ctx context.Context , o client.Object ) []ctrl.Request {
357
490
rosaCluster , ok := o .(* expinfrav1.ROSACluster )
@@ -391,3 +524,24 @@ func (r *ROSAControlPlaneReconciler) rosaClusterToROSAControlPlane(log *logger.L
391
524
}
392
525
}
393
526
}
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