github.com/argoproj/argo-cd/v3@v3.2.1/util/db/cluster.go (about) 1 package db 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "maps" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 log "github.com/sirupsen/logrus" 14 "google.golang.org/grpc/codes" 15 "google.golang.org/grpc/status" 16 corev1 "k8s.io/api/core/v1" 17 apierrors "k8s.io/apimachinery/pkg/api/errors" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/watch" 20 "k8s.io/utils/ptr" 21 22 "github.com/argoproj/argo-cd/v3/common" 23 appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 24 "github.com/argoproj/argo-cd/v3/util/settings" 25 ) 26 27 var ( 28 localCluster = appv1.Cluster{ 29 Name: "in-cluster", 30 Server: appv1.KubernetesInternalAPIServerAddr, 31 ConnectionState: appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful}, 32 } 33 initLocalCluster sync.Once 34 ) 35 36 func (db *db) getLocalCluster() *appv1.Cluster { 37 initLocalCluster.Do(func() { 38 info, err := db.kubeclientset.Discovery().ServerVersion() 39 if err == nil { 40 //nolint:staticcheck 41 localCluster.ServerVersion = fmt.Sprintf("%s.%s", info.Major, info.Minor) 42 //nolint:staticcheck 43 localCluster.ConnectionState = appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful} 44 } else { 45 //nolint:staticcheck 46 localCluster.ConnectionState = appv1.ConnectionState{ 47 Status: appv1.ConnectionStatusFailed, 48 Message: err.Error(), 49 } 50 } 51 }) 52 cluster := localCluster.DeepCopy() 53 now := metav1.Now() 54 //nolint:staticcheck 55 cluster.ConnectionState.ModifiedAt = &now 56 return cluster 57 } 58 59 // ListClusters returns list of clusters 60 func (db *db) ListClusters(_ context.Context) (*appv1.ClusterList, error) { 61 clusterSecrets, err := db.listSecretsByType(common.LabelValueSecretTypeCluster) 62 if err != nil { 63 return nil, err 64 } 65 clusterList := appv1.ClusterList{ 66 Items: make([]appv1.Cluster, 0), 67 } 68 settings, err := db.settingsMgr.GetSettings() 69 if err != nil { 70 return nil, err 71 } 72 inClusterEnabled := settings.InClusterEnabled 73 hasInClusterCredentials := false 74 for _, clusterSecret := range clusterSecrets { 75 cluster, err := SecretToCluster(clusterSecret) 76 if err != nil { 77 log.Errorf("could not unmarshal cluster secret %s", clusterSecret.Name) 78 continue 79 } 80 if cluster.Server == appv1.KubernetesInternalAPIServerAddr { 81 if inClusterEnabled { 82 hasInClusterCredentials = true 83 clusterList.Items = append(clusterList.Items, *cluster) 84 } 85 } else { 86 clusterList.Items = append(clusterList.Items, *cluster) 87 } 88 } 89 if inClusterEnabled && !hasInClusterCredentials { 90 clusterList.Items = append(clusterList.Items, *db.getLocalCluster()) 91 } 92 return &clusterList, nil 93 } 94 95 // CreateCluster creates a cluster 96 func (db *db) CreateCluster(ctx context.Context, c *appv1.Cluster) (*appv1.Cluster, error) { 97 if c.Server == appv1.KubernetesInternalAPIServerAddr { 98 settings, err := db.settingsMgr.GetSettings() 99 if err != nil { 100 return nil, err 101 } 102 if !settings.InClusterEnabled { 103 return nil, status.Errorf(codes.InvalidArgument, "cannot register cluster: in-cluster has been disabled") 104 } 105 } 106 secName, err := URIToSecretName("cluster", c.Server) 107 if err != nil { 108 return nil, err 109 } 110 111 clusterSecret := &corev1.Secret{ 112 ObjectMeta: metav1.ObjectMeta{ 113 Name: secName, 114 }, 115 } 116 117 err = clusterToSecret(c, clusterSecret) 118 if err != nil { 119 return nil, err 120 } 121 122 clusterSecret, err = db.createSecret(ctx, clusterSecret) 123 if err != nil { 124 if apierrors.IsAlreadyExists(err) { 125 return nil, status.Errorf(codes.AlreadyExists, "cluster %q already exists", c.Server) 126 } 127 return nil, err 128 } 129 130 cluster, err := SecretToCluster(clusterSecret) 131 if err != nil { 132 return nil, status.Errorf(codes.InvalidArgument, "could not unmarshal cluster secret %s", clusterSecret.Name) 133 } 134 return cluster, db.settingsMgr.ResyncInformers() 135 } 136 137 // ClusterEvent contains information about cluster event 138 type ClusterEvent struct { 139 Type watch.EventType 140 Cluster *appv1.Cluster 141 } 142 143 func (db *db) WatchClusters(ctx context.Context, 144 handleAddEvent func(cluster *appv1.Cluster), 145 handleModEvent func(oldCluster *appv1.Cluster, newCluster *appv1.Cluster), 146 handleDeleteEvent func(clusterServer string), 147 ) error { 148 argoSettings, err := db.settingsMgr.GetSettings() 149 if err != nil { 150 return err 151 } 152 153 localCls := db.getLocalCluster() 154 if argoSettings.InClusterEnabled { 155 localCls, err = db.GetCluster(ctx, appv1.KubernetesInternalAPIServerAddr) 156 if err != nil { 157 return fmt.Errorf("could not get local cluster: %w", err) 158 } 159 handleAddEvent(localCls) 160 } 161 162 db.watchSecrets( 163 ctx, 164 common.LabelValueSecretTypeCluster, 165 166 func(secret *corev1.Secret) { 167 cluster, err := SecretToCluster(secret) 168 if err != nil { 169 log.Errorf("could not unmarshal cluster secret %s", secret.Name) 170 return 171 } 172 if cluster.Server == appv1.KubernetesInternalAPIServerAddr { 173 if argoSettings.InClusterEnabled { 174 // change local cluster event to modified, since it cannot be added at runtime 175 handleModEvent(localCls, cluster) 176 localCls = cluster 177 } 178 return 179 } 180 handleAddEvent(cluster) 181 }, 182 183 func(oldSecret *corev1.Secret, newSecret *corev1.Secret) { 184 oldCluster, err := SecretToCluster(oldSecret) 185 if err != nil { 186 log.Errorf("could not unmarshal cluster secret %s", oldSecret.Name) 187 return 188 } 189 newCluster, err := SecretToCluster(newSecret) 190 if err != nil { 191 log.Errorf("could not unmarshal cluster secret %s", newSecret.Name) 192 return 193 } 194 if newCluster.Server == appv1.KubernetesInternalAPIServerAddr { 195 localCls = newCluster 196 } 197 handleModEvent(oldCluster, newCluster) 198 }, 199 200 func(secret *corev1.Secret) { 201 if string(secret.Data["server"]) == appv1.KubernetesInternalAPIServerAddr && argoSettings.InClusterEnabled { 202 // change local cluster event to modified, since it cannot be deleted at runtime, unless disabled. 203 newLocalCls := db.getLocalCluster() 204 handleModEvent(localCls, newLocalCls) 205 localCls = newLocalCls 206 } else { 207 handleDeleteEvent(string(secret.Data["server"])) 208 } 209 }, 210 ) 211 212 return err 213 } 214 215 func (db *db) getClusterSecret(server string) (*corev1.Secret, error) { 216 clusterSecrets, err := db.listSecretsByType(common.LabelValueSecretTypeCluster) 217 if err != nil { 218 return nil, err 219 } 220 srv := strings.TrimRight(server, "/") 221 for _, clusterSecret := range clusterSecrets { 222 if strings.TrimRight(string(clusterSecret.Data["server"]), "/") == srv { 223 return clusterSecret, nil 224 } 225 } 226 return nil, status.Errorf(codes.NotFound, "cluster %q not found", server) 227 } 228 229 // GetCluster returns a cluster from a query 230 func (db *db) GetCluster(_ context.Context, server string) (*appv1.Cluster, error) { 231 if server == appv1.KubernetesInternalAPIServerAddr { 232 argoSettings, err := db.settingsMgr.GetSettings() 233 if err != nil { 234 return nil, err 235 } 236 if !argoSettings.InClusterEnabled { 237 return nil, status.Errorf(codes.NotFound, "cluster %q is disabled", server) 238 } 239 } 240 241 informer, err := db.settingsMgr.GetSecretsInformer() 242 if err != nil { 243 return nil, err 244 } 245 res, err := informer.GetIndexer().ByIndex(settings.ByClusterURLIndexer, server) 246 if err != nil { 247 return nil, err 248 } 249 250 if len(res) > 0 { 251 return SecretToCluster(res[0].(*corev1.Secret)) 252 } 253 if server == appv1.KubernetesInternalAPIServerAddr { 254 return db.getLocalCluster(), nil 255 } 256 257 return nil, status.Errorf(codes.NotFound, "cluster %q not found", server) 258 } 259 260 // GetProjectClusters return project scoped clusters by given project name 261 func (db *db) GetProjectClusters(_ context.Context, project string) ([]*appv1.Cluster, error) { 262 informer, err := db.settingsMgr.GetSecretsInformer() 263 if err != nil { 264 return nil, fmt.Errorf("failed to get secrets informer: %w", err) 265 } 266 secrets, err := informer.GetIndexer().ByIndex(settings.ByProjectClusterIndexer, project) 267 if err != nil { 268 return nil, fmt.Errorf("failed to get index by project cluster indexer for project %q: %w", project, err) 269 } 270 var res []*appv1.Cluster 271 for i := range secrets { 272 cluster, err := SecretToCluster(secrets[i].(*corev1.Secret)) 273 if err != nil { 274 return nil, fmt.Errorf("failed to convert secret to cluster: %w", err) 275 } 276 res = append(res, cluster) 277 } 278 return res, nil 279 } 280 281 func (db *db) GetClusterServersByName(_ context.Context, name string) ([]string, error) { 282 argoSettings, err := db.settingsMgr.GetSettings() 283 if err != nil { 284 return nil, err 285 } 286 informer, err := db.settingsMgr.GetSecretsInformer() 287 if err != nil { 288 return nil, err 289 } 290 291 // if local cluster name is not overridden and specified name is local cluster name, return local cluster server 292 localClusterSecrets, err := informer.GetIndexer().ByIndex(settings.ByClusterURLIndexer, appv1.KubernetesInternalAPIServerAddr) 293 if err != nil { 294 return nil, err 295 } 296 297 if len(localClusterSecrets) == 0 && db.getLocalCluster().Name == name && argoSettings.InClusterEnabled { 298 return []string{appv1.KubernetesInternalAPIServerAddr}, nil 299 } 300 301 secrets, err := informer.GetIndexer().ByIndex(settings.ByClusterNameIndexer, name) 302 if err != nil { 303 return nil, err 304 } 305 var res []string 306 for i := range secrets { 307 s := secrets[i].(*corev1.Secret) 308 server := strings.TrimRight(string(s.Data["server"]), "/") 309 if !argoSettings.InClusterEnabled && server == appv1.KubernetesInternalAPIServerAddr { 310 continue 311 } 312 res = append(res, server) 313 } 314 return res, nil 315 } 316 317 // UpdateCluster updates a cluster 318 func (db *db) UpdateCluster(ctx context.Context, c *appv1.Cluster) (*appv1.Cluster, error) { 319 clusterSecret, err := db.getClusterSecret(c.Server) 320 if err != nil { 321 if status.Code(err) == codes.NotFound { 322 return db.CreateCluster(ctx, c) 323 } 324 return nil, err 325 } 326 if err := clusterToSecret(c, clusterSecret); err != nil { 327 return nil, err 328 } 329 330 clusterSecret, err = db.kubeclientset.CoreV1().Secrets(db.ns).Update(ctx, clusterSecret, metav1.UpdateOptions{}) 331 if err != nil { 332 return nil, err 333 } 334 cluster, err := SecretToCluster(clusterSecret) 335 if err != nil { 336 log.Errorf("could not unmarshal cluster secret %s", clusterSecret.Name) 337 return nil, err 338 } 339 return cluster, db.settingsMgr.ResyncInformers() 340 } 341 342 // DeleteCluster deletes a cluster by name 343 func (db *db) DeleteCluster(ctx context.Context, server string) error { 344 secret, err := db.getClusterSecret(server) 345 if err != nil { 346 return err 347 } 348 349 err = db.deleteSecret(ctx, secret) 350 if err != nil { 351 return err 352 } 353 354 return db.settingsMgr.ResyncInformers() 355 } 356 357 // clusterToSecret converts a cluster object to string data for serialization to a secret 358 func clusterToSecret(c *appv1.Cluster, secret *corev1.Secret) error { 359 data := make(map[string][]byte) 360 data["server"] = []byte(strings.TrimRight(c.Server, "/")) 361 if c.Name == "" { 362 data["name"] = []byte(c.Server) 363 } else { 364 data["name"] = []byte(c.Name) 365 } 366 if len(c.Namespaces) != 0 { 367 data["namespaces"] = []byte(strings.Join(c.Namespaces, ",")) 368 } 369 configBytes, err := json.Marshal(c.Config) 370 if err != nil { 371 return err 372 } 373 data["config"] = configBytes 374 if c.Shard != nil { 375 data["shard"] = []byte(strconv.Itoa(int(*c.Shard))) 376 } 377 if c.ClusterResources { 378 data["clusterResources"] = []byte("true") 379 } 380 if c.Project != "" { 381 data["project"] = []byte(c.Project) 382 } 383 secret.Data = data 384 385 secret.Labels = c.Labels 386 if c.Annotations != nil && c.Annotations[corev1.LastAppliedConfigAnnotation] != "" { 387 return status.Errorf(codes.InvalidArgument, "annotation %s cannot be set", corev1.LastAppliedConfigAnnotation) 388 } 389 secret.Annotations = c.Annotations 390 391 if secret.Annotations == nil { 392 secret.Annotations = make(map[string]string) 393 } 394 395 if c.RefreshRequestedAt != nil { 396 secret.Annotations[appv1.AnnotationKeyRefresh] = c.RefreshRequestedAt.Format(time.RFC3339) 397 } else { 398 delete(secret.Annotations, appv1.AnnotationKeyRefresh) 399 } 400 addSecretMetadata(secret, common.LabelValueSecretTypeCluster) 401 return nil 402 } 403 404 // SecretToCluster converts a secret into a Cluster object 405 func SecretToCluster(s *corev1.Secret) (*appv1.Cluster, error) { 406 var config appv1.ClusterConfig 407 if len(s.Data["config"]) > 0 { 408 err := json.Unmarshal(s.Data["config"], &config) 409 if err != nil { 410 return nil, fmt.Errorf("failed to unmarshal cluster config: %w", err) 411 } 412 } 413 414 var namespaces []string 415 for _, ns := range strings.Split(string(s.Data["namespaces"]), ",") { 416 if ns = strings.TrimSpace(ns); ns != "" { 417 namespaces = append(namespaces, ns) 418 } 419 } 420 var refreshRequestedAt *metav1.Time 421 if v, found := s.Annotations[appv1.AnnotationKeyRefresh]; found { 422 requestedAt, err := time.Parse(time.RFC3339, v) 423 if err != nil { 424 log.Warnf("Error while parsing date in cluster secret '%s': %v", s.Name, err) 425 } else { 426 refreshRequestedAt = &metav1.Time{Time: requestedAt} 427 } 428 } 429 var shard *int64 430 if shardStr := s.Data["shard"]; shardStr != nil { 431 if val, err := strconv.Atoi(string(shardStr)); err != nil { 432 log.Warnf("Error while parsing shard in cluster secret '%s': %v", s.Name, err) 433 } else { 434 shard = ptr.To(int64(val)) 435 } 436 } 437 438 // copy labels and annotations excluding system ones 439 labels := map[string]string{} 440 if s.Labels != nil { 441 labels = maps.Clone(s.Labels) 442 delete(labels, common.LabelKeySecretType) 443 } 444 annotations := map[string]string{} 445 if s.Annotations != nil { 446 annotations = maps.Clone(s.Annotations) 447 // delete system annotations 448 delete(annotations, corev1.LastAppliedConfigAnnotation) 449 delete(annotations, common.AnnotationKeyManagedBy) 450 } 451 452 cluster := appv1.Cluster{ 453 ID: string(s.UID), 454 Server: strings.TrimRight(string(s.Data["server"]), "/"), 455 Name: string(s.Data["name"]), 456 Namespaces: namespaces, 457 ClusterResources: string(s.Data["clusterResources"]) == "true", 458 Config: config, 459 RefreshRequestedAt: refreshRequestedAt, 460 Shard: shard, 461 Project: string(s.Data["project"]), 462 Labels: labels, 463 Annotations: annotations, 464 } 465 return &cluster, nil 466 }