github.com/argoproj/argo-cd/v2@v2.10.9/server/cluster/cluster.go (about)

     1  package cluster
     2  
     3  import (
     4  	"context"
     5  	"net/url"
     6  	"time"
     7  
     8  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
     9  	log "github.com/sirupsen/logrus"
    10  	"google.golang.org/grpc/codes"
    11  	"google.golang.org/grpc/status"
    12  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/util/sets"
    14  	"k8s.io/client-go/kubernetes"
    15  
    16  	"github.com/argoproj/argo-cd/v2/common"
    17  	"github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster"
    18  	appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    19  	servercache "github.com/argoproj/argo-cd/v2/server/cache"
    20  	"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
    21  	"github.com/argoproj/argo-cd/v2/util/argo"
    22  	"github.com/argoproj/argo-cd/v2/util/clusterauth"
    23  	"github.com/argoproj/argo-cd/v2/util/db"
    24  	"github.com/argoproj/argo-cd/v2/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, 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, 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"), rbacpolicy.ResourceClusters, rbacpolicy.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, 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, 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"), rbacpolicy.ResourceClusters, rbacpolicy.ActionCreate, CreateClusterRBACObject(q.Cluster.Project, q.Cluster.Server)); err != nil {
   146  		return nil, err
   147  	}
   148  	c := q.Cluster
   149  	serverVersion, err := s.kubectl.GetServerVersion(c.RESTConfig())
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	clust, err := s.db.CreateCluster(ctx, c)
   155  	if err != nil {
   156  		if status.Convert(err).Code() == codes.AlreadyExists {
   157  			// act idempotent if existing spec matches new spec
   158  			existing, getErr := s.db.GetCluster(ctx, c.Server)
   159  			if getErr != nil {
   160  				return nil, status.Errorf(codes.Internal, "unable to check existing cluster details: %v", getErr)
   161  			}
   162  
   163  			if existing.Equals(c) {
   164  				clust = existing
   165  			} else if q.Upsert {
   166  				return s.Update(ctx, &cluster.ClusterUpdateRequest{Cluster: c})
   167  			} else {
   168  				return nil, status.Errorf(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("cluster", existing, c))
   169  			}
   170  		} else {
   171  			return nil, err
   172  		}
   173  	}
   174  
   175  	err = s.cache.SetClusterInfo(c.Server, &appv1.ClusterInfo{
   176  		ServerVersion: serverVersion,
   177  		ConnectionState: appv1.ConnectionState{
   178  			Status:     appv1.ConnectionStatusSuccessful,
   179  			ModifiedAt: &v1.Time{Time: time.Now()},
   180  		},
   181  	})
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	return s.toAPIResponse(clust), err
   186  }
   187  
   188  // Get returns a cluster from a query
   189  func (s *Server) Get(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   190  	c, err := s.getClusterWith403IfNotExist(ctx, q)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionGet, CreateClusterRBACObject(c.Project, q.Server)); err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	return s.toAPIResponse(c), nil
   200  }
   201  
   202  func (s *Server) getClusterWith403IfNotExist(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   203  	repo, err := s.getCluster(ctx, q)
   204  	if err != nil || repo == nil {
   205  		return nil, common.PermissionDeniedAPIError
   206  	}
   207  	return repo, nil
   208  }
   209  
   210  func (s *Server) getCluster(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   211  	if q.Id != nil {
   212  		q.Server = ""
   213  		q.Name = ""
   214  		if q.Id.Type == "name" {
   215  			q.Name = q.Id.Value
   216  		} else if q.Id.Type == "name_escaped" {
   217  			nameUnescaped, err := url.QueryUnescape(q.Id.Value)
   218  			if err != nil {
   219  				return nil, err
   220  			}
   221  			q.Name = nameUnescaped
   222  		} else {
   223  			q.Server = q.Id.Value
   224  		}
   225  	}
   226  
   227  	if q.Server != "" {
   228  		c, err := s.db.GetCluster(ctx, q.Server)
   229  		if err != nil {
   230  			return nil, err
   231  		}
   232  		return c, nil
   233  	}
   234  
   235  	//we only get the name when we specify Name in ApplicationDestination and next
   236  	//we want to find the server in order to populate ApplicationDestination.Server
   237  	if q.Name != "" {
   238  		clusterList, err := s.db.ListClusters(ctx)
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		for _, c := range clusterList.Items {
   243  			if c.Name == q.Name {
   244  				return &c, nil
   245  			}
   246  		}
   247  	}
   248  
   249  	return nil, nil
   250  }
   251  
   252  var clusterFieldsByPath = map[string]func(updated *appv1.Cluster, existing *appv1.Cluster){
   253  	"name": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   254  		updated.Name = existing.Name
   255  	},
   256  	"namespaces": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   257  		updated.Namespaces = existing.Namespaces
   258  	},
   259  	"config": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   260  		updated.Config = existing.Config
   261  	},
   262  	"shard": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   263  		updated.Shard = existing.Shard
   264  	},
   265  	"clusterResources": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   266  		updated.ClusterResources = existing.ClusterResources
   267  	},
   268  	"labels": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   269  		updated.Labels = existing.Labels
   270  	},
   271  	"annotations": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   272  		updated.Annotations = existing.Annotations
   273  	},
   274  	"project": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   275  		updated.Project = existing.Project
   276  	},
   277  }
   278  
   279  // Update updates a cluster
   280  func (s *Server) Update(ctx context.Context, q *cluster.ClusterUpdateRequest) (*appv1.Cluster, error) {
   281  	c, err := s.getClusterWith403IfNotExist(ctx, &cluster.ClusterQuery{
   282  		Server: q.Cluster.Server,
   283  		Name:   q.Cluster.Name,
   284  		Id:     q.Id,
   285  	})
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	// verify that user can do update inside project where cluster is located
   291  	if !s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(c.Project, c.Server)) {
   292  		return nil, common.PermissionDeniedAPIError
   293  	}
   294  
   295  	if len(q.UpdatedFields) == 0 || sets.NewString(q.UpdatedFields...).Has("project") {
   296  		// verify that user can do update inside project where cluster will be located
   297  		if !s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(q.Cluster.Project, c.Server)) {
   298  			return nil, common.PermissionDeniedAPIError
   299  		}
   300  	}
   301  
   302  	if len(q.UpdatedFields) != 0 {
   303  		for _, path := range q.UpdatedFields {
   304  			if updater, ok := clusterFieldsByPath[path]; ok {
   305  				updater(c, q.Cluster)
   306  			}
   307  		}
   308  		q.Cluster = c
   309  	}
   310  
   311  	// Test the token we just created before persisting it
   312  	serverVersion, err := s.kubectl.GetServerVersion(q.Cluster.RESTConfig())
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  
   317  	clust, err := s.db.UpdateCluster(ctx, q.Cluster)
   318  	if err != nil {
   319  		return nil, err
   320  	}
   321  	err = s.cache.SetClusterInfo(clust.Server, &appv1.ClusterInfo{
   322  		ServerVersion: serverVersion,
   323  		ConnectionState: appv1.ConnectionState{
   324  			Status:     appv1.ConnectionStatusSuccessful,
   325  			ModifiedAt: &v1.Time{Time: time.Now()},
   326  		},
   327  	})
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	return s.toAPIResponse(clust), nil
   332  }
   333  
   334  // Delete deletes a cluster by server/name
   335  func (s *Server) Delete(ctx context.Context, q *cluster.ClusterQuery) (*cluster.ClusterResponse, error) {
   336  	c, err := s.getClusterWith403IfNotExist(ctx, q)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	if q.Name != "" {
   342  		servers, err := s.db.GetClusterServersByName(ctx, q.Name)
   343  		if err != nil {
   344  			return nil, err
   345  		}
   346  		for _, server := range servers {
   347  			if err := enforceAndDelete(s, ctx, server, c.Project); err != nil {
   348  				return nil, err
   349  			}
   350  		}
   351  	} else {
   352  		if err := enforceAndDelete(s, ctx, q.Server, c.Project); err != nil {
   353  			return nil, err
   354  		}
   355  	}
   356  
   357  	return &cluster.ClusterResponse{}, nil
   358  }
   359  
   360  func enforceAndDelete(s *Server, ctx context.Context, server, project string) error {
   361  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionDelete, CreateClusterRBACObject(project, server)); err != nil {
   362  		return err
   363  	}
   364  	if err := s.db.DeleteCluster(ctx, server); err != nil {
   365  		return err
   366  	}
   367  	return nil
   368  }
   369  
   370  // RotateAuth rotates the bearer token used for a cluster
   371  func (s *Server) RotateAuth(ctx context.Context, q *cluster.ClusterQuery) (*cluster.ClusterResponse, error) {
   372  	clust, err := s.getClusterWith403IfNotExist(ctx, q)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	var servers []string
   378  	if q.Name != "" {
   379  		servers, err = s.db.GetClusterServersByName(ctx, q.Name)
   380  		if err != nil {
   381  			return nil, status.Errorf(codes.NotFound, "failed to get cluster servers by name: %v", err)
   382  		}
   383  		for _, server := range servers {
   384  			if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(clust.Project, server)); err != nil {
   385  				return nil, status.Errorf(codes.PermissionDenied, "encountered permissions issue while processing request: %v", err)
   386  			}
   387  		}
   388  	} else {
   389  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(clust.Project, q.Server)); err != nil {
   390  			return nil, status.Errorf(codes.PermissionDenied, "encountered permissions issue while processing request: %v", err)
   391  		}
   392  		servers = append(servers, q.Server)
   393  	}
   394  
   395  	for _, server := range servers {
   396  		logCtx := log.WithField("cluster", server)
   397  		logCtx.Info("Rotating auth")
   398  		restCfg := clust.RESTConfig()
   399  		if restCfg.BearerToken == "" {
   400  			return nil, status.Errorf(codes.InvalidArgument, "Cluster '%s' does not use bearer token authentication", server)
   401  		}
   402  
   403  		claims, err := clusterauth.ParseServiceAccountToken(restCfg.BearerToken)
   404  		if err != nil {
   405  			return nil, err
   406  		}
   407  		kubeclientset, err := kubernetes.NewForConfig(restCfg)
   408  		if err != nil {
   409  			return nil, err
   410  		}
   411  		newSecret, err := clusterauth.GenerateNewClusterManagerSecret(kubeclientset, claims)
   412  		if err != nil {
   413  			return nil, err
   414  		}
   415  		// we are using token auth, make sure we don't store client-cert information
   416  		clust.Config.KeyData = nil
   417  		clust.Config.CertData = nil
   418  		clust.Config.BearerToken = string(newSecret.Data["token"])
   419  
   420  		// Test the token we just created before persisting it
   421  		serverVersion, err := s.kubectl.GetServerVersion(clust.RESTConfig())
   422  		if err != nil {
   423  			return nil, err
   424  		}
   425  		_, err = s.db.UpdateCluster(ctx, clust)
   426  		if err != nil {
   427  			return nil, err
   428  		}
   429  		err = s.cache.SetClusterInfo(clust.Server, &appv1.ClusterInfo{
   430  			ServerVersion: serverVersion,
   431  			ConnectionState: appv1.ConnectionState{
   432  				Status:     appv1.ConnectionStatusSuccessful,
   433  				ModifiedAt: &v1.Time{Time: time.Now()},
   434  			},
   435  		})
   436  		if err != nil {
   437  			return nil, err
   438  		}
   439  		err = clusterauth.RotateServiceAccountSecrets(kubeclientset, claims, newSecret)
   440  		if err != nil {
   441  			return nil, err
   442  		}
   443  		logCtx.Infof("Rotated auth (old: %s, new: %s)", claims.SecretName, newSecret.Name)
   444  	}
   445  	return &cluster.ClusterResponse{}, nil
   446  }
   447  
   448  func (s *Server) toAPIResponse(clust *appv1.Cluster) *appv1.Cluster {
   449  	_ = s.cache.GetClusterInfo(clust.Server, &clust.Info)
   450  
   451  	clust.Config.Password = ""
   452  	clust.Config.BearerToken = ""
   453  	clust.Config.TLSClientConfig.KeyData = nil
   454  	if clust.Config.ExecProviderConfig != nil {
   455  		// We can't know what the user has put into args or
   456  		// env vars on the exec provider that might be sensitive
   457  		// (e.g. --private-key=XXX, PASSWORD=XXX)
   458  		// Implicitly assumes the command executable name is non-sensitive
   459  		clust.Config.ExecProviderConfig.Env = make(map[string]string)
   460  		clust.Config.ExecProviderConfig.Args = nil
   461  	}
   462  	// populate deprecated fields for backward compatibility
   463  	clust.ServerVersion = clust.Info.ServerVersion
   464  	clust.ConnectionState = clust.Info.ConnectionState
   465  	return clust
   466  }
   467  
   468  // InvalidateCache invalidates cluster cache
   469  func (s *Server) InvalidateCache(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   470  	cls, err := s.getClusterWith403IfNotExist(ctx, q)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(cls.Project, q.Server)); err != nil {
   475  		return nil, err
   476  	}
   477  	now := v1.Now()
   478  	cls.RefreshRequestedAt = &now
   479  	cls, err = s.db.UpdateCluster(ctx, cls)
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  	return s.toAPIResponse(cls), nil
   484  }