github.com/argoproj/argo-cd/v2@v2.10.5/test/e2e/fixture/applicationsets/actions.go (about)

     1  package applicationsets
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
    11  
    12  	log "github.com/sirupsen/logrus"
    13  	corev1 "k8s.io/api/core/v1"
    14  	v1 "k8s.io/api/rbac/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    17  	"k8s.io/apimachinery/pkg/runtime/schema"
    18  	"k8s.io/apimachinery/pkg/util/wait"
    19  	"k8s.io/client-go/dynamic"
    20  
    21  	"github.com/argoproj/argo-cd/v2/common"
    22  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    23  	"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
    24  	"github.com/argoproj/argo-cd/v2/util/clusterauth"
    25  )
    26  
    27  // this implements the "when" part of given/when/then
    28  //
    29  // none of the func implement error checks, and that is complete intended, you should check for errors
    30  // using the Then()
    31  type Actions struct {
    32  	context        *Context
    33  	lastOutput     string
    34  	lastError      error
    35  	describeAction string
    36  	ignoreErrors   bool
    37  }
    38  
    39  var pdGVR = schema.GroupVersionResource{
    40  	Group:    "cluster.open-cluster-management.io",
    41  	Version:  "v1alpha1",
    42  	Resource: "placementdecisions",
    43  }
    44  
    45  // IgnoreErrors sets whether to ignore
    46  func (a *Actions) IgnoreErrors() *Actions {
    47  	a.ignoreErrors = true
    48  	return a
    49  }
    50  
    51  func (a *Actions) DoNotIgnoreErrors() *Actions {
    52  	a.ignoreErrors = false
    53  	return a
    54  }
    55  
    56  func (a *Actions) And(block func()) *Actions {
    57  	a.context.t.Helper()
    58  	block()
    59  	return a
    60  }
    61  
    62  func (a *Actions) Then() *Consequences {
    63  	a.context.t.Helper()
    64  	return &Consequences{a.context, a}
    65  }
    66  
    67  func (a *Actions) SwitchToExternalNamespace(namespace utils.ExternalNamespace) *Actions {
    68  	a.context.switchToNamespace = namespace
    69  	log.Infof("switched to external namespace: %s", namespace)
    70  	return a
    71  }
    72  
    73  func (a *Actions) SwitchToArgoCDNamespace() *Actions {
    74  	a.context.switchToNamespace = ""
    75  	log.Infof("switched to argocd namespace: %s", utils.ArgoCDNamespace)
    76  	return a
    77  }
    78  
    79  // CreateClusterSecret creates a faux cluster secret, with the given cluster server and cluster name (this cluster
    80  // will not actually be used by the Argo CD controller, but that's not needed for our E2E tests)
    81  func (a *Actions) CreateClusterSecret(secretName string, clusterName string, clusterServer string) *Actions {
    82  
    83  	fixtureClient := utils.GetE2EFixtureK8sClient()
    84  
    85  	var serviceAccountName string
    86  
    87  	// Look for a service account matching '*application-controller*'
    88  	err := wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
    89  
    90  		serviceAccountList, err := fixtureClient.KubeClientset.CoreV1().ServiceAccounts(fixture.TestNamespace()).List(context.Background(), metav1.ListOptions{})
    91  		if err != nil {
    92  			fmt.Println("Unable to retrieve ServiceAccount list", err)
    93  			return false, nil
    94  		}
    95  
    96  		// If 'application-controller' service account is present, use that
    97  		for _, sa := range serviceAccountList.Items {
    98  			if strings.Contains(sa.Name, "application-controller") {
    99  				serviceAccountName = sa.Name
   100  				return true, nil
   101  			}
   102  		}
   103  
   104  		// Otherwise, use 'default'
   105  		for _, sa := range serviceAccountList.Items {
   106  			if sa.Name == "default" {
   107  				serviceAccountName = sa.Name
   108  				return true, nil
   109  			}
   110  		}
   111  
   112  		return false, nil
   113  	})
   114  
   115  	if err == nil {
   116  		var bearerToken string
   117  		bearerToken, err = clusterauth.GetServiceAccountBearerToken(fixtureClient.KubeClientset, fixture.TestNamespace(), serviceAccountName, common.BearerTokenTimeout)
   118  
   119  		// bearerToken
   120  		secret := &corev1.Secret{
   121  			ObjectMeta: metav1.ObjectMeta{
   122  				Name:      secretName,
   123  				Namespace: fixture.TestNamespace(),
   124  				Labels: map[string]string{
   125  					common.LabelKeySecretType: common.LabelValueSecretTypeCluster,
   126  					utils.TestingLabel:        "true",
   127  				},
   128  			},
   129  			Data: map[string][]byte{
   130  				"name":   []byte(clusterName),
   131  				"server": []byte(clusterServer),
   132  				"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
   133  			},
   134  		}
   135  
   136  		// If the bearer token is available, use it rather than the fake username/password
   137  		if bearerToken != "" && err == nil {
   138  			secret.Data = map[string][]byte{
   139  				"name":   []byte(clusterName),
   140  				"server": []byte(clusterServer),
   141  				"config": []byte("{\"bearerToken\":\"" + bearerToken + "\"}"),
   142  			}
   143  		}
   144  
   145  		_, err = fixtureClient.KubeClientset.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
   146  	}
   147  
   148  	a.describeAction = fmt.Sprintf("creating cluster Secret '%s'", secretName)
   149  	a.lastOutput, a.lastError = "", err
   150  	a.verifyAction()
   151  
   152  	return a
   153  }
   154  
   155  // DeleteClusterSecret deletes a faux cluster secret
   156  func (a *Actions) DeleteClusterSecret(secretName string) *Actions {
   157  
   158  	err := utils.GetE2EFixtureK8sClient().KubeClientset.CoreV1().Secrets(fixture.TestNamespace()).Delete(context.Background(), secretName, metav1.DeleteOptions{})
   159  
   160  	a.describeAction = fmt.Sprintf("deleting cluster Secret '%s'", secretName)
   161  	a.lastOutput, a.lastError = "", err
   162  	a.verifyAction()
   163  
   164  	return a
   165  }
   166  
   167  // DeleteConfigMap deletes a faux cluster secret
   168  func (a *Actions) DeleteConfigMap(configMapName string) *Actions {
   169  
   170  	err := utils.GetE2EFixtureK8sClient().KubeClientset.CoreV1().ConfigMaps(fixture.TestNamespace()).Delete(context.Background(), configMapName, metav1.DeleteOptions{})
   171  
   172  	a.describeAction = fmt.Sprintf("deleting configMap '%s'", configMapName)
   173  	a.lastOutput, a.lastError = "", err
   174  	a.verifyAction()
   175  
   176  	return a
   177  }
   178  
   179  // DeletePlacementDecision deletes a faux cluster secret
   180  func (a *Actions) DeletePlacementDecision(placementDecisionName string) *Actions {
   181  
   182  	err := utils.GetE2EFixtureK8sClient().DynamicClientset.Resource(pdGVR).Namespace(fixture.TestNamespace()).Delete(context.Background(), placementDecisionName, metav1.DeleteOptions{})
   183  
   184  	a.describeAction = fmt.Sprintf("deleting placement decision '%s'", placementDecisionName)
   185  	a.lastOutput, a.lastError = "", err
   186  	a.verifyAction()
   187  
   188  	return a
   189  }
   190  
   191  // Create a temporary namespace, from utils.ApplicationSet, for use by the test.
   192  // This namespace will be deleted on subsequent tests.
   193  func (a *Actions) CreateNamespace(namespace string) *Actions {
   194  	a.context.t.Helper()
   195  
   196  	fixtureClient := utils.GetE2EFixtureK8sClient()
   197  
   198  	_, err := fixtureClient.KubeClientset.CoreV1().Namespaces().Create(context.Background(),
   199  		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{})
   200  
   201  	a.describeAction = fmt.Sprintf("creating namespace '%s'", namespace)
   202  	a.lastOutput, a.lastError = "", err
   203  	a.verifyAction()
   204  
   205  	return a
   206  }
   207  
   208  // Create creates an ApplicationSet using the provided value
   209  func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
   210  	a.context.t.Helper()
   211  
   212  	fixtureClient := utils.GetE2EFixtureK8sClient()
   213  
   214  	appSet.APIVersion = "argoproj.io/v1alpha1"
   215  	appSet.Kind = "ApplicationSet"
   216  
   217  	var appSetClientSet dynamic.ResourceInterface
   218  
   219  	if a.context.switchToNamespace != "" {
   220  		externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[utils.ExternalNamespace(a.context.switchToNamespace)]
   221  		if !found {
   222  			a.lastOutput, a.lastError = "", fmt.Errorf("No external clientset found for %s", a.context.switchToNamespace)
   223  			return a
   224  		}
   225  		appSetClientSet = externalAppSetClientset
   226  	} else {
   227  		appSetClientSet = fixtureClient.AppSetClientset
   228  	}
   229  
   230  	newResource, err := appSetClientSet.Create(context.Background(), utils.MustToUnstructured(&appSet), metav1.CreateOptions{})
   231  
   232  	if err == nil {
   233  		a.context.name = newResource.GetName()
   234  		a.context.namespace = newResource.GetNamespace()
   235  	}
   236  
   237  	a.describeAction = fmt.Sprintf("creating ApplicationSet '%s/%s'", appSet.Namespace, appSet.Name)
   238  	a.lastOutput, a.lastError = "", err
   239  	a.verifyAction()
   240  
   241  	return a
   242  }
   243  
   244  // Create Role/RoleBinding to allow ApplicationSet to list the PlacementDecisions
   245  func (a *Actions) CreatePlacementRoleAndRoleBinding() *Actions {
   246  	fixtureClient := utils.GetE2EFixtureK8sClient()
   247  
   248  	var err error
   249  
   250  	_, err = fixtureClient.KubeClientset.RbacV1().Roles(fixture.TestNamespace()).Create(context.Background(), &v1.Role{
   251  		ObjectMeta: metav1.ObjectMeta{Name: "placement-role", Namespace: fixture.TestNamespace()},
   252  		Rules: []v1.PolicyRule{
   253  			{
   254  				Verbs:     []string{"get", "list", "watch"},
   255  				APIGroups: []string{"cluster.open-cluster-management.io"},
   256  				Resources: []string{"placementdecisions"},
   257  			},
   258  		},
   259  	}, metav1.CreateOptions{})
   260  	if err != nil && strings.Contains(err.Error(), "already exists") {
   261  		err = nil
   262  	}
   263  
   264  	if err == nil {
   265  		_, err = fixtureClient.KubeClientset.RbacV1().RoleBindings(fixture.TestNamespace()).Create(context.Background(),
   266  			&v1.RoleBinding{
   267  				ObjectMeta: metav1.ObjectMeta{Name: "placement-role-binding", Namespace: fixture.TestNamespace()},
   268  				Subjects: []v1.Subject{
   269  					{
   270  						Name:      "argocd-applicationset-controller",
   271  						Namespace: fixture.TestNamespace(),
   272  						Kind:      "ServiceAccount",
   273  					},
   274  				},
   275  				RoleRef: v1.RoleRef{
   276  					Kind:     "Role",
   277  					APIGroup: "rbac.authorization.k8s.io",
   278  					Name:     "placement-role",
   279  				},
   280  			}, metav1.CreateOptions{})
   281  	}
   282  	if err != nil && strings.Contains(err.Error(), "already exists") {
   283  		err = nil
   284  	}
   285  
   286  	a.describeAction = "creating placement role/rolebinding"
   287  	a.lastOutput, a.lastError = "", err
   288  	a.verifyAction()
   289  
   290  	return a
   291  }
   292  
   293  // Create a ConfigMap for the ClusterResourceList generator
   294  func (a *Actions) CreatePlacementDecisionConfigMap(configMapName string) *Actions {
   295  	a.context.t.Helper()
   296  
   297  	fixtureClient := utils.GetE2EFixtureK8sClient()
   298  
   299  	_, err := fixtureClient.KubeClientset.CoreV1().ConfigMaps(fixture.TestNamespace()).Get(context.Background(), configMapName, metav1.GetOptions{})
   300  
   301  	// Don't do anything if it exists
   302  	if err == nil {
   303  		return a
   304  	}
   305  
   306  	_, err = fixtureClient.KubeClientset.CoreV1().ConfigMaps(fixture.TestNamespace()).Create(context.Background(),
   307  		&corev1.ConfigMap{
   308  			ObjectMeta: metav1.ObjectMeta{
   309  				Name: configMapName,
   310  			},
   311  			Data: map[string]string{
   312  				"apiVersion":    "cluster.open-cluster-management.io/v1alpha1",
   313  				"kind":          "placementdecisions",
   314  				"statusListKey": "decisions",
   315  				"matchKey":      "clusterName",
   316  			},
   317  		}, metav1.CreateOptions{})
   318  
   319  	a.describeAction = fmt.Sprintf("creating configmap '%s'", configMapName)
   320  	a.lastOutput, a.lastError = "", err
   321  	a.verifyAction()
   322  
   323  	return a
   324  }
   325  
   326  func (a *Actions) CreatePlacementDecision(placementDecisionName string) *Actions {
   327  	a.context.t.Helper()
   328  
   329  	fixtureClient := utils.GetE2EFixtureK8sClient().DynamicClientset
   330  
   331  	_, err := fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).Get(
   332  		context.Background(),
   333  		placementDecisionName,
   334  		metav1.GetOptions{})
   335  	// If already exists
   336  	if err == nil {
   337  		return a
   338  	}
   339  
   340  	placementDecision := &unstructured.Unstructured{
   341  		Object: map[string]interface{}{
   342  			"metadata": map[string]interface{}{
   343  				"name":      placementDecisionName,
   344  				"namespace": fixture.TestNamespace(),
   345  			},
   346  			"kind":       "PlacementDecision",
   347  			"apiVersion": "cluster.open-cluster-management.io/v1alpha1",
   348  			"status":     map[string]interface{}{},
   349  		},
   350  	}
   351  
   352  	_, err = fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).Create(
   353  		context.Background(),
   354  		placementDecision,
   355  		metav1.CreateOptions{})
   356  
   357  	a.describeAction = fmt.Sprintf("creating placementDecision '%v'", placementDecisionName)
   358  	a.lastOutput, a.lastError = "", err
   359  	a.verifyAction()
   360  
   361  	return a
   362  }
   363  
   364  func (a *Actions) StatusUpdatePlacementDecision(placementDecisionName string, clusterList []interface{}) *Actions {
   365  	a.context.t.Helper()
   366  
   367  	fixtureClient := utils.GetE2EFixtureK8sClient().DynamicClientset
   368  	placementDecision, err := fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).Get(
   369  		context.Background(),
   370  		placementDecisionName,
   371  		metav1.GetOptions{})
   372  
   373  	placementDecision.Object["status"] = map[string]interface{}{
   374  		"decisions": clusterList,
   375  	}
   376  
   377  	if err == nil {
   378  		_, err = fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).UpdateStatus(
   379  			context.Background(),
   380  			placementDecision,
   381  			metav1.UpdateOptions{})
   382  	}
   383  	a.describeAction = fmt.Sprintf("status update placementDecision for '%v'", clusterList)
   384  	a.lastOutput, a.lastError = "", err
   385  	a.verifyAction()
   386  
   387  	return a
   388  }
   389  
   390  // Delete deletes the ApplicationSet within the context
   391  func (a *Actions) Delete() *Actions {
   392  	a.context.t.Helper()
   393  
   394  	fixtureClient := utils.GetE2EFixtureK8sClient()
   395  
   396  	var appSetClientSet dynamic.ResourceInterface
   397  
   398  	if a.context.switchToNamespace != "" {
   399  		externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[utils.ExternalNamespace(a.context.switchToNamespace)]
   400  		if !found {
   401  			a.lastOutput, a.lastError = "", fmt.Errorf("No external clientset found for %s", a.context.switchToNamespace)
   402  			return a
   403  		}
   404  		appSetClientSet = externalAppSetClientset
   405  	} else {
   406  		appSetClientSet = fixtureClient.AppSetClientset
   407  	}
   408  
   409  	deleteProp := metav1.DeletePropagationForeground
   410  	err := appSetClientSet.Delete(context.Background(), a.context.name, metav1.DeleteOptions{PropagationPolicy: &deleteProp})
   411  	a.describeAction = fmt.Sprintf("Deleting ApplicationSet '%s/%s' %v", a.context.namespace, a.context.name, err)
   412  	a.lastOutput, a.lastError = "", err
   413  	a.verifyAction()
   414  
   415  	return a
   416  }
   417  
   418  // get retrieves the ApplicationSet (by name) that was created by an earlier Create action
   419  func (a *Actions) get() (*v1alpha1.ApplicationSet, error) {
   420  	appSet := v1alpha1.ApplicationSet{}
   421  
   422  	fixtureClient := utils.GetE2EFixtureK8sClient()
   423  
   424  	var appSetClientSet dynamic.ResourceInterface
   425  
   426  	if a.context.switchToNamespace != "" {
   427  		externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[utils.ExternalNamespace(a.context.switchToNamespace)]
   428  		if !found {
   429  			return nil, fmt.Errorf("No external clientset found for %s", a.context.switchToNamespace)
   430  		}
   431  		appSetClientSet = externalAppSetClientset
   432  	} else {
   433  		appSetClientSet = fixtureClient.AppSetClientset
   434  	}
   435  
   436  	newResource, err := appSetClientSet.Get(context.Background(), a.context.name, metav1.GetOptions{})
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  
   441  	bytes, err := newResource.MarshalJSON()
   442  	if err != nil {
   443  		return nil, err
   444  	}
   445  
   446  	err = json.Unmarshal(bytes, &appSet)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	return &appSet, nil
   452  
   453  }
   454  
   455  // Update retrieves the latest copy the ApplicationSet, then allows the caller to mutate it via 'toUpdate', with
   456  // the result applied back to the cluster resource
   457  func (a *Actions) Update(toUpdate func(*v1alpha1.ApplicationSet)) *Actions {
   458  	a.context.t.Helper()
   459  
   460  	timeout := 30 * time.Second
   461  
   462  	var mostRecentError error
   463  
   464  	for start := time.Now(); time.Since(start) < timeout; time.Sleep(3 * time.Second) {
   465  
   466  		appSet, err := a.get()
   467  		mostRecentError = err
   468  		if err == nil {
   469  			// Keep trying to update until it succeeds, or the test times out
   470  			toUpdate(appSet)
   471  			a.describeAction = fmt.Sprintf("updating ApplicationSet '%s/%s'", appSet.Namespace, appSet.Name)
   472  
   473  			fixtureClient := utils.GetE2EFixtureK8sClient()
   474  
   475  			var appSetClientSet dynamic.ResourceInterface
   476  
   477  			if a.context.switchToNamespace != "" {
   478  				externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[utils.ExternalNamespace(a.context.switchToNamespace)]
   479  				if !found {
   480  					a.lastOutput, a.lastError = "", fmt.Errorf("No external clientset found for %s", a.context.switchToNamespace)
   481  					return a
   482  				}
   483  				appSetClientSet = externalAppSetClientset
   484  			} else {
   485  				appSetClientSet = fixtureClient.AppSetClientset
   486  			}
   487  
   488  			_, err = appSetClientSet.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{})
   489  
   490  			if err != nil {
   491  				mostRecentError = err
   492  			} else {
   493  				mostRecentError = nil
   494  				break
   495  			}
   496  		}
   497  	}
   498  
   499  	a.lastOutput, a.lastError = "", mostRecentError
   500  	a.verifyAction()
   501  
   502  	return a
   503  }
   504  
   505  func (a *Actions) verifyAction() {
   506  	a.context.t.Helper()
   507  
   508  	if a.describeAction != "" {
   509  		log.Infof("action: %s", a.describeAction)
   510  		a.describeAction = ""
   511  	}
   512  
   513  	if !a.ignoreErrors {
   514  		a.Then().Expect(Success(""))
   515  	}
   516  }
   517  
   518  func (a *Actions) AppSet(appName string, flags ...string) *Actions {
   519  	a.context.t.Helper()
   520  	args := []string{"app", "set", appName}
   521  	args = append(args, flags...)
   522  	a.runCli(args...)
   523  	return a
   524  }
   525  
   526  func (a *Actions) runCli(args ...string) {
   527  	a.context.t.Helper()
   528  	a.lastOutput, a.lastError = fixture.RunCli(args...)
   529  	a.verifyAction()
   530  }