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 }