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  }