github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/runner/v1/dev_test.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v1 18 19 import ( 20 "context" 21 "errors" 22 "io/ioutil" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 k8s "k8s.io/client-go/kubernetes" 27 fakekubeclientset "k8s.io/client-go/kubernetes/fake" 28 "k8s.io/client-go/tools/clientcmd/api" 29 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync" 36 "github.com/GoogleContainerTools/skaffold/testutil" 37 ) 38 39 type NoopMonitor struct{} 40 41 func (t *NoopMonitor) Register(func() ([]string, error), func(filemon.Events)) error { 42 return nil 43 } 44 45 func (t *NoopMonitor) Run(bool) error { 46 return nil 47 } 48 49 func (t *NoopMonitor) Reset() {} 50 51 type FailMonitor struct{} 52 53 func (t *FailMonitor) Register(func() ([]string, error), func(filemon.Events)) error { 54 return nil 55 } 56 57 func (t *FailMonitor) Run(bool) error { 58 return errors.New("BUG") 59 } 60 61 func (t *FailMonitor) Reset() {} 62 63 type TestMonitor struct { 64 events []filemon.Events 65 callbacks []func(filemon.Events) 66 testBench *TestBench 67 } 68 69 func (t *TestMonitor) Register(deps func() ([]string, error), onChange func(filemon.Events)) error { 70 t.callbacks = append(t.callbacks, onChange) 71 return nil 72 } 73 74 func (t *TestMonitor) Run(bool) error { 75 if t.testBench.intentTrigger { 76 return nil 77 } 78 79 evt := t.events[t.testBench.currentCycle] 80 81 for _, file := range evt.Modified { 82 switch file { 83 case "file1": 84 t.callbacks[0](evt) // 1st artifact changed 85 case "file2": 86 t.callbacks[1](evt) // 2nd artifact changed 87 // callbacks[2] and callbacks[3] are for `test` dependency triggers 88 case "manifest.yaml": 89 t.callbacks[4](evt) // deployment configuration changed 90 } 91 } 92 93 return nil 94 } 95 96 func (t *TestMonitor) Reset() {} 97 98 func mockK8sClient(string) (k8s.Interface, error) { 99 return fakekubeclientset.NewSimpleClientset(), nil 100 } 101 102 func TestDevFailFirstCycle(t *testing.T) { 103 tests := []struct { 104 description string 105 testBench *TestBench 106 monitor filemon.Monitor 107 expectedActions []Actions 108 }{ 109 { 110 description: "fails to build the first time", 111 testBench: &TestBench{buildErrors: []error{errors.New("")}}, 112 monitor: &NoopMonitor{}, 113 expectedActions: []Actions{{}}, 114 }, 115 { 116 description: "fails to test the first time", 117 testBench: &TestBench{testErrors: []error{errors.New("")}}, 118 monitor: &NoopMonitor{}, 119 expectedActions: []Actions{{ 120 Built: []string{"img:1"}, 121 }}, 122 }, 123 { 124 description: "fails to deploy the first time", 125 testBench: &TestBench{deployErrors: []error{errors.New("")}}, 126 monitor: &NoopMonitor{}, 127 expectedActions: []Actions{{ 128 Built: []string{"img:1"}, 129 Tested: []string{"img:1"}, 130 }}, 131 }, 132 { 133 description: "fails to watch after first cycle", 134 testBench: &TestBench{}, 135 monitor: &FailMonitor{}, 136 expectedActions: []Actions{{ 137 Built: []string{"img:1"}, 138 Tested: []string{"img:1"}, 139 Deployed: []string{"img:1"}, 140 }}, 141 }, 142 } 143 for _, test := range tests { 144 testutil.Run(t, test.description, func(t *testutil.T) { 145 t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"}) 146 t.Override(&client.Client, mockK8sClient) 147 artifacts := []*latest.Artifact{{ 148 ImageName: "img", 149 }} 150 r := createRunner(t, test.testBench, test.monitor, artifacts, nil) 151 test.testBench.firstMonitor = test.monitor.Run 152 153 err := r.Dev(context.Background(), ioutil.Discard, artifacts) 154 155 t.CheckErrorAndDeepEqual(true, err, test.expectedActions, test.testBench.Actions()) 156 }) 157 } 158 } 159 160 func TestDev(t *testing.T) { 161 tests := []struct { 162 description string 163 testBench *TestBench 164 watchEvents []filemon.Events 165 expectedActions []Actions 166 }{ 167 { 168 description: "ignore subsequent build errors", 169 testBench: NewTestBench().WithBuildErrors([]error{nil, errors.New("")}), 170 watchEvents: []filemon.Events{ 171 {Modified: []string{"file1", "file2"}}, 172 }, 173 expectedActions: []Actions{ 174 { 175 Built: []string{"img1:1", "img2:1"}, 176 Tested: []string{"img1:1", "img2:1"}, 177 Deployed: []string{"img1:1", "img2:1"}, 178 }, 179 {}, 180 }, 181 }, 182 { 183 description: "ignore subsequent test errors", 184 testBench: &TestBench{testErrors: []error{nil, errors.New("")}}, 185 watchEvents: []filemon.Events{ 186 {Modified: []string{"file1", "file2"}}, 187 }, 188 expectedActions: []Actions{ 189 { 190 Built: []string{"img1:1", "img2:1"}, 191 Tested: []string{"img1:1", "img2:1"}, 192 Deployed: []string{"img1:1", "img2:1"}, 193 }, 194 { 195 Built: []string{"img1:2", "img2:2"}, 196 }, 197 }, 198 }, 199 { 200 description: "ignore subsequent deploy errors", 201 testBench: &TestBench{deployErrors: []error{nil, errors.New("")}}, 202 watchEvents: []filemon.Events{ 203 {Modified: []string{"file1", "file2"}}, 204 }, 205 expectedActions: []Actions{ 206 { 207 Built: []string{"img1:1", "img2:1"}, 208 Tested: []string{"img1:1", "img2:1"}, 209 Deployed: []string{"img1:1", "img2:1"}, 210 }, 211 { 212 Built: []string{"img1:2", "img2:2"}, 213 Tested: []string{"img1:2", "img2:2"}, 214 }, 215 }, 216 }, 217 { 218 description: "full cycle twice", 219 testBench: &TestBench{}, 220 watchEvents: []filemon.Events{ 221 {Modified: []string{"file1", "file2"}}, 222 }, 223 expectedActions: []Actions{ 224 { 225 Built: []string{"img1:1", "img2:1"}, 226 Tested: []string{"img1:1", "img2:1"}, 227 Deployed: []string{"img1:1", "img2:1"}, 228 }, 229 { 230 Built: []string{"img1:2", "img2:2"}, 231 Tested: []string{"img1:2", "img2:2"}, 232 Deployed: []string{"img1:2", "img2:2"}, 233 }, 234 }, 235 }, 236 { 237 description: "only change second artifact", 238 testBench: &TestBench{}, 239 watchEvents: []filemon.Events{ 240 {Modified: []string{"file2"}}, 241 }, 242 expectedActions: []Actions{ 243 { 244 Built: []string{"img1:1", "img2:1"}, 245 Tested: []string{"img1:1", "img2:1"}, 246 Deployed: []string{"img1:1", "img2:1"}, 247 }, 248 { 249 Built: []string{"img2:2"}, 250 Tested: []string{"img2:2"}, 251 Deployed: []string{"img1:1", "img2:2"}, 252 }, 253 }, 254 }, 255 { 256 description: "redeploy", 257 testBench: &TestBench{}, 258 watchEvents: []filemon.Events{ 259 {Modified: []string{"manifest.yaml"}}, 260 }, 261 expectedActions: []Actions{ 262 { 263 Built: []string{"img1:1", "img2:1"}, 264 Tested: []string{"img1:1", "img2:1"}, 265 Deployed: []string{"img1:1", "img2:1"}, 266 }, 267 { 268 Deployed: []string{"img1:1", "img2:1"}, 269 }, 270 }, 271 }, 272 } 273 for _, test := range tests { 274 testutil.Run(t, test.description, func(t *testutil.T) { 275 t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"}) 276 t.Override(&client.Client, mockK8sClient) 277 test.testBench.cycles = len(test.watchEvents) 278 artifacts := []*latest.Artifact{ 279 {ImageName: "img1"}, 280 {ImageName: "img2"}, 281 } 282 r := createRunner(t, test.testBench, &TestMonitor{ 283 events: test.watchEvents, 284 testBench: test.testBench, 285 }, artifacts, nil) 286 287 err := r.Dev(context.Background(), ioutil.Discard, artifacts) 288 289 t.CheckNoError(err) 290 t.CheckDeepEqual(test.expectedActions, test.testBench.Actions()) 291 }) 292 } 293 } 294 295 func TestDevAutoTriggers(t *testing.T) { 296 tests := []struct { 297 description string 298 watchEvents []filemon.Events 299 expectedActions []Actions 300 autoTriggers triggerState // the state of auto triggers 301 singleTriggers triggerState // the state of single intent triggers at the end of dev loop 302 userIntents []func(i *runner.Intents) 303 }{ 304 { 305 description: "build on; sync on; deploy on", 306 watchEvents: []filemon.Events{ 307 {Modified: []string{"file1"}}, 308 {Modified: []string{"file2"}}, 309 }, 310 autoTriggers: triggerState{true, true, true}, 311 singleTriggers: triggerState{true, true, true}, 312 expectedActions: []Actions{ 313 { 314 Synced: []string{"img1:1"}, 315 }, 316 { 317 Built: []string{"img2:2"}, 318 Tested: []string{"img2:2"}, 319 Deployed: []string{"img1:1", "img2:2"}, 320 }, 321 }, 322 }, 323 { 324 description: "build off; sync off; deploy off", 325 watchEvents: []filemon.Events{ 326 {Modified: []string{"file1"}}, 327 {Modified: []string{"file2"}}, 328 }, 329 expectedActions: []Actions{{}, {}}, 330 }, 331 { 332 description: "build on; sync off; deploy off", 333 watchEvents: []filemon.Events{ 334 {Modified: []string{"file1"}}, 335 {Modified: []string{"file2"}}, 336 }, 337 autoTriggers: triggerState{true, false, false}, 338 singleTriggers: triggerState{true, false, false}, 339 expectedActions: []Actions{{}, { 340 Built: []string{"img2:2"}, 341 Tested: []string{"img2:2"}, 342 }}, 343 }, 344 { 345 description: "build off; sync on; deploy off", 346 watchEvents: []filemon.Events{ 347 {Modified: []string{"file1"}}, 348 {Modified: []string{"file2"}}, 349 }, 350 autoTriggers: triggerState{false, true, false}, 351 singleTriggers: triggerState{false, true, false}, 352 expectedActions: []Actions{{ 353 Synced: []string{"img1:1"}, 354 }, {}}, 355 }, 356 { 357 description: "build off; sync off; deploy on", 358 watchEvents: []filemon.Events{ 359 {Modified: []string{"file1"}}, 360 {Modified: []string{"file2"}}, 361 }, 362 autoTriggers: triggerState{false, false, true}, 363 singleTriggers: triggerState{false, false, true}, 364 expectedActions: []Actions{{}, {}}, 365 }, 366 { 367 description: "build off; sync off; deploy off; user requests build, but no change so intent is discarded", 368 watchEvents: []filemon.Events{}, 369 autoTriggers: triggerState{false, false, false}, 370 singleTriggers: triggerState{false, false, false}, 371 expectedActions: []Actions{}, 372 userIntents: []func(i *runner.Intents){ 373 func(i *runner.Intents) { 374 i.SetBuild(true) 375 }, 376 }, 377 }, 378 { 379 description: "build off; sync off; deploy off; user requests build, and then sync, but no change so both intents are discarded", 380 watchEvents: []filemon.Events{}, 381 autoTriggers: triggerState{false, false, false}, 382 singleTriggers: triggerState{false, false, false}, 383 expectedActions: []Actions{}, 384 userIntents: []func(i *runner.Intents){ 385 func(i *runner.Intents) { 386 i.SetBuild(true) 387 i.SetSync(true) 388 }, 389 }, 390 }, 391 { 392 description: "build off; sync off; deploy off; user requests build, and then sync, but no change so both intents are discarded", 393 watchEvents: []filemon.Events{}, 394 autoTriggers: triggerState{false, false, false}, 395 singleTriggers: triggerState{false, false, false}, 396 expectedActions: []Actions{}, 397 userIntents: []func(i *runner.Intents){ 398 func(i *runner.Intents) { 399 i.SetBuild(true) 400 }, 401 func(i *runner.Intents) { 402 i.SetSync(true) 403 }, 404 }, 405 }, 406 } 407 // first build-test-deploy sequence always happens 408 firstAction := Actions{ 409 Built: []string{"img1:1", "img2:1"}, 410 Tested: []string{"img1:1", "img2:1"}, 411 Deployed: []string{"img1:1", "img2:1"}, 412 } 413 414 for _, test := range tests { 415 testutil.Run(t, test.description, func(t *testutil.T) { 416 t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"}) 417 t.Override(&client.Client, mockK8sClient) 418 t.Override(&sync.WorkingDir, func(context.Context, string, docker.Config) (string, error) { return "/", nil }) 419 testBench := &TestBench{} 420 testBench.cycles = len(test.watchEvents) 421 testBench.userIntents = test.userIntents 422 artifacts := []*latest.Artifact{ 423 { 424 ImageName: "img1", 425 Sync: &latest.Sync{ 426 Manual: []*latest.SyncRule{{Src: "file1", Dest: "file1"}}, 427 }, 428 }, 429 { 430 ImageName: "img2", 431 }, 432 } 433 r := createRunner(t, testBench, &TestMonitor{ 434 events: test.watchEvents, 435 testBench: testBench, 436 }, artifacts, &test.autoTriggers) 437 438 testBench.intents = r.intents 439 440 err := r.Dev(context.Background(), ioutil.Discard, artifacts) 441 442 t.CheckNoError(err) 443 t.CheckDeepEqual(append([]Actions{firstAction}, test.expectedActions...), testBench.Actions()) 444 445 build, _sync, deploy := r.intents.GetIntentsAttrs() 446 singleTriggers := triggerState{ 447 build: build, 448 sync: _sync, 449 deploy: deploy, 450 } 451 t.CheckDeepEqual(test.singleTriggers, singleTriggers, cmp.AllowUnexported(triggerState{})) 452 }) 453 } 454 } 455 456 func TestDevSync(t *testing.T) { 457 type fileSyncEventCalls struct { 458 InProgress int 459 Failed int 460 Succeeded int 461 } 462 463 tests := []struct { 464 description string 465 testBench *TestBench 466 watchEvents []filemon.Events 467 expectedActions []Actions 468 expectedFileSyncEventCalls fileSyncEventCalls 469 }{ 470 { 471 description: "sync", 472 testBench: &TestBench{}, 473 watchEvents: []filemon.Events{ 474 {Modified: []string{"file1"}}, 475 }, 476 expectedActions: []Actions{ 477 { 478 Built: []string{"img1:1", "img2:1"}, 479 Tested: []string{"img1:1", "img2:1"}, 480 Deployed: []string{"img1:1", "img2:1"}, 481 }, 482 { 483 Synced: []string{"img1:1"}, 484 }, 485 }, 486 expectedFileSyncEventCalls: fileSyncEventCalls{ 487 InProgress: 1, 488 Failed: 0, 489 Succeeded: 1, 490 }, 491 }, 492 { 493 description: "sync twice", 494 testBench: &TestBench{}, 495 watchEvents: []filemon.Events{ 496 {Modified: []string{"file1"}}, 497 {Modified: []string{"file1"}}, 498 }, 499 expectedActions: []Actions{ 500 { 501 Built: []string{"img1:1", "img2:1"}, 502 Tested: []string{"img1:1", "img2:1"}, 503 Deployed: []string{"img1:1", "img2:1"}, 504 }, 505 { 506 Synced: []string{"img1:1"}, 507 }, 508 { 509 Synced: []string{"img1:1"}, 510 }, 511 }, 512 expectedFileSyncEventCalls: fileSyncEventCalls{ 513 InProgress: 2, 514 Failed: 0, 515 Succeeded: 2, 516 }, 517 }, 518 } 519 for _, test := range tests { 520 testutil.Run(t, test.description, func(t *testutil.T) { 521 var actualFileSyncEventCalls fileSyncEventCalls 522 t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"}) 523 t.Override(&client.Client, mockK8sClient) 524 t.Override(&fileSyncInProgress, func(int, string) { actualFileSyncEventCalls.InProgress++ }) 525 t.Override(&fileSyncFailed, func(int, string, error) { actualFileSyncEventCalls.Failed++ }) 526 t.Override(&fileSyncSucceeded, func(int, string) { actualFileSyncEventCalls.Succeeded++ }) 527 t.Override(&sync.WorkingDir, func(context.Context, string, docker.Config) (string, error) { return "/", nil }) 528 test.testBench.cycles = len(test.watchEvents) 529 artifacts := []*latest.Artifact{ 530 { 531 ImageName: "img1", 532 Sync: &latest.Sync{ 533 Manual: []*latest.SyncRule{{Src: "file1", Dest: "file1"}}, 534 }, 535 }, 536 { 537 ImageName: "img2", 538 }, 539 } 540 r := createRunner(t, test.testBench, &TestMonitor{ 541 events: test.watchEvents, 542 testBench: test.testBench, 543 }, artifacts, nil) 544 545 err := r.Dev(context.Background(), ioutil.Discard, artifacts) 546 547 t.CheckNoError(err) 548 t.CheckDeepEqual(test.expectedActions, test.testBench.Actions()) 549 t.CheckDeepEqual(test.expectedFileSyncEventCalls, actualFileSyncEventCalls) 550 }) 551 } 552 }