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 }