github.com/argoproj/argo-cd/v3@v3.2.1/test/e2e/fixture/applicationsets/utils/fixture.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	log "github.com/sirupsen/logrus"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"k8s.io/apimachinery/pkg/api/equality"
    18  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    21  	"k8s.io/apimachinery/pkg/runtime"
    22  	"k8s.io/client-go/dynamic"
    23  	"k8s.io/client-go/kubernetes"
    24  	"k8s.io/client-go/rest"
    25  	"k8s.io/client-go/tools/clientcmd"
    26  
    27  	"github.com/argoproj/argo-cd/v3/common"
    28  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    29  	appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned"
    30  	"github.com/argoproj/argo-cd/v3/test/e2e/fixture"
    31  	"github.com/argoproj/argo-cd/v3/util/errors"
    32  )
    33  
    34  type ExternalNamespace string
    35  
    36  const (
    37  	// ArgoCDNamespace is the namespace into which Argo CD and ApplicationSet controller are deployed,
    38  	// and in which Application resources should be created.
    39  	ArgoCDNamespace = "argocd-e2e"
    40  
    41  	// ArgoCDExternalNamespace is an external namespace to test additional namespaces
    42  	ArgoCDExternalNamespace ExternalNamespace = "argocd-e2e-external"
    43  
    44  	// ArgoCDExternalNamespace2 is an external namespace to test additional namespaces
    45  	ArgoCDExternalNamespace2 ExternalNamespace = "argocd-e2e-external-2"
    46  
    47  	// ApplicationsResourcesNamespace is the namespace into which temporary resources (such as Deployments/Pods/etc)
    48  	// can be deployed, such as using it as the target namespace in an Application resource.
    49  	// Note: this is NOT the namespace the ApplicationSet controller is deployed to; see ArgoCDNamespace.
    50  	ApplicationsResourcesNamespace = "applicationset-e2e"
    51  
    52  	TmpDir       = "/tmp/applicationset-e2e"
    53  	TestingLabel = "e2e.argoproj.io"
    54  )
    55  
    56  var (
    57  	id string
    58  
    59  	// call GetClientVars() to retrieve the Kubernetes client data for E2E test fixtures
    60  	clientInitialized  sync.Once
    61  	internalClientVars *E2EFixtureK8sClient
    62  )
    63  
    64  // E2EFixtureK8sClient contains Kubernetes clients initialized from local k8s configuration
    65  type E2EFixtureK8sClient struct {
    66  	KubeClientset            kubernetes.Interface
    67  	DynamicClientset         dynamic.Interface
    68  	AppClientset             appclientset.Interface
    69  	AppSetClientset          dynamic.ResourceInterface
    70  	ExternalAppSetClientsets map[ExternalNamespace]dynamic.ResourceInterface
    71  }
    72  
    73  func GetEnvWithDefault(envName, defaultValue string) string {
    74  	r := os.Getenv(envName)
    75  	if r == "" {
    76  		return defaultValue
    77  	}
    78  	return r
    79  }
    80  
    81  // TestNamespace returns the namespace where Argo CD E2E test instance will be
    82  // running in.
    83  func TestNamespace() string {
    84  	return GetEnvWithDefault("ARGOCD_E2E_NAMESPACE", ArgoCDNamespace)
    85  }
    86  
    87  // GetE2EFixtureK8sClient initializes the Kubernetes clients (if needed), and returns the most recently initialized value.
    88  // Note: this requires a local Kubernetes configuration (for example, while running the E2E tests).
    89  func GetE2EFixtureK8sClient(t *testing.T) *E2EFixtureK8sClient {
    90  	t.Helper()
    91  	// Initialize the Kubernetes clients only on first use
    92  	clientInitialized.Do(func() {
    93  		// set-up variables
    94  		config := getKubeConfig(t, "", clientcmd.ConfigOverrides{})
    95  
    96  		internalClientVars = &E2EFixtureK8sClient{
    97  			AppClientset:     appclientset.NewForConfigOrDie(config),
    98  			DynamicClientset: dynamic.NewForConfigOrDie(config),
    99  			KubeClientset:    kubernetes.NewForConfigOrDie(config),
   100  		}
   101  
   102  		internalClientVars.AppSetClientset = internalClientVars.DynamicClientset.Resource(v1alpha1.SchemeGroupVersion.WithResource("applicationsets")).Namespace(TestNamespace())
   103  		internalClientVars.ExternalAppSetClientsets = map[ExternalNamespace]dynamic.ResourceInterface{
   104  			ArgoCDExternalNamespace:  internalClientVars.DynamicClientset.Resource(v1alpha1.SchemeGroupVersion.WithResource("applicationsets")).Namespace(string(ArgoCDExternalNamespace)),
   105  			ArgoCDExternalNamespace2: internalClientVars.DynamicClientset.Resource(v1alpha1.SchemeGroupVersion.WithResource("applicationsets")).Namespace(string(ArgoCDExternalNamespace2)),
   106  		}
   107  	})
   108  	return internalClientVars
   109  }
   110  
   111  // EnsureCleanSlate ensures that the Kubernetes resources on the cluster are in a 'clean' state, before a test is run.
   112  func EnsureCleanState(t *testing.T) {
   113  	t.Helper()
   114  	start := time.Now()
   115  
   116  	fixtureClient := GetE2EFixtureK8sClient(t)
   117  
   118  	policy := metav1.DeletePropagationForeground
   119  
   120  	fixture.RunFunctionsInParallelAndCheckErrors(t, []func() error{
   121  		func() error {
   122  			// kubectl delete secrets -l argocd.argoproj.io/secret-type=repository
   123  			return fixtureClient.KubeClientset.CoreV1().Secrets(TestNamespace()).DeleteCollection(
   124  				t.Context(),
   125  				metav1.DeleteOptions{PropagationPolicy: &policy},
   126  				metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeRepository})
   127  		},
   128  		func() error {
   129  			// kubectl delete secrets -l argocd.argoproj.io/secret-type=repo-creds
   130  			return fixtureClient.KubeClientset.CoreV1().Secrets(TestNamespace()).DeleteCollection(
   131  				t.Context(),
   132  				metav1.DeleteOptions{PropagationPolicy: &policy},
   133  				metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeRepoCreds})
   134  		},
   135  		func() error {
   136  			// Delete the applicationset-e2e namespace, if it exists
   137  			err := fixtureClient.KubeClientset.CoreV1().Namespaces().Delete(t.Context(), ApplicationsResourcesNamespace, metav1.DeleteOptions{PropagationPolicy: &policy})
   138  			if err != nil && !apierrors.IsNotFound(err) { // 'not found' error is expected
   139  				return err
   140  			}
   141  			return nil
   142  		},
   143  		func() error {
   144  			// Delete the argocd-e2e-external namespace, if it exists
   145  			err := fixtureClient.KubeClientset.CoreV1().Namespaces().Delete(t.Context(), string(ArgoCDExternalNamespace), metav1.DeleteOptions{PropagationPolicy: &policy})
   146  			if err != nil && !apierrors.IsNotFound(err) { // 'not found' error is expected
   147  				return err
   148  			}
   149  			return nil
   150  		},
   151  		func() error {
   152  			// Delete the argocd-e2e-external namespace, if it exists
   153  			err := fixtureClient.KubeClientset.CoreV1().Namespaces().Delete(t.Context(), string(ArgoCDExternalNamespace2), metav1.DeleteOptions{PropagationPolicy: &policy})
   154  			if err != nil && !apierrors.IsNotFound(err) { // 'not found' error is expected
   155  				return err
   156  			}
   157  			return nil
   158  		},
   159  		// delete resources
   160  		func() error {
   161  			// kubectl delete applicationsets --all
   162  			return fixtureClient.AppSetClientset.DeleteCollection(t.Context(), metav1.DeleteOptions{PropagationPolicy: &policy}, metav1.ListOptions{})
   163  		},
   164  		func() error {
   165  			// kubectl delete apps --all
   166  			return fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(TestNamespace()).DeleteCollection(t.Context(), metav1.DeleteOptions{PropagationPolicy: &policy}, metav1.ListOptions{})
   167  		},
   168  		func() error {
   169  			// kubectl delete secrets -l e2e.argoproj.io=true
   170  			return fixtureClient.KubeClientset.CoreV1().Secrets(TestNamespace()).DeleteCollection(
   171  				t.Context(),
   172  				metav1.DeleteOptions{PropagationPolicy: &policy},
   173  				metav1.ListOptions{LabelSelector: TestingLabel + "=true"})
   174  		},
   175  	})
   176  
   177  	// First we wait up to 30 seconds for all the ApplicationSets to delete, but we don't fail if they don't.
   178  	// Why? We want to give Argo CD time to delete the Application's child resources, before we remove the finalizers below.
   179  	_ = waitForSuccess(func() error {
   180  		list, err := fixtureClient.AppSetClientset.List(t.Context(), metav1.ListOptions{})
   181  		if err != nil {
   182  			return err
   183  		}
   184  		if list != nil && len(list.Items) > 0 {
   185  			// Fail
   186  			return fmt.Errorf("waiting for list of ApplicationSets to be size zero: %d", len(list.Items))
   187  		}
   188  
   189  		return nil // Pass
   190  	}, time.Now().Add(30*time.Second))
   191  
   192  	// Remove finalizers from Argo CD Application resources in the namespace
   193  	err := waitForSuccess(func() error {
   194  		appList, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(TestNamespace()).List(t.Context(), metav1.ListOptions{})
   195  		if err != nil {
   196  			return err
   197  		}
   198  		for _, app := range appList.Items {
   199  			t.Log("Removing finalizer for: ", app.Name)
   200  			app.Finalizers = []string{}
   201  			_, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(TestNamespace()).Update(t.Context(), &app, metav1.UpdateOptions{})
   202  			if err != nil {
   203  				return err
   204  			}
   205  		}
   206  		return nil
   207  	}, time.Now().Add(120*time.Second))
   208  	require.NoError(t, err)
   209  
   210  	require.NoError(t, waitForExpectedClusterState(t))
   211  
   212  	// remove tmp dir
   213  	require.NoError(t, os.RemoveAll(TmpDir))
   214  
   215  	// create tmp dir
   216  	errors.NewHandler(t).FailOnErr(Run("", "mkdir", "-p", TmpDir))
   217  
   218  	// We can switch user and as result in previous state we will have non-admin user, this case should be reset
   219  	require.NoError(t, fixture.LoginAs("admin"))
   220  
   221  	log.WithFields(log.Fields{"duration": time.Since(start), "name": t.Name(), "id": id, "username": "admin", "password": "password"}).Info("clean state")
   222  }
   223  
   224  func waitForExpectedClusterState(t *testing.T) error {
   225  	t.Helper()
   226  	fixtureClient := GetE2EFixtureK8sClient(t)
   227  
   228  	SetProjectSpec(t, fixtureClient, "default", v1alpha1.AppProjectSpec{
   229  		OrphanedResources:        nil,
   230  		SourceRepos:              []string{"*"},
   231  		Destinations:             []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}},
   232  		ClusterResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}},
   233  		SourceNamespaces:         []string{string(ArgoCDExternalNamespace), string(ArgoCDExternalNamespace2)},
   234  	})
   235  
   236  	// Wait up to 60 seconds for all the ApplicationSets to delete
   237  	if err := waitForSuccess(func() error {
   238  		list, err := fixtureClient.AppSetClientset.List(t.Context(), metav1.ListOptions{})
   239  		if err != nil {
   240  			return err
   241  		}
   242  		if list != nil && len(list.Items) > 0 {
   243  			// Fail
   244  			return fmt.Errorf("waiting for list of ApplicationSets to be size zero: %d", len(list.Items))
   245  		}
   246  
   247  		return nil // Pass
   248  	}, time.Now().Add(60*time.Second)); err != nil {
   249  		return err
   250  	}
   251  
   252  	// Wait up to 60 seconds for all the Applications to delete
   253  	if err := waitForSuccess(func() error {
   254  		appList, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(TestNamespace()).List(t.Context(), metav1.ListOptions{})
   255  		if err != nil {
   256  			return err
   257  		}
   258  		if appList != nil && len(appList.Items) > 0 {
   259  			// Fail
   260  			return fmt.Errorf("waiting for list of Applications to be size zero: %d", len(appList.Items))
   261  		}
   262  		return nil // Pass
   263  	}, time.Now().Add(60*time.Second)); err != nil {
   264  		return err
   265  	}
   266  
   267  	// Wait up to 120 seconds for namespace to not exist
   268  	for _, namespace := range []string{string(ApplicationsResourcesNamespace), string(ArgoCDExternalNamespace), string(ArgoCDExternalNamespace2)} {
   269  		// Wait up to 120 seconds for namespace to not exist
   270  		if err := waitForSuccess(func() error {
   271  			return cleanUpNamespace(fixtureClient, namespace)
   272  		}, time.Now().Add(120*time.Second)); err != nil {
   273  			return err
   274  		}
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func SetProjectSpec(t *testing.T, fixtureClient *E2EFixtureK8sClient, project string, spec v1alpha1.AppProjectSpec) {
   281  	t.Helper()
   282  	proj, err := fixtureClient.AppClientset.ArgoprojV1alpha1().AppProjects(TestNamespace()).Get(t.Context(), project, metav1.GetOptions{})
   283  	require.NoError(t, err)
   284  	proj.Spec = spec
   285  	_, err = fixtureClient.AppClientset.ArgoprojV1alpha1().AppProjects(TestNamespace()).Update(t.Context(), proj, metav1.UpdateOptions{})
   286  	require.NoError(t, err)
   287  }
   288  
   289  func cleanUpNamespace(fixtureClient *E2EFixtureK8sClient, namespace string) error {
   290  	_, err := fixtureClient.KubeClientset.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})
   291  
   292  	msg := ""
   293  
   294  	if err == nil {
   295  		msg = fmt.Sprintf("namespace '%s' still exists, after delete", namespace)
   296  	}
   297  
   298  	if msg == "" && err != nil && apierrors.IsNotFound(err) {
   299  		// Success is an error containing 'applicationset-e2e' not found.
   300  		return nil
   301  	}
   302  
   303  	if msg == "" {
   304  		msg = err.Error()
   305  	}
   306  
   307  	return fmt.Errorf("%s", msg)
   308  }
   309  
   310  // waitForSuccess waits for the condition to return a non-error value.
   311  // Returns if condition returns nil, or the expireTime has elapsed (in which
   312  // case the last error will be returned)
   313  func waitForSuccess(condition func() error, expireTime time.Time) error {
   314  	var mostRecentError error
   315  
   316  	sleepIntervals := []time.Duration{
   317  		10 * time.Millisecond,
   318  		20 * time.Millisecond,
   319  		50 * time.Millisecond,
   320  		100 * time.Millisecond,
   321  		200 * time.Millisecond,
   322  		300 * time.Millisecond,
   323  		500 * time.Millisecond,
   324  		1 * time.Second,
   325  	}
   326  	sleepIntervalsIdx := -1
   327  
   328  	for !time.Now().After(expireTime) {
   329  		conditionErr := condition()
   330  		if conditionErr == nil {
   331  			// Pass!
   332  			mostRecentError = nil
   333  			break
   334  		}
   335  		// Fail!
   336  		mostRecentError = conditionErr
   337  
   338  		// Wait on fail
   339  		if sleepIntervalsIdx < len(sleepIntervals)-1 {
   340  			sleepIntervalsIdx++
   341  		}
   342  		time.Sleep(sleepIntervals[sleepIntervalsIdx])
   343  	}
   344  	return mostRecentError
   345  }
   346  
   347  // getKubeConfig creates new kubernetes client config using specified config path and config overrides variables
   348  func getKubeConfig(t *testing.T, configPath string, overrides clientcmd.ConfigOverrides) *rest.Config {
   349  	t.Helper()
   350  	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
   351  	loadingRules.ExplicitPath = configPath
   352  	clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)
   353  
   354  	restConfig, err := clientConfig.ClientConfig()
   355  	require.NoError(t, err)
   356  	return restConfig
   357  }
   358  
   359  // creates e2e tests fixture: ensures that Application CRD is installed, creates temporal namespace, starts repo and api server,
   360  // configure currently available cluster.
   361  func init() {
   362  	// ensure we log all shell execs
   363  	log.SetLevel(log.DebugLevel)
   364  }
   365  
   366  // PrettyPrintJson is a utility function for debugging purposes
   367  func PrettyPrintJson(obj any) string { //nolint:revive //FIXME(var-naming)
   368  	bytes, err := json.MarshalIndent(obj, "", "    ")
   369  	if err != nil {
   370  		return err.Error()
   371  	}
   372  	return string(bytes)
   373  }
   374  
   375  // returns dns friends string which is no longer than 63 characters and has specified postfix at the end
   376  func DnsFriendly(str string, postfix string) string { //nolint:revive //FIXME(var-naming)
   377  	matchFirstCap := regexp.MustCompile("(.)([A-Z][a-z]+)")
   378  	matchAllCap := regexp.MustCompile("([a-z0-9])([A-Z])")
   379  
   380  	str = matchFirstCap.ReplaceAllString(str, "${1}-${2}")
   381  	str = matchAllCap.ReplaceAllString(str, "${1}-${2}")
   382  	str = strings.ToLower(str)
   383  
   384  	if diff := len(str) + len(postfix) - 63; diff > 0 {
   385  		str = str[:len(str)-diff]
   386  	}
   387  	return str + postfix
   388  }
   389  
   390  func MustToUnstructured(obj any) *unstructured.Unstructured {
   391  	uObj, err := ToUnstructured(obj)
   392  	if err != nil {
   393  		panic(err)
   394  	}
   395  	return uObj
   396  }
   397  
   398  // ToUnstructured converts a concrete K8s API type to an unstructured object
   399  func ToUnstructured(obj any) (*unstructured.Unstructured, error) {
   400  	uObj, err := runtime.NewTestUnstructuredConverter(equality.Semantic).ToUnstructured(obj)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	return &unstructured.Unstructured{Object: uObj}, nil
   405  }
   406  
   407  // IsGitHubSkippedTest returns true if the test should be skipped because it requires a GitHub API Token
   408  // and one has not been provided.
   409  // Unfortunately, GitHub Actions cannot use repository secrets, so we need to skip these tests for PRs.
   410  //
   411  // Tests that call this function require a GITHUB_TOKEN to be present, otherwise they will fail, due to
   412  // GitHub's rate limiting on anonymous API requests.
   413  //
   414  // Note: This only applies to tests that use the GitHub API (different from GitHub's Git service)
   415  func IsGitHubAPISkippedTest(t *testing.T) bool {
   416  	t.Helper()
   417  	if strings.TrimSpace(os.Getenv("GITHUB_TOKEN")) == "" {
   418  		t.Skip("Skipping this test, as the GITHUB_TOKEN is not set. Please ensure this test passes locally, with your own GITHUB_TOKEN.")
   419  		return true
   420  	}
   421  
   422  	return false
   423  }