github.com/verrazzano/verrazzano@v1.7.0/cluster-operator/controllers/vmc/argocd.go (about) 1 // Copyright (c) 2022, 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 "encoding/json" 10 "fmt" 11 "net/http" 12 "net/url" 13 "os" 14 "time" 15 16 clusterapi "github.com/verrazzano/verrazzano/cluster-operator/apis/clusters/v1alpha1" 17 vzconst "github.com/verrazzano/verrazzano/pkg/constants" 18 "github.com/verrazzano/verrazzano/pkg/httputil" 19 "github.com/verrazzano/verrazzano/pkg/rancherutil" 20 "github.com/verrazzano/verrazzano/pkg/vzcr" 21 "github.com/verrazzano/verrazzano/platform-operator/constants" 22 "github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/common" 23 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/apimachinery/pkg/types" 29 controllerruntime "sigs.k8s.io/controller-runtime" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 ) 32 33 const ( 34 clusterSecretName = "argocd-cluster-secret" //nolint:gosec 35 argocdClusterTokenTTLEnvVarName = "ARGOCD_CLUSTER_TOKEN_TTL" //nolint:gosec 36 createTimestamp = "verrazzano.io/create-timestamp" 37 expiresAtTimestamp = "verrazzano.io/expires-at-timestamp" 38 clusterroletemplatebindingsPath = "/v3/clusterroletemplatebindings" 39 clusterroletemplatebindingByUserIDPath = "/v3/clusterroletemplatebindings?userId=" 40 ) 41 42 func (r *VerrazzanoManagedClusterReconciler) isArgoCDEnabled() (bool, error) { 43 vz, err := r.getVerrazzanoResource() 44 if err != nil { 45 return false, err 46 } 47 return vzcr.IsArgoCDEnabled(vz), nil 48 } 49 50 func (r *VerrazzanoManagedClusterReconciler) isRancherEnabled() (bool, error) { 51 vz, err := r.getVerrazzanoResource() 52 if err != nil { 53 return false, err 54 } 55 return vzcr.IsRancherEnabled(vz), nil 56 } 57 58 // registerManagedClusterWithArgoCD creates an argocd cluster secret to register a managed cluster in Argo CD 59 func (r *VerrazzanoManagedClusterReconciler) registerManagedClusterWithArgoCD(vmc *clusterapi.VerrazzanoManagedCluster) (*clusterapi.ArgoCDRegistration, error) { 60 clusterID := vmc.Status.RancherRegistration.ClusterID 61 if len(clusterID) == 0 { 62 msg := "Waiting for Rancher manifest to be applied on the managed cluster" 63 return newArgoCDRegistration(clusterapi.RegistrationPendingRancher, msg), nil 64 } 65 66 vz, err := r.getVerrazzanoResource() 67 if err != nil { 68 msg := "Failed to find instance information in Verrazzano resource status" 69 return newArgoCDRegistration(clusterapi.MCRegistrationFailed, msg), err 70 } 71 if vz.Status.VerrazzanoInstance == nil { 72 msg := "Failed to find instance information in Verrazzano resource status" 73 return newArgoCDRegistration(clusterapi.MCRegistrationFailed, msg), r.log.ErrorfNewErr("Unable to find instance information in Verrazzano resource status") 74 } 75 if vz.Status.VerrazzanoInstance.RancherURL == nil { 76 msg := "Failed to find Rancher URL in Verrazzano resource status" 77 return newArgoCDRegistration(clusterapi.MCRegistrationFailed, msg), r.log.ErrorfNewErr("Unable to find Rancher URL in Verrazzano resource status") 78 } 79 var rancherURL = *(vz.Status.VerrazzanoInstance.RancherURL) + k8sClustersPath + clusterID 80 81 // If the managed cluster is not active, we should not attempt to register in Argo CD 82 rc, err := rancherutil.NewAdminRancherConfig(r.Client, r.RancherIngressHost, r.log) 83 if err != nil { 84 msg := "Could not create rancher config that authenticates with the admin user" 85 return newArgoCDRegistration(clusterapi.MCRegistrationFailed, msg), err 86 } 87 isActive, err := isManagedClusterActiveInRancher(rc, clusterID, r.log) 88 if err != nil { 89 msg := fmt.Sprintf("Error checking Rancher status of managed cluster with id %s: %v", clusterID, err) 90 return newArgoCDRegistration(clusterapi.RegistrationPendingRancher, msg), err 91 } 92 if !isActive { 93 msg := fmt.Sprintf("Waiting for managed cluster with id %s to become active before registering in Argo CD", clusterID) 94 return newArgoCDRegistration(clusterapi.RegistrationPendingRancher, msg), err 95 } 96 97 err = r.updateArgoCDClusterRoleBindingTemplate(rc, vmc) 98 if err != nil { 99 msg := "Failed to update Argo CD ClusterRoleBindingTemplate" 100 return newArgoCDRegistration(clusterapi.MCRegistrationFailed, msg), err 101 } 102 103 err = r.createArgoCDClusterSecret(vmc, clusterID, rancherURL) 104 if err != nil { 105 msg := "Failed to create Argo CD cluster secret" 106 return newArgoCDRegistration(clusterapi.MCRegistrationFailed, msg), err 107 } 108 msg := "Successfully registered managed cluster in ArgoCD" 109 return newArgoCDRegistration(clusterapi.MCRegistrationCompleted, msg), nil 110 } 111 112 // createArgoCDClusterSecret registers cluster with ArgoCD using the "vz-argoCD-reg" user and the Rancher proxy URL for the cluster 113 func (r *VerrazzanoManagedClusterReconciler) createArgoCDClusterSecret(vmc *clusterapi.VerrazzanoManagedCluster, clusterID, rancherURL string) error { 114 r.log.Debugf("Configuring Rancher user for cluster registration in ArgoCD") 115 116 caCert, err := common.GetRootCA(r.Client) 117 if err != nil { 118 return r.log.ErrorfNewErr("Fail to get the root CA certificate from the Rancher TLS secret: %v", err) 119 } 120 secret, err := r.getArgoCDClusterUserSecret() 121 if err != nil { 122 return err 123 } 124 rc, err := rancherutil.NewRancherConfigForUser(r.Client, vzconst.ArgoCDClusterRancherUsername, secret, r.RancherIngressHost, r.log) 125 if err != nil { 126 return err 127 } 128 129 // create/update the cluster secret with the rancher config 130 err = r.createOrUpdateArgoCDSecret(rc, vmc, rancherURL, clusterID, caCert) 131 if err != nil { 132 return err 133 } 134 135 r.log.Oncef("Successfully registered managed cluster in ArgoCD with name: %s", vmc.Name) 136 return nil 137 } 138 139 // GetArgoCDClusterUserSecret fetches the Argo CD Verrazzano user secret 140 func (r *VerrazzanoManagedClusterReconciler) getArgoCDClusterUserSecret() (string, error) { 141 var err error 142 secret := &corev1.Secret{} 143 nsName := types.NamespacedName{ 144 Namespace: constants.VerrazzanoMultiClusterNamespace, 145 Name: vzconst.ArgoCDClusterRancherSecretName, 146 } 147 if err = r.Get(context.TODO(), nsName, secret); err != nil { 148 return "", r.log.ErrorfNewErr("Failed to get the Argo CD secret: %v", err) 149 } 150 if pw, ok := secret.Data["password"]; ok { 151 return string(pw), nil 152 } 153 return "", r.log.ErrorfNewErr("Failed to get password from Argo CD secret") 154 } 155 156 type TLSClientConfig struct { 157 CaData string `json:"caData"` 158 Insecure bool `json:"insecure"` 159 } 160 161 type ArgoCDRancherConfig struct { 162 BearerToken string `json:"bearerToken"` 163 TLSClientConfig `json:"tlsClientConfig"` 164 } 165 166 // createOrUpdateArgoCDSecret create or update the Argo CD cluster secret 167 func (r *VerrazzanoManagedClusterReconciler) createOrUpdateArgoCDSecret(rc *rancherutil.RancherConfig, vmc *clusterapi.VerrazzanoManagedCluster, rancherURL, clusterID string, caData []byte) error { 168 var secret corev1.Secret 169 secret.Name = vmc.Name + "-" + clusterSecretName 170 secret.Namespace = constants.ArgoCDNamespace 171 172 // Create or update on the local cluster 173 _, err := controllerruntime.CreateOrUpdate(context.TODO(), r.Client, &secret, func() error { 174 return r.mutateArgoCDClusterSecret(&secret, rc, vmc.Name, clusterID, rancherURL, caData) 175 }) 176 return err 177 } 178 179 func (r *VerrazzanoManagedClusterReconciler) mutateArgoCDClusterSecret(secret *corev1.Secret, rc *rancherutil.RancherConfig, clusterName, clusterID, rancherURL string, caData []byte) error { 180 token := rc.APIAccessToken 181 if secret.Annotations == nil { 182 secret.Annotations = map[string]string{} 183 } 184 tokenCreated, okCreated := secret.Annotations[createTimestamp] 185 tokenExpiresAt, okExpires := secret.Annotations[expiresAtTimestamp] 186 createNewToken := true 187 188 if okCreated && okExpires { 189 now := time.Now() 190 timeCreated, err := time.Parse(time.RFC3339, tokenCreated) 191 if err != nil { 192 return r.log.ErrorfNewErr("Failed to parse created timestamp: %v", err) 193 } 194 timeExpires, err := time.Parse(time.RFC3339, tokenExpiresAt) 195 if err != nil { 196 return r.log.ErrorfNewErr("Failed to parse expired timestamp: %v", err) 197 } 198 // Obtain new token if the time elapsed between time created and expired is greater than 3/4 of the lifespan of the token 199 lifespan := timeExpires.Sub(timeCreated) 200 createNewToken = now.After(timeCreated.Add(lifespan * 3 / 4)) 201 } 202 if okCreated && !okExpires { 203 // get token by userId/clusterId 204 userID, err := r.getArgoCDClusterUserID() 205 if err != nil { 206 return err 207 } 208 created, expiresAt, err := rancherutil.GetTokenWithFilter(rc, r.log, userID, clusterID) 209 if err != nil { 210 return err 211 } 212 213 secret.Annotations[createTimestamp] = created 214 if expiresAt != "" { 215 secret.Annotations[expiresAtTimestamp] = expiresAt 216 } 217 createNewToken = false 218 219 if created == "" { 220 createNewToken = true 221 } 222 } 223 if createNewToken { 224 // Obtain a new token with ttl set using bearer token obtained 225 ttl := os.Getenv(argocdClusterTokenTTLEnvVarName) 226 newToken, createTS, err := rancherutil.CreateTokenWithTTL(rc, r.log, ttl, clusterID) 227 if err != nil { 228 return err 229 } 230 secret.Annotations[createTimestamp] = createTS 231 delete(secret.Annotations, expiresAtTimestamp) 232 token = newToken 233 } 234 235 if secret.StringData == nil { 236 secret.StringData = make(map[string]string) 237 } 238 secret.Type = corev1.SecretTypeOpaque 239 if secret.ObjectMeta.Labels == nil { 240 secret.ObjectMeta.Labels = map[string]string{} 241 } 242 secret.StringData["name"] = clusterName 243 secret.StringData["server"] = rancherURL 244 secret.ObjectMeta.Labels["argocd.argoproj.io/secret-type"] = "cluster" 245 246 rancherConfig := &ArgoCDRancherConfig{ 247 BearerToken: token, 248 TLSClientConfig: TLSClientConfig{ 249 CaData: base64.StdEncoding.EncodeToString(caData), 250 Insecure: false, 251 }, 252 } 253 data, err := json.Marshal(rancherConfig) 254 if err != nil { 255 r.log.ErrorfNewErr("Failed to encode Argo CD rancher config object: %v", err) 256 return err 257 } 258 secret.StringData["config"] = string(data) 259 260 return nil 261 } 262 263 // updateArgoCDClusterRoleBindingTemplate invokes Rancher API creates a new ClusterRoleBindingTemplate for the given VMC 264 // to grant the Verrazzano argocd cluster user correct permission on the managed cluster 265 func (r *VerrazzanoManagedClusterReconciler) updateArgoCDClusterRoleBindingTemplate(rc *rancherutil.RancherConfig, vmc *clusterapi.VerrazzanoManagedCluster) error { 266 if vmc == nil { 267 r.log.Debugf("Empty VMC, no ClusterRoleBindingTemplate created") 268 return nil 269 } 270 271 clusterID := vmc.Status.RancherRegistration.ClusterID 272 userID, err := r.getArgoCDClusterUserID() 273 if err != nil { 274 return err 275 } 276 277 // Send a request to see if the clusterroletemplatebinding for the given user exists 278 reqURL := rc.BaseURL + clusterroletemplatebindingByUserIDPath + url.PathEscape(userID) + "&clusterId=" + url.PathEscape(vmc.Status.RancherRegistration.ClusterID) 279 headers := map[string]string{"Authorization": "Bearer " + rc.APIAccessToken} 280 response, body, err := rancherutil.SendRequest(http.MethodGet, reqURL, headers, "", rc, r.log) 281 if err != nil { 282 return err 283 } 284 if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { 285 return r.log.ErrorfNewErr("Failed to find clusterroletemplatebinding given user %s in Rancher, got status code: %d", userID, response.StatusCode) 286 } 287 288 if response.StatusCode == http.StatusOK { 289 data, err := httputil.ExtractFieldFromResponseBodyOrReturnError(body, "data", "failed to locate the data field of the response body") 290 if err != nil { 291 return r.log.ErrorfNewErr("Failed to locate the data field in the Rancher response: %v", err) 292 } 293 if data != "[]" { 294 r.log.Once("clusterroletemplatebinding for user: %s was located, skipping the creation process", userID) 295 return nil 296 } 297 } 298 299 action := http.MethodPost 300 payloadData := map[string]string{ 301 "userId": userID, 302 "roleTemplateId": "cluster-owner", 303 "clusterId": clusterID, 304 } 305 payload, err := json.Marshal(payloadData) 306 if err != nil { 307 return r.log.ErrorfNewErr("Failed to encode payload object: %v", err) 308 } 309 reqURL = rc.BaseURL + clusterroletemplatebindingsPath 310 headers = map[string]string{"Authorization": "Bearer " + rc.APIAccessToken, "Content-Type": "application/json"} 311 312 response, _, err = rancherutil.SendRequest(action, reqURL, headers, string(payload), rc, r.log) 313 if err != nil { 314 return err 315 } 316 err = httputil.ValidateResponseCode(response, http.StatusCreated) 317 if err != nil { 318 return r.log.ErrorfThrottledNewErr("Failed configuring Argo CD user cluster role template bindings: %v", err) 319 } 320 return nil 321 } 322 323 // getArgoCDClusterUserID returns the Rancher-generated user ID for the Verrazzano argocd cluster user 324 func (r *VerrazzanoManagedClusterReconciler) getArgoCDClusterUserID() (string, error) { 325 usersList := unstructured.UnstructuredList{} 326 usersList.SetGroupVersionKind(schema.GroupVersionKind{ 327 Group: APIGroupRancherManagement, 328 Version: APIGroupVersionRancherManagement, 329 Kind: UserListKind, 330 }) 331 err := r.List(context.TODO(), &usersList, &client.ListOptions{}) 332 if err != nil { 333 return "", r.log.ErrorfNewErr("Failed to list Rancher Users: %v", err) 334 } 335 336 for _, user := range usersList.Items { 337 userData := user.UnstructuredContent() 338 if userData[UserUsernameAttribute] == vzconst.ArgoCDClusterRancherUsername { 339 return user.GetName(), nil 340 } 341 } 342 return "", r.log.ErrorfNewErr("Failed to find a Rancher user with username %s", vzconst.ArgoCDClusterRancherUsername) 343 } 344 345 func (r *VerrazzanoManagedClusterReconciler) unregisterClusterFromArgoCD(ctx context.Context, vmc *clusterapi.VerrazzanoManagedCluster) error { 346 clusterSec := corev1.Secret{ 347 ObjectMeta: metav1.ObjectMeta{ 348 Name: vmc.Name + "-" + clusterSecretName, 349 Namespace: constants.ArgoCDNamespace, 350 }, 351 } 352 if err := r.Delete(context.TODO(), &clusterSec); client.IgnoreNotFound(err) != nil { 353 return r.log.ErrorfNewErr("Failed to delete Argo CD cluster secret: %v", err) 354 } 355 356 return nil 357 } 358 359 func newArgoCDRegistration(status clusterapi.ArgoCDRegistrationStatus, message string) *clusterapi.ArgoCDRegistration { 360 now := metav1.Now() 361 return &clusterapi.ArgoCDRegistration{ 362 Status: status, 363 Timestamp: &now, 364 Message: message, 365 } 366 }