github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/buildcontroller_test.go (about) 1 package engine 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 v1 "k8s.io/api/core/v1" 13 14 "github.com/windmilleng/tilt/internal/hud/server" 15 "github.com/windmilleng/tilt/internal/k8s/testyaml" 16 "github.com/windmilleng/tilt/internal/testutils/manifestbuilder" 17 "github.com/windmilleng/tilt/internal/testutils/podbuilder" 18 19 "github.com/windmilleng/tilt/internal/container" 20 "github.com/windmilleng/tilt/internal/store" 21 "github.com/windmilleng/tilt/internal/watch" 22 "github.com/windmilleng/tilt/pkg/model" 23 ) 24 25 func TestBuildControllerOnePod(t *testing.T) { 26 f := newTestFixture(t) 27 defer f.TearDown() 28 29 manifest := f.newManifest("fe") 30 f.Start([]model.Manifest{manifest}, true) 31 32 call := f.nextCall() 33 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 34 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 35 36 pod := podbuilder.New(f.T(), manifest).Build() 37 f.podEvent(pod, manifest.Name) 38 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 39 40 call = f.nextCall() 41 assert.Equal(t, pod.Name, call.oneState().OneContainerInfo().PodID.String()) 42 43 err := f.Stop() 44 assert.NoError(t, err) 45 f.assertAllBuildsConsumed() 46 } 47 48 func TestBuildControllerTooManyPodsForLiveUpdateErrorMessage(t *testing.T) { 49 f := newTestFixture(t) 50 defer f.TearDown() 51 52 manifest := NewSanchoLiveUpdateManifest(f) 53 f.Start([]model.Manifest{manifest}, true) 54 55 // initial build 56 call := f.nextCall() 57 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 58 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 59 60 p1 := podbuilder.New(t, manifest).WithPodID("pod1").Build() 61 p2 := podbuilder.New(t, manifest).WithPodID("pod2").Build() 62 63 f.podEvent(p1, manifest.Name) 64 f.podEvent(p2, manifest.Name) 65 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 66 67 call = f.nextCall() 68 // Should not have sent container info b/c too many pods 69 assert.Equal(t, store.ContainerInfo{}, call.oneState().OneContainerInfo()) 70 71 err := f.Stop() 72 assert.NoError(t, err) 73 f.assertAllBuildsConsumed() 74 75 err = call.oneState().RunningContainerError 76 if assert.Error(t, err) { 77 assert.Contains(t, err.Error(), "can only get container info for a single pod", 78 "should print error message when trying to get Running Containers for manifest with more than one pod") 79 } 80 } 81 82 func TestBuildControllerTooManyPodsForDockerBuildNoErrorMessage(t *testing.T) { 83 f := newTestFixture(t) 84 defer f.TearDown() 85 86 manifest := NewSanchoDockerBuildManifest(f) 87 f.Start([]model.Manifest{manifest}, true) 88 89 // intial build 90 call := f.nextCall() 91 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 92 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 93 94 p1 := podbuilder.New(t, manifest).WithPodID("pod1").Build() 95 p2 := podbuilder.New(t, manifest).WithPodID("pod2").Build() 96 97 f.podEvent(p1, manifest.Name) 98 f.podEvent(p2, manifest.Name) 99 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 100 101 call = f.nextCall() 102 // Should not have sent container info b/c too many pods 103 assert.Equal(t, store.ContainerInfo{}, call.oneState().OneContainerInfo()) 104 105 err := f.Stop() 106 assert.NoError(t, err) 107 f.assertAllBuildsConsumed() 108 109 // Should not have surfaced this log line b/c manifest doesn't have LiveUpdate instructions 110 assert.NotContains(t, f.log.String(), "can only get container info for a single pod", 111 "should print error message when trying to get Running Containers for manifest with more than one pod") 112 } 113 114 func TestBuildControllerIgnoresImageTags(t *testing.T) { 115 f := newTestFixture(t) 116 defer f.TearDown() 117 118 ref := container.MustParseNamed("image-foo:tagged") 119 manifest := f.newManifestWithRef("fe", ref) 120 f.Start([]model.Manifest{manifest}, true) 121 122 call := f.nextCall() 123 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 124 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 125 126 pod := podbuilder.New(t, manifest). 127 WithPodID("pod-id"). 128 WithImage("image-foo:othertag"). 129 Build() 130 f.podEvent(pod, manifest.Name) 131 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 132 133 call = f.nextCall() 134 assert.Equal(t, "pod-id", call.oneState().OneContainerInfo().PodID.String()) 135 136 err := f.Stop() 137 assert.NoError(t, err) 138 f.assertAllBuildsConsumed() 139 } 140 141 func TestBuildControllerDockerCompose(t *testing.T) { 142 f := newTestFixture(t) 143 defer f.TearDown() 144 145 manifest := NewSanchoFastBuildDCManifest(f) 146 f.Start([]model.Manifest{manifest}, true) 147 148 call := f.nextCall() 149 imageTarget := manifest.ImageTargetAt(0) 150 assert.Equal(t, imageTarget, call.firstImgTarg()) 151 152 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 153 154 call = f.nextCall() 155 imageState := call.state[imageTarget.ID()] 156 assert.Equal(t, "dc-sancho", imageState.OneContainerInfo().ContainerID.String()) 157 158 err := f.Stop() 159 assert.NoError(t, err) 160 f.assertAllBuildsConsumed() 161 } 162 163 func TestBuildControllerLocalResource(t *testing.T) { 164 f := newTestFixture(t) 165 defer f.TearDown() 166 167 dep := f.JoinPath("stuff.json") 168 manifest := manifestbuilder.New(f, "yarn-add"). 169 WithLocalResource("echo beep boop", []string{dep}).Build() 170 f.Start([]model.Manifest{manifest}, true) 171 172 call := f.nextCall() 173 lt := manifest.LocalTarget() 174 assert.Equal(t, lt, call.local()) 175 176 f.fsWatcher.events <- watch.NewFileEvent(dep) 177 178 call = f.nextCall() 179 assert.Equal(t, lt, call.local()) 180 181 err := f.Stop() 182 assert.NoError(t, err) 183 f.assertAllBuildsConsumed() 184 } 185 186 func TestBuildControllerWontContainerBuildWithTwoPods(t *testing.T) { 187 f := newTestFixture(t) 188 defer f.TearDown() 189 190 manifest := f.newManifest("fe") 191 f.Start([]model.Manifest{manifest}, true) 192 193 call := f.nextCall() 194 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 195 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 196 197 // Associate the pods with the manifest state 198 podA := podbuilder.New(f.T(), manifest).WithPodID("pod-a").Build() 199 podB := podbuilder.New(f.T(), manifest).WithPodID("pod-b").Build() 200 f.podEvent(podA, manifest.Name) 201 f.podEvent(podB, manifest.Name) 202 203 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 204 205 // We expect two pods associated with this manifest. We don't want to container-build 206 // if there are multiple pods, so make sure we're not sending deploy info (i.e. that 207 // we're doing an image build) 208 call = f.nextCall() 209 assert.Equal(t, "", call.oneState().OneContainerInfo().PodID.String()) 210 211 err := f.Stop() 212 assert.NoError(t, err) 213 f.assertAllBuildsConsumed() 214 } 215 216 func TestBuildControllerTwoContainers(t *testing.T) { 217 f := newTestFixture(t) 218 defer f.TearDown() 219 220 manifest := f.newManifest("fe") 221 f.Start([]model.Manifest{manifest}, true) 222 223 call := f.nextCall() 224 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 225 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 226 227 // container already on this pod matches the image built by this manifest 228 pod := podbuilder.New(f.T(), manifest).Build() 229 imgName := pod.Status.ContainerStatuses[0].Image 230 pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, v1.ContainerStatus{ 231 Name: "same image", 232 Image: imgName, // matches image built by this manifest 233 Ready: true, 234 ContainerID: "docker://cID-same-image", 235 }, v1.ContainerStatus{ 236 Name: "different image", 237 Image: "different-image", // does NOT match image built by this manifest 238 Ready: false, 239 ContainerID: "docker://cID-different-image", 240 }) 241 f.podEvent(pod, manifest.Name) 242 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 243 244 call = f.nextCall() 245 runningContainers := call.oneState().RunningContainers 246 247 require.Len(t, runningContainers, 2, "expect info for two containers (those "+ 248 "matching the image built by this manifest") 249 250 c0 := runningContainers[0] 251 c1 := runningContainers[1] 252 253 assert.Equal(t, pod.Name, c0.PodID.String(), "pod ID for cInfo at index 0") 254 assert.Equal(t, pod.Name, c1.PodID.String(), "pod ID for cInfo at index 1") 255 256 assert.Equal(t, podbuilder.FakeContainerID(), c0.ContainerID, "container ID for cInfo at index 0") 257 assert.Equal(t, "cID-same-image", c1.ContainerID.String(), "container ID for cInfo at index 1") 258 259 assert.Equal(t, "sancho", c0.ContainerName.String(), "container name for cInfo at index 0") 260 assert.Equal(t, "same image", c1.ContainerName.String(), "container name for cInfo at index 1") 261 262 err := f.Stop() 263 assert.NoError(t, err) 264 f.assertAllBuildsConsumed() 265 } 266 267 func TestBuildControllerWontContainerBuildWithSomeButNotAllReadyContainers(t *testing.T) { 268 f := newTestFixture(t) 269 defer f.TearDown() 270 271 manifest := f.newManifest("fe") 272 f.Start([]model.Manifest{manifest}, true) 273 274 call := f.nextCall() 275 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 276 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 277 278 // container already on this pod matches the image built by this manifest 279 pod := podbuilder.New(f.T(), manifest).Build() 280 imgName := pod.Status.ContainerStatuses[0].Image 281 pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, v1.ContainerStatus{ 282 Name: "same image", 283 Image: imgName, // matches image built by this manifest 284 Ready: false, 285 ContainerID: "docker://cID-same-image", 286 }) 287 f.podEvent(pod, manifest.Name) 288 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 289 290 // If even one of the containers matching this image is !ready, we have to do a 291 // full rebuild, so don't return ANY RunningContainers. 292 call = f.nextCall() 293 runningContainers := call.oneState().RunningContainers 294 assert.Empty(t, runningContainers) 295 296 err := f.Stop() 297 assert.NoError(t, err) 298 f.assertAllBuildsConsumed() 299 } 300 301 func TestBuildControllerCrashRebuild(t *testing.T) { 302 f := newTestFixture(t) 303 defer f.TearDown() 304 305 manifest := f.newManifest("fe") 306 f.Start([]model.Manifest{manifest}, true) 307 308 call := f.nextCall() 309 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 310 assert.Equal(t, []string{}, call.oneState().FilesChanged()) 311 f.waitForCompletedBuildCount(1) 312 313 f.b.nextLiveUpdateContainerIDs = []container.ID{podbuilder.FakeContainerID()} 314 pb := podbuilder.New(f.T(), manifest) 315 pod := pb.Build() 316 f.podEvent(pod, manifest.Name) 317 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 318 319 call = f.nextCall() 320 assert.Equal(t, pod.Name, call.oneState().OneContainerInfo().PodID.String()) 321 f.waitForCompletedBuildCount(2) 322 f.withManifestState("fe", func(ms store.ManifestState) { 323 assert.Equal(t, model.BuildReasonFlagChangedFiles, ms.LastBuild().Reason) 324 assert.Equal(t, podbuilder.FakeContainerIDSet(1), ms.LiveUpdatedContainerIDs) 325 }) 326 327 // Restart the pod with a new container id, to simulate a container restart. 328 f.podEvent(pb.WithContainerID("funnyContainerID").Build(), manifest.Name) 329 call = f.nextCall() 330 assert.True(t, call.oneState().OneContainerInfo().Empty()) 331 f.waitForCompletedBuildCount(3) 332 333 f.withManifestState("fe", func(ms store.ManifestState) { 334 assert.Equal(t, model.BuildReasonFlagCrash, ms.LastBuild().Reason) 335 }) 336 337 err := f.Stop() 338 assert.NoError(t, err) 339 f.assertAllBuildsConsumed() 340 } 341 342 func TestCrashRebuildTwoContainersOneImage(t *testing.T) { 343 f := newTestFixture(t) 344 defer f.TearDown() 345 346 manifest := manifestbuilder.New(f, "sancho"). 347 WithK8sYAML(testyaml.SanchoTwoContainersOneImageYAML). 348 WithImageTarget(NewSanchoLiveUpdateImageTarget(f)). 349 Build() 350 f.Start([]model.Manifest{manifest}, true) 351 352 call := f.nextCall() 353 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 354 f.waitForCompletedBuildCount(1) 355 356 f.b.nextLiveUpdateContainerIDs = []container.ID{"c1", "c2"} 357 f.podEvent(podbuilder.New(t, manifest). 358 WithContainerIDAtIndex("c1", 0). 359 WithContainerIDAtIndex("c2", 1). 360 Build(), manifest.Name) 361 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 362 363 call = f.nextCall() 364 f.waitForCompletedBuildCount(2) 365 f.withManifestState("sancho", func(ms store.ManifestState) { 366 assert.Equal(t, 2, len(ms.LiveUpdatedContainerIDs)) 367 }) 368 369 // Simulate pod event where one of the containers has been restarted with a new ID. 370 f.podEvent(podbuilder.New(t, manifest). 371 WithContainerID("c1"). 372 WithContainerIDAtIndex("c3", 1). 373 Build(), manifest.Name) 374 375 call = f.nextCall() 376 f.waitForCompletedBuildCount(3) 377 378 f.withManifestState("sancho", func(ms store.ManifestState) { 379 assert.Equal(t, model.BuildReasonFlagCrash, ms.LastBuild().Reason) 380 }) 381 382 err := f.Stop() 383 assert.NoError(t, err) 384 f.assertAllBuildsConsumed() 385 } 386 387 func TestCrashRebuildTwoContainersTwoImages(t *testing.T) { 388 f := newTestFixture(t) 389 defer f.TearDown() 390 391 manifest := manifestbuilder.New(f, "sancho"). 392 WithK8sYAML(testyaml.SanchoTwoContainersOneImageYAML). 393 WithImageTarget(NewSanchoLiveUpdateImageTarget(f)). 394 WithImageTarget(NewSanchoSidecarLiveUpdateImageTarget(f)). 395 Build() 396 f.Start([]model.Manifest{manifest}, true) 397 398 call := f.nextCall() 399 iTargs := call.imageTargets() 400 require.Len(t, iTargs, 2) 401 assert.Equal(t, manifest.ImageTargetAt(0), iTargs[0]) 402 assert.Equal(t, manifest.ImageTargetAt(1), iTargs[1]) 403 f.waitForCompletedBuildCount(1) 404 405 f.b.nextLiveUpdateContainerIDs = []container.ID{"c1", "c2"} 406 f.podEvent(podbuilder.New(t, manifest). 407 WithContainerIDAtIndex("c1", 0). 408 WithContainerIDAtIndex("c2", 1). 409 Build(), manifest.Name) 410 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 411 412 call = f.nextCall() 413 f.waitForCompletedBuildCount(2) 414 f.withManifestState("sancho", func(ms store.ManifestState) { 415 assert.Equal(t, 2, len(ms.LiveUpdatedContainerIDs)) 416 }) 417 418 // Simulate pod event where one of the containers has been restarted with a new ID. 419 f.podEvent(podbuilder.New(t, manifest). 420 WithContainerID("c1"). 421 WithContainerIDAtIndex("c3", 1). 422 Build(), manifest.Name) 423 424 call = f.nextCall() 425 f.waitForCompletedBuildCount(3) 426 427 f.withManifestState("sancho", func(ms store.ManifestState) { 428 assert.Equal(t, model.BuildReasonFlagCrash, ms.LastBuild().Reason) 429 }) 430 431 err := f.Stop() 432 assert.NoError(t, err) 433 f.assertAllBuildsConsumed() 434 } 435 436 func TestRecordLiveUpdatedContainerIDsForFailedLiveUpdate(t *testing.T) { 437 f := newTestFixture(t) 438 defer f.TearDown() 439 440 manifest := manifestbuilder.New(f, "sancho"). 441 WithK8sYAML(testyaml.SanchoTwoContainersOneImageYAML). 442 WithImageTarget(NewSanchoLiveUpdateImageTarget(f)). 443 Build() 444 f.Start([]model.Manifest{manifest}, true) 445 446 call := f.nextCall() 447 assert.Equal(t, manifest.ImageTargetAt(0), call.firstImgTarg()) 448 f.waitForCompletedBuildCount(1) 449 450 expectedErr := fmt.Errorf("i can't let you do that dave") 451 f.b.nextBuildFailure = expectedErr 452 f.b.nextLiveUpdateContainerIDs = []container.ID{"c1", "c2"} 453 454 f.podEvent(podbuilder.New(t, manifest). 455 WithContainerIDAtIndex("c1", 0). 456 WithContainerIDAtIndex("c2", 1). 457 Build(), manifest.Name) 458 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 459 460 call = f.nextCall() 461 f.waitForCompletedBuildCount(2) 462 f.withManifestState("sancho", func(ms store.ManifestState) { 463 // Manifest should have recorded last build as a failure, but 464 // ALSO have recorded the LiveUpdatedContainerIDs 465 require.Equal(t, expectedErr, ms.BuildHistory[0].Error) 466 467 assert.Equal(t, 2, len(ms.LiveUpdatedContainerIDs)) 468 }) 469 } 470 471 func TestBuildControllerManualTriggerWithFileChanges(t *testing.T) { 472 for _, tc := range []struct { 473 name string 474 triggerMode model.TriggerMode 475 }{ 476 {"manual including initial", model.TriggerModeManualIncludingInitial}, 477 {"manual after initial", model.TriggerModeManualAfterInitial}, 478 } { 479 t.Run(tc.name, func(t *testing.T) { 480 f := newTestFixture(t) 481 defer f.TearDown() 482 mName := model.ManifestName("foobar") 483 484 manifest := f.newManifest(mName.String()).WithTriggerMode(tc.triggerMode) 485 manifests := []model.Manifest{manifest} 486 f.Start(manifests, true) 487 488 // if we expect an initial build from the manifest, wait for it to complete 489 if manifest.TriggerMode.AutoInitial() { 490 f.nextCallComplete() 491 } 492 493 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 494 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 495 return len(st.BuildStatus(manifest.ImageTargetAt(0).ID()).PendingFileChanges) > 0 496 }) 497 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 498 499 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: mName}) 500 call := f.nextCallComplete() 501 state := call.oneState() 502 assert.Equal(t, []string{f.JoinPath("main.go")}, state.FilesChanged()) 503 assert.False(t, state.NeedsForceUpdate) // it was NOT a force update, b/c there were changed files 504 505 f.WaitUntil("manifest removed from queue", func(st store.EngineState) bool { 506 for _, mn := range st.TriggerQueue { 507 if mn == mName { 508 return false 509 } 510 } 511 return true 512 }) 513 }) 514 } 515 } 516 517 func TestBuildControllerManualTriggerWithoutFileChangesForceUpdates(t *testing.T) { 518 f := newTestFixture(t) 519 defer f.TearDown() 520 mName := model.ManifestName("foobar") 521 522 manifest := f.newManifest(mName.String()) 523 manifests := []model.Manifest{manifest} 524 f.Start(manifests, true) 525 526 f.nextCall() 527 f.waitForCompletedBuildCount(1) 528 529 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: mName}) 530 call := f.nextCall() 531 state := call.oneState() 532 assert.Empty(t, state.FilesChanged()) 533 assert.True(t, state.NeedsForceUpdate) 534 f.waitForCompletedBuildCount(2) 535 536 f.WaitUntil("manifest removed from queue", func(st store.EngineState) bool { 537 for _, mn := range st.TriggerQueue { 538 if mn == mName { 539 return false 540 } 541 } 542 return true 543 }) 544 } 545 546 func TestBuildQueueOrdering(t *testing.T) { 547 f := newTestFixture(t) 548 defer f.TearDown() 549 550 m1 := f.newManifestWithRef("manifest1", container.MustParseNamed("manifest1")). 551 WithTriggerMode(model.TriggerModeManualAfterInitial) 552 m2 := f.newManifestWithRef("manifest2", container.MustParseNamed("manifest2")). 553 WithTriggerMode(model.TriggerModeManualAfterInitial) 554 m3 := f.newManifestWithRef("manifest3", container.MustParseNamed("manifest3")). 555 WithTriggerMode(model.TriggerModeManualIncludingInitial) 556 m4 := f.newManifestWithRef("manifest4", container.MustParseNamed("manifest4")). 557 WithTriggerMode(model.TriggerModeManualIncludingInitial) 558 559 // attach to state in different order than we plan to trigger them 560 manifests := []model.Manifest{m4, m2, m3, m1} 561 f.Start(manifests, true) 562 563 expectedInitialBuildCount := 0 564 for _, m := range manifests { 565 if m.TriggerMode.AutoInitial() { 566 expectedInitialBuildCount++ 567 f.nextCall() 568 } 569 } 570 571 f.waitForCompletedBuildCount(expectedInitialBuildCount) 572 573 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("main.go")) 574 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 575 return len(st.BuildStatus(m1.ImageTargetAt(0).ID()).PendingFileChanges) > 0 && 576 len(st.BuildStatus(m2.ImageTargetAt(0).ID()).PendingFileChanges) > 0 && 577 len(st.BuildStatus(m3.ImageTargetAt(0).ID()).PendingFileChanges) > 0 && 578 len(st.BuildStatus(m4.ImageTargetAt(0).ID()).PendingFileChanges) > 0 579 }) 580 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 581 582 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest1"}) 583 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest2"}) 584 time.Sleep(10 * time.Millisecond) 585 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest3"}) 586 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest4"}) 587 588 for i, _ := range manifests { 589 expName := fmt.Sprintf("manifest%d", i+1) 590 call := f.nextCall() 591 imgID := call.firstImgTarg().ID().String() 592 if assert.True(t, strings.HasSuffix(imgID, expName), 593 "expected to get manifest '%s' but instead got: '%s' (checking suffix for manifest name)", expName, imgID) { 594 assert.Equal(t, []string{f.JoinPath("main.go")}, call.oneState().FilesChanged(), 595 "for manifest '%s", expName) 596 } 597 } 598 f.waitForCompletedBuildCount(expectedInitialBuildCount + len(manifests)) 599 } 600 601 // This test is tightly coupled with FastBuild, and needs to be 602 // rewritten to use DockerBuild 603 func TestBuildQueueAndAutobuildOrdering(t *testing.T) { 604 f := newTestFixture(t) 605 defer f.TearDown() 606 607 // changes to this dir. will register with our manual manifests 608 syncDirManual := model.Sync{LocalPath: f.JoinPath("dirManual/"), ContainerPath: "/go"} 609 // changes to this dir. will register with our automatic manifests 610 syncDirAuto := model.Sync{LocalPath: f.JoinPath("dirAuto/"), ContainerPath: "/go"} 611 612 m1 := f.newFastBuildManifest("manifest1", []model.Sync{syncDirManual}).WithTriggerMode(model.TriggerModeManualAfterInitial) 613 m2 := f.newFastBuildManifest("manifest2", []model.Sync{syncDirManual}).WithTriggerMode(model.TriggerModeManualAfterInitial) 614 m3 := f.newFastBuildManifest("manifest3", []model.Sync{syncDirManual}).WithTriggerMode(model.TriggerModeManualIncludingInitial) 615 m4 := f.newFastBuildManifest("manifest4", []model.Sync{syncDirManual}).WithTriggerMode(model.TriggerModeManualIncludingInitial) 616 m5 := f.newFastBuildManifest("manifest5", []model.Sync{syncDirAuto}).WithTriggerMode(model.TriggerModeAuto) 617 618 // attach to state in different order than we plan to trigger them 619 manifests := []model.Manifest{m5, m4, m2, m3, m1} 620 f.Start(manifests, true) 621 622 expectedInitialBuildCount := 0 623 for _, m := range manifests { 624 if m.TriggerMode.AutoInitial() { 625 expectedInitialBuildCount++ 626 f.nextCall() 627 } 628 } 629 630 f.waitForCompletedBuildCount(expectedInitialBuildCount) 631 632 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("dirManual/main.go")) 633 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 634 return len(st.BuildStatus(m1.ImageTargetAt(0).ID()).PendingFileChanges) > 0 && 635 len(st.BuildStatus(m2.ImageTargetAt(0).ID()).PendingFileChanges) > 0 && 636 len(st.BuildStatus(m3.ImageTargetAt(0).ID()).PendingFileChanges) > 0 && 637 len(st.BuildStatus(m4.ImageTargetAt(0).ID()).PendingFileChanges) > 0 638 }) 639 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 640 641 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest1"}) 642 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest2"}) 643 // make our one auto-trigger manifest build - should be evaluated LAST, after 644 // all the manual manifests waiting in the queue 645 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("dirAuto/main.go")) 646 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest3"}) 647 f.store.Dispatch(server.AppendToTriggerQueueAction{Name: "manifest4"}) 648 649 for i, _ := range manifests { 650 call := f.nextCall() 651 assert.True(t, strings.HasSuffix(call.firstImgTarg().ID().String(), fmt.Sprintf("manifest%d", i+1))) 652 653 if i < 4 { 654 assert.Equal(t, []string{f.JoinPath("dirManual/main.go")}, call.oneState().FilesChanged(), "for manifest %d", i+1) 655 } else { 656 // the automatic manifest 657 assert.Equal(t, []string{f.JoinPath("dirAuto/main.go")}, call.oneState().FilesChanged(), "for manifest %d", i+1) 658 } 659 } 660 f.waitForCompletedBuildCount(len(manifests) + expectedInitialBuildCount) 661 } 662 663 // any manifests without image targets should be deployed before any manifests WITH image targets 664 func TestBuildControllerNoBuildManifestsFirst(t *testing.T) { 665 f := newTestFixture(t) 666 defer f.TearDown() 667 668 manifests := make([]model.Manifest, 10) 669 for i := 0; i < 10; i++ { 670 manifests[i] = f.newManifest(fmt.Sprintf("built%d", i+1)) 671 } 672 673 for _, i := range []int{3, 7, 8} { 674 manifests[i] = manifestbuilder.New(f, model.ManifestName(fmt.Sprintf("unbuilt%d", i+1))). 675 WithK8sYAML(SanchoYAML). 676 Build() 677 } 678 f.Start(manifests, true) 679 680 var observedBuildOrder []string 681 for i := 0; i < len(manifests); i++ { 682 call := f.nextCall() 683 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 684 } 685 686 // throwing a bunch of elements at it to increase confidence we maintain order between built and unbuilt 687 // this might miss bugs since we might just get these elements back in the right order via luck 688 expectedBuildOrder := []string{ 689 "unbuilt4", 690 "unbuilt8", 691 "unbuilt9", 692 "built1", 693 "built2", 694 "built3", 695 "built5", 696 "built6", 697 "built7", 698 "built10", 699 } 700 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 701 } 702 703 func TestBuildControllerUnresourcedYAMLFirst(t *testing.T) { 704 f := newTestFixture(t) 705 defer f.TearDown() 706 707 manifests := []model.Manifest{ 708 f.newManifest("built1"), 709 f.newManifest("built2"), 710 f.newManifest("built3"), 711 f.newManifest("built4"), 712 } 713 714 manifests = append(manifests, manifestbuilder.New(f, model.UnresourcedYAMLManifestName). 715 WithK8sYAML(testyaml.SecretYaml).Build()) 716 f.Start(manifests, true) 717 718 var observedBuildOrder []string 719 for i := 0; i < len(manifests); i++ { 720 call := f.nextCall() 721 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 722 } 723 724 expectedBuildOrder := []string{ 725 model.UnresourcedYAMLManifestName.String(), 726 "built1", 727 "built2", 728 "built3", 729 "built4", 730 } 731 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 732 } 733 734 func TestBuildControllerRespectDockerComposeOrder(t *testing.T) { 735 f := newTestFixture(t) 736 defer f.TearDown() 737 738 sancho := NewSanchoFastBuildDCManifest(f) 739 redis := manifestbuilder.New(f, "redis").WithDockerCompose().Build() 740 donQuixote := manifestbuilder.New(f, "don-quixote").WithDockerCompose().Build() 741 manifests := []model.Manifest{redis, sancho, donQuixote} 742 f.Start(manifests, true) 743 744 var observedBuildOrder []string 745 for i := 0; i < len(manifests); i++ { 746 call := f.nextCall() 747 observedBuildOrder = append(observedBuildOrder, call.dc().Name.String()) 748 } 749 750 // If these were Kubernetes resources, we would try to deploy don-quixote 751 // before sancho, because it doesn't have an image build. 752 // 753 // But this would be wrong, because Docker Compose has stricter ordering requirements, see: 754 // https://docs.docker.com/compose/startup-order/ 755 expectedBuildOrder := []string{ 756 "redis", 757 "sancho", 758 "don-quixote", 759 } 760 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 761 } 762 763 func TestBuildControllerLocalResourcesBeforeClusterResources(t *testing.T) { 764 f := newTestFixture(t) 765 defer f.TearDown() 766 767 manifests := []model.Manifest{ 768 f.newManifest("clusterBuilt1"), 769 f.newManifest("clusterBuilt2"), 770 manifestbuilder.New(f, "clusterUnbuilt"). 771 WithK8sYAML(SanchoYAML).Build(), 772 manifestbuilder.New(f, "local1"). 773 WithLocalResource("echo local1", nil).Build(), 774 f.newManifest("clusterBuilt3"), 775 manifestbuilder.New(f, "local2"). 776 WithLocalResource("echo local2", nil).Build(), 777 } 778 779 manifests = append(manifests, manifestbuilder.New(f, model.UnresourcedYAMLManifestName). 780 WithK8sYAML(testyaml.SecretYaml).Build()) 781 f.Start(manifests, true) 782 783 var observedBuildOrder []string 784 for i := 0; i < len(manifests); i++ { 785 call := f.nextCall() 786 if !call.k8s().Empty() { 787 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 788 continue 789 } 790 observedBuildOrder = append(observedBuildOrder, call.local().Name.String()) 791 } 792 793 expectedBuildOrder := []string{ 794 model.UnresourcedYAMLManifestName.String(), 795 "local1", // local resource comes after UnresourcedYAML but before all cluster resources 796 "local2", 797 "clusterUnbuilt", 798 "clusterBuilt1", 799 "clusterBuilt2", 800 "clusterBuilt3", 801 } 802 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 803 } 804 805 func TestBuildControllerResourceDeps(t *testing.T) { 806 f := newTestFixture(t) 807 defer f.TearDown() 808 809 depGraph := map[string][]string{ 810 "a": {"e"}, 811 "b": {"e"}, 812 "c": {"d", "g"}, 813 "d": {}, 814 "e": {"d", "f"}, 815 "f": {"c"}, 816 "g": {}, 817 } 818 819 var manifests []model.Manifest 820 manifestsByName := make(map[string]model.Manifest) 821 for name, deps := range depGraph { 822 m := f.newManifest(name) 823 for _, dep := range deps { 824 m.ResourceDependencies = append(m.ResourceDependencies, model.ManifestName(dep)) 825 } 826 manifests = append(manifests, m) 827 manifestsByName[name] = m 828 } 829 830 f.Start(manifests, true) 831 832 var observedOrder []string 833 for i := range manifests { 834 call := f.nextCall("%dth build. have built: %v", i, observedOrder) 835 name := call.k8s().Name.String() 836 observedOrder = append(observedOrder, name) 837 pb := podbuilder.New(t, manifestsByName[name]).WithContainerReady(true) 838 f.podEvent(pb.Build(), model.ManifestName(name)) 839 } 840 841 var expectedManifests []string 842 for name := range depGraph { 843 expectedManifests = append(expectedManifests, name) 844 } 845 846 // make sure everything built 847 require.ElementsMatch(t, expectedManifests, observedOrder) 848 849 buildIndexes := make(map[string]int) 850 for i, n := range observedOrder { 851 buildIndexes[n] = i 852 } 853 854 // make sure it happened in an acceptable order 855 for name, deps := range depGraph { 856 for _, dep := range deps { 857 require.Truef(t, buildIndexes[name] > buildIndexes[dep], "%s built before %s, contrary to resource deps", name, dep) 858 } 859 } 860 } 861 862 // normally, local builds go before k8s builds 863 // if the local build depends on the k8s build, the k8s build should go first 864 func TestBuildControllerResourceDepTrumpsLocalResourcePriority(t *testing.T) { 865 f := newTestFixture(t) 866 defer f.TearDown() 867 868 k8sManifest := f.newManifest("foo") 869 localManifest := manifestbuilder.New(f, "bar"). 870 WithLocalResource("echo bar", nil). 871 WithResourceDeps("foo").Build() 872 manifests := []model.Manifest{localManifest, k8sManifest} 873 f.Start(manifests, true) 874 875 var observedBuildOrder []string 876 for i := 0; i < len(manifests); i++ { 877 call := f.nextCall() 878 if !call.k8s().Empty() { 879 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 880 pb := podbuilder.New(t, k8sManifest).WithContainerReady(true) 881 f.podEvent(pb.Build(), k8sManifest.Name) 882 continue 883 } 884 observedBuildOrder = append(observedBuildOrder, call.local().Name.String()) 885 } 886 887 expectedBuildOrder := []string{"foo", "bar"} 888 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 889 } 890 891 // bar depends on foo, we build foo three times before marking it ready, and make sure bar waits 892 func TestBuildControllerResourceDepTrumpsInitialBuild(t *testing.T) { 893 f := newTestFixture(t) 894 defer f.TearDown() 895 896 foo := manifestbuilder.New(f, "foo"). 897 WithLocalResource("foo cmd", []string{f.JoinPath("foo")}). 898 Build() 899 bar := manifestbuilder.New(f, "bar"). 900 WithLocalResource("bar cmd", []string{f.JoinPath("bar")}). 901 WithResourceDeps("foo"). 902 Build() 903 manifests := []model.Manifest{foo, bar} 904 f.b.nextBuildFailure = errors.New("failure") 905 f.Start(manifests, true) 906 907 call := f.nextCall() 908 require.Equal(t, "foo", call.local().Name.String()) 909 910 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("foo", "main.go")) 911 f.b.nextBuildFailure = errors.New("failure") 912 call = f.nextCall() 913 require.Equal(t, "foo", call.local().Name.String()) 914 915 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("foo", "main.go")) 916 call = f.nextCall() 917 require.Equal(t, "foo", call.local().Name.String()) 918 919 // now that the foo build has succeeded, bar should get queued 920 call = f.nextCall() 921 require.Equal(t, "bar", call.local().Name.String()) 922 } 923 924 // bar depends on foo, we build foo three times before marking it ready, and make sure bar waits 925 func TestBuildControllerResourceDepTrumpsPendingBuild(t *testing.T) { 926 f := newTestFixture(t) 927 defer f.TearDown() 928 929 foo := manifestbuilder.New(f, "foo"). 930 WithLocalResource("foo cmd", []string{f.JoinPath("foo")}). 931 Build() 932 bar := manifestbuilder.New(f, "bar"). 933 WithLocalResource("bar cmd", []string{f.JoinPath("bar")}). 934 WithResourceDeps("foo"). 935 Build() 936 937 manifests := []model.Manifest{bar, foo} 938 f.b.nextBuildFailure = errors.New("failure") 939 f.Start(manifests, true) 940 941 // trigger a change for bar so that it would try to build if not for its resource dep 942 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("bar", "main.go")) 943 944 call := f.nextCall() 945 require.Equal(t, "foo", call.local().Name.String()) 946 947 f.b.nextBuildFailure = errors.New("failure") 948 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("foo", "main.go")) 949 call = f.nextCall() 950 require.Equal(t, "foo", call.local().Name.String()) 951 952 f.fsWatcher.events <- watch.NewFileEvent(f.JoinPath("foo", "main2.go")) 953 call = f.nextCall() 954 require.Equal(t, "foo", call.local().Name.String()) 955 956 // since the foo build succeeded, bar should now queue 957 call = f.nextCall() 958 require.Equal(t, "bar", call.local().Name.String()) 959 }