github.com/argoproj/argo-cd/v2@v2.10.9/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 }