github.com/verrazzano/verrazzano@v1.7.0/cluster-operator/controllers/vmc/sync_agent_secret.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package vmc
     5  
     6  import (
     7  	"context"
     8  	"encoding/base64"
     9  	"fmt"
    10  	clusterapi "github.com/verrazzano/verrazzano/cluster-operator/apis/clusters/v1alpha1"
    11  	vzk8s "github.com/verrazzano/verrazzano/cluster-operator/internal/k8s"
    12  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    13  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    14  	"github.com/verrazzano/verrazzano/pkg/mcconstants"
    15  	corev1 "k8s.io/api/core/v1"
    16  	"k8s.io/apimachinery/pkg/types"
    17  	"k8s.io/client-go/rest"
    18  	"os"
    19  	"sigs.k8s.io/controller-runtime/pkg/client"
    20  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    21  	"sigs.k8s.io/yaml"
    22  )
    23  
    24  // Needed for unit testing
    25  var getConfigFunc = k8sutil.GetConfigFromController
    26  
    27  func setConfigFunc(f func() (*rest.Config, error)) {
    28  	getConfigFunc = f
    29  }
    30  
    31  // Create an agent secret with a kubeconfig that has a token allowing access to the managed cluster
    32  // with restricted access as defined in the verrazzano-managed-cluster role.
    33  // The code does the following:
    34  //  1. get the service account for the managed cluster
    35  //  2. get the name of the service account token from the service account secret name field
    36  //  3. get the in-memory client configuration used to access the admin cluster
    37  //  4. build a kubeconfig struct using data from the client config and the service account token
    38  //  5. save the kubeconfig as a secret
    39  //  6. update VMC with the admin secret name
    40  func (r *VerrazzanoManagedClusterReconciler) syncAgentSecret(vmc *clusterapi.VerrazzanoManagedCluster) error {
    41  	// The same managed name and  vmc namespace is used for the service account and the kubeconfig secret,
    42  	// for clarity use different vars
    43  	saName := generateManagedResourceName(vmc.Name)
    44  	secretName := GetAgentSecretName(vmc.Name)
    45  	managedNamespace := vmc.Namespace
    46  
    47  	// Get the service account
    48  	var sa corev1.ServiceAccount
    49  	saNsn := types.NamespacedName{
    50  		Namespace: managedNamespace,
    51  		Name:      saName,
    52  	}
    53  	if err := r.Get(context.TODO(), saNsn, &sa); err != nil {
    54  		return fmt.Errorf("Failed to fetch the service account for VMC %s/%s, %v", managedNamespace, saName, err)
    55  	}
    56  	var tokenName string
    57  	if len(sa.Secrets) == 0 {
    58  		r.log.Oncef("Service account %s/%s is missing a secret name. Using the service account token secret created"+
    59  			" by the VerrazzanoManagedCluster controller", managedNamespace, saName)
    60  		tokenName = sa.Name + "-token"
    61  	} else {
    62  		// Get the service account token from the secret
    63  		tokenName = sa.Secrets[0].Name
    64  	}
    65  
    66  	var serviceAccountSecret corev1.Secret
    67  	secretNsn := types.NamespacedName{
    68  		Namespace: managedNamespace,
    69  		Name:      tokenName,
    70  	}
    71  	if err := r.Get(context.TODO(), secretNsn, &serviceAccountSecret); err != nil {
    72  		return fmt.Errorf("Failed to fetch the service account secret %s/%s, %v", managedNamespace, tokenName, err)
    73  	}
    74  
    75  	// Build the kubeconfig
    76  	var err error
    77  	var kc *vzk8s.KubeConfig
    78  
    79  	// Try to use Rancher URL in the kubeconfig - this will fail if Rancher is not enabled
    80  	kc, err = r.buildKubeConfigUsingRancherURL(serviceAccountSecret)
    81  	if err != nil {
    82  		r.log.Oncef("Failed to build admin kubeconfig using Rancher URL: %v", err)
    83  		kc, err = r.buildKubeConfigUsingAdminConfigMap(serviceAccountSecret)
    84  	}
    85  	if err != nil {
    86  		return fmt.Errorf("Failed to create kubeconfig for cluster %s: %v", vmc.Name, err)
    87  	}
    88  
    89  	// Convert the kubeconfig to yaml then write it to a secret
    90  	kcBytes, err := yaml.Marshal(kc)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	_, err = r.createOrUpdateAgentSecret(vmc, string(kcBytes), secretName, managedNamespace)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  // buildKubeConfigUsingRancherURL builds the kubeconfig using the Rancher URL as the api server, and
   103  // the CA cert of the Rancher ingress as the cert authority
   104  func (r *VerrazzanoManagedClusterReconciler) buildKubeConfigUsingRancherURL(serviceAccountSecret corev1.Secret) (*vzk8s.KubeConfig, error) {
   105  	vz, err := r.getVerrazzanoResource()
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	if vz.Status.VerrazzanoInstance == nil {
   110  		return nil, r.log.ErrorfNewErr("No instance information found in Verrazzano resource status")
   111  	}
   112  	rancherURL := vz.Status.VerrazzanoInstance.RancherURL
   113  	if rancherURL == nil {
   114  		return nil, fmt.Errorf("No Rancher URL found in Verrazzano resource status")
   115  	}
   116  	caCert, err := r.getRancherCACert()
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	userToken := serviceAccountSecret.Data[mcconstants.TokenKey]
   121  	return buildAdminKubeConfig(*rancherURL, caCert, string(userToken))
   122  }
   123  
   124  // buildKubeConfig builds the kubeconfig from the user-provided admin cluster URL and
   125  // CA cert of the local cluster configuration
   126  func (r *VerrazzanoManagedClusterReconciler) buildKubeConfigUsingAdminConfigMap(serviceAccountSecret corev1.Secret) (*vzk8s.KubeConfig, error) {
   127  	// Get client config, this has some of the info needed to build a kubeconfig
   128  	config, err := getConfigFunc()
   129  	if err != nil {
   130  		return nil, fmt.Errorf("Failed to get the client config, %v", err)
   131  	}
   132  
   133  	token := serviceAccountSecret.Data[mcconstants.TokenKey]
   134  	b64Cert, err := getB64CAData(config)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	serverURL, err := vzk8s.GetAPIServerURL(r.Client)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	return buildAdminKubeConfig(serverURL, b64Cert, string(token))
   143  }
   144  
   145  // buildAdminKubeConfig builds the kubeconfig for the admin i.e. local cluster, given the API server
   146  // URL, user credentials and server CA data
   147  func buildAdminKubeConfig(serverURL string, caCert string, userToken string) (*vzk8s.KubeConfig, error) {
   148  	// These names are used internally in the generated kubeconfig. The names
   149  	// are meant to be descriptive and the actual values don't affect behavior.
   150  	const (
   151  		clusterName = "admin"
   152  		userName    = "mcAgent"
   153  		contextName = "defaultContext"
   154  	)
   155  
   156  	// Load the kubeconfig struct and build the kubeconfig
   157  	kb := vzk8s.KubeconfigBuilder{
   158  		ClusterName: clusterName,
   159  		Server:      serverURL,
   160  		CertAuth:    caCert,
   161  		UserName:    userName,
   162  		UserToken:   userToken,
   163  		ContextName: contextName,
   164  	}
   165  	kc := kb.Build()
   166  	return &kc, nil
   167  }
   168  
   169  // Create or update the kubeconfig secret
   170  func (r *VerrazzanoManagedClusterReconciler) createOrUpdateAgentSecret(vmc *clusterapi.VerrazzanoManagedCluster, kubeconfig string, name string, namespace string) (controllerutil.OperationResult, error) {
   171  	var secret corev1.Secret
   172  	secret.Namespace = namespace
   173  	secret.Name = name
   174  
   175  	return controllerutil.CreateOrUpdate(context.TODO(), r.Client, &secret, func() error {
   176  		r.mutateAgentSecret(&secret, kubeconfig, vmc.Name)
   177  		// This SetControllerReference call will trigger garbage collection i.e. the secret
   178  		// will automatically get deleted when the VerrazzanoManagedCluster is deleted
   179  		return controllerutil.SetControllerReference(vmc, &secret, r.Scheme)
   180  	})
   181  }
   182  
   183  // Mutate the secret, setting the kubeconfig data
   184  func (r *VerrazzanoManagedClusterReconciler) mutateAgentSecret(secret *corev1.Secret, kubeconfig string, manageClusterName string) error {
   185  	secret.Type = corev1.SecretTypeOpaque
   186  	secret.Data = map[string][]byte{
   187  		mcconstants.KubeconfigKey:         []byte(kubeconfig),
   188  		mcconstants.ManagedClusterNameKey: []byte(manageClusterName),
   189  	}
   190  	return nil
   191  }
   192  
   193  // getRancherCACert returns the certificate authority data from Rancher's TLS secret
   194  func (r *VerrazzanoManagedClusterReconciler) getRancherCACert() (string, error) {
   195  	ingressSecret := corev1.Secret{}
   196  
   197  	err := r.Client.Get(context.TODO(), client.ObjectKey{
   198  		Namespace: vzconst.VerrazzanoSystemNamespace,
   199  		Name:      vzconst.PrivateCABundle,
   200  	}, &ingressSecret)
   201  	if client.IgnoreNotFound(err) != nil {
   202  		return "", err
   203  	}
   204  
   205  	var caData []byte
   206  	if err == nil {
   207  		caData = ingressSecret.Data[vzconst.RancherTLSCAKey]
   208  	} else {
   209  		err = r.Client.Get(context.TODO(), types.NamespacedName{Name: rancherTLSSecret, Namespace: vzconst.RancherSystemNamespace}, &ingressSecret)
   210  		if err != nil {
   211  			return "", err
   212  		}
   213  		caData = ingressSecret.Data[mcconstants.CaCrtKey]
   214  	}
   215  	return base64.StdEncoding.EncodeToString(caData), nil
   216  }
   217  
   218  // Get the CAData from memory or a file
   219  func getB64CAData(config *rest.Config) (string, error) {
   220  	if len(config.CAData) > 0 {
   221  		return base64.StdEncoding.EncodeToString(config.CAData), nil
   222  	}
   223  	s, err := os.ReadFile(config.CAFile)
   224  	if err != nil {
   225  		return "", fmt.Errorf("Error %v reading CAData file %s", err, config.CAFile)
   226  	}
   227  	return base64.StdEncoding.EncodeToString(s), nil
   228  }
   229  
   230  // GetAgentSecretName returns the admin secret name
   231  func GetAgentSecretName(vmcName string) string {
   232  	const suffix = "-agent"
   233  	return generateManagedResourceName(vmcName) + suffix
   234  }