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 }