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 }