github.com/argoproj/argo-cd/v3@v3.2.1/server/cluster/cluster_test.go (about)

     1  package cluster
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"reflect"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/golang-jwt/jwt/v5"
    12  
    13  	"github.com/argoproj/argo-cd/v3/util/assets"
    14  
    15  	"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/mock"
    18  	"github.com/stretchr/testify/require"
    19  	corev1 "k8s.io/api/core/v1"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/runtime"
    22  	"k8s.io/client-go/kubernetes/fake"
    23  	"k8s.io/utils/ptr"
    24  
    25  	"github.com/argoproj/argo-cd/v3/common"
    26  	"github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
    27  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    28  	servercache "github.com/argoproj/argo-cd/v3/server/cache"
    29  	"github.com/argoproj/argo-cd/v3/test"
    30  	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
    31  	appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
    32  	"github.com/argoproj/argo-cd/v3/util/db"
    33  	dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks"
    34  	"github.com/argoproj/argo-cd/v3/util/rbac"
    35  	"github.com/argoproj/argo-cd/v3/util/settings"
    36  )
    37  
    38  const (
    39  	rootCACert = `-----BEGIN CERTIFICATE-----
    40  MIIC4DCCAcqgAwIBAgIBATALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu
    41  MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIxNTczN1oXDTE2MDExNTIxNTcz
    42  OFowIzEhMB8GA1UEAwwYMTAuMTMuMTI5LjEwNkAxNDIxMzU5MDU4MIIBIjANBgkq
    43  hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunDRXGwsiYWGFDlWH6kjGun+PshDGeZX
    44  xtx9lUnL8pIRWH3wX6f13PO9sktaOWW0T0mlo6k2bMlSLlSZgG9H6og0W6gLS3vq
    45  s4VavZ6DbXIwemZG2vbRwsvR+t4G6Nbwelm6F8RFnA1Fwt428pavmNQ/wgYzo+T1
    46  1eS+HiN4ACnSoDSx3QRWcgBkB1g6VReofVjx63i0J+w8Q/41L9GUuLqquFxu6ZnH
    47  60vTB55lHgFiDLjA1FkEz2dGvGh/wtnFlRvjaPC54JH2K1mPYAUXTreoeJtLJKX0
    48  ycoiyB24+zGCniUmgIsmQWRPaOPircexCp1BOeze82BT1LCZNTVaxQIDAQABoyMw
    49  ITAOBgNVHQ8BAf8EBAMCAKQwDwYDVR0TAQH/BAUwAwEB/zALBgkqhkiG9w0BAQsD
    50  ggEBADMxsUuAFlsYDpF4fRCzXXwrhbtj4oQwcHpbu+rnOPHCZupiafzZpDu+rw4x
    51  YGPnCb594bRTQn4pAu3Ac18NbLD5pV3uioAkv8oPkgr8aUhXqiv7KdDiaWm6sbAL
    52  EHiXVBBAFvQws10HMqMoKtO8f1XDNAUkWduakR/U6yMgvOPwS7xl0eUTqyRB6zGb
    53  K55q2dejiFWaFqB/y78txzvz6UlOZKE44g2JAVoJVM6kGaxh33q8/FmrL4kuN3ut
    54  W+MmJCVDvd4eEqPwbp7146ZWTqpIJ8lvA6wuChtqV8lhAPka2hD/LMqY8iXNmfXD
    55  uml0obOEy+ON91k+SWTJ3ggmF/U=
    56  -----END CERTIFICATE-----`
    57  
    58  	certData = `-----BEGIN CERTIFICATE-----
    59  MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu
    60  MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz
    61  MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB
    62  BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v
    63  b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj
    64  lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2
    65  I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb
    66  1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F
    67  kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P
    68  AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ
    69  KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/
    70  p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3
    71  jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq
    72  6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ
    73  HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ
    74  BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0
    75  -----END CERTIFICATE-----`
    76  
    77  	keyData = `-----BEGIN RSA PRIVATE KEY-----
    78  MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i
    79  wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc
    80  kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG
    81  0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv
    82  RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi
    83  ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU
    84  FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK
    85  aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm
    86  5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M
    87  ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0
    88  JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr
    89  7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI
    90  cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey
    91  OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/
    92  rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9
    93  8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg
    94  nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k
    95  2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII
    96  NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+
    97  GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S
    98  3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG
    99  77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/
   100  bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/
   101  F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX
   102  stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa
   103  -----END RSA PRIVATE KEY-----`
   104  )
   105  
   106  func newServerInMemoryCache() *servercache.Cache {
   107  	return servercache.NewCache(
   108  		appstatecache.NewCache(
   109  			cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Hour)),
   110  			1*time.Minute,
   111  		),
   112  		1*time.Minute,
   113  		1*time.Minute,
   114  	)
   115  }
   116  
   117  func newNoopEnforcer() *rbac.Enforcer {
   118  	enf := rbac.NewEnforcer(fake.NewClientset(test.NewFakeConfigMap()), test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil)
   119  	enf.EnableEnforce(false)
   120  	return enf
   121  }
   122  
   123  func newEnforcer() *rbac.Enforcer {
   124  	enforcer := rbac.NewEnforcer(fake.NewClientset(test.NewFakeConfigMap()), test.FakeArgoCDNamespace, common.ArgoCDRBACConfigMapName, nil)
   125  	_ = enforcer.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
   126  	enforcer.SetDefaultRole("role:test")
   127  	enforcer.SetClaimsEnforcerFunc(func(_ jwt.Claims, _ ...any) bool {
   128  		return true
   129  	})
   130  	return enforcer
   131  }
   132  
   133  func TestUpdateCluster_RejectInvalidParams(t *testing.T) {
   134  	t.Parallel()
   135  
   136  	testCases := []struct {
   137  		name    string
   138  		request cluster.ClusterUpdateRequest
   139  	}{
   140  		{
   141  			name:    "allowed cluster URL in body, disallowed cluster URL in query",
   142  			request: cluster.ClusterUpdateRequest{Cluster: &v1alpha1.Cluster{Name: "", Server: "https://127.0.0.1", Project: "", ClusterResources: true}, Id: &cluster.ClusterID{Type: "", Value: "https://127.0.0.2"}, UpdatedFields: []string{"clusterResources", "project"}},
   143  		},
   144  		{
   145  			name:    "allowed cluster URL in body, disallowed cluster name in query",
   146  			request: cluster.ClusterUpdateRequest{Cluster: &v1alpha1.Cluster{Name: "", Server: "https://127.0.0.1", Project: "", ClusterResources: true}, Id: &cluster.ClusterID{Type: "name", Value: "disallowed-unscoped"}, UpdatedFields: []string{"clusterResources", "project"}},
   147  		},
   148  		{
   149  			name:    "allowed cluster URL in body, disallowed cluster name in query, changing unscoped to scoped",
   150  			request: cluster.ClusterUpdateRequest{Cluster: &v1alpha1.Cluster{Name: "", Server: "https://127.0.0.1", Project: "allowed-project", ClusterResources: true}, Id: &cluster.ClusterID{Type: "", Value: "https://127.0.0.2"}, UpdatedFields: []string{"clusterResources", "project"}},
   151  		},
   152  		{
   153  			name:    "allowed cluster URL in body, disallowed cluster URL in query, changing unscoped to scoped",
   154  			request: cluster.ClusterUpdateRequest{Cluster: &v1alpha1.Cluster{Name: "", Server: "https://127.0.0.1", Project: "allowed-project", ClusterResources: true}, Id: &cluster.ClusterID{Type: "name", Value: "disallowed-unscoped"}, UpdatedFields: []string{"clusterResources", "project"}},
   155  		},
   156  	}
   157  
   158  	db := &dbmocks.ArgoDB{}
   159  
   160  	clusters := []v1alpha1.Cluster{
   161  		{
   162  			Name:   "allowed-unscoped",
   163  			Server: "https://127.0.0.1",
   164  		},
   165  		{
   166  			Name:   "disallowed-unscoped",
   167  			Server: "https://127.0.0.2",
   168  		},
   169  		{
   170  			Name:    "allowed-scoped",
   171  			Server:  "https://127.0.0.3",
   172  			Project: "allowed-project",
   173  		},
   174  		{
   175  			Name:    "disallowed-scoped",
   176  			Server:  "https://127.0.0.4",
   177  			Project: "disallowed-project",
   178  		},
   179  	}
   180  
   181  	db.On("ListClusters", mock.Anything).Return(
   182  		func(_ context.Context) *v1alpha1.ClusterList {
   183  			return &v1alpha1.ClusterList{
   184  				ListMeta: metav1.ListMeta{},
   185  				Items:    clusters,
   186  			}
   187  		},
   188  		func(_ context.Context) error {
   189  			return nil
   190  		},
   191  	)
   192  	db.On("UpdateCluster", mock.Anything, mock.Anything).Return(
   193  		func(_ context.Context, c *v1alpha1.Cluster) *v1alpha1.Cluster {
   194  			for _, cluster := range clusters {
   195  				if c.Server == cluster.Server {
   196  					return c
   197  				}
   198  			}
   199  			return nil
   200  		},
   201  		func(_ context.Context, c *v1alpha1.Cluster) error {
   202  			for _, cluster := range clusters {
   203  				if c.Server == cluster.Server {
   204  					return nil
   205  				}
   206  			}
   207  			return fmt.Errorf("cluster '%s' not found", c.Server)
   208  		},
   209  	)
   210  	db.On("GetCluster", mock.Anything, mock.Anything).Return(
   211  		func(_ context.Context, server string) *v1alpha1.Cluster {
   212  			for _, cluster := range clusters {
   213  				if server == cluster.Server {
   214  					return &cluster
   215  				}
   216  			}
   217  			return nil
   218  		},
   219  		func(_ context.Context, server string) error {
   220  			for _, cluster := range clusters {
   221  				if server == cluster.Server {
   222  					return nil
   223  				}
   224  			}
   225  			return fmt.Errorf("cluster '%s' not found", server)
   226  		},
   227  	)
   228  
   229  	enf := rbac.NewEnforcer(fake.NewClientset(test.NewFakeConfigMap()), test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil)
   230  	_ = enf.SetBuiltinPolicy(`p, role:test, clusters, *, https://127.0.0.1, allow
   231  p, role:test, clusters, *, allowed-project/*, allow`)
   232  	enf.SetDefaultRole("role:test")
   233  	server := NewServer(db, enf, newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   234  
   235  	for _, c := range testCases {
   236  		cc := c
   237  		t.Run(cc.name, func(t *testing.T) {
   238  			t.Parallel()
   239  			out, err := server.Update(t.Context(), &cc.request)
   240  			require.Nil(t, out)
   241  			assert.ErrorIs(t, err, common.PermissionDeniedAPIError)
   242  		})
   243  	}
   244  }
   245  
   246  func TestGetCluster_UrlEncodedName(t *testing.T) {
   247  	db := &dbmocks.ArgoDB{}
   248  
   249  	mockCluster := v1alpha1.Cluster{
   250  		Name:       "test/ing",
   251  		Server:     "https://127.0.0.1",
   252  		Namespaces: []string{"default", "kube-system"},
   253  	}
   254  	mockClusterList := v1alpha1.ClusterList{
   255  		ListMeta: metav1.ListMeta{},
   256  		Items: []v1alpha1.Cluster{
   257  			mockCluster,
   258  		},
   259  	}
   260  
   261  	db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
   262  
   263  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   264  
   265  	localCluster, err := server.Get(t.Context(), &cluster.ClusterQuery{
   266  		Id: &cluster.ClusterID{
   267  			Type:  "name_escaped",
   268  			Value: "test%2fing",
   269  		},
   270  	})
   271  	require.NoError(t, err)
   272  
   273  	assert.Equal(t, "test/ing", localCluster.Name)
   274  }
   275  
   276  func TestGetCluster_NameWithUrlEncodingButShouldNotBeUnescaped(t *testing.T) {
   277  	db := &dbmocks.ArgoDB{}
   278  
   279  	mockCluster := v1alpha1.Cluster{
   280  		Name:       "test%2fing",
   281  		Server:     "https://127.0.0.1",
   282  		Namespaces: []string{"default", "kube-system"},
   283  	}
   284  	mockClusterList := v1alpha1.ClusterList{
   285  		ListMeta: metav1.ListMeta{},
   286  		Items: []v1alpha1.Cluster{
   287  			mockCluster,
   288  		},
   289  	}
   290  
   291  	db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
   292  
   293  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   294  
   295  	localCluster, err := server.Get(t.Context(), &cluster.ClusterQuery{
   296  		Id: &cluster.ClusterID{
   297  			Type:  "name",
   298  			Value: "test%2fing",
   299  		},
   300  	})
   301  	require.NoError(t, err)
   302  
   303  	assert.Equal(t, "test%2fing", localCluster.Name)
   304  }
   305  
   306  func TestGetCluster_CannotSetCADataAndInsecureTrue(t *testing.T) {
   307  	testNamespace := "default"
   308  	localCluster := &v1alpha1.Cluster{
   309  		Name:       "my-cluster-name",
   310  		Server:     "https://my-cluster-server",
   311  		Namespaces: []string{testNamespace},
   312  		Config: v1alpha1.ClusterConfig{
   313  			TLSClientConfig: v1alpha1.TLSClientConfig{
   314  				Insecure: true,
   315  				CAData:   []byte(rootCACert),
   316  				CertData: []byte(certData),
   317  				KeyData:  []byte(keyData),
   318  			},
   319  		},
   320  	}
   321  	clientset := getClientset(nil, testNamespace)
   322  	db := db.NewDB(testNamespace, settings.NewSettingsManager(t.Context(), clientset, testNamespace), clientset)
   323  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   324  
   325  	t.Run("Create Fails When CAData is Set and Insecure is True", func(t *testing.T) {
   326  		_, err := server.Create(t.Context(), &cluster.ClusterCreateRequest{
   327  			Cluster: localCluster,
   328  		})
   329  
   330  		assert.EqualError(t, err, `error getting REST config: unable to apply K8s REST config defaults: specifying a root certificates file with the insecure flag is not allowed`)
   331  	})
   332  
   333  	localCluster.Config.CAData = nil
   334  	t.Run("Create Succeeds When CAData is nil and Insecure is True", func(t *testing.T) {
   335  		_, err := server.Create(t.Context(), &cluster.ClusterCreateRequest{
   336  			Cluster: localCluster,
   337  		})
   338  		require.NoError(t, err)
   339  	})
   340  }
   341  
   342  func TestUpdateCluster_NoFieldsPaths(t *testing.T) {
   343  	db := &dbmocks.ArgoDB{}
   344  	var updated *v1alpha1.Cluster
   345  
   346  	clusters := []v1alpha1.Cluster{
   347  		{
   348  			Name:       "minikube",
   349  			Server:     "https://127.0.0.1",
   350  			Namespaces: []string{"default", "kube-system"},
   351  		},
   352  	}
   353  
   354  	clusterList := v1alpha1.ClusterList{
   355  		ListMeta: metav1.ListMeta{},
   356  		Items:    clusters,
   357  	}
   358  
   359  	db.On("ListClusters", mock.Anything).Return(&clusterList, nil)
   360  	db.On("UpdateCluster", mock.Anything, mock.MatchedBy(func(c *v1alpha1.Cluster) bool {
   361  		updated = c
   362  		return true
   363  	})).Return(&v1alpha1.Cluster{}, nil)
   364  
   365  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   366  
   367  	_, err := server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   368  		Cluster: &v1alpha1.Cluster{
   369  			Name:       "minikube",
   370  			Namespaces: []string{"default", "kube-system"},
   371  		},
   372  	})
   373  
   374  	require.NoError(t, err)
   375  
   376  	assert.Equal(t, "minikube", updated.Name)
   377  	assert.Equal(t, []string{"default", "kube-system"}, updated.Namespaces)
   378  }
   379  
   380  func TestUpdateCluster_FieldsPathSet(t *testing.T) {
   381  	db := &dbmocks.ArgoDB{}
   382  	var updated *v1alpha1.Cluster
   383  	db.On("GetCluster", mock.Anything, "https://127.0.0.1").Return(&v1alpha1.Cluster{
   384  		Name:       "minikube",
   385  		Server:     "https://127.0.0.1",
   386  		Namespaces: []string{"default", "kube-system"},
   387  	}, nil)
   388  	db.On("UpdateCluster", mock.Anything, mock.MatchedBy(func(c *v1alpha1.Cluster) bool {
   389  		updated = c
   390  		return true
   391  	})).Return(&v1alpha1.Cluster{}, nil)
   392  
   393  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   394  
   395  	_, err := server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   396  		Cluster: &v1alpha1.Cluster{
   397  			Server: "https://127.0.0.1",
   398  			Shard:  ptr.To(int64(1)),
   399  		},
   400  		UpdatedFields: []string{"shard"},
   401  	})
   402  
   403  	require.NoError(t, err)
   404  
   405  	assert.Equal(t, "minikube", updated.Name)
   406  	assert.Equal(t, []string{"default", "kube-system"}, updated.Namespaces)
   407  	assert.Equal(t, int64(1), *updated.Shard)
   408  
   409  	labelEnv := map[string]string{
   410  		"env": "qa",
   411  	}
   412  	_, err = server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   413  		Cluster: &v1alpha1.Cluster{
   414  			Server: "https://127.0.0.1",
   415  			Labels: labelEnv,
   416  		},
   417  		UpdatedFields: []string{"labels"},
   418  	})
   419  
   420  	require.NoError(t, err)
   421  
   422  	assert.Equal(t, "minikube", updated.Name)
   423  	assert.Equal(t, []string{"default", "kube-system"}, updated.Namespaces)
   424  	assert.Equal(t, updated.Labels, labelEnv)
   425  
   426  	annotationEnv := map[string]string{
   427  		"env": "qa",
   428  	}
   429  	_, err = server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   430  		Cluster: &v1alpha1.Cluster{
   431  			Server:      "https://127.0.0.1",
   432  			Annotations: annotationEnv,
   433  		},
   434  		UpdatedFields: []string{"annotations"},
   435  	})
   436  
   437  	require.NoError(t, err)
   438  
   439  	assert.Equal(t, "minikube", updated.Name)
   440  	assert.Equal(t, []string{"default", "kube-system"}, updated.Namespaces)
   441  	assert.Equal(t, updated.Annotations, annotationEnv)
   442  
   443  	_, err = server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   444  		Cluster: &v1alpha1.Cluster{
   445  			Server:  "https://127.0.0.1",
   446  			Project: "new-project",
   447  		},
   448  		UpdatedFields: []string{"project"},
   449  	})
   450  
   451  	require.NoError(t, err)
   452  
   453  	assert.Equal(t, "minikube", updated.Name)
   454  	assert.Equal(t, []string{"default", "kube-system"}, updated.Namespaces)
   455  	assert.Equal(t, "new-project", updated.Project)
   456  }
   457  
   458  func TestDeleteClusterByName(t *testing.T) {
   459  	testNamespace := "default"
   460  	clientset := getClientset(nil, testNamespace, &corev1.Secret{
   461  		ObjectMeta: metav1.ObjectMeta{
   462  			Name:      "my-cluster-secret",
   463  			Namespace: testNamespace,
   464  			Labels: map[string]string{
   465  				common.LabelKeySecretType: common.LabelValueSecretTypeCluster,
   466  			},
   467  			Annotations: map[string]string{
   468  				common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
   469  			},
   470  		},
   471  		Data: map[string][]byte{
   472  			"name":   []byte("my-cluster-name"),
   473  			"server": []byte("https://my-cluster-server"),
   474  			"config": []byte("{}"),
   475  		},
   476  	})
   477  	db := db.NewDB(testNamespace, settings.NewSettingsManager(t.Context(), clientset, testNamespace), clientset)
   478  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   479  
   480  	t.Run("Delete Fails When Deleting by Unknown Name", func(t *testing.T) {
   481  		_, err := server.Delete(t.Context(), &cluster.ClusterQuery{
   482  			Name: "foo",
   483  		})
   484  
   485  		assert.EqualError(t, err, `failed to get cluster with permissions check: rpc error: code = PermissionDenied desc = permission denied`)
   486  	})
   487  
   488  	t.Run("Delete Succeeds When Deleting by Name", func(t *testing.T) {
   489  		_, err := server.Delete(t.Context(), &cluster.ClusterQuery{
   490  			Name: "my-cluster-name",
   491  		})
   492  		require.NoError(t, err)
   493  
   494  		_, err = db.GetCluster(t.Context(), "https://my-cluster-server")
   495  		assert.EqualError(t, err, `rpc error: code = NotFound desc = cluster "https://my-cluster-server" not found`)
   496  	})
   497  }
   498  
   499  func TestRotateAuth(t *testing.T) {
   500  	testNamespace := "kube-system"
   501  	token := "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhcmdvY2QtbWFuYWdlci10b2tlbi10ajc5ciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhcmdvY2QtbWFuYWdlciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjkxZGQzN2NmLThkOTItMTFlOS1hMDkxLWQ2NWYyYWU3ZmE4ZCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphcmdvY2QtbWFuYWdlciJ9.ytZjt2pDV8-A7DBMR06zQ3wt9cuVEfq262TQw7sdra-KRpDpMPnziMhc8bkwvgW-LGhTWUh5iu1y-1QhEx6mtbCt7vQArlBRxfvM5ys6ClFkplzq5c2TtZ7EzGSD0Up7tdxuG9dvR6TGXYdfFcG779yCdZo2H48sz5OSJfdEriduMEY1iL5suZd3ebOoVi1fGflmqFEkZX6SvxkoArl5mtNP6TvZ1eTcn64xh4ws152hxio42E-eSnl_CET4tpB5vgP5BVlSKW2xB7w2GJxqdETA5LJRI_OilY77dTOp8cMr_Ck3EOeda3zHfh4Okflg8rZFEeAuJYahQNeAILLkcA"
   502  	config := v1alpha1.ClusterConfig{
   503  		BearerToken: token,
   504  	}
   505  
   506  	configMarshal, err := json.Marshal(config)
   507  	require.NoError(t, err, "failed to marshal config for test")
   508  
   509  	clientset := getClientset(nil, testNamespace,
   510  		&corev1.Secret{
   511  			ObjectMeta: metav1.ObjectMeta{
   512  				Name:      "my-cluster-secret",
   513  				Namespace: testNamespace,
   514  				Labels: map[string]string{
   515  					common.LabelKeySecretType: common.LabelValueSecretTypeCluster,
   516  				},
   517  				Annotations: map[string]string{
   518  					common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
   519  				},
   520  			},
   521  			Data: map[string][]byte{
   522  				"name":   []byte("my-cluster-name"),
   523  				"server": []byte("https://my-cluster-name"),
   524  				"config": configMarshal,
   525  			},
   526  		},
   527  		&corev1.Namespace{
   528  			ObjectMeta: metav1.ObjectMeta{
   529  				Name: "kube-system",
   530  			},
   531  		},
   532  		&corev1.Secret{
   533  			ObjectMeta: metav1.ObjectMeta{
   534  				Name:      "argocd-manager-token-tj79r",
   535  				Namespace: "kube-system",
   536  			},
   537  			Data: map[string][]byte{
   538  				"token": []byte(token),
   539  			},
   540  		},
   541  		&corev1.ServiceAccount{
   542  			ObjectMeta: metav1.ObjectMeta{
   543  				Name:      "argocd-manager",
   544  				Namespace: "kube-system",
   545  			},
   546  			Secrets: []corev1.ObjectReference{
   547  				{
   548  					Kind: "Secret",
   549  					Name: "argocd-manager-token-tj79r",
   550  				},
   551  			},
   552  		})
   553  
   554  	db := db.NewDB(testNamespace, settings.NewSettingsManager(t.Context(), clientset, testNamespace), clientset)
   555  	server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   556  
   557  	t.Run("RotateAuth by Unknown Name", func(t *testing.T) {
   558  		_, err := server.RotateAuth(t.Context(), &cluster.ClusterQuery{
   559  			Name: "foo",
   560  		})
   561  
   562  		assert.EqualError(t, err, `failed to get cluster with permissions check: rpc error: code = PermissionDenied desc = permission denied`)
   563  	})
   564  
   565  	// While the tests results for the next two tests result in an error, they do
   566  	// demonstrate the proper mapping of cluster names/server to server info (i.e. my-cluster-name
   567  	// results in https://my-cluster-name info being used and https://my-cluster-name results in https://my-cluster-name).
   568  	t.Run("RotateAuth by Name - Error from no such host", func(t *testing.T) {
   569  		_, err := server.RotateAuth(t.Context(), &cluster.ClusterQuery{
   570  			Name: "my-cluster-name",
   571  		})
   572  
   573  		assert.ErrorContains(t, err, "Get \"https://my-cluster-name/")
   574  	})
   575  
   576  	t.Run("RotateAuth by Server - Error from no such host", func(t *testing.T) {
   577  		_, err := server.RotateAuth(t.Context(), &cluster.ClusterQuery{
   578  			Server: "https://my-cluster-name",
   579  		})
   580  
   581  		assert.ErrorContains(t, err, "Get \"https://my-cluster-name/")
   582  	})
   583  }
   584  
   585  func getClientset(config map[string]string, ns string, objects ...runtime.Object) *fake.Clientset {
   586  	secret := corev1.Secret{
   587  		ObjectMeta: metav1.ObjectMeta{
   588  			Name:      "argocd-secret",
   589  			Namespace: ns,
   590  		},
   591  		Data: map[string][]byte{
   592  			"admin.password":   []byte("test"),
   593  			"server.secretkey": []byte("test"),
   594  		},
   595  	}
   596  	cm := corev1.ConfigMap{
   597  		ObjectMeta: metav1.ObjectMeta{
   598  			Name:      "argocd-cm",
   599  			Namespace: ns,
   600  			Labels: map[string]string{
   601  				"app.kubernetes.io/part-of": "argocd",
   602  			},
   603  		},
   604  		Data: config,
   605  	}
   606  	return fake.NewClientset(append(objects, &cm, &secret)...)
   607  }
   608  
   609  func TestListCluster(t *testing.T) {
   610  	t.Parallel()
   611  
   612  	db := &dbmocks.ArgoDB{}
   613  
   614  	fooCluster := v1alpha1.Cluster{
   615  		Name:       "foo",
   616  		Server:     "https://127.0.0.1",
   617  		Namespaces: []string{"default", "kube-system"},
   618  	}
   619  	barCluster := v1alpha1.Cluster{
   620  		Name:       "bar",
   621  		Server:     "https://192.168.0.1",
   622  		Namespaces: []string{"default", "kube-system"},
   623  	}
   624  	bazCluster := v1alpha1.Cluster{
   625  		Name:       "test/ing",
   626  		Server:     "https://testing.com",
   627  		Namespaces: []string{"default", "kube-system"},
   628  	}
   629  
   630  	mockClusterList := v1alpha1.ClusterList{
   631  		ListMeta: metav1.ListMeta{},
   632  		Items:    []v1alpha1.Cluster{fooCluster, barCluster, bazCluster},
   633  	}
   634  
   635  	db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
   636  
   637  	s := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   638  
   639  	tests := []struct {
   640  		name    string
   641  		q       *cluster.ClusterQuery
   642  		want    *v1alpha1.ClusterList
   643  		wantErr bool
   644  	}{
   645  		{
   646  			name: "filter by name",
   647  			q: &cluster.ClusterQuery{
   648  				Name: fooCluster.Name,
   649  			},
   650  			want: &v1alpha1.ClusterList{
   651  				ListMeta: metav1.ListMeta{},
   652  				Items:    []v1alpha1.Cluster{fooCluster},
   653  			},
   654  		},
   655  		{
   656  			name: "filter by server",
   657  			q: &cluster.ClusterQuery{
   658  				Server: barCluster.Server,
   659  			},
   660  			want: &v1alpha1.ClusterList{
   661  				ListMeta: metav1.ListMeta{},
   662  				Items:    []v1alpha1.Cluster{barCluster},
   663  			},
   664  		},
   665  		{
   666  			name: "filter by id - name",
   667  			q: &cluster.ClusterQuery{
   668  				Id: &cluster.ClusterID{
   669  					Type:  "name",
   670  					Value: fooCluster.Name,
   671  				},
   672  			},
   673  			want: &v1alpha1.ClusterList{
   674  				ListMeta: metav1.ListMeta{},
   675  				Items:    []v1alpha1.Cluster{fooCluster},
   676  			},
   677  		},
   678  		{
   679  			name: "filter by id - name_escaped",
   680  			q: &cluster.ClusterQuery{
   681  				Id: &cluster.ClusterID{
   682  					Type:  "name_escaped",
   683  					Value: "test%2fing",
   684  				},
   685  			},
   686  			want: &v1alpha1.ClusterList{
   687  				ListMeta: metav1.ListMeta{},
   688  				Items:    []v1alpha1.Cluster{bazCluster},
   689  			},
   690  		},
   691  		{
   692  			name: "filter by id - server",
   693  			q: &cluster.ClusterQuery{
   694  				Id: &cluster.ClusterID{
   695  					Type:  "server",
   696  					Value: barCluster.Server,
   697  				},
   698  			},
   699  			want: &v1alpha1.ClusterList{
   700  				ListMeta: metav1.ListMeta{},
   701  				Items:    []v1alpha1.Cluster{barCluster},
   702  			},
   703  		},
   704  	}
   705  	for _, tt := range tests {
   706  		tt := tt
   707  
   708  		t.Run(tt.name, func(t *testing.T) {
   709  			t.Parallel()
   710  
   711  			got, err := s.List(t.Context(), tt.q)
   712  			if tt.wantErr {
   713  				assert.Error(t, err, "Server.List()")
   714  			} else {
   715  				require.NoError(t, err)
   716  				assert.Truef(t, reflect.DeepEqual(got, tt.want), "Server.List() = %v, want %v", got, tt.want)
   717  			}
   718  		})
   719  	}
   720  }
   721  
   722  func TestGetClusterAndVerifyAccess(t *testing.T) {
   723  	t.Run("GetClusterAndVerifyAccess - No Cluster", func(t *testing.T) {
   724  		db := &dbmocks.ArgoDB{}
   725  
   726  		mockCluster := v1alpha1.Cluster{
   727  			Name:       "test/ing",
   728  			Server:     "https://127.0.0.1",
   729  			Namespaces: []string{"default", "kube-system"},
   730  		}
   731  		mockClusterList := v1alpha1.ClusterList{
   732  			ListMeta: metav1.ListMeta{},
   733  			Items: []v1alpha1.Cluster{
   734  				mockCluster,
   735  			},
   736  		}
   737  
   738  		db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
   739  
   740  		server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   741  		localCluster, err := server.getClusterAndVerifyAccess(t.Context(), &cluster.ClusterQuery{
   742  			Name: "test/not-exists",
   743  		}, rbac.ActionGet)
   744  
   745  		assert.Nil(t, localCluster)
   746  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError)
   747  	})
   748  
   749  	t.Run("GetClusterAndVerifyAccess - Permissions Denied", func(t *testing.T) {
   750  		db := &dbmocks.ArgoDB{}
   751  
   752  		mockCluster := v1alpha1.Cluster{
   753  			Name:       "test/ing",
   754  			Server:     "https://127.0.0.1",
   755  			Namespaces: []string{"default", "kube-system"},
   756  		}
   757  		mockClusterList := v1alpha1.ClusterList{
   758  			ListMeta: metav1.ListMeta{},
   759  			Items: []v1alpha1.Cluster{
   760  				mockCluster,
   761  			},
   762  		}
   763  
   764  		db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
   765  
   766  		server := NewServer(db, newEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   767  		localCluster, err := server.getClusterAndVerifyAccess(t.Context(), &cluster.ClusterQuery{
   768  			Name: "test/ing",
   769  		}, rbac.ActionGet)
   770  
   771  		assert.Nil(t, localCluster)
   772  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError)
   773  	})
   774  }
   775  
   776  func TestNoClusterEnumeration(t *testing.T) {
   777  	db := &dbmocks.ArgoDB{}
   778  
   779  	mockCluster := v1alpha1.Cluster{
   780  		Name:       "test/ing",
   781  		Server:     "https://127.0.0.1",
   782  		Namespaces: []string{"default", "kube-system"},
   783  	}
   784  	mockClusterList := v1alpha1.ClusterList{
   785  		ListMeta: metav1.ListMeta{},
   786  		Items: []v1alpha1.Cluster{
   787  			mockCluster,
   788  		},
   789  	}
   790  
   791  	db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
   792  	db.On("GetCluster", mock.Anything, mock.Anything).Return(&mockCluster, nil)
   793  
   794  	server := NewServer(db, newEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
   795  
   796  	t.Run("Get", func(t *testing.T) {
   797  		_, err := server.Get(t.Context(), &cluster.ClusterQuery{
   798  			Name: "cluster-not-exists",
   799  		})
   800  		require.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   801  
   802  		_, err = server.Get(t.Context(), &cluster.ClusterQuery{
   803  			Name: "test/ing",
   804  		})
   805  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   806  	})
   807  
   808  	t.Run("Update", func(t *testing.T) {
   809  		_, err := server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   810  			Cluster: &v1alpha1.Cluster{
   811  				Name: "cluster-not-exists",
   812  			},
   813  		})
   814  		require.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   815  
   816  		_, err = server.Update(t.Context(), &cluster.ClusterUpdateRequest{
   817  			Cluster: &v1alpha1.Cluster{
   818  				Name: "test/ing",
   819  			},
   820  		})
   821  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   822  	})
   823  
   824  	t.Run("Delete", func(t *testing.T) {
   825  		_, err := server.Delete(t.Context(), &cluster.ClusterQuery{
   826  			Server: "https://127.0.0.2",
   827  		})
   828  		require.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   829  
   830  		_, err = server.Delete(t.Context(), &cluster.ClusterQuery{
   831  			Server: "https://127.0.0.1",
   832  		})
   833  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   834  	})
   835  
   836  	t.Run("RotateAuth", func(t *testing.T) {
   837  		_, err := server.RotateAuth(t.Context(), &cluster.ClusterQuery{
   838  			Server: "https://127.0.0.2",
   839  		})
   840  		require.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   841  
   842  		_, err = server.RotateAuth(t.Context(), &cluster.ClusterQuery{
   843  			Server: "https://127.0.0.1",
   844  		})
   845  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   846  	})
   847  
   848  	t.Run("InvalidateCache", func(t *testing.T) {
   849  		_, err := server.InvalidateCache(t.Context(), &cluster.ClusterQuery{
   850  			Server: "https://127.0.0.2",
   851  		})
   852  		require.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   853  
   854  		_, err = server.InvalidateCache(t.Context(), &cluster.ClusterQuery{
   855  			Server: "https://127.0.0.1",
   856  		})
   857  		assert.ErrorIs(t, err, common.PermissionDeniedAPIError, "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
   858  	})
   859  }