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