github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/rollout-service/pkg/service/dispatcher_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 service
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    26  	"github.com/cenkalti/backoff/v4"
    27  	"github.com/freiheit-com/kuberpult/pkg/setup"
    28  	"github.com/freiheit-com/kuberpult/services/rollout-service/pkg/versions"
    29  	"golang.org/x/sync/errgroup"
    30  )
    31  
    32  type expectedVersionCall struct {
    33  	call  getVersionCall
    34  	reply getVersionReply
    35  }
    36  
    37  type dispatcherStep struct {
    38  	Key   Key
    39  	Event *v1alpha1.ApplicationWatchEvent
    40  
    41  	ExpectedVersionCalls []expectedVersionCall
    42  	ExpectedArgoEvent    bool
    43  }
    44  
    45  type getVersionCall struct {
    46  	revision, environment, application string
    47  }
    48  type getVersionReply struct {
    49  	info *versions.VersionInfo
    50  	err  error
    51  }
    52  
    53  type dispatcherVersionMock struct {
    54  	versions.VersionClient
    55  	requests chan getVersionCall
    56  	replies  chan getVersionReply
    57  }
    58  
    59  func (d *dispatcherVersionMock) GetVersion(ctx context.Context, revision, environment, application string) (*versions.VersionInfo, error) {
    60  	d.requests <- getVersionCall{revision, environment, application}
    61  	select {
    62  	case reply := <-d.replies:
    63  		return reply.info, reply.err
    64  	case <-time.After(time.Second):
    65  		panic(fmt.Sprintf("timeout waiting for reply for %s/%s@%s", environment, application, revision))
    66  	}
    67  }
    68  
    69  type argoEventProcessorMock struct {
    70  	events chan *ArgoEvent
    71  }
    72  
    73  func (a *argoEventProcessorMock) ProcessArgoEvent(ctx context.Context, ev ArgoEvent) {
    74  	select {
    75  	case a.events <- &ev:
    76  	case <-time.After(time.Second):
    77  		panic("timeout sending argo event")
    78  	}
    79  }
    80  
    81  func TestDispatcher(t *testing.T) {
    82  	tcs := []struct {
    83  		Name  string
    84  		Steps []dispatcherStep
    85  	}{
    86  		{
    87  			Name: "can retry things",
    88  			Steps: []dispatcherStep{
    89  				{
    90  					Key: Key{Environment: "env", Application: "app"},
    91  					Event: &v1alpha1.ApplicationWatchEvent{
    92  						Application: v1alpha1.Application{
    93  							Status: v1alpha1.ApplicationStatus{
    94  								Sync: v1alpha1.SyncStatus{Revision: "1234"},
    95  							},
    96  						},
    97  					},
    98  
    99  					ExpectedVersionCalls: []expectedVersionCall{
   100  						{
   101  							call: getVersionCall{
   102  								revision:    "1234",
   103  								environment: "env",
   104  								application: "app",
   105  							},
   106  							reply: getVersionReply{
   107  								err: fmt.Errorf("no"),
   108  							},
   109  						},
   110  						{
   111  							call: getVersionCall{
   112  								revision:    "1234",
   113  								environment: "env",
   114  								application: "app",
   115  							},
   116  							reply: getVersionReply{
   117  								info: &versions.VersionInfo{
   118  									Version: 1,
   119  								},
   120  							},
   121  						},
   122  					},
   123  
   124  					ExpectedArgoEvent: true,
   125  				},
   126  			},
   127  		},
   128  		{
   129  			Name: "doesn't retry old versions",
   130  			Steps: []dispatcherStep{
   131  				{
   132  					Key: Key{Environment: "env", Application: "app"},
   133  					Event: &v1alpha1.ApplicationWatchEvent{
   134  						Application: v1alpha1.Application{
   135  							Status: v1alpha1.ApplicationStatus{
   136  								Sync: v1alpha1.SyncStatus{Revision: "1234"},
   137  							},
   138  						},
   139  					},
   140  
   141  					ExpectedVersionCalls: []expectedVersionCall{
   142  						{
   143  							call: getVersionCall{
   144  								revision:    "1234",
   145  								environment: "env",
   146  								application: "app",
   147  							},
   148  							reply: getVersionReply{
   149  								err: fmt.Errorf("no"),
   150  							},
   151  						},
   152  						{
   153  							call: getVersionCall{
   154  								revision:    "1234",
   155  								environment: "env",
   156  								application: "app",
   157  							},
   158  							reply: getVersionReply{
   159  								err: fmt.Errorf("no"),
   160  							},
   161  						},
   162  					},
   163  				},
   164  				{
   165  					Key: Key{Environment: "env", Application: "app"},
   166  					Event: &v1alpha1.ApplicationWatchEvent{
   167  						Application: v1alpha1.Application{
   168  							Status: v1alpha1.ApplicationStatus{
   169  								Sync: v1alpha1.SyncStatus{Revision: "4567"},
   170  							},
   171  						},
   172  					},
   173  
   174  					ExpectedVersionCalls: []expectedVersionCall{
   175  						{
   176  							call: getVersionCall{
   177  								revision:    "4567",
   178  								environment: "env",
   179  								application: "app",
   180  							},
   181  							reply: getVersionReply{
   182  								info: &versions.VersionInfo{
   183  									Version: 1,
   184  								},
   185  							},
   186  						},
   187  					},
   188  
   189  					ExpectedArgoEvent: true,
   190  				},
   191  			},
   192  		},
   193  		{
   194  			Name: "calls version endpoint once for known revision",
   195  			Steps: []dispatcherStep{
   196  				{
   197  					Key: Key{Environment: "env", Application: "app"},
   198  					Event: &v1alpha1.ApplicationWatchEvent{
   199  						Application: v1alpha1.Application{
   200  							Status: v1alpha1.ApplicationStatus{
   201  								Sync: v1alpha1.SyncStatus{Revision: "1234"},
   202  							},
   203  						},
   204  					},
   205  
   206  					ExpectedVersionCalls: []expectedVersionCall{
   207  						{
   208  							call: getVersionCall{
   209  								revision:    "1234",
   210  								environment: "env",
   211  								application: "app",
   212  							},
   213  							reply: getVersionReply{
   214  								info: &versions.VersionInfo{
   215  									Version: 1,
   216  								},
   217  							},
   218  						},
   219  					},
   220  
   221  					ExpectedArgoEvent: true,
   222  				},
   223  				{
   224  					Key: Key{Environment: "env", Application: "app"},
   225  					Event: &v1alpha1.ApplicationWatchEvent{
   226  						Application: v1alpha1.Application{
   227  							Status: v1alpha1.ApplicationStatus{
   228  								Sync: v1alpha1.SyncStatus{Revision: "1234"},
   229  							},
   230  						},
   231  					},
   232  					ExpectedVersionCalls: []expectedVersionCall{},
   233  
   234  					ExpectedArgoEvent: true,
   235  				},
   236  			},
   237  		},
   238  	}
   239  	for _, tc := range tcs {
   240  		tc := tc
   241  		t.Run(tc.Name, func(t *testing.T) {
   242  			t.Parallel()
   243  			ctx, cancel := context.WithCancel(context.Background())
   244  			defer cancel()
   245  			dvc := &dispatcherVersionMock{
   246  				requests: make(chan getVersionCall, 1),
   247  				replies:  make(chan getVersionReply),
   248  			}
   249  			aep := &argoEventProcessorMock{
   250  				events: make(chan *ArgoEvent),
   251  			}
   252  			hlth := &setup.HealthServer{}
   253  			hlth.BackOffFactory = func() backoff.BackOff { return backoff.NewConstantBackOff(0) }
   254  			dispatcher := NewDispatcher(aep, dvc)
   255  			go dispatcher.Work(ctx, hlth.Reporter("dispatcher"))
   256  			for _, step := range tc.Steps {
   257  				var group errgroup.Group
   258  				if step.Event != nil {
   259  					group.Go(func() error { dispatcher.Dispatch(ctx, step.Key, step.Event); return nil })
   260  				}
   261  				for i, call := range step.ExpectedVersionCalls {
   262  					select {
   263  					case req := <-dvc.requests:
   264  						if req.application != call.call.application {
   265  							t.Fatalf("got wrong application in step %d: expected %q but got %q", i, req.application, call.call.application)
   266  						}
   267  						if req.environment != call.call.environment {
   268  							t.Fatalf("got wrong environment in step %d: expected %q but got %q", i, req.environment, call.call.environment)
   269  						}
   270  						if req.revision != call.call.revision {
   271  							t.Fatalf("got wrong revision in step %d: expected %q but got %q", i, req.revision, call.call.revision)
   272  						}
   273  						dvc.replies <- call.reply
   274  					case <-time.After(time.Second):
   275  						t.Fatalf("expected call %d never happened", i)
   276  					}
   277  				}
   278  				if step.ExpectedArgoEvent {
   279  					select {
   280  					case <-aep.events:
   281  						// all good, we got an event
   282  					case <-time.After(time.Second):
   283  						t.Fatalf("timedout waiting for argoevent")
   284  					}
   285  				}
   286  				group.Wait()
   287  			}
   288  		})
   289  	}
   290  }