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  }