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  }