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 }