github.com/argoproj/argo-cd/v3@v3.2.1/server/cluster/cluster.go (about)

     1  package cluster
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"time"
     8  
     9  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    10  	log "github.com/sirupsen/logrus"
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/util/sets"
    15  	"k8s.io/client-go/kubernetes"
    16  
    17  	"github.com/argoproj/argo-cd/v3/common"
    18  	"github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
    19  	appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    20  	servercache "github.com/argoproj/argo-cd/v3/server/cache"
    21  	"github.com/argoproj/argo-cd/v3/util/argo"
    22  	"github.com/argoproj/argo-cd/v3/util/clusterauth"
    23  	"github.com/argoproj/argo-cd/v3/util/db"
    24  	"github.com/argoproj/argo-cd/v3/util/rbac"
    25  )
    26  
    27  // Server provides a Cluster service
    28  type Server struct {
    29  	db      db.ArgoDB
    30  	enf     *rbac.Enforcer
    31  	cache   *servercache.Cache
    32  	kubectl kube.Kubectl
    33  }
    34  
    35  // NewServer returns a new instance of the Cluster service
    36  func NewServer(db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache, kubectl kube.Kubectl) *Server {
    37  	return &Server{
    38  		db:      db,
    39  		enf:     enf,
    40  		cache:   cache,
    41  		kubectl: kubectl,
    42  	}
    43  }
    44  
    45  func CreateClusterRBACObject(project string, server string) string {
    46  	if project != "" {
    47  		return project + "/" + server
    48  	}
    49  	return server
    50  }
    51  
    52  // List returns list of clusters
    53  func (s *Server) List(ctx context.Context, q *cluster.ClusterQuery) (*appv1.ClusterList, error) {
    54  	clusterList, err := s.db.ListClusters(ctx)
    55  	if err != nil {
    56  		return nil, fmt.Errorf("failed to list clusters: %w", err)
    57  	}
    58  
    59  	filteredItems := clusterList.Items
    60  
    61  	// Filter clusters by id
    62  	if filteredItems, err = filterClustersById(filteredItems, q.Id); err != nil {
    63  		return nil, fmt.Errorf("error filtering clusters by id: %w", err)
    64  	}
    65  
    66  	// Filter clusters by name
    67  	filteredItems = filterClustersByName(filteredItems, q.Name)
    68  
    69  	// Filter clusters by server
    70  	filteredItems = filterClustersByServer(filteredItems, q.Server)
    71  
    72  	items := make([]appv1.Cluster, 0)
    73  	for _, clust := range filteredItems {
    74  		if s.enf.Enforce(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionGet, CreateClusterRBACObject(clust.Project, clust.Server)) {
    75  			items = append(items, clust)
    76  		}
    77  	}
    78  	err = kube.RunAllAsync(len(items), func(i int) error {
    79  		items[i] = *s.toAPIResponse(&items[i])
    80  		return nil
    81  	})
    82  	if err != nil {
    83  		return nil, fmt.Errorf("error running async cluster responses: %w", err)
    84  	}
    85  
    86  	cl := *clusterList
    87  	cl.Items = items
    88  
    89  	return &cl, nil
    90  }
    91  
    92  func filterClustersById(clusters []appv1.Cluster, id *cluster.ClusterID) ([]appv1.Cluster, error) {
    93  	if id == nil {
    94  		return clusters, nil
    95  	}
    96  
    97  	var items []appv1.Cluster
    98  
    99  	switch id.Type {
   100  	case "name":
   101  		items = filterClustersByName(clusters, id.Value)
   102  	case "name_escaped":
   103  		nameUnescaped, err := url.QueryUnescape(id.Value)
   104  		if err != nil {
   105  			return nil, fmt.Errorf("failed to unescape cluster name: %w", err)
   106  		}
   107  		items = filterClustersByName(clusters, nameUnescaped)
   108  	default:
   109  		items = filterClustersByServer(clusters, id.Value)
   110  	}
   111  
   112  	return items, nil
   113  }
   114  
   115  func filterClustersByName(clusters []appv1.Cluster, name string) []appv1.Cluster {
   116  	if name == "" {
   117  		return clusters
   118  	}
   119  	items := make([]appv1.Cluster, 0)
   120  	for i := 0; i < len(clusters); i++ {
   121  		if clusters[i].Name == name {
   122  			items = append(items, clusters[i])
   123  			return items
   124  		}
   125  	}
   126  	return items
   127  }
   128  
   129  func filterClustersByServer(clusters []appv1.Cluster, server string) []appv1.Cluster {
   130  	if server == "" {
   131  		return clusters
   132  	}
   133  	items := make([]appv1.Cluster, 0)
   134  	for i := 0; i < len(clusters); i++ {
   135  		if clusters[i].Server == server {
   136  			items = append(items, clusters[i])
   137  			return items
   138  		}
   139  	}
   140  	return items
   141  }
   142  
   143  // Create creates a cluster
   144  func (s *Server) Create(ctx context.Context, q *cluster.ClusterCreateRequest) (*appv1.Cluster, error) {
   145  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionCreate, CreateClusterRBACObject(q.Cluster.Project, q.Cluster.Server)); err != nil {
   146  		return nil, fmt.Errorf("permission denied while creating cluster: %w", err)
   147  	}
   148  	c := q.Cluster
   149  	clusterRESTConfig, err := c.RESTConfig()
   150  	if err != nil {
   151  		return nil, fmt.Errorf("error getting REST config: %w", err)
   152  	}
   153  
   154  	serverVersion, err := s.kubectl.GetServerVersion(clusterRESTConfig)
   155  	if err != nil {
   156  		return nil, fmt.Errorf("error getting server version: %w", err)
   157  	}
   158  
   159  	clust, err := s.db.CreateCluster(ctx, c)
   160  	if err != nil {
   161  		if status.Convert(err).Code() != codes.AlreadyExists {
   162  			return nil, fmt.Errorf("error creating cluster: %w", err)
   163  		}
   164  		// act idempotent if existing spec matches new spec
   165  		existing, getErr := s.db.GetCluster(ctx, c.Server)
   166  		if getErr != nil {
   167  			return nil, status.Errorf(codes.Internal, "unable to check existing cluster details: %v", getErr)
   168  		}
   169  
   170  		switch {
   171  		case existing.Equals(c):
   172  			clust = existing
   173  		case q.Upsert:
   174  			return s.Update(ctx, &cluster.ClusterUpdateRequest{Cluster: c})
   175  		default:
   176  			return nil, status.Error(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("cluster", existing, c))
   177  		}
   178  	}
   179  
   180  	err = s.cache.SetClusterInfo(c.Server, &appv1.ClusterInfo{
   181  		ServerVersion: serverVersion,
   182  		ConnectionState: appv1.ConnectionState{
   183  			Status:     appv1.ConnectionStatusSuccessful,
   184  			ModifiedAt: &metav1.Time{Time: time.Now()},
   185  		},
   186  	})
   187  	if err != nil {
   188  		return nil, fmt.Errorf("error setting cluster info in cache: %w", err)
   189  	}
   190  	return s.toAPIResponse(clust), err
   191  }
   192  
   193  // Get returns a cluster from a query
   194  func (s *Server) Get(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   195  	c, err := s.getClusterAndVerifyAccess(ctx, q, rbac.ActionGet)
   196  	if err != nil {
   197  		return nil, fmt.Errorf("error verifying access to update cluster: %w", err)
   198  	}
   199  
   200  	return s.toAPIResponse(c), nil
   201  }
   202  
   203  func (s *Server) getClusterWith403IfNotExist(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   204  	c, err := s.getCluster(ctx, q)
   205  	if err != nil || c == nil {
   206  		return nil, common.PermissionDeniedAPIError
   207  	}
   208  	return c, nil
   209  }
   210  
   211  func (s *Server) getClusterAndVerifyAccess(ctx context.Context, q *cluster.ClusterQuery, action string) (*appv1.Cluster, error) {
   212  	c, err := s.getClusterWith403IfNotExist(ctx, q)
   213  	if err != nil {
   214  		return nil, fmt.Errorf("failed to get cluster with permissions check: %w", err)
   215  	}
   216  
   217  	// verify that user can do the specified action inside project where cluster is located
   218  	if !s.enf.Enforce(ctx.Value("claims"), rbac.ResourceClusters, action, CreateClusterRBACObject(c.Project, c.Server)) {
   219  		log.WithField("cluster", q.Server).Warnf("encountered permissions issue while processing request: %v", err)
   220  		return nil, common.PermissionDeniedAPIError
   221  	}
   222  
   223  	return c, nil
   224  }
   225  
   226  func (s *Server) getCluster(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   227  	if q.Id != nil {
   228  		q.Server = ""
   229  		q.Name = ""
   230  		switch q.Id.Type {
   231  		case "name":
   232  			q.Name = q.Id.Value
   233  		case "name_escaped":
   234  			nameUnescaped, err := url.QueryUnescape(q.Id.Value)
   235  			if err != nil {
   236  				return nil, fmt.Errorf("failed to unescape cluster name: %w", err)
   237  			}
   238  			q.Name = nameUnescaped
   239  		default:
   240  			q.Server = q.Id.Value
   241  		}
   242  	}
   243  
   244  	if q.Server != "" {
   245  		c, err := s.db.GetCluster(ctx, q.Server)
   246  		if err != nil {
   247  			return nil, fmt.Errorf("failed to get cluster by server: %w", err)
   248  		}
   249  		return c, nil
   250  	}
   251  
   252  	// we only get the name when we specify Name in ApplicationDestination and next
   253  	// we want to find the server in order to populate ApplicationDestination.Server
   254  	if q.Name != "" {
   255  		clusterList, err := s.db.ListClusters(ctx)
   256  		if err != nil {
   257  			return nil, fmt.Errorf("failed to list clusters: %w", err)
   258  		}
   259  		for _, c := range clusterList.Items {
   260  			if c.Name == q.Name {
   261  				return &c, nil
   262  			}
   263  		}
   264  	}
   265  
   266  	return nil, nil
   267  }
   268  
   269  var clusterFieldsByPath = map[string]func(updated *appv1.Cluster, existing *appv1.Cluster){
   270  	"name": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   271  		updated.Name = existing.Name
   272  	},
   273  	"namespaces": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   274  		updated.Namespaces = existing.Namespaces
   275  	},
   276  	"config": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   277  		updated.Config = existing.Config
   278  	},
   279  	"shard": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   280  		updated.Shard = existing.Shard
   281  	},
   282  	"clusterResources": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   283  		updated.ClusterResources = existing.ClusterResources
   284  	},
   285  	"labels": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   286  		updated.Labels = existing.Labels
   287  	},
   288  	"annotations": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   289  		updated.Annotations = existing.Annotations
   290  	},
   291  	"project": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   292  		updated.Project = existing.Project
   293  	},
   294  }
   295  
   296  // Update updates a cluster
   297  func (s *Server) Update(ctx context.Context, q *cluster.ClusterUpdateRequest) (*appv1.Cluster, error) {
   298  	c, err := s.getClusterAndVerifyAccess(ctx, &cluster.ClusterQuery{
   299  		Server: q.Cluster.Server,
   300  		Name:   q.Cluster.Name,
   301  		Id:     q.Id,
   302  	}, rbac.ActionUpdate)
   303  	if err != nil {
   304  		return nil, fmt.Errorf("failed to verify access for updating cluster: %w", err)
   305  	}
   306  
   307  	if len(q.UpdatedFields) == 0 || sets.NewString(q.UpdatedFields...).Has("project") {
   308  		// verify that user can do update inside project where cluster will be located
   309  		if !s.enf.Enforce(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionUpdate, CreateClusterRBACObject(q.Cluster.Project, c.Server)) {
   310  			return nil, common.PermissionDeniedAPIError
   311  		}
   312  	}
   313  
   314  	if len(q.UpdatedFields) != 0 {
   315  		for _, path := range q.UpdatedFields {
   316  			if updater, ok := clusterFieldsByPath[path]; ok {
   317  				updater(c, q.Cluster)
   318  			}
   319  		}
   320  		q.Cluster = c
   321  	}
   322  	clusterRESTConfig, err := q.Cluster.RESTConfig()
   323  	if err != nil {
   324  		return nil, fmt.Errorf("failed to get REST config for cluster: %w", err)
   325  	}
   326  
   327  	// Test the token we just created before persisting it
   328  	serverVersion, err := s.kubectl.GetServerVersion(clusterRESTConfig)
   329  	if err != nil {
   330  		return nil, fmt.Errorf("failed to get server version: %w", err)
   331  	}
   332  
   333  	clust, err := s.db.UpdateCluster(ctx, q.Cluster)
   334  	if err != nil {
   335  		return nil, fmt.Errorf("failed to update cluster in database: %w", err)
   336  	}
   337  	err = s.cache.SetClusterInfo(clust.Server, &appv1.ClusterInfo{
   338  		ServerVersion: serverVersion,
   339  		ConnectionState: appv1.ConnectionState{
   340  			Status:     appv1.ConnectionStatusSuccessful,
   341  			ModifiedAt: &metav1.Time{Time: time.Now()},
   342  		},
   343  	})
   344  	if err != nil {
   345  		return nil, fmt.Errorf("failed to set cluster info in cache: %w", err)
   346  	}
   347  	return s.toAPIResponse(clust), nil
   348  }
   349  
   350  // Delete deletes a cluster by server/name
   351  func (s *Server) Delete(ctx context.Context, q *cluster.ClusterQuery) (*cluster.ClusterResponse, error) {
   352  	c, err := s.getClusterWith403IfNotExist(ctx, q)
   353  	if err != nil {
   354  		return nil, fmt.Errorf("failed to get cluster with permissions check: %w", err)
   355  	}
   356  
   357  	if q.Name != "" {
   358  		servers, err := s.db.GetClusterServersByName(ctx, q.Name)
   359  		if err != nil {
   360  			log.WithField("cluster", q.Name).Warnf("failed to get cluster servers by name: %v", err)
   361  			return nil, common.PermissionDeniedAPIError
   362  		}
   363  		for _, server := range servers {
   364  			if err := enforceAndDelete(ctx, s, server, c.Project); err != nil {
   365  				return nil, fmt.Errorf("failed to enforce and delete cluster server: %w", err)
   366  			}
   367  		}
   368  	} else {
   369  		if err := enforceAndDelete(ctx, s, q.Server, c.Project); err != nil {
   370  			return nil, fmt.Errorf("failed to enforce and delete cluster server: %w", err)
   371  		}
   372  	}
   373  
   374  	return &cluster.ClusterResponse{}, nil
   375  }
   376  
   377  func enforceAndDelete(ctx context.Context, s *Server, server, project string) error {
   378  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionDelete, CreateClusterRBACObject(project, server)); err != nil {
   379  		log.WithField("cluster", server).Warnf("encountered permissions issue while processing request: %v", err)
   380  		return common.PermissionDeniedAPIError
   381  	}
   382  	return s.db.DeleteCluster(ctx, server)
   383  }
   384  
   385  // RotateAuth rotates the bearer token used for a cluster
   386  func (s *Server) RotateAuth(ctx context.Context, q *cluster.ClusterQuery) (*cluster.ClusterResponse, error) {
   387  	clust, err := s.getClusterWith403IfNotExist(ctx, q)
   388  	if err != nil {
   389  		return nil, fmt.Errorf("failed to get cluster with permissions check: %w", err)
   390  	}
   391  
   392  	var servers []string
   393  	if q.Name != "" {
   394  		servers, err = s.db.GetClusterServersByName(ctx, q.Name)
   395  		if err != nil {
   396  			log.WithField("cluster", q.Name).Warnf("failed to get cluster servers by name: %v", err)
   397  			return nil, common.PermissionDeniedAPIError
   398  		}
   399  		for _, server := range servers {
   400  			if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionUpdate, CreateClusterRBACObject(clust.Project, server)); err != nil {
   401  				log.WithField("cluster", server).Warnf("encountered permissions issue while processing request: %v", err)
   402  				return nil, common.PermissionDeniedAPIError
   403  			}
   404  		}
   405  	} else {
   406  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionUpdate, CreateClusterRBACObject(clust.Project, q.Server)); err != nil {
   407  			log.WithField("cluster", q.Server).Warnf("encountered permissions issue while processing request: %v", err)
   408  			return nil, common.PermissionDeniedAPIError
   409  		}
   410  		servers = append(servers, q.Server)
   411  	}
   412  
   413  	for _, server := range servers {
   414  		logCtx := log.WithField("cluster", server)
   415  		logCtx.Info("Rotating auth")
   416  		restCfg, err := clust.RESTConfig()
   417  		if err != nil {
   418  			return nil, fmt.Errorf("failed to get REST config for cluster: %w", err)
   419  		}
   420  		if restCfg.BearerToken == "" {
   421  			return nil, status.Errorf(codes.InvalidArgument, "Cluster '%s' does not use bearer token authentication", server)
   422  		}
   423  
   424  		claims, err := clusterauth.ParseServiceAccountToken(restCfg.BearerToken)
   425  		if err != nil {
   426  			return nil, fmt.Errorf("failed to parse service account token: %w", err)
   427  		}
   428  		kubeclientset, err := kubernetes.NewForConfig(restCfg)
   429  		if err != nil {
   430  			return nil, fmt.Errorf("failed to create Kubernetes clientset: %w", err)
   431  		}
   432  		newSecret, err := clusterauth.GenerateNewClusterManagerSecret(kubeclientset, claims)
   433  		if err != nil {
   434  			return nil, fmt.Errorf("failed to generate new cluster manager secret: %w", err)
   435  		}
   436  		// we are using token auth, make sure we don't store client-cert information
   437  		clust.Config.KeyData = nil
   438  		clust.Config.CertData = nil
   439  		clust.Config.BearerToken = string(newSecret.Data["token"])
   440  
   441  		clusterRESTConfig, err := clust.RESTConfig()
   442  		if err != nil {
   443  			return nil, fmt.Errorf("failed to get REST config for cluster: %w", err)
   444  		}
   445  		// Test the token we just created before persisting it
   446  		serverVersion, err := s.kubectl.GetServerVersion(clusterRESTConfig)
   447  		if err != nil {
   448  			return nil, fmt.Errorf("failed to get server version: %w", err)
   449  		}
   450  		_, err = s.db.UpdateCluster(ctx, clust)
   451  		if err != nil {
   452  			return nil, fmt.Errorf("failed to update cluster in database: %w", err)
   453  		}
   454  		err = s.cache.SetClusterInfo(clust.Server, &appv1.ClusterInfo{
   455  			ServerVersion: serverVersion,
   456  			ConnectionState: appv1.ConnectionState{
   457  				Status:     appv1.ConnectionStatusSuccessful,
   458  				ModifiedAt: &metav1.Time{Time: time.Now()},
   459  			},
   460  		})
   461  		if err != nil {
   462  			return nil, fmt.Errorf("failed to set cluster info in cache: %w", err)
   463  		}
   464  		err = clusterauth.RotateServiceAccountSecrets(kubeclientset, claims, newSecret)
   465  		if err != nil {
   466  			return nil, fmt.Errorf("failed to rotate service account secrets: %w", err)
   467  		}
   468  		logCtx.Infof("Rotated auth (old: %s, new: %s)", claims.SecretName, newSecret.Name)
   469  	}
   470  	return &cluster.ClusterResponse{}, nil
   471  }
   472  
   473  func (s *Server) toAPIResponse(clust *appv1.Cluster) *appv1.Cluster {
   474  	clust = clust.Sanitized()
   475  	_ = s.cache.GetClusterInfo(clust.Server, &clust.Info)
   476  	// populate deprecated fields for backward compatibility
   477  	//nolint:staticcheck
   478  	clust.ServerVersion = clust.Info.ServerVersion
   479  	//nolint:staticcheck
   480  	clust.ConnectionState = clust.Info.ConnectionState
   481  	return clust
   482  }
   483  
   484  // InvalidateCache invalidates cluster cache
   485  func (s *Server) InvalidateCache(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   486  	cls, err := s.getClusterAndVerifyAccess(ctx, q, rbac.ActionUpdate)
   487  	if err != nil {
   488  		return nil, fmt.Errorf("failed to verify access for cluster: %w", err)
   489  	}
   490  	now := metav1.Now()
   491  	cls.RefreshRequestedAt = &now
   492  	cls, err = s.db.UpdateCluster(ctx, cls)
   493  	if err != nil {
   494  		return nil, fmt.Errorf("failed to update cluster in database: %w", err)
   495  	}
   496  	return s.toAPIResponse(cls), nil
   497  }