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