github.com/argoproj/argo-cd/v3@v3.2.1/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/v3/test/e2e/fixture" 11 12 log "github.com/sirupsen/logrus" 13 corev1 "k8s.io/api/core/v1" 14 rbacv1 "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/v3/common" 22 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 23 "github.com/argoproj/argo-cd/v3/test/e2e/fixture/applicationsets/utils" 24 "github.com/argoproj/argo-cd/v3/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 time.Sleep(fixture.WhenThenSleepInterval) 65 return &Consequences{a.context, a} 66 } 67 68 func (a *Actions) SwitchToExternalNamespace(namespace utils.ExternalNamespace) *Actions { 69 a.context.switchToNamespace = namespace 70 log.Infof("switched to external namespace: %s", namespace) 71 return a 72 } 73 74 func (a *Actions) SwitchToArgoCDNamespace() *Actions { 75 a.context.switchToNamespace = "" 76 log.Infof("switched to argocd namespace: %s", utils.ArgoCDNamespace) 77 return a 78 } 79 80 // CreateClusterSecret creates a faux cluster secret, with the given cluster server and cluster name (this cluster 81 // will not actually be used by the Argo CD controller, but that's not needed for our E2E tests) 82 func (a *Actions) CreateClusterSecret(secretName string, clusterName string, clusterServer string) *Actions { 83 a.context.t.Helper() 84 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t) 85 86 var serviceAccountName string 87 88 // Look for a service account matching '*application-controller*' 89 err := wait.PollUntilContextTimeout(context.Background(), 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (bool, error) { 90 serviceAccountList, err := fixtureClient.KubeClientset.CoreV1().ServiceAccounts(fixture.TestNamespace()).List(ctx, 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 a.context.t.Helper() 158 err := utils.GetE2EFixtureK8sClient(a.context.t).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 a.context.t.Helper() 170 err := utils.GetE2EFixtureK8sClient(a.context.t).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 a.context.t.Helper() 182 err := utils.GetE2EFixtureK8sClient(a.context.t).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(a.context.t) 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(a.context.t) 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[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 a.context.t.Helper() 247 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t) 248 249 var err error 250 251 _, err = fixtureClient.KubeClientset.RbacV1().Roles(fixture.TestNamespace()).Create(context.Background(), &rbacv1.Role{ 252 ObjectMeta: metav1.ObjectMeta{Name: "placement-role", Namespace: fixture.TestNamespace()}, 253 Rules: []rbacv1.PolicyRule{ 254 { 255 Verbs: []string{"get", "list", "watch"}, 256 APIGroups: []string{"cluster.open-cluster-management.io"}, 257 Resources: []string{"placementdecisions"}, 258 }, 259 }, 260 }, metav1.CreateOptions{}) 261 if err != nil && strings.Contains(err.Error(), "already exists") { 262 err = nil 263 } 264 265 if err == nil { 266 _, err = fixtureClient.KubeClientset.RbacV1().RoleBindings(fixture.TestNamespace()).Create(context.Background(), 267 &rbacv1.RoleBinding{ 268 ObjectMeta: metav1.ObjectMeta{Name: "placement-role-binding", Namespace: fixture.TestNamespace()}, 269 Subjects: []rbacv1.Subject{ 270 { 271 Name: "argocd-applicationset-controller", 272 Namespace: fixture.TestNamespace(), 273 Kind: "ServiceAccount", 274 }, 275 }, 276 RoleRef: rbacv1.RoleRef{ 277 Kind: "Role", 278 APIGroup: "rbac.authorization.k8s.io", 279 Name: "placement-role", 280 }, 281 }, metav1.CreateOptions{}) 282 } 283 if err != nil && strings.Contains(err.Error(), "already exists") { 284 err = nil 285 } 286 287 a.describeAction = "creating placement role/rolebinding" 288 a.lastOutput, a.lastError = "", err 289 a.verifyAction() 290 291 return a 292 } 293 294 // Create a ConfigMap for the ClusterResourceList generator 295 func (a *Actions) CreatePlacementDecisionConfigMap(configMapName string) *Actions { 296 a.context.t.Helper() 297 298 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t) 299 300 _, err := fixtureClient.KubeClientset.CoreV1().ConfigMaps(fixture.TestNamespace()).Get(context.Background(), configMapName, metav1.GetOptions{}) 301 302 // Don't do anything if it exists 303 if err == nil { 304 return a 305 } 306 307 _, err = fixtureClient.KubeClientset.CoreV1().ConfigMaps(fixture.TestNamespace()).Create(context.Background(), 308 &corev1.ConfigMap{ 309 ObjectMeta: metav1.ObjectMeta{ 310 Name: configMapName, 311 }, 312 Data: map[string]string{ 313 "apiVersion": "cluster.open-cluster-management.io/v1alpha1", 314 "kind": "placementdecisions", 315 "statusListKey": "decisions", 316 "matchKey": "clusterName", 317 }, 318 }, metav1.CreateOptions{}) 319 320 a.describeAction = fmt.Sprintf("creating configmap '%s'", configMapName) 321 a.lastOutput, a.lastError = "", err 322 a.verifyAction() 323 324 return a 325 } 326 327 func (a *Actions) CreatePlacementDecision(placementDecisionName string) *Actions { 328 a.context.t.Helper() 329 330 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t).DynamicClientset 331 332 _, err := fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).Get( 333 context.Background(), 334 placementDecisionName, 335 metav1.GetOptions{}) 336 // If already exists 337 if err == nil { 338 return a 339 } 340 341 placementDecision := &unstructured.Unstructured{ 342 Object: map[string]any{ 343 "metadata": map[string]any{ 344 "name": placementDecisionName, 345 "namespace": fixture.TestNamespace(), 346 }, 347 "kind": "PlacementDecision", 348 "apiVersion": "cluster.open-cluster-management.io/v1alpha1", 349 "status": map[string]any{}, 350 }, 351 } 352 353 _, err = fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).Create( 354 context.Background(), 355 placementDecision, 356 metav1.CreateOptions{}) 357 358 a.describeAction = fmt.Sprintf("creating placementDecision '%v'", placementDecisionName) 359 a.lastOutput, a.lastError = "", err 360 a.verifyAction() 361 362 return a 363 } 364 365 func (a *Actions) StatusUpdatePlacementDecision(placementDecisionName string, clusterList []any) *Actions { 366 a.context.t.Helper() 367 368 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t).DynamicClientset 369 placementDecision, err := fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).Get( 370 context.Background(), 371 placementDecisionName, 372 metav1.GetOptions{}) 373 374 placementDecision.Object["status"] = map[string]any{ 375 "decisions": clusterList, 376 } 377 378 if err == nil { 379 _, err = fixtureClient.Resource(pdGVR).Namespace(fixture.TestNamespace()).UpdateStatus( 380 context.Background(), 381 placementDecision, 382 metav1.UpdateOptions{}) 383 } 384 a.describeAction = fmt.Sprintf("status update placementDecision for '%v'", clusterList) 385 a.lastOutput, a.lastError = "", err 386 a.verifyAction() 387 388 return a 389 } 390 391 // Delete deletes the ApplicationSet within the context 392 func (a *Actions) Delete() *Actions { 393 a.context.t.Helper() 394 395 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t) 396 397 var appSetClientSet dynamic.ResourceInterface 398 399 if a.context.switchToNamespace != "" { 400 externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[a.context.switchToNamespace] 401 if !found { 402 a.lastOutput, a.lastError = "", fmt.Errorf("no external clientset found for %s", a.context.switchToNamespace) 403 return a 404 } 405 appSetClientSet = externalAppSetClientset 406 } else { 407 appSetClientSet = fixtureClient.AppSetClientset 408 } 409 410 deleteProp := metav1.DeletePropagationForeground 411 err := appSetClientSet.Delete(context.Background(), a.context.name, metav1.DeleteOptions{PropagationPolicy: &deleteProp}) 412 a.describeAction = fmt.Sprintf("Deleting ApplicationSet '%s/%s' %v", a.context.namespace, a.context.name, err) 413 a.lastOutput, a.lastError = "", err 414 a.verifyAction() 415 416 return a 417 } 418 419 // get retrieves the ApplicationSet (by name) that was created by an earlier Create action 420 func (a *Actions) get() (*v1alpha1.ApplicationSet, error) { 421 appSet := v1alpha1.ApplicationSet{} 422 423 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t) 424 425 var appSetClientSet dynamic.ResourceInterface 426 427 if a.context.switchToNamespace != "" { 428 externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[a.context.switchToNamespace] 429 if !found { 430 return nil, fmt.Errorf("no external clientset found for %s", a.context.switchToNamespace) 431 } 432 appSetClientSet = externalAppSetClientset 433 } else { 434 appSetClientSet = fixtureClient.AppSetClientset 435 } 436 437 newResource, err := appSetClientSet.Get(context.Background(), a.context.name, metav1.GetOptions{}) 438 if err != nil { 439 return nil, err 440 } 441 442 bytes, err := newResource.MarshalJSON() 443 if err != nil { 444 return nil, err 445 } 446 447 err = json.Unmarshal(bytes, &appSet) 448 if err != nil { 449 return nil, err 450 } 451 452 return &appSet, nil 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 sleepIntervals := []time.Duration{ 465 10 * time.Millisecond, 466 20 * time.Millisecond, 467 50 * time.Millisecond, 468 100 * time.Millisecond, 469 200 * time.Millisecond, 470 300 * time.Millisecond, 471 500 * time.Millisecond, 472 1 * time.Second, 473 } 474 sleepIntervalsIdx := -1 475 for start := time.Now(); time.Since(start) < timeout; time.Sleep(sleepIntervals[sleepIntervalsIdx]) { 476 if sleepIntervalsIdx < len(sleepIntervals)-1 { 477 sleepIntervalsIdx++ 478 } 479 appSet, err := a.get() 480 mostRecentError = err 481 if err == nil { 482 // Keep trying to update until it succeeds, or the test times out 483 toUpdate(appSet) 484 a.describeAction = fmt.Sprintf("updating ApplicationSet '%s/%s'", appSet.Namespace, appSet.Name) 485 486 fixtureClient := utils.GetE2EFixtureK8sClient(a.context.t) 487 488 var appSetClientSet dynamic.ResourceInterface 489 490 if a.context.switchToNamespace != "" { 491 externalAppSetClientset, found := fixtureClient.ExternalAppSetClientsets[a.context.switchToNamespace] 492 if !found { 493 a.lastOutput, a.lastError = "", fmt.Errorf("no external clientset found for %s", a.context.switchToNamespace) 494 return a 495 } 496 appSetClientSet = externalAppSetClientset 497 } else { 498 appSetClientSet = fixtureClient.AppSetClientset 499 } 500 501 _, err = appSetClientSet.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{}) 502 503 if err == nil { 504 mostRecentError = nil 505 break 506 } 507 mostRecentError = err 508 } 509 } 510 511 a.lastOutput, a.lastError = "", mostRecentError 512 a.verifyAction() 513 514 return a 515 } 516 517 func (a *Actions) verifyAction() { 518 a.context.t.Helper() 519 520 if a.describeAction != "" { 521 log.Infof("action: %s", a.describeAction) 522 a.describeAction = "" 523 } 524 525 if !a.ignoreErrors { 526 a.Then().Expect(Success("")) 527 } 528 } 529 530 func (a *Actions) AppSet(appName string, flags ...string) *Actions { 531 a.context.t.Helper() 532 args := []string{"app", "set", appName} 533 args = append(args, flags...) 534 a.runCli(args...) 535 return a 536 } 537 538 func (a *Actions) runCli(args ...string) { 539 a.context.t.Helper() 540 a.lastOutput, a.lastError = fixture.RunCli(args...) 541 a.verifyAction() 542 } 543 544 func (a *Actions) AddSignedFile(fileName, fileContents string) *Actions { 545 a.context.t.Helper() 546 fixture.AddSignedFile(a.context.t, a.context.path+"/"+fileName, fileContents) 547 return a 548 }