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  }