Skip to content

Commit e439ae2

Browse files
authored
Merge pull request #4742 from muraee/rosa-kubeconfig
✨ ROSA: Generate CAPI kubeconfig secret
2 parents 0f0e9b0 + a159b67 commit e439ae2

File tree

9 files changed

+567
-19
lines changed

9 files changed

+567
-19
lines changed

controlplane/rosa/controllers/rosacontrolplane_controller.go

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4857
const (
@@ -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+
355488
func (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+
}

pkg/cloud/scope/rosacontrolplane.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package scope
1818

1919
import (
2020
"context"
21+
"fmt"
2122

2223
"github.com/pkg/errors"
2324
corev1 "k8s.io/api/core/v1"
@@ -112,6 +113,15 @@ func (s *ROSAControlPlaneScope) CredentialsSecret() *corev1.Secret {
112113
}
113114
}
114115

116+
func (s *ROSAControlPlaneScope) ClusterAdminPasswordSecret() *corev1.Secret {
117+
return &corev1.Secret{
118+
ObjectMeta: metav1.ObjectMeta{
119+
Name: fmt.Sprintf("%s-admin-password", s.Cluster.Name),
120+
Namespace: s.ControlPlane.Namespace,
121+
},
122+
}
123+
}
124+
115125
// PatchObject persists the control plane configuration and status.
116126
func (s *ROSAControlPlaneScope) PatchObject() error {
117127
return s.patchHelper.Patch(

pkg/rosa/client.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@ const (
1616
ocmAPIURLKey = "ocmApiUrl"
1717
)
1818

19-
type rosaClient struct {
19+
type RosaClient struct {
2020
ocm *sdk.Connection
2121
rosaScope *scope.ROSAControlPlaneScope
2222
}
2323

2424
// NewRosaClientWithConnection creates a client with a preexisting connection for testing purposes.
25-
func NewRosaClientWithConnection(connection *sdk.Connection, rosaScope *scope.ROSAControlPlaneScope) *rosaClient {
26-
return &rosaClient{
25+
func NewRosaClientWithConnection(connection *sdk.Connection, rosaScope *scope.ROSAControlPlaneScope) *RosaClient {
26+
return &RosaClient{
2727
ocm: connection,
2828
rosaScope: rosaScope,
2929
}
3030
}
3131

32-
func NewRosaClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (*rosaClient, error) {
32+
func NewRosaClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (*RosaClient, error) {
3333
var token string
3434
var ocmAPIUrl string
3535

@@ -70,20 +70,20 @@ func NewRosaClient(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope)
7070
return nil, fmt.Errorf("failed to create ocm connection: %w", err)
7171
}
7272

73-
return &rosaClient{
73+
return &RosaClient{
7474
ocm: connection,
7575
rosaScope: rosaScope,
7676
}, nil
7777
}
7878

79-
func (c *rosaClient) Close() error {
79+
func (c *RosaClient) Close() error {
8080
return c.ocm.Close()
8181
}
8282

83-
func (c *rosaClient) GetConnectionURL() string {
83+
func (c *RosaClient) GetConnectionURL() string {
8484
return c.ocm.URL()
8585
}
8686

87-
func (c *rosaClient) GetConnectionTokens() (string, string, error) {
87+
func (c *RosaClient) GetConnectionTokens() (string, string, error) {
8888
return c.ocm.Tokens()
8989
}

pkg/rosa/clusters.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const (
1010
rosaCreatorArnProperty = "rosa_creator_arn"
1111
)
1212

13-
func (c *rosaClient) CreateCluster(spec *cmv1.Cluster) (*cmv1.Cluster, error) {
13+
// CreateCluster creates a new ROSA cluster using the specified spec.
14+
func (c *RosaClient) CreateCluster(spec *cmv1.Cluster) (*cmv1.Cluster, error) {
1415
cluster, err := c.ocm.ClustersMgmt().V1().Clusters().
1516
Add().
1617
Body(spec).
@@ -23,7 +24,8 @@ func (c *rosaClient) CreateCluster(spec *cmv1.Cluster) (*cmv1.Cluster, error) {
2324
return clusterObject, nil
2425
}
2526

26-
func (c *rosaClient) DeleteCluster(clusterID string) error {
27+
// DeleteCluster deletes the ROSA cluster.
28+
func (c *RosaClient) DeleteCluster(clusterID string) error {
2729
response, err := c.ocm.ClustersMgmt().V1().Clusters().
2830
Cluster(clusterID).
2931
Delete().
@@ -36,7 +38,8 @@ func (c *rosaClient) DeleteCluster(clusterID string) error {
3638
return nil
3739
}
3840

39-
func (c *rosaClient) GetCluster() (*cmv1.Cluster, error) {
41+
// GetCluster retrieves the ROSA/OCM cluster object.
42+
func (c *RosaClient) GetCluster() (*cmv1.Cluster, error) {
4043
clusterKey := c.rosaScope.RosaClusterName()
4144
query := fmt.Sprintf("%s AND (id = '%s' OR name = '%s' OR external_id = '%s')",
4245
getClusterFilter(c.rosaScope.ControlPlane.Spec.CreatorARN),

0 commit comments

Comments
 (0)