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  }