github.com/argoproj/argo-cd/v3@v3.2.1/test/e2e/deployment_test.go (about) 1 package e2e 2 3 import ( 4 "context" 5 "encoding/json" 6 stderrors "errors" 7 "fmt" 8 "os" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 corev1 "k8s.io/api/core/v1" 15 rbacv1 "k8s.io/api/rbac/v1" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/apimachinery/pkg/util/wait" 18 "k8s.io/client-go/tools/clientcmd" 19 20 "github.com/argoproj/argo-cd/v3/common" 21 "github.com/argoproj/argo-cd/v3/util/clusterauth" 22 23 "github.com/argoproj/gitops-engine/pkg/health" 24 . "github.com/argoproj/gitops-engine/pkg/sync/common" 25 26 . "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 27 . "github.com/argoproj/argo-cd/v3/test/e2e/fixture" 28 . "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app" 29 ) 30 31 // when we have a config map generator, AND the ignore annotation, it is ignored in the app's sync status 32 func TestDeployment(t *testing.T) { 33 Given(t). 34 Path("deployment"). 35 When(). 36 CreateApp(). 37 Sync(). 38 Then(). 39 Expect(OperationPhaseIs(OperationSucceeded)). 40 Expect(SyncStatusIs(SyncStatusCodeSynced)). 41 Expect(HealthIs(health.HealthStatusHealthy)). 42 When(). 43 PatchFile("deployment.yaml", `[ 44 { 45 "op": "replace", 46 "path": "/spec/template/spec/containers/0/image", 47 "value": "nginx:1.17.4-alpine" 48 } 49 ]`). 50 Sync() 51 } 52 53 func TestDeploymentWithAnnotationTrackingMode(t *testing.T) { 54 ctx := Given(t) 55 56 require.NoError(t, SetTrackingMethod(string(TrackingMethodAnnotation))) 57 ctx. 58 Path("deployment"). 59 When(). 60 CreateApp(). 61 Sync(). 62 Then(). 63 Expect(OperationPhaseIs(OperationSucceeded)). 64 Expect(SyncStatusIs(SyncStatusCodeSynced)). 65 Expect(HealthIs(health.HealthStatusHealthy)). 66 When(). 67 Then(). 68 And(func(_ *Application) { 69 out, err := RunCli("app", "manifests", ctx.AppName()) 70 require.NoError(t, err) 71 assert.Contains(t, out, fmt.Sprintf(`annotations: 72 argocd.argoproj.io/tracking-id: %s:apps/Deployment:%s/nginx-deployment 73 `, ctx.AppName(), DeploymentNamespace())) 74 }) 75 } 76 77 func TestDeploymentWithLabelTrackingMode(t *testing.T) { 78 ctx := Given(t) 79 require.NoError(t, SetTrackingMethod(string(TrackingMethodLabel))) 80 ctx. 81 Path("deployment"). 82 When(). 83 CreateApp(). 84 Sync(). 85 Then(). 86 Expect(OperationPhaseIs(OperationSucceeded)). 87 Expect(SyncStatusIs(SyncStatusCodeSynced)). 88 Expect(HealthIs(health.HealthStatusHealthy)). 89 When(). 90 Then(). 91 And(func(_ *Application) { 92 out, err := RunCli("app", "manifests", ctx.AppName()) 93 require.NoError(t, err) 94 assert.Contains(t, out, fmt.Sprintf(`labels: 95 app: nginx 96 app.kubernetes.io/instance: %s 97 `, ctx.AppName())) 98 }) 99 } 100 101 func TestDeploymentWithoutTrackingMode(t *testing.T) { 102 ctx := Given(t) 103 ctx. 104 Path("deployment"). 105 When(). 106 CreateApp(). 107 Sync(). 108 Then(). 109 Expect(OperationPhaseIs(OperationSucceeded)). 110 Expect(SyncStatusIs(SyncStatusCodeSynced)). 111 Expect(HealthIs(health.HealthStatusHealthy)). 112 When(). 113 Then(). 114 And(func(_ *Application) { 115 out, err := RunCli("app", "manifests", ctx.AppName()) 116 require.NoError(t, err) 117 assert.Contains(t, out, fmt.Sprintf(`annotations: 118 argocd.argoproj.io/tracking-id: %s:apps/Deployment:%s/nginx-deployment 119 `, ctx.AppName(), DeploymentNamespace())) 120 }) 121 } 122 123 // This test verifies that Argo CD can: 124 // A) Deploy to a cluster where the URL of the cluster contains a query parameter: e.g. https://(kubernetes-url):443/?context=some-val 125 // and 126 // B) Multiple users can deploy to the same K8s cluster, using above mechanism (but with different Argo CD Cluster Secrets, and different ServiceAccounts) 127 func TestDeployToKubernetesAPIURLWithQueryParameter(t *testing.T) { 128 // We test with both a cluster-scoped, and a non-cluster scoped, Argo CD Cluster Secret. 129 clusterScopedParam := []bool{false, true} 130 for _, clusterScoped := range clusterScopedParam { 131 EnsureCleanState(t) 132 133 // Simulate two users, each with their own Argo CD cluster secret that can only deploy to their Namespace 134 users := []string{E2ETestPrefix + "user1", E2ETestPrefix + "user2"} 135 136 for _, username := range users { 137 createNamespaceScopedUser(t, username, clusterScoped) 138 139 GivenWithSameState(t). 140 Name("e2e-test-app-"+username). 141 Path("deployment"). 142 When(). 143 CreateWithNoNameSpace("--dest-namespace", username). 144 Sync(). 145 Then(). 146 Expect(OperationPhaseIs(OperationSucceeded)). 147 Expect(SyncStatusIs(SyncStatusCodeSynced)). 148 Expect(HealthIs(health.HealthStatusHealthy)) 149 } 150 } 151 } 152 153 // This test verifies that Argo CD can: 154 // When multiple Argo CD cluster secrets used to deploy to the same cluster (using query parameters), that the ServiceAccount RBAC 155 // fully enforces user boundary. 156 // Our simulated user's ServiceAccounts should not be able to deploy into a namespace that is outside that SA's RBAC. 157 func TestArgoCDSupportsMultipleServiceAccountsWithDifferingRBACOnSameCluster(t *testing.T) { 158 // We test with both a cluster-scoped, and a non-cluster scoped, Argo CD Cluster Secret. 159 clusterScopedParam := []bool{ /*false,*/ true} 160 161 for _, clusterScoped := range clusterScopedParam { 162 EnsureCleanState(t) 163 164 // Simulate two users, each with their own Argo CD cluster secret that can only deploy to their Namespace 165 users := []string{E2ETestPrefix + "user1", E2ETestPrefix + "user2"} 166 167 for _, username := range users { 168 createNamespaceScopedUser(t, username, clusterScoped) 169 } 170 171 for idx, username := range users { 172 // we should use user-a's serviceaccount to deploy to user-b's namespace, and vice versa 173 // - If everything as working as expected, this should fail. 174 otherUser := users[(idx+1)%len(users)] 175 176 // e.g. Attempt to deploy to user1's namespace, with user2's cluster Secret. This should fail, as user2's cluster Secret does not have the requisite permissions. 177 consequences := GivenWithSameState(t). 178 Name("e2e-test-app-"+username). 179 DestName(E2ETestPrefix+"cluster-"+otherUser). 180 Path("deployment"). 181 When(). 182 CreateWithNoNameSpace("--dest-namespace", username).IgnoreErrors(). 183 Sync().Then() 184 185 // The error message differs based on whether the Argo CD Cluster Secret is namespace-scoped or cluster-scoped, but the idea is the same: 186 // - Even when deploying to the same cluster using 2 separate ServiceAccounts, the RBAC of those ServiceAccounts should continue to fully enforce RBAC boundaries. 187 188 if !clusterScoped { 189 consequences.Expect(Condition(ApplicationConditionComparisonError, "Namespace \""+username+"\" for Deployment \"nginx-deployment\" is not managed")) 190 } else { 191 consequences.Expect(OperationMessageContains("User \"system:serviceaccount:" + otherUser + ":" + otherUser + "-serviceaccount\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"" + username + "\"")) 192 } 193 } 194 } 195 } 196 197 // generateReadOnlyClusterRoleandBindingForServiceAccount creates a ClusterRole/Binding that allows a ServiceAccount in a given namespace to read all resources on a cluster. 198 // - This allows the ServiceAccount to be used within a cluster-scoped Argo CD Cluster Secret 199 func generateReadOnlyClusterRoleandBindingForServiceAccount(roleSuffix string, serviceAccountNS string) (rbacv1.ClusterRole, rbacv1.ClusterRoleBinding) { 200 clusterRole := rbacv1.ClusterRole{ 201 ObjectMeta: metav1.ObjectMeta{ 202 Name: E2ETestPrefix + "read-all-" + roleSuffix, 203 }, 204 Rules: []rbacv1.PolicyRule{{ 205 Verbs: []string{"get", "list", "watch"}, 206 Resources: []string{"*"}, 207 APIGroups: []string{"*"}, 208 }}, 209 } 210 211 clusterRoleBinding := rbacv1.ClusterRoleBinding{ 212 ObjectMeta: metav1.ObjectMeta{ 213 Name: E2ETestPrefix + "read-all-" + roleSuffix, 214 }, 215 Subjects: []rbacv1.Subject{{ 216 Kind: rbacv1.ServiceAccountKind, 217 Namespace: serviceAccountNS, 218 Name: roleSuffix + "-serviceaccount", 219 }}, 220 RoleRef: rbacv1.RoleRef{ 221 APIGroup: "rbac.authorization.k8s.io", 222 Kind: "ClusterRole", 223 Name: clusterRole.Name, 224 }, 225 } 226 227 return clusterRole, clusterRoleBinding 228 } 229 230 // buildArgoCDClusterSecret build (but does not create) an Argo CD Cluster Secret object with the given values 231 func buildArgoCDClusterSecret(secretName, secretNamespace, clusterName, clusterServer, clusterConfigJSON, clusterResources, clusterNamespaces string) corev1.Secret { 232 res := corev1.Secret{ 233 ObjectMeta: metav1.ObjectMeta{ 234 Name: secretName, 235 Namespace: secretNamespace, 236 Labels: map[string]string{ 237 common.LabelKeySecretType: common.LabelValueSecretTypeCluster, 238 }, 239 }, 240 Data: map[string][]byte{ 241 "name": []byte(clusterName), 242 "server": []byte(clusterServer), 243 "config": []byte(string(clusterConfigJSON)), 244 }, 245 } 246 247 if clusterResources != "" { 248 res.Data["clusterResources"] = []byte(clusterResources) 249 } 250 251 if clusterNamespaces != "" { 252 res.Data["namespaces"] = []byte(clusterNamespaces) 253 } 254 255 return res 256 } 257 258 // createNamespaceScopedUser 259 // - username = name of Namespace the simulated user is able to deploy to 260 // - clusterScopedSecrets = whether the Service Account is namespace-scoped or cluster-scoped. 261 func createNamespaceScopedUser(t *testing.T, username string, clusterScopedSecrets bool) { 262 t.Helper() 263 // Create a new Namespace for our simulated user 264 ns := corev1.Namespace{ 265 ObjectMeta: metav1.ObjectMeta{ 266 Name: username, 267 }, 268 } 269 _, err := KubeClientset.CoreV1().Namespaces().Create(t.Context(), &ns, metav1.CreateOptions{}) 270 require.NoError(t, err) 271 272 // Create a ServiceAccount in that Namespace, which will be used for the Argo CD Cluster SEcret 273 serviceAccountName := username + "-serviceaccount" 274 err = clusterauth.CreateServiceAccount(KubeClientset, serviceAccountName, ns.Name) 275 require.NoError(t, err) 276 277 // Create a Role that allows the ServiceAccount to read/write all within the Namespace 278 role := rbacv1.Role{ 279 ObjectMeta: metav1.ObjectMeta{ 280 Name: E2ETestPrefix + "allow-all", 281 Namespace: ns.Name, 282 }, 283 Rules: []rbacv1.PolicyRule{{ 284 Verbs: []string{"*"}, 285 Resources: []string{"*"}, 286 APIGroups: []string{"*"}, 287 }}, 288 } 289 _, err = KubeClientset.RbacV1().Roles(role.Namespace).Create(t.Context(), &role, metav1.CreateOptions{}) 290 require.NoError(t, err) 291 292 // Bind the Role with the ServiceAccount in the Namespace 293 roleBinding := rbacv1.RoleBinding{ 294 ObjectMeta: metav1.ObjectMeta{ 295 Name: E2ETestPrefix + "allow-all-binding", 296 Namespace: ns.Name, 297 }, 298 Subjects: []rbacv1.Subject{{ 299 Kind: rbacv1.ServiceAccountKind, 300 Name: serviceAccountName, 301 Namespace: ns.Name, 302 }}, 303 RoleRef: rbacv1.RoleRef{ 304 APIGroup: "rbac.authorization.k8s.io", 305 Kind: "Role", 306 Name: role.Name, 307 }, 308 } 309 _, err = KubeClientset.RbacV1().RoleBindings(roleBinding.Namespace).Create(t.Context(), &roleBinding, metav1.CreateOptions{}) 310 require.NoError(t, err) 311 312 var token string 313 314 // Attempting to patch the ServiceAccount can intermittently fail with 'failed to patch serviceaccount "(...)" with bearer token secret: Operation cannot be fulfilled on serviceaccounts "(...)": the object has been modified; please apply your changes to the latest version and try again' 315 // We thus keep trying for up to 20 seconds. 316 waitErr := wait.PollUntilContextTimeout(t.Context(), 1*time.Second, 20*time.Second, true, func(context.Context) (done bool, err error) { 317 // Retrieve the bearer token from the ServiceAccount 318 token, err = clusterauth.GetServiceAccountBearerToken(KubeClientset, ns.Name, serviceAccountName, time.Second*60) 319 320 // Success is no error and a real token, otherwise keep trying 321 return (err == nil && token != ""), nil 322 }) 323 require.NoError(t, waitErr) 324 require.NotEmpty(t, token) 325 326 // In order to test a cluster-scoped Argo CD Cluster Secret, we may optionally grant the ServiceAccount read-all permissions at cluster scope. 327 if clusterScopedSecrets { 328 clusterRole, clusterRoleBinding := generateReadOnlyClusterRoleandBindingForServiceAccount(username, username) 329 330 _, err := KubeClientset.RbacV1().ClusterRoles().Create(t.Context(), &clusterRole, metav1.CreateOptions{}) 331 require.NoError(t, err) 332 333 _, err = KubeClientset.RbacV1().ClusterRoleBindings().Create(t.Context(), &clusterRoleBinding, metav1.CreateOptions{}) 334 require.NoError(t, err) 335 } 336 337 // Build the Argo CD Cluster Secret by using the service account token, and extracting needed values from kube config 338 clusterSecretConfigJSON := ClusterConfig{ 339 BearerToken: token, 340 TLSClientConfig: TLSClientConfig{ 341 Insecure: true, 342 }, 343 } 344 345 jsonStringBytes, err := json.Marshal(clusterSecretConfigJSON) 346 require.NoError(t, err) 347 348 _, apiURL, err := extractKubeConfigValues() 349 require.NoError(t, err) 350 351 clusterResourcesField := "" 352 namespacesField := "" 353 354 if !clusterScopedSecrets { 355 clusterResourcesField = "false" 356 namespacesField = ns.Name 357 } 358 359 // We create an Argo CD cluster Secret declaratively, using the K8s client, rather than via CLI, as the CLI doesn't currently 360 // support Kubernetes API server URLs with query parameters. 361 362 secret := buildArgoCDClusterSecret("test-"+username, ArgoCDNamespace, E2ETestPrefix+"cluster-"+username, apiURL+"?user="+username, 363 string(jsonStringBytes), clusterResourcesField, namespacesField) 364 365 // Finally, create the Cluster secret in the Argo CD E2E namespace 366 _, err = KubeClientset.CoreV1().Secrets(secret.Namespace).Create(t.Context(), &secret, metav1.CreateOptions{}) 367 require.NoError(t, err) 368 } 369 370 // extractKubeConfigValues returns contents of the local environment's kubeconfig, using standard path resolution mechanism. 371 // Returns: 372 // - contents of kubeconfig 373 // - server name (within the kubeconfig) 374 // - error 375 func extractKubeConfigValues() (string, string, error) { 376 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 377 378 config, err := loadingRules.Load() 379 if err != nil { 380 return "", "", err 381 } 382 383 context, ok := config.Contexts[config.CurrentContext] 384 if !ok || context == nil { 385 return "", "", stderrors.New("no context") 386 } 387 388 cluster, ok := config.Clusters[context.Cluster] 389 if !ok || cluster == nil { 390 return "", "", stderrors.New("no cluster") 391 } 392 393 var kubeConfigDefault string 394 395 paths := loadingRules.Precedence 396 // For all the kubeconfig paths, look for one that exists 397 for _, path := range paths { 398 _, err = os.Stat(path) 399 if err == nil { 400 // Success 401 kubeConfigDefault = path 402 break 403 } // Otherwise, continue. 404 } 405 406 if kubeConfigDefault == "" { 407 return "", "", stderrors.New("unable to retrieve kube config path") 408 } 409 410 kubeConfigContents, err := os.ReadFile(kubeConfigDefault) 411 if err != nil { 412 return "", "", err 413 } 414 415 return string(kubeConfigContents), cluster.Server, nil 416 }