github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/rollout-service/pkg/argo/argo_test.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package argo 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "testing" 24 "time" 25 26 "k8s.io/apimachinery/pkg/watch" 27 28 "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" 29 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 30 "github.com/argoproj/gitops-engine/pkg/health" 31 "github.com/cenkalti/backoff/v4" 32 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 33 "github.com/freiheit-com/kuberpult/pkg/logger" 34 "github.com/freiheit-com/kuberpult/pkg/ptr" 35 "github.com/freiheit-com/kuberpult/pkg/setup" 36 "github.com/google/go-cmp/cmp" 37 "github.com/google/go-cmp/cmp/cmpopts" 38 "google.golang.org/grpc" 39 "google.golang.org/grpc/codes" 40 "google.golang.org/grpc/status" 41 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 42 ) 43 44 // Used to compare two error message strings, needed because errors.Is(fmt.Errorf(text),fmt.Errorf(text)) == false 45 type errMatcher struct { 46 msg string 47 } 48 49 func (e errMatcher) Error() string { 50 return e.msg 51 } 52 53 func (e errMatcher) Is(err error) bool { 54 return e.Error() == err.Error() 55 } 56 57 type step struct { 58 Event *v1alpha1.ApplicationWatchEvent 59 WatchErr error 60 RecvErr error 61 CancelContext bool 62 } 63 64 func (m *mockApplicationServiceClient) Recv() (*v1alpha1.ApplicationWatchEvent, error) { 65 if m.current >= len(m.Steps) { 66 return nil, fmt.Errorf("exhausted: %w", io.EOF) 67 } 68 reply := m.Steps[m.current] 69 if reply.CancelContext { 70 m.cancel() 71 } 72 m.current = m.current + 1 73 return reply.Event, reply.RecvErr 74 } 75 76 type mockApplicationServiceClient struct { 77 Steps []step 78 Apps []*ArgoApp 79 current int 80 t *testing.T 81 lastEvent chan *ArgoEvent 82 cancel context.CancelFunc 83 grpc.ClientStream 84 } 85 86 type ArgoApp struct { 87 App *v1alpha1.Application 88 LastEvent string 89 } 90 91 type mockArgoProcessor struct { 92 trigger chan *api.GetOverviewResponse 93 lastOverview *api.GetOverviewResponse 94 argoApps chan *v1alpha1.ApplicationWatchEvent 95 ApplicationClient *mockApplicationServiceClient 96 HealthReporter *setup.HealthReporter 97 } 98 99 func (a *mockArgoProcessor) Push(ctx context.Context, last *api.GetOverviewResponse) { 100 a.lastOverview = last 101 select { 102 case a.trigger <- a.lastOverview: 103 default: 104 } 105 } 106 107 func (a *mockArgoProcessor) checkEvent(ev *v1alpha1.ApplicationWatchEvent) bool { 108 for _, argoApp := range a.ApplicationClient.Apps { 109 if argoApp.App.Name == ev.Application.Name && string(ev.Type) == argoApp.LastEvent { 110 return true 111 } 112 } 113 return false 114 } 115 116 func (a *mockArgoProcessor) Consume(t *testing.T, ctx context.Context, expectedTypes []string, existingArgoApps bool) error { 117 appsKnownToArgo := map[string]map[string]*v1alpha1.Application{} 118 envAppsKnownToArgo := make(map[string]*v1alpha1.Application) 119 120 for { 121 select { 122 case overview := <-a.trigger: 123 for _, envGroup := range overview.EnvironmentGroups { 124 for _, env := range envGroup.Environments { 125 if ok := appsKnownToArgo[env.Name]; ok != nil { 126 envAppsKnownToArgo = appsKnownToArgo[env.Name] 127 err := a.DeleteArgoApps(ctx, envAppsKnownToArgo, env.Applications) 128 129 if err != nil { 130 continue 131 } 132 } 133 134 for _, app := range env.Applications { 135 if existingArgoApps { 136 envAppsKnownToArgo[app.Name] = CreateArgoApplication(overview, app, env) 137 appsKnownToArgo[env.Name] = envAppsKnownToArgo 138 } 139 a.CreateOrUpdateApp(ctx, overview, app, env, envAppsKnownToArgo) 140 } 141 } 142 } 143 case ev := <-a.argoApps: 144 switch ev.Type { 145 case "ADDED", "MODIFIED", "DELETED": 146 if ev.Type != watch.EventType(expectedTypes[0]) { 147 t.Fatalf("expected type to be %s, but got %s", expectedTypes[0], ev.Type) 148 } 149 if len(expectedTypes) > 1 { 150 expectedTypes = expectedTypes[1:] 151 } 152 } 153 154 case <-ctx.Done(): 155 return nil 156 } 157 } 158 } 159 160 func (a *mockArgoProcessor) ConsumeArgo(ctx context.Context, hlth *setup.HealthReporter) error { 161 for { 162 watch, err := a.ApplicationClient.Watch(ctx, &application.ApplicationQuery{}) 163 if err != nil { 164 if status.Code(err) == codes.Canceled { 165 // context is cancelled -> we are shutting down 166 return setup.Permanent(nil) 167 } 168 return fmt.Errorf("watching applications: %w", err) 169 } 170 hlth.ReportReady("consuming events") 171 ev, err := watch.Recv() 172 if err != nil { 173 if status.Code(err) == codes.Canceled { 174 // context is cancelled -> we are shutting down 175 return setup.Permanent(nil) 176 } 177 return err 178 } 179 180 if ev.Type == "ADDED" || ev.Type == "MODIFIED" || ev.Type == "DELETED" { 181 a.argoApps <- ev 182 } 183 } 184 } 185 186 func (m *mockApplicationServiceClient) Watch(ctx context.Context, qry *application.ApplicationQuery, opts ...grpc.CallOption) (application.ApplicationService_WatchClient, error) { 187 if m.current >= len(m.Steps) { 188 return nil, setup.Permanent(fmt.Errorf("exhausted: %w", io.EOF)) 189 } 190 reply := m.Steps[m.current] 191 if reply.WatchErr != nil { 192 if reply.CancelContext { 193 m.cancel() 194 } 195 m.current = m.current + 1 196 return nil, reply.WatchErr 197 } 198 return m, nil 199 } 200 201 func (m *mockApplicationServiceClient) Delete(ctx context.Context, req *application.ApplicationDeleteRequest) { 202 for _, app := range m.Apps { 203 if app.App.Name == *req.Name { 204 deleteApp := &ArgoApp{App: app.App, LastEvent: "DELETED"} 205 m.Apps = append(m.Apps, deleteApp) 206 return 207 } 208 } 209 } 210 211 func (m *mockApplicationServiceClient) Update(ctx context.Context, req *application.ApplicationUpdateRequest) { 212 for _, a := range m.Apps { 213 if a.App.Name == req.Application.Name { 214 updateApp := &ArgoApp{App: a.App, LastEvent: "MODIFIED"} 215 m.Apps = append(m.Apps, updateApp) 216 break 217 } 218 } 219 // If reached here, no application in the request is known to argo 220 } 221 222 func (m *mockApplicationServiceClient) Create(ctx context.Context, req *application.ApplicationCreateRequest) error { 223 newApp := &ArgoApp{ 224 App: req.Application, 225 LastEvent: "ADDED", 226 } 227 for _, existingArgoApp := range m.Apps { 228 if existingArgoApp.App.Name == req.Application.Name { 229 // App alrady exists 230 return nil 231 } 232 } 233 m.Apps = append(m.Apps, newApp) 234 235 return nil 236 } 237 238 func (m *mockApplicationServiceClient) testAllConsumed(t *testing.T, expectedConsumed int) { 239 for _, app := range m.Apps { 240 if !app.App.Spec.SyncPolicy.Automated.SelfHeal { 241 t.Errorf("expected app %s to have selfHeal enabled", app.App.Name) 242 } 243 if !app.App.Spec.SyncPolicy.Automated.Prune { 244 t.Errorf("expected app %s to have prune enabled", app.App.Name) 245 } 246 } 247 if expectedConsumed != m.current && m.current < len(m.Steps) { 248 t.Errorf("expected to consume all %d replies, only consumed %d", len(m.Steps), m.current) 249 } 250 } 251 252 func TestArgoConsume(t *testing.T) { 253 tcs := []struct { 254 Name string 255 Steps []step 256 Overview *api.GetOverviewResponse 257 ExpectedError error 258 ExpectedConsumed int 259 ExpectedConsumedTypes []string 260 ExistingArgoApps bool 261 }{ 262 { 263 Name: "when ctx in cancelled no app is processed", 264 Steps: []step{ 265 { 266 WatchErr: status.Error(codes.Canceled, "context cancelled"), 267 CancelContext: true, 268 }, 269 }, 270 Overview: &api.GetOverviewResponse{ 271 Applications: map[string]*api.Application{ 272 "foo": { 273 Releases: []*api.Release{ 274 { 275 Version: 1, 276 SourceCommitId: "00001", 277 }, 278 }, 279 Team: "footeam", 280 }, 281 }, 282 EnvironmentGroups: []*api.EnvironmentGroup{ 283 { 284 285 EnvironmentGroupName: "staging-group", 286 Environments: []*api.Environment{ 287 { 288 Name: "staging", 289 Applications: map[string]*api.Environment_Application{ 290 "foo": { 291 Name: "foo", 292 Version: 1, 293 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 294 DeployTime: "123456789", 295 }, 296 }, 297 }, 298 Priority: api.Priority_UPSTREAM, 299 Config: &api.EnvironmentConfig{ 300 Argocd: &api.EnvironmentConfig_ArgoCD{ 301 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 302 Name: "staging", 303 Server: "test-server", 304 }, 305 }, 306 }, 307 }, 308 }, 309 }, 310 }, 311 GitRevision: "1234", 312 }, 313 }, 314 { 315 Name: "an error is detected", 316 Steps: []step{ 317 { 318 WatchErr: fmt.Errorf("no"), 319 }, 320 { 321 WatchErr: status.Error(codes.Canceled, "context cancelled"), 322 CancelContext: true, 323 }, 324 }, 325 Overview: &api.GetOverviewResponse{ 326 Applications: map[string]*api.Application{ 327 "foo": { 328 Releases: []*api.Release{ 329 { 330 Version: 1, 331 SourceCommitId: "00001", 332 }, 333 }, 334 Team: "footeam", 335 }, 336 }, 337 EnvironmentGroups: []*api.EnvironmentGroup{ 338 { 339 340 EnvironmentGroupName: "staging-group", 341 Environments: []*api.Environment{ 342 { 343 Name: "staging", 344 Applications: map[string]*api.Environment_Application{ 345 "foo": { 346 Name: "foo", 347 Version: 1, 348 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 349 DeployTime: "123456789", 350 }, 351 }, 352 }, 353 Priority: api.Priority_UPSTREAM, 354 Config: &api.EnvironmentConfig{ 355 Argocd: &api.EnvironmentConfig_ArgoCD{ 356 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 357 Name: "staging", 358 Server: "test-server", 359 }, 360 }, 361 }, 362 }, 363 }, 364 }, 365 }, 366 GitRevision: "1234", 367 }, 368 ExpectedError: errMatcher{"watching applications: no"}, 369 ExpectedConsumed: 1, 370 }, 371 { 372 Name: "create an app", 373 Steps: []step{ 374 { 375 Event: &v1alpha1.ApplicationWatchEvent{ 376 Type: "ADDED", 377 Application: v1alpha1.Application{ 378 ObjectMeta: metav1.ObjectMeta{ 379 Name: "foo", 380 Annotations: map[string]string{}, 381 }, 382 Spec: v1alpha1.ApplicationSpec{ 383 Project: "", 384 }, 385 Status: v1alpha1.ApplicationStatus{ 386 Sync: v1alpha1.SyncStatus{Revision: "1234"}, 387 Health: v1alpha1.HealthStatus{}, 388 }, 389 }, 390 }, 391 }, 392 { 393 RecvErr: status.Error(codes.Canceled, "context cancelled"), 394 CancelContext: true, 395 }, 396 }, 397 Overview: &api.GetOverviewResponse{ 398 Applications: map[string]*api.Application{ 399 "foo": { 400 Releases: []*api.Release{ 401 { 402 Version: 1, 403 SourceCommitId: "00001", 404 }, 405 }, 406 Team: "footeam", 407 }, 408 }, 409 EnvironmentGroups: []*api.EnvironmentGroup{ 410 { 411 412 EnvironmentGroupName: "staging-group", 413 Environments: []*api.Environment{ 414 { 415 Name: "staging", 416 Applications: map[string]*api.Environment_Application{ 417 "foo": { 418 Name: "foo", 419 Version: 1, 420 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 421 DeployTime: "123456789", 422 }, 423 }, 424 }, 425 Priority: api.Priority_UPSTREAM, 426 Config: &api.EnvironmentConfig{ 427 Argocd: &api.EnvironmentConfig_ArgoCD{ 428 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 429 Name: "staging", 430 Server: "test-server", 431 }, 432 }, 433 }, 434 }, 435 }, 436 }, 437 }, 438 GitRevision: "1234", 439 }, 440 ExpectedConsumedTypes: []string{"ADDED"}, 441 ExpectedConsumed: 2, 442 }, 443 { 444 Name: "updates an already existing app", 445 Steps: []step{ 446 { 447 Event: &v1alpha1.ApplicationWatchEvent{ 448 Type: "MODIFIED", 449 Application: v1alpha1.Application{ 450 ObjectMeta: metav1.ObjectMeta{ 451 Name: "foo", 452 Annotations: map[string]string{}, 453 }, 454 Spec: v1alpha1.ApplicationSpec{ 455 Project: "", 456 }, 457 Status: v1alpha1.ApplicationStatus{ 458 Sync: v1alpha1.SyncStatus{Revision: "1234"}, 459 Health: v1alpha1.HealthStatus{}, 460 }, 461 }, 462 }, 463 }, 464 { 465 RecvErr: status.Error(codes.Canceled, "context cancelled"), 466 CancelContext: true, 467 }, 468 }, 469 Overview: &api.GetOverviewResponse{ 470 Applications: map[string]*api.Application{ 471 "foo": { 472 Releases: []*api.Release{ 473 { 474 Version: 1, 475 SourceCommitId: "00001", 476 }, 477 }, 478 Team: "footeam", 479 }, 480 }, 481 EnvironmentGroups: []*api.EnvironmentGroup{ 482 { 483 484 EnvironmentGroupName: "staging-group", 485 Environments: []*api.Environment{ 486 { 487 Name: "staging", 488 Applications: map[string]*api.Environment_Application{ 489 "foo": { 490 Name: "foo", 491 Version: 1, 492 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 493 DeployTime: "123456789", 494 }, 495 }, 496 }, 497 Priority: api.Priority_UPSTREAM, 498 Config: &api.EnvironmentConfig{ 499 Argocd: &api.EnvironmentConfig_ArgoCD{ 500 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 501 Name: "staging", 502 Server: "test-server", 503 }, 504 }, 505 }, 506 }, 507 }, 508 }, 509 }, 510 GitRevision: "1234", 511 }, 512 ExpectedConsumed: 1, 513 ExpectedConsumedTypes: []string{"MODIFIED"}, 514 ExistingArgoApps: true, 515 }, 516 { 517 Name: "two applications in the overview but only one is updated", 518 Steps: []step{ 519 { 520 Event: &v1alpha1.ApplicationWatchEvent{ 521 Type: "MODIFIED", 522 Application: v1alpha1.Application{ 523 ObjectMeta: metav1.ObjectMeta{ 524 Name: "foo", 525 Annotations: map[string]string{}, 526 }, 527 Spec: v1alpha1.ApplicationSpec{ 528 Project: "", 529 }, 530 Status: v1alpha1.ApplicationStatus{ 531 Sync: v1alpha1.SyncStatus{Revision: "1234"}, 532 Health: v1alpha1.HealthStatus{}, 533 }, 534 }, 535 }, 536 }, 537 { 538 RecvErr: status.Error(codes.Canceled, "context cancelled"), 539 CancelContext: true, 540 }, 541 }, 542 Overview: &api.GetOverviewResponse{ 543 Applications: map[string]*api.Application{ 544 "foo": { 545 Releases: []*api.Release{ 546 { 547 Version: 1, 548 SourceCommitId: "00001", 549 }, 550 }, 551 Team: "footeam", 552 }, 553 "foo2": { 554 Releases: []*api.Release{ 555 { 556 Version: 1, 557 SourceCommitId: "00012", 558 }, 559 }, 560 Team: "footeam", 561 }, 562 }, 563 EnvironmentGroups: []*api.EnvironmentGroup{ 564 { 565 566 EnvironmentGroupName: "staging-group", 567 Environments: []*api.Environment{ 568 { 569 Name: "staging", 570 Applications: map[string]*api.Environment_Application{ 571 "foo": { 572 Name: "foo", 573 Version: 1, 574 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 575 DeployTime: "123456789", 576 }, 577 }, 578 "foo2": { 579 Name: "foo2", 580 Version: 1, 581 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 582 DeployTime: "1234567892", 583 }, 584 }, 585 }, 586 Priority: api.Priority_UPSTREAM, 587 Config: &api.EnvironmentConfig{ 588 Argocd: &api.EnvironmentConfig_ArgoCD{ 589 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 590 Name: "staging", 591 Server: "test-server", 592 }, 593 }, 594 }, 595 }, 596 }, 597 }, 598 }, 599 GitRevision: "1234", 600 }, 601 ExpectedConsumed: 1, 602 ExpectedConsumedTypes: []string{"MODIFIED"}, 603 ExistingArgoApps: true, 604 }, 605 { 606 Name: "two applications in the overview but none is updated", 607 Steps: []step{ 608 { 609 RecvErr: status.Error(codes.Canceled, "context cancelled"), 610 CancelContext: true, 611 }, 612 }, 613 Overview: &api.GetOverviewResponse{ 614 Applications: map[string]*api.Application{ 615 "foo": { 616 Releases: []*api.Release{ 617 { 618 Version: 1, 619 SourceCommitId: "00001", 620 }, 621 }, 622 Team: "footeam", 623 }, 624 "foo2": { 625 Releases: []*api.Release{ 626 { 627 Version: 1, 628 SourceCommitId: "00012", 629 }, 630 }, 631 Team: "footeam", 632 }, 633 }, 634 EnvironmentGroups: []*api.EnvironmentGroup{ 635 { 636 637 EnvironmentGroupName: "staging-group", 638 Environments: []*api.Environment{ 639 { 640 Name: "staging", 641 Applications: map[string]*api.Environment_Application{ 642 "foo": { 643 Name: "foo", 644 Version: 1, 645 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 646 DeployTime: "123456789", 647 }, 648 }, 649 "foo2": { 650 Name: "foo2", 651 Version: 1, 652 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 653 DeployTime: "1234567892", 654 }, 655 }, 656 }, 657 Priority: api.Priority_UPSTREAM, 658 Config: &api.EnvironmentConfig{ 659 Argocd: &api.EnvironmentConfig_ArgoCD{ 660 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 661 Name: "staging", 662 Server: "test-server", 663 }, 664 }, 665 }, 666 }, 667 }, 668 }, 669 }, 670 GitRevision: "1234", 671 }, 672 ExpectedConsumed: 0, 673 ExpectedConsumedTypes: []string{}, 674 ExistingArgoApps: true, 675 }, 676 } 677 for _, tc := range tcs { 678 tc := tc 679 t.Run(tc.Name, func(t *testing.T) { 680 ctx, cancel := context.WithCancel(context.Background()) 681 as := &mockApplicationServiceClient{ 682 Steps: tc.Steps, 683 cancel: cancel, 684 t: t, 685 lastEvent: make(chan *ArgoEvent, 10), 686 } 687 hlth := &setup.HealthServer{} 688 argoProcessor := &mockArgoProcessor{ 689 lastOverview: tc.Overview, 690 ApplicationClient: as, 691 trigger: make(chan *api.GetOverviewResponse, 10), 692 argoApps: make(chan *v1alpha1.ApplicationWatchEvent, 10), 693 } 694 hlth.BackOffFactory = func() backoff.BackOff { return backoff.NewConstantBackOff(0) } 695 errCh := make(chan error) 696 go func() { 697 errCh <- argoProcessor.Consume(t, ctx, tc.ExpectedConsumedTypes, tc.ExistingArgoApps) 698 }() 699 700 go func() { 701 errCh <- argoProcessor.ConsumeArgo(ctx, hlth.Reporter("consume-argo")) 702 }() 703 704 argoProcessor.Push(ctx, tc.Overview) 705 //We add a delay so that all the events are reported by the application client 706 time.Sleep(10 * time.Second) 707 err := <-errCh 708 709 if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" { 710 t.Errorf("error mismatch (-want, +got):\n%s", diff) 711 } 712 as.testAllConsumed(t, tc.ExpectedConsumed) 713 }) 714 } 715 } 716 717 func (a mockArgoProcessor) DeleteArgoApps(ctx context.Context, appsKnownToArgo map[string]*v1alpha1.Application, apps map[string]*api.Environment_Application) error { 718 toDelete := make([]*v1alpha1.Application, 0) 719 for _, argoApp := range appsKnownToArgo { 720 for i, app := range apps { 721 if argoApp.Name == fmt.Sprintf("%s-%s", i, app.Name) { 722 break 723 } 724 } 725 toDelete = append(toDelete, argoApp) 726 } 727 728 for i := range toDelete { 729 a.ApplicationClient.Delete(ctx, &application.ApplicationDeleteRequest{ 730 Name: ptr.FromString(toDelete[i].Name), 731 }) 732 733 } 734 735 return nil 736 } 737 738 func (a mockArgoProcessor) CreateOrUpdateApp(ctx context.Context, overview *api.GetOverviewResponse, app *api.Environment_Application, env *api.Environment, appsKnownToArgo map[string]*v1alpha1.Application) { 739 var existingApp *v1alpha1.Application 740 for _, argoApp := range appsKnownToArgo { 741 if argoApp.Name == fmt.Sprintf("%s-%s", env.Name, app.Name) && argoApp.Annotations["com.freiheit.kuberpult/application"] != "" { 742 existingApp = argoApp 743 break 744 } 745 } 746 747 if existingApp == nil { 748 appToCreate := CreateArgoApplication(overview, app, env) 749 appToCreate.ResourceVersion = "" 750 upsert := false 751 validate := false 752 appCreateRequest := &application.ApplicationCreateRequest{ 753 Application: appToCreate, 754 Upsert: &upsert, 755 Validate: &validate, 756 } 757 err := a.ApplicationClient.Create(ctx, appCreateRequest) 758 if err != nil { 759 // We check if the application was created in the meantime 760 if status.Code(err) != codes.InvalidArgument { 761 logger.FromContext(ctx).Error("creating application: %w") 762 } 763 } 764 } else { 765 appToUpdate := CreateArgoApplication(overview, app, env) 766 appUpdateRequest := &application.ApplicationUpdateRequest{ 767 XXX_NoUnkeyedLiteral: struct{}{}, 768 XXX_unrecognized: nil, 769 XXX_sizecache: 0, 770 Validate: ptr.Bool(false), 771 Application: appToUpdate, 772 Project: ptr.FromString(appToUpdate.Spec.Project), 773 } 774 775 if !cmp.Equal(appUpdateRequest.Application.Spec, existingApp.Spec, cmp.AllowUnexported(v1alpha1.ApplicationSpec{}.Destination)) { 776 a.ApplicationClient.Update(ctx, appUpdateRequest) 777 } 778 } 779 } 780 781 type ArgoEvent struct { 782 Environment string 783 Application string 784 SyncStatusCode v1alpha1.SyncStatusCode 785 HealthStatusCode health.HealthStatusCode 786 OperationState *v1alpha1.OperationState 787 }