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 }