github.com/argoproj/argo-cd@v1.8.7/server/cluster/cluster.go (about)

     1  package cluster
     2  
     3  import (
     4  	"time"
     5  
     6  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
     7  	log "github.com/sirupsen/logrus"
     8  	"golang.org/x/net/context"
     9  	"google.golang.org/grpc/codes"
    10  	"google.golang.org/grpc/status"
    11  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/client-go/kubernetes"
    13  
    14  	"github.com/argoproj/argo-cd/pkg/apiclient/cluster"
    15  	appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    16  	servercache "github.com/argoproj/argo-cd/server/cache"
    17  	"github.com/argoproj/argo-cd/server/rbacpolicy"
    18  	"github.com/argoproj/argo-cd/util/clusterauth"
    19  	"github.com/argoproj/argo-cd/util/db"
    20  	"github.com/argoproj/argo-cd/util/rbac"
    21  )
    22  
    23  // Server provides a Cluster service
    24  type Server struct {
    25  	db      db.ArgoDB
    26  	enf     *rbac.Enforcer
    27  	cache   *servercache.Cache
    28  	kubectl kube.Kubectl
    29  }
    30  
    31  // NewServer returns a new instance of the Cluster service
    32  func NewServer(db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache, kubectl kube.Kubectl) *Server {
    33  	return &Server{
    34  		db:      db,
    35  		enf:     enf,
    36  		cache:   cache,
    37  		kubectl: kubectl,
    38  	}
    39  }
    40  
    41  // List returns list of clusters
    42  func (s *Server) List(ctx context.Context, q *cluster.ClusterQuery) (*appv1.ClusterList, error) {
    43  	clusterList, err := s.db.ListClusters(ctx)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  
    48  	items := make([]appv1.Cluster, 0)
    49  	for _, clust := range clusterList.Items {
    50  		if s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionGet, clust.Server) {
    51  			items = append(items, clust)
    52  		}
    53  	}
    54  	err = kube.RunAllAsync(len(items), func(i int) error {
    55  		items[i] = *s.toAPIResponse(&items[i])
    56  		return nil
    57  	})
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	clusterList.Items = items
    62  	return clusterList, nil
    63  }
    64  
    65  // Create creates a cluster
    66  func (s *Server) Create(ctx context.Context, q *cluster.ClusterCreateRequest) (*appv1.Cluster, error) {
    67  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionCreate, q.Cluster.Server); err != nil {
    68  		return nil, err
    69  	}
    70  	c := q.Cluster
    71  	serverVersion, err := s.kubectl.GetServerVersion(c.RESTConfig())
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	clust, err := s.db.CreateCluster(ctx, c)
    77  	if status.Convert(err).Code() == codes.AlreadyExists {
    78  		// act idempotent if existing spec matches new spec
    79  		existing, getErr := s.db.GetCluster(ctx, c.Server)
    80  		if getErr != nil {
    81  			return nil, status.Errorf(codes.Internal, "unable to check existing cluster details: %v", getErr)
    82  		}
    83  
    84  		if existing.Equals(c) {
    85  			clust = existing
    86  		} else if q.Upsert {
    87  			return s.Update(ctx, &cluster.ClusterUpdateRequest{Cluster: c})
    88  		} else {
    89  			return nil, status.Errorf(codes.InvalidArgument, "existing cluster spec is different; use upsert flag to force update")
    90  		}
    91  	}
    92  	err = s.cache.SetClusterInfo(c.Server, &appv1.ClusterInfo{
    93  		ServerVersion: serverVersion,
    94  		ConnectionState: appv1.ConnectionState{
    95  			Status:     appv1.ConnectionStatusSuccessful,
    96  			ModifiedAt: &v1.Time{Time: time.Now()},
    97  		},
    98  	})
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	return s.toAPIResponse(clust), err
   103  }
   104  
   105  // Get returns a cluster from a query
   106  func (s *Server) Get(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   107  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionGet, q.Server); err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	c, err := s.getCluster(ctx, q)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	return s.toAPIResponse(c), nil
   116  }
   117  
   118  func (s *Server) getCluster(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   119  
   120  	if q.Server != "" {
   121  		c, err := s.db.GetCluster(ctx, q.Server)
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  		return c, nil
   126  	}
   127  
   128  	//we only get the name when we specify Name in ApplicationDestination and next
   129  	//we want to find the server in order to populate ApplicationDestination.Server
   130  	if q.Name != "" {
   131  		clusterList, err := s.db.ListClusters(ctx)
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  		for _, c := range clusterList.Items {
   136  			if c.Name == q.Name {
   137  				return &c, nil
   138  			}
   139  		}
   140  	}
   141  
   142  	return nil, nil
   143  }
   144  
   145  var clusterFieldsByPath = map[string]func(updated *appv1.Cluster, existing *appv1.Cluster){
   146  	"name": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   147  		updated.Name = existing.Name
   148  	},
   149  	"namespaces": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   150  		updated.Namespaces = existing.Namespaces
   151  	},
   152  	"config": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   153  		updated.Config = existing.Config
   154  	},
   155  	"shard": func(updated *appv1.Cluster, existing *appv1.Cluster) {
   156  		updated.Shard = existing.Shard
   157  	},
   158  }
   159  
   160  // Update updates a cluster
   161  func (s *Server) Update(ctx context.Context, q *cluster.ClusterUpdateRequest) (*appv1.Cluster, error) {
   162  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, q.Cluster.Server); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	if len(q.UpdatedFields) != 0 {
   167  		existing, err := s.db.GetCluster(ctx, q.Cluster.Server)
   168  		if err != nil {
   169  			return nil, err
   170  		}
   171  
   172  		for _, path := range q.UpdatedFields {
   173  			if updater, ok := clusterFieldsByPath[path]; ok {
   174  				updater(existing, q.Cluster)
   175  			}
   176  		}
   177  		q.Cluster = existing
   178  	}
   179  
   180  	// Test the token we just created before persisting it
   181  	serverVersion, err := s.kubectl.GetServerVersion(q.Cluster.RESTConfig())
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	clust, err := s.db.UpdateCluster(ctx, q.Cluster)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	err = s.cache.SetClusterInfo(clust.Server, &appv1.ClusterInfo{
   191  		ServerVersion: serverVersion,
   192  		ConnectionState: appv1.ConnectionState{
   193  			Status:     appv1.ConnectionStatusSuccessful,
   194  			ModifiedAt: &v1.Time{Time: time.Now()},
   195  		},
   196  	})
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	return s.toAPIResponse(clust), nil
   201  }
   202  
   203  // Delete deletes a cluster by name
   204  func (s *Server) Delete(ctx context.Context, q *cluster.ClusterQuery) (*cluster.ClusterResponse, error) {
   205  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionDelete, q.Server); err != nil {
   206  		return nil, err
   207  	}
   208  	err := s.db.DeleteCluster(ctx, q.Server)
   209  	return &cluster.ClusterResponse{}, err
   210  }
   211  
   212  // RotateAuth rotates the bearer token used for a cluster
   213  func (s *Server) RotateAuth(ctx context.Context, q *cluster.ClusterQuery) (*cluster.ClusterResponse, error) {
   214  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, q.Server); err != nil {
   215  		return nil, err
   216  	}
   217  	logCtx := log.WithField("cluster", q.Server)
   218  	logCtx.Info("Rotating auth")
   219  	clust, err := s.db.GetCluster(ctx, q.Server)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  	restCfg := clust.RESTConfig()
   224  	if restCfg.BearerToken == "" {
   225  		return nil, status.Errorf(codes.InvalidArgument, "Cluster '%s' does not use bearer token authentication", q.Server)
   226  	}
   227  	claims, err := clusterauth.ParseServiceAccountToken(restCfg.BearerToken)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	kubeclientset, err := kubernetes.NewForConfig(restCfg)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	newSecret, err := clusterauth.GenerateNewClusterManagerSecret(kubeclientset, claims)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	// we are using token auth, make sure we don't store client-cert information
   240  	clust.Config.KeyData = nil
   241  	clust.Config.CertData = nil
   242  	clust.Config.BearerToken = string(newSecret.Data["token"])
   243  
   244  	// Test the token we just created before persisting it
   245  	serverVersion, err := s.kubectl.GetServerVersion(clust.RESTConfig())
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	_, err = s.db.UpdateCluster(ctx, clust)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	err = s.cache.SetClusterInfo(clust.Server, &appv1.ClusterInfo{
   254  		ServerVersion: serverVersion,
   255  		ConnectionState: appv1.ConnectionState{
   256  			Status:     appv1.ConnectionStatusSuccessful,
   257  			ModifiedAt: &v1.Time{Time: time.Now()},
   258  		},
   259  	})
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	err = clusterauth.RotateServiceAccountSecrets(kubeclientset, claims, newSecret)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	logCtx.Infof("Rotated auth (old: %s, new: %s)", claims.SecretName, newSecret.Name)
   268  	return &cluster.ClusterResponse{}, nil
   269  }
   270  
   271  func (s *Server) toAPIResponse(clust *appv1.Cluster) *appv1.Cluster {
   272  	_ = s.cache.GetClusterInfo(clust.Server, &clust.Info)
   273  
   274  	clust.Config.Password = ""
   275  	clust.Config.BearerToken = ""
   276  	clust.Config.TLSClientConfig.KeyData = nil
   277  	if clust.Config.ExecProviderConfig != nil {
   278  		// We can't know what the user has put into args or
   279  		// env vars on the exec provider that might be sensitive
   280  		// (e.g. --private-key=XXX, PASSWORD=XXX)
   281  		// Implicitly assumes the command executable name is non-sensitive
   282  		clust.Config.ExecProviderConfig.Env = make(map[string]string)
   283  		clust.Config.ExecProviderConfig.Args = nil
   284  	}
   285  	// populate deprecated fields for backward compatibility
   286  	clust.ServerVersion = clust.Info.ServerVersion
   287  	clust.ConnectionState = clust.Info.ConnectionState
   288  	return clust
   289  }
   290  
   291  // InvalidateCache invalidates cluster cache
   292  func (s *Server) InvalidateCache(ctx context.Context, q *cluster.ClusterQuery) (*appv1.Cluster, error) {
   293  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, q.Server); err != nil {
   294  		return nil, err
   295  	}
   296  	cls, err := s.db.GetCluster(ctx, q.Server)
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	now := v1.Now()
   301  	cls.RefreshRequestedAt = &now
   302  	cls, err = s.db.UpdateCluster(ctx, cls)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  	return s.toAPIResponse(cls), nil
   307  }