github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/build_and_deployer_test.go (about)

     1  package engine
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/docker/docker/api/types"
    15  	"github.com/opencontainers/go-digest"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/windmilleng/wmclient/pkg/dirs"
    20  
    21  	"github.com/windmilleng/tilt/internal/build"
    22  	"github.com/windmilleng/tilt/internal/container"
    23  	"github.com/windmilleng/tilt/internal/dockercompose"
    24  	"github.com/windmilleng/tilt/internal/k8s/testyaml"
    25  	"github.com/windmilleng/tilt/internal/ospath"
    26  	"github.com/windmilleng/tilt/internal/store"
    27  
    28  	"github.com/windmilleng/tilt/internal/docker"
    29  	"github.com/windmilleng/tilt/internal/k8s"
    30  	"github.com/windmilleng/tilt/internal/synclet"
    31  	"github.com/windmilleng/tilt/internal/synclet/sidecar"
    32  	"github.com/windmilleng/tilt/internal/testutils"
    33  	"github.com/windmilleng/tilt/internal/testutils/manifestbuilder"
    34  	"github.com/windmilleng/tilt/internal/testutils/tempdir"
    35  	"github.com/windmilleng/tilt/pkg/model"
    36  )
    37  
    38  var testImageRef = container.MustParseNamedTagged("gcr.io/some-project-162817/sancho:deadbeef")
    39  var imageTargetID = model.TargetID{
    40  	Type: model.TargetTypeImage,
    41  	Name: "gcr.io/some-project-162817/sancho",
    42  }
    43  
    44  var alreadyBuilt = store.NewImageBuildResult(imageTargetID, testImageRef)
    45  var alreadyBuiltSet = store.BuildResultSet{imageTargetID: alreadyBuilt}
    46  
    47  type expectedFile = testutils.ExpectedFile
    48  
    49  var testPodID k8s.PodID = "pod-id"
    50  var testContainerInfo = store.ContainerInfo{
    51  	PodID:         testPodID,
    52  	ContainerID:   k8s.MagicTestContainerID,
    53  	ContainerName: "container-name",
    54  }
    55  
    56  func TestGKEDeploy(t *testing.T) {
    57  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
    58  	defer f.TearDown()
    59  
    60  	manifest := NewSanchoLiveUpdateManifest(f)
    61  	targets := buildTargets(manifest)
    62  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
    63  	if err != nil {
    64  		t.Fatal(err)
    65  	}
    66  
    67  	if f.docker.BuildCount != 1 {
    68  		t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount)
    69  	}
    70  
    71  	if f.docker.PushCount != 1 {
    72  		t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount)
    73  	}
    74  
    75  	expectedYaml := "image: gcr.io/some-project-162817/sancho:tilt-11cd0b38bc3ceb95"
    76  	if !strings.Contains(f.k8s.Yaml, expectedYaml) {
    77  		t.Errorf("Expected yaml to contain %q. Actual:\n%s", expectedYaml, f.k8s.Yaml)
    78  	}
    79  
    80  	if !strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
    81  		t.Errorf("Should deploy the synclet on docker-for-desktop: %s", f.k8s.Yaml)
    82  	}
    83  }
    84  
    85  func TestDockerForMacDeploy(t *testing.T) {
    86  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
    87  	defer f.TearDown()
    88  
    89  	manifest := NewSanchoFastBuildManifest(f)
    90  	targets := buildTargets(manifest)
    91  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  
    96  	if f.docker.BuildCount != 1 {
    97  		t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount)
    98  	}
    99  
   100  	if f.docker.PushCount != 0 {
   101  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   102  	}
   103  
   104  	expectedYaml := "image: gcr.io/some-project-162817/sancho:tilt-11cd0b38bc3ceb95"
   105  	if !strings.Contains(f.k8s.Yaml, expectedYaml) {
   106  		t.Errorf("Expected yaml to contain %q. Actual:\n%s", expectedYaml, f.k8s.Yaml)
   107  	}
   108  
   109  	if strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   110  		t.Errorf("Should not deploy the synclet on docker-for-desktop: %s", f.k8s.Yaml)
   111  	}
   112  }
   113  
   114  func TestNamespaceGKE(t *testing.T) {
   115  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   116  	defer f.TearDown()
   117  
   118  	assert.Equal(t, "", string(f.sCli.Namespace))
   119  	assert.Equal(t, "", string(f.k8s.LastPodQueryNamespace))
   120  
   121  	manifest := NewSanchoFastBuildManifest(f)
   122  	targets := buildTargets(manifest)
   123  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   124  	if err != nil {
   125  		t.Fatal(err)
   126  	}
   127  
   128  	cInfo := testContainerInfo
   129  	cInfo.Namespace = "sancho-ns"
   130  
   131  	changed := f.WriteFile("a.txt", "a")
   132  	bs := resultToStateSet(result, []string{changed}, cInfo)
   133  	result, err = f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), bs)
   134  	if err != nil {
   135  		t.Fatal(err)
   136  	}
   137  
   138  	assert.Equal(t, "sancho-ns", string(f.sCli.Namespace))
   139  }
   140  
   141  func TestYamlManifestDeploy(t *testing.T) {
   142  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   143  	defer f.TearDown()
   144  
   145  	manifest := manifestbuilder.New(f, "some_yaml").
   146  		WithK8sYAML(testyaml.TracerYAML).Build()
   147  	targets := buildTargets(manifest)
   148  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   149  	if err != nil {
   150  		t.Fatal(err)
   151  	}
   152  
   153  	assert.Equal(t, 0, f.sCli.UpdateContainerCount)
   154  	assert.Equal(t, 0, f.docker.BuildCount)
   155  	assert.Equal(t, 0, f.docker.PushCount)
   156  	f.assertK8sUpsertCalled(true)
   157  }
   158  
   159  func TestContainerBuildLocal(t *testing.T) {
   160  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   161  	defer f.TearDown()
   162  
   163  	changed := f.WriteFile("a.txt", "a")
   164  	manifest := NewSanchoFastBuildManifest(f)
   165  	targets := buildTargets(manifest)
   166  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   167  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   168  	if err != nil {
   169  		t.Fatal(err)
   170  	}
   171  
   172  	if f.docker.BuildCount != 0 {
   173  		t.Errorf("Expected no docker build, actual: %d", f.docker.BuildCount)
   174  	}
   175  	if f.docker.PushCount != 0 {
   176  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   177  	}
   178  	if f.docker.CopyCount != 1 {
   179  		t.Errorf("Expected 1 copy to docker container call, actual: %d", f.docker.CopyCount)
   180  	}
   181  	if len(f.docker.ExecCalls) != 1 {
   182  		t.Errorf("Expected 1 exec in container call, actual: %d", len(f.docker.ExecCalls))
   183  	}
   184  	f.assertContainerRestarts(1)
   185  
   186  	id := manifest.ImageTargetAt(0).ID()
   187  	_, hasResult := result[id]
   188  	assert.True(t, hasResult)
   189  	assert.Equal(t, k8s.MagicTestContainerID, result.OneAndOnlyLiveUpdatedContainerID().String())
   190  }
   191  
   192  func TestContainerBuildSynclet(t *testing.T) {
   193  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   194  	defer f.TearDown()
   195  
   196  	changed := f.WriteFile("a.txt", "a")
   197  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   198  	manifest := NewSanchoFastBuildManifest(f)
   199  	targets := buildTargets(manifest)
   200  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   201  	if err != nil {
   202  		t.Fatal(err)
   203  	}
   204  
   205  	assert.Equal(t, 0, f.docker.BuildCount,
   206  		"Expected no docker build, actual: %d", f.docker.BuildCount)
   207  	assert.Equal(t, 0, f.docker.PushCount,
   208  		"Expected no push to docker, actual: %d", f.docker.PushCount)
   209  	assert.Equal(t, 1, f.sCli.UpdateContainerCount,
   210  		"Expected 1 synclet UpdateContainer call, actual: %d", f.sCli.UpdateContainerCount)
   211  	assert.Equal(t, 1, f.docker.CopyCount,
   212  		"Expected 1 docker CopyToContainer (via synclet), actual: %d", f.docker.CopyCount)
   213  	assert.Len(t, f.docker.ExecCalls, 1,
   214  		"Expected 1 docker Exec call (via synclet), actual: %d", len(f.docker.ExecCalls))
   215  	f.assertContainerRestarts(1)
   216  	assert.Equal(t, k8s.MagicTestContainerID, result.OneAndOnlyLiveUpdatedContainerID().String())
   217  }
   218  
   219  func TestContainerBuildLocalTriggeredRuns(t *testing.T) {
   220  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   221  	defer f.TearDown()
   222  
   223  	changed := f.WriteFile("a.txt", "a")
   224  	manifest := NewSanchoFastBuildManifest(f)
   225  	iTarg := manifest.ImageTargetAt(0)
   226  	fb := iTarg.TopFastBuildInfo()
   227  	runs := []model.Run{
   228  		model.Run{Cmd: model.ToShellCmd("echo hello")},
   229  		model.Run{Cmd: model.ToShellCmd("echo a"), Triggers: f.NewPathSet("a.txt")}, // matches changed file
   230  		model.Run{Cmd: model.ToShellCmd("echo b"), Triggers: f.NewPathSet("b.txt")}, // does NOT match changed file
   231  	}
   232  	fb.Runs = runs
   233  	iTarg = iTarg.WithBuildDetails(fb)
   234  	manifest = manifest.WithImageTarget(iTarg)
   235  
   236  	targets := buildTargets(manifest)
   237  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   238  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  
   243  	if f.docker.BuildCount != 0 {
   244  		t.Errorf("Expected no docker build, actual: %d", f.docker.BuildCount)
   245  	}
   246  	if f.docker.PushCount != 0 {
   247  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   248  	}
   249  	if f.docker.CopyCount != 1 {
   250  		t.Errorf("Expected 1 copy to docker container call, actual: %d", f.docker.CopyCount)
   251  	}
   252  	if len(f.docker.ExecCalls) != 2 {
   253  		t.Errorf("Expected 2 exec in container calls, actual: %d", len(f.docker.ExecCalls))
   254  	}
   255  	f.assertContainerRestarts(1)
   256  
   257  	id := manifest.ImageTargetAt(0).ID()
   258  	_, hasResult := result[id]
   259  	assert.True(t, hasResult)
   260  	assert.Equal(t, k8s.MagicTestContainerID, result.OneAndOnlyLiveUpdatedContainerID().String())
   261  }
   262  
   263  func TestContainerBuildSyncletTriggeredRuns(t *testing.T) {
   264  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   265  	defer f.TearDown()
   266  
   267  	changed := f.WriteFile("a.txt", "a")
   268  	manifest := NewSanchoFastBuildManifest(f)
   269  	iTarg := manifest.ImageTargetAt(0)
   270  	fb := iTarg.TopFastBuildInfo()
   271  	runs := []model.Run{
   272  		model.Run{Cmd: model.ToShellCmd("echo hello")},
   273  		model.Run{Cmd: model.ToShellCmd("echo a"), Triggers: f.NewPathSet("a.txt")}, // matches changed file
   274  		model.Run{Cmd: model.ToShellCmd("echo b"), Triggers: f.NewPathSet("b.txt")}, // does NOT match changed file
   275  	}
   276  	fb.Runs = runs
   277  	iTarg = iTarg.WithBuildDetails(fb)
   278  	manifest = manifest.WithImageTarget(iTarg)
   279  
   280  	targets := buildTargets(manifest)
   281  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   282  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   283  	if err != nil {
   284  		t.Fatal(err)
   285  	}
   286  
   287  	assert.Equal(t, 0, f.docker.BuildCount, "Expected no docker build, actual: %d", f.docker.BuildCount)
   288  	assert.Equal(t, 0, f.docker.PushCount, "Expected no push to docker, actual: %d", f.docker.PushCount)
   289  	assert.Equal(t, 1, f.sCli.UpdateContainerCount,
   290  		"Expected 1 synclet UpdateContainer call, actual: %d", f.sCli.UpdateContainerCount)
   291  	assert.Equal(t, 1, f.docker.CopyCount,
   292  		"Expected 1 docker CopyToContainer (via synclet), actual: %d", f.docker.CopyCount)
   293  	assert.Len(t, f.docker.ExecCalls, 2,
   294  		"Expected 1 docker Exec call (via synclet), actual: %d", len(f.docker.ExecCalls))
   295  	f.assertContainerRestarts(1)
   296  	assert.Equal(t, k8s.MagicTestContainerID, result.OneAndOnlyLiveUpdatedContainerID().String())
   297  }
   298  
   299  func TestContainerBuildSyncletHotReload(t *testing.T) {
   300  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   301  	defer f.TearDown()
   302  
   303  	changed := f.WriteFile("a.txt", "a")
   304  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   305  	manifest := NewSanchoFastBuildManifest(f)
   306  	iTarget := manifest.ImageTargetAt(0)
   307  	fbInfo := iTarget.TopFastBuildInfo()
   308  	fbInfo.HotReload = true
   309  	manifest = manifest.WithImageTarget(iTarget.WithBuildDetails(fbInfo))
   310  	targets := buildTargets(manifest)
   311  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   312  	if err != nil {
   313  		t.Fatal(err)
   314  	}
   315  	assert.Equal(t, 1, f.sCli.UpdateContainerCount,
   316  		"Expected 1 synclet UpdateContainer call, actual: %d", f.sCli.UpdateContainerCount)
   317  	f.assertContainerRestarts(0)
   318  }
   319  
   320  func TestDockerBuildWithNestedFastBuildDeploysSynclet(t *testing.T) {
   321  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   322  	defer f.TearDown()
   323  
   324  	manifest := NewSanchoDockerBuildManifestWithNestedFastBuild(f)
   325  	targets := buildTargets(manifest)
   326  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   327  	if err != nil {
   328  		t.Fatal(err)
   329  	}
   330  
   331  	if f.docker.BuildCount != 1 {
   332  		t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount)
   333  	}
   334  
   335  	if f.docker.PushCount != 1 {
   336  		t.Errorf("Expected 1 docker push, actual: %d", f.docker.PushCount)
   337  	}
   338  
   339  	expectedYaml := "image: gcr.io/some-project-162817/sancho:tilt-11cd0b38bc3ceb95"
   340  	if !strings.Contains(f.k8s.Yaml, expectedYaml) {
   341  		t.Errorf("Expected yaml to contain %q. Actual:\n%s", expectedYaml, f.k8s.Yaml)
   342  	}
   343  
   344  	if !strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   345  		t.Errorf("Should deploy the synclet for a docker build with a nested fast build: %s", f.k8s.Yaml)
   346  	}
   347  }
   348  
   349  func TestDockerBuildWithNestedFastBuildContainerUpdate(t *testing.T) {
   350  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   351  	defer f.TearDown()
   352  
   353  	changed := f.WriteFile("a.txt", "a")
   354  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   355  	manifest := NewSanchoDockerBuildManifestWithNestedFastBuild(f)
   356  	targets := buildTargets(manifest)
   357  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   358  	if err != nil {
   359  		t.Fatal(err)
   360  	}
   361  
   362  	if f.docker.BuildCount != 0 {
   363  		t.Errorf("Expected no docker build, actual: %d", f.docker.BuildCount)
   364  	}
   365  	if f.docker.PushCount != 0 {
   366  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   367  	}
   368  	if f.docker.CopyCount != 1 {
   369  		t.Errorf("Expected 1 copy to docker container call, actual: %d", f.docker.CopyCount)
   370  	}
   371  	if len(f.docker.ExecCalls) != 1 {
   372  		t.Errorf("Expected 1 exec in container call, actual: %d", len(f.docker.ExecCalls))
   373  	}
   374  	f.assertContainerRestarts(1)
   375  
   376  	id := manifest.ImageTargetAt(0).ID()
   377  	_, hasResult := result[id]
   378  	assert.True(t, hasResult)
   379  	assert.Equal(t, k8s.MagicTestContainerID, result.OneAndOnlyLiveUpdatedContainerID().String())
   380  }
   381  
   382  func TestIncrementalBuildFailure(t *testing.T) {
   383  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   384  	defer f.TearDown()
   385  
   386  	changed := f.WriteFile("a.txt", "a")
   387  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   388  	f.docker.SetExecError(docker.ExitError{ExitCode: 1})
   389  
   390  	manifest := NewSanchoFastBuildManifest(f)
   391  	targets := buildTargets(manifest)
   392  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   393  	msg := "Run step \"go install github.com/windmilleng/sancho\" failed with exit code: 1"
   394  	if err == nil || !strings.Contains(err.Error(), msg) {
   395  		t.Fatalf("Expected error message %q, actual: %v", msg, err)
   396  	}
   397  
   398  	if f.docker.BuildCount != 0 {
   399  		t.Errorf("Expected no docker build, actual: %d", f.docker.BuildCount)
   400  	}
   401  
   402  	if f.docker.PushCount != 0 {
   403  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   404  	}
   405  	if f.docker.CopyCount != 1 {
   406  		t.Errorf("Expected 1 copy to docker container call, actual: %d", f.docker.CopyCount)
   407  	}
   408  	if len(f.docker.ExecCalls) != 1 {
   409  		t.Errorf("Expected 1 exec in container call, actual: %d", len(f.docker.ExecCalls))
   410  	}
   411  	f.assertContainerRestarts(0)
   412  }
   413  
   414  func TestIncrementalBuildKilled(t *testing.T) {
   415  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   416  	defer f.TearDown()
   417  
   418  	changed := f.WriteFile("a.txt", "a")
   419  
   420  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   421  	f.docker.SetExecError(docker.ExitError{ExitCode: build.TaskKillExitCode})
   422  
   423  	manifest := NewSanchoFastBuildManifest(f)
   424  	targets := buildTargets(manifest)
   425  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   426  	if err != nil {
   427  		t.Fatal(err)
   428  	}
   429  
   430  	assert.Equal(t, 1, f.docker.CopyCount)
   431  	assert.Equal(t, 1, len(f.docker.ExecCalls))
   432  
   433  	// Falls back to a build when the exec fails
   434  	assert.Equal(t, 1, f.docker.BuildCount)
   435  }
   436  
   437  func TestFallBackToImageDeploy(t *testing.T) {
   438  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   439  	defer f.TearDown()
   440  
   441  	f.docker.SetExecError(errors.New("some random error"))
   442  
   443  	changed := f.WriteFile("a.txt", "a")
   444  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   445  
   446  	manifest := NewSanchoFastBuildManifest(f)
   447  	targets := buildTargets(manifest)
   448  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   449  	if err != nil {
   450  		t.Fatal(err)
   451  	}
   452  
   453  	f.assertContainerRestarts(0)
   454  	if f.docker.BuildCount != 1 {
   455  		t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount)
   456  	}
   457  }
   458  
   459  func TestNoFallbackForDontFallBackError(t *testing.T) {
   460  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   461  	defer f.TearDown()
   462  	f.docker.SetExecError(DontFallBackErrorf("i'm melllting"))
   463  
   464  	changed := f.WriteFile("a.txt", "a")
   465  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   466  
   467  	manifest := NewSanchoFastBuildManifest(f)
   468  	targets := buildTargets(manifest)
   469  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   470  	if err == nil {
   471  		t.Errorf("Expected this error to fail fallback tester and propogate back up")
   472  	}
   473  
   474  	if f.docker.BuildCount != 0 {
   475  		t.Errorf("Expected no docker build, actual: %d", f.docker.BuildCount)
   476  	}
   477  
   478  	if f.docker.PushCount != 0 {
   479  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   480  	}
   481  }
   482  
   483  func TestIncrementalBuildTwice(t *testing.T) {
   484  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   485  	defer f.TearDown()
   486  
   487  	manifest := NewSanchoFastBuildManifest(f)
   488  	targets := buildTargets(manifest)
   489  	aPath := f.WriteFile("a.txt", "a")
   490  	bPath := f.WriteFile("b.txt", "b")
   491  
   492  	firstState := resultToStateSet(alreadyBuiltSet, []string{aPath}, testContainerInfo)
   493  	firstResult, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, firstState)
   494  	if err != nil {
   495  		t.Fatal(err)
   496  	}
   497  
   498  	secondState := resultToStateSet(firstResult, []string{bPath}, testContainerInfo)
   499  	_, err = f.bd.BuildAndDeploy(f.ctx, f.st, targets, secondState)
   500  	if err != nil {
   501  		t.Fatal(err)
   502  	}
   503  
   504  	if f.docker.BuildCount != 0 {
   505  		t.Errorf("Expected no docker build, actual: %d", f.docker.BuildCount)
   506  	}
   507  	if f.docker.PushCount != 0 {
   508  		t.Errorf("Expected no push to docker, actual: %d", f.docker.PushCount)
   509  	}
   510  	if f.docker.CopyCount != 2 {
   511  		t.Errorf("Expected 2 copy to docker container call, actual: %d", f.docker.CopyCount)
   512  	}
   513  	if len(f.docker.ExecCalls) != 2 {
   514  		t.Errorf("Expected 2 exec in container call, actual: %d", len(f.docker.ExecCalls))
   515  	}
   516  	f.assertContainerRestarts(2)
   517  }
   518  
   519  // Kill the pod after the first container update,
   520  // and make sure the next image build gets the right file updates.
   521  func TestIncrementalBuildTwiceDeadPod(t *testing.T) {
   522  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   523  	defer f.TearDown()
   524  
   525  	manifest := NewSanchoFastBuildManifest(f)
   526  	targets := buildTargets(manifest)
   527  	aPath := f.WriteFile("a.txt", "a")
   528  	bPath := f.WriteFile("b.txt", "b")
   529  
   530  	firstState := resultToStateSet(alreadyBuiltSet, []string{aPath}, testContainerInfo)
   531  	firstResult, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, firstState)
   532  	if err != nil {
   533  		t.Fatal(err)
   534  	}
   535  
   536  	// Kill the pod
   537  	f.docker.SetExecError(fmt.Errorf("Dead pod"))
   538  
   539  	secondState := resultToStateSet(firstResult, []string{bPath}, testContainerInfo)
   540  	_, err = f.bd.BuildAndDeploy(f.ctx, f.st, targets, secondState)
   541  	if err != nil {
   542  		t.Fatal(err)
   543  	}
   544  
   545  	if f.docker.BuildCount != 1 {
   546  		t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount)
   547  	}
   548  	if f.docker.PushCount != 0 {
   549  		t.Errorf("Expected 0 pushes to docker, actual: %d", f.docker.PushCount)
   550  	}
   551  	if f.docker.CopyCount != 2 {
   552  		t.Errorf("Expected 2 copy to docker container call, actual: %d", f.docker.CopyCount)
   553  	}
   554  	if len(f.docker.ExecCalls) != 2 {
   555  		t.Errorf("Expected 2 exec in container call, actual: %d", len(f.docker.ExecCalls))
   556  	}
   557  	f.assertContainerRestarts(1)
   558  }
   559  
   560  func TestIgnoredFiles(t *testing.T) {
   561  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   562  	defer f.TearDown()
   563  
   564  	manifest := NewSanchoFastBuildManifest(f)
   565  
   566  	tiltfile := filepath.Join(f.Path(), "Tiltfile")
   567  	manifest = manifest.WithImageTarget(manifest.ImageTargetAt(0).WithRepos([]model.LocalGitRepo{
   568  		model.LocalGitRepo{
   569  			LocalPath: f.Path(),
   570  		},
   571  	}).WithTiltFilename(tiltfile))
   572  
   573  	f.WriteFile("Tiltfile", "# hello world")
   574  	f.WriteFile("a.txt", "a")
   575  	f.WriteFile(".git/index", "garbage")
   576  
   577  	targets := buildTargets(manifest)
   578  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   579  	if err != nil {
   580  		t.Fatal(err)
   581  	}
   582  
   583  	tr := tar.NewReader(f.docker.BuildOptions.Context)
   584  	testutils.AssertFilesInTar(t, tr, []expectedFile{
   585  		expectedFile{
   586  			Path:     "go/src/github.com/windmilleng/sancho/a.txt",
   587  			Contents: "a",
   588  		},
   589  		expectedFile{
   590  			Path:    "go/src/github.com/windmilleng/sancho/.git/index",
   591  			Missing: true,
   592  		},
   593  		expectedFile{
   594  			Path:    "go/src/github.com/windmilleng/sancho/Tiltfile",
   595  			Missing: true,
   596  		},
   597  	})
   598  }
   599  
   600  func TestCustomBuildWithFastBuild(t *testing.T) {
   601  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   602  	defer f.TearDown()
   603  	sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab")
   604  	f.docker.Images["gcr.io/some-project-162817/sancho:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)}
   605  
   606  	manifest := NewSanchoCustomBuildManifestWithFastBuild(f)
   607  	targets := buildTargets(manifest)
   608  
   609  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   610  	if err != nil {
   611  		t.Fatal(err)
   612  	}
   613  
   614  	if f.docker.BuildCount != 0 {
   615  		t.Errorf("Expected 0 docker build, actual: %d", f.docker.BuildCount)
   616  	}
   617  
   618  	if f.docker.PushCount != 1 {
   619  		t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount)
   620  	}
   621  
   622  	if !strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   623  		t.Errorf("Should deploy the synclet for a custom build with a fast build: %s", f.k8s.Yaml)
   624  	}
   625  }
   626  
   627  func TestCustomBuildWithoutFastBuild(t *testing.T) {
   628  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   629  	defer f.TearDown()
   630  	sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab")
   631  	f.docker.Images["gcr.io/some-project-162817/sancho:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)}
   632  
   633  	manifest := NewSanchoCustomBuildManifest(f)
   634  	targets := buildTargets(manifest)
   635  
   636  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   637  	if err != nil {
   638  		t.Fatal(err)
   639  	}
   640  
   641  	if f.docker.BuildCount != 0 {
   642  		t.Errorf("Expected 0 docker build, actual: %d", f.docker.BuildCount)
   643  	}
   644  
   645  	if f.docker.PushCount != 1 {
   646  		t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount)
   647  	}
   648  
   649  	if strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   650  		t.Errorf("Should not deploy the synclet for a custom build: %s", f.k8s.Yaml)
   651  	}
   652  }
   653  
   654  func TestCustomBuildDeterministicTag(t *testing.T) {
   655  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   656  	defer f.TearDown()
   657  	refStr := "gcr.io/some-project-162817/sancho:deterministic-tag"
   658  	sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab")
   659  	f.docker.Images[refStr] = types.ImageInspect{ID: string(sha)}
   660  
   661  	manifest := NewSanchoCustomBuildManifestWithTag(f, "deterministic-tag")
   662  	targets := buildTargets(manifest)
   663  
   664  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   665  	if err != nil {
   666  		t.Fatal(err)
   667  	}
   668  
   669  	if f.docker.BuildCount != 0 {
   670  		t.Errorf("Expected 0 docker build, actual: %d", f.docker.BuildCount)
   671  	}
   672  
   673  	if f.docker.PushCount != 1 {
   674  		t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount)
   675  	}
   676  
   677  	if strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   678  		t.Errorf("Should not deploy the synclet for a custom build: %s", f.k8s.Yaml)
   679  	}
   680  }
   681  
   682  func TestContainerBuildMultiStage(t *testing.T) {
   683  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   684  	defer f.TearDown()
   685  
   686  	manifest := NewSanchoLiveUpdateMultiStageManifest(f)
   687  	targets := buildTargets(manifest)
   688  	changed := f.WriteFile("a.txt", "a")
   689  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   690  
   691  	// There are two image targets. The first has a build result,
   692  	// the second does not --> second target needs build
   693  	iTargetID := targets[0].ID()
   694  	firstResult := store.NewImageBuildResult(iTargetID, container.MustParseNamedTagged("sancho-base:tilt-prebuilt"))
   695  	bs[iTargetID] = store.NewBuildState(firstResult, nil)
   696  
   697  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   698  	if err != nil {
   699  		t.Fatal(err)
   700  	}
   701  
   702  	// Docker Build/Push would imply an image build. Make sure they didn't happen,
   703  	// i.e. that we did a LiveUpdate
   704  	assert.Equal(t, 0, f.docker.BuildCount)
   705  	assert.Equal(t, 0, f.docker.PushCount)
   706  
   707  	// Make sure we did a LiveUpdate (copy files to container, exec in container, restart)
   708  	assert.Equal(t, 1, f.docker.CopyCount)
   709  	assert.Equal(t, 1, len(f.docker.ExecCalls))
   710  	f.assertContainerRestarts(1)
   711  
   712  	// The BuildComplete action handler expects to get exactly one result
   713  	_, hasResult0 := result[manifest.ImageTargetAt(0).ID()]
   714  	assert.False(t, hasResult0)
   715  	_, hasResult1 := result[manifest.ImageTargetAt(1).ID()]
   716  	assert.True(t, hasResult1)
   717  	assert.Equal(t, k8s.MagicTestContainerID, result.OneAndOnlyLiveUpdatedContainerID().String())
   718  }
   719  
   720  func TestDockerComposeImageBuild(t *testing.T) {
   721  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   722  	defer f.TearDown()
   723  
   724  	manifest := NewSanchoFastBuildDCManifest(f)
   725  	targets := buildTargets(manifest)
   726  
   727  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, store.BuildStateSet{})
   728  	if err != nil {
   729  		t.Fatal(err)
   730  	}
   731  
   732  	assert.Equal(t, 1, f.docker.BuildCount)
   733  	assert.Equal(t, 0, f.docker.PushCount)
   734  	if strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   735  		t.Errorf("Should not deploy the synclet for a docker-compose build: %s", f.k8s.Yaml)
   736  	}
   737  	assert.Len(t, f.dcCli.UpCalls, 1)
   738  }
   739  
   740  func TestDockerComposeInPlaceUpdate(t *testing.T) {
   741  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   742  	defer f.TearDown()
   743  
   744  	manifest := NewSanchoFastBuildDCManifest(f)
   745  	targets := buildTargets(manifest)
   746  	changed := f.WriteFile("a.txt", "a")
   747  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   748  
   749  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   750  	if err != nil {
   751  		t.Fatal(err)
   752  	}
   753  
   754  	assert.Equal(t, 0, f.docker.BuildCount)
   755  	assert.Equal(t, 0, f.docker.PushCount)
   756  	assert.Equal(t, 1, f.docker.CopyCount)
   757  	assert.Equal(t, 1, len(f.docker.ExecCalls))
   758  	assert.Equal(t, 0, f.sCli.UpdateContainerCount,
   759  		"Expected no synclet UpdateContainer call, actual: %d", f.sCli.UpdateContainerCount)
   760  	if strings.Contains(f.k8s.Yaml, sidecar.DefaultSyncletImageName) {
   761  		t.Errorf("Should not deploy the synclet for a docker-compose build: %s", f.k8s.Yaml)
   762  	}
   763  	f.assertContainerRestarts(1)
   764  }
   765  
   766  func TestReturnLastUnexpectedError(t *testing.T) {
   767  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   768  	defer f.TearDown()
   769  
   770  	// next Docker build will throw an unexpected error -- this is one we want to return,
   771  	// even if subsequent builders throw expected errors.
   772  	f.docker.BuildErrorToThrow = fmt.Errorf("no one expects the unexpected error")
   773  
   774  	manifest := NewSanchoFastBuildDCManifest(f)
   775  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), store.BuildStateSet{})
   776  	if assert.Error(t, err) {
   777  		assert.Contains(t, err.Error(), "no one expects the unexpected error")
   778  	}
   779  }
   780  
   781  func TestLiveUpdateWithRunFailureReturnsContainerIDs(t *testing.T) {
   782  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   783  	defer f.TearDown()
   784  
   785  	// LiveUpdate will failure with a RunStepFailure
   786  	f.docker.SetExecError(userFailureErrDocker)
   787  
   788  	manifest := NewSanchoLiveUpdateManifest(f)
   789  	targets := buildTargets(manifest)
   790  	changed := f.WriteFile("a.txt", "a")
   791  	bs := resultToStateSet(alreadyBuiltSet, []string{changed}, testContainerInfo)
   792  	resultSet, err := f.bd.BuildAndDeploy(f.ctx, f.st, targets, bs)
   793  	require.NotNil(t, err, "expected failed LiveUpdate to return error")
   794  
   795  	iTargID := manifest.ImageTargetAt(0).ID()
   796  	result := resultSet[iTargID]
   797  	res, ok := result.(store.LiveUpdateBuildResult)
   798  	require.True(t, ok, "expected build result for image target %s", iTargID)
   799  	require.Len(t, res.LiveUpdatedContainerIDs, 1)
   800  	require.Equal(t, res.LiveUpdatedContainerIDs[0].String(), k8s.MagicTestContainerID)
   801  
   802  	// LiveUpdate failed due to RunStepError, should NOT fall back to image build
   803  	assert.Equal(t, 0, f.docker.BuildCount, "expect no image build -> no docker build calls")
   804  	f.assertK8sUpsertCalled(false)
   805  
   806  	// Copied files and tried to docker.exec before hitting error
   807  	assert.Equal(t, 1, f.docker.CopyCount)
   808  	assert.Equal(t, 1, len(f.docker.ExecCalls))
   809  }
   810  
   811  func TestLiveUpdateMultipleImagesSamePod(t *testing.T) {
   812  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   813  	defer f.TearDown()
   814  
   815  	manifest, bs := multiImageLiveUpdateManifestAndBuildState(f)
   816  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), bs)
   817  	if err != nil {
   818  		t.Fatal(err)
   819  	}
   820  
   821  	// expect live update and NOT an image build
   822  	assert.Equal(t, 0, f.docker.BuildCount)
   823  	assert.Equal(t, 0, f.docker.PushCount)
   824  	f.assertK8sUpsertCalled(false)
   825  
   826  	// one for each container update
   827  	assert.Equal(t, 2, f.sCli.UpdateContainerCount)
   828  }
   829  
   830  func TestOneLiveUpdateOneDockerBuildDoesImageBuild(t *testing.T) {
   831  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   832  	defer f.TearDown()
   833  
   834  	sanchoTarg := NewSanchoLiveUpdateImageTarget(f)          // first target = LiveUpdate
   835  	sidecarTarg := NewSanchoSidecarDockerBuildImageTarget(f) // second target = DockerBuild
   836  	sanchoRef := container.MustParseNamedTagged(fmt.Sprintf("%s:tilt-123", testyaml.SanchoImage))
   837  	sidecarRef := container.MustParseNamedTagged(fmt.Sprintf("%s:tilt-123", testyaml.SanchoSidecarImage))
   838  	sanchoCInfo := store.ContainerInfo{
   839  		PodID:         testPodID,
   840  		ContainerName: "sancho",
   841  		ContainerID:   "sancho-c",
   842  	}
   843  
   844  	manifest := manifestbuilder.New(f, "sancho").
   845  		WithK8sYAML(testyaml.SanchoSidecarYAML).
   846  		WithImageTargets(sanchoTarg, sidecarTarg).
   847  		Build()
   848  	changed := f.WriteFile("a.txt", "a")
   849  	sanchoState := store.NewBuildState(store.NewImageBuildResult(sanchoTarg.ID(), sanchoRef), []string{changed}).
   850  		WithRunningContainers([]store.ContainerInfo{sanchoCInfo})
   851  	sidecarState := store.NewBuildState(store.NewImageBuildResult(sidecarTarg.ID(), sidecarRef), []string{changed})
   852  
   853  	bs := store.BuildStateSet{sanchoTarg.ID(): sanchoState, sidecarTarg.ID(): sidecarState}
   854  
   855  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), bs)
   856  	if err != nil {
   857  		t.Fatal(err)
   858  	}
   859  
   860  	// expect an image build
   861  	assert.Equal(t, 2, f.docker.BuildCount)
   862  	assert.Equal(t, 2, f.docker.PushCount)
   863  	f.assertK8sUpsertCalled(true)
   864  
   865  	// should NOT have run live update
   866  	assert.Equal(t, 0, f.sCli.UpdateContainerCount)
   867  }
   868  
   869  func TestLiveUpdateMultipleImagesOneRunErrorExecutesRestOfLiveUpdatesAndDoesntImageBuild(t *testing.T) {
   870  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   871  	defer f.TearDown()
   872  
   873  	// First LiveUpdate will simulate a failed Run step
   874  	f.docker.ExecErrorsToThrow = []error{userFailureErrDocker}
   875  
   876  	manifest, bs := multiImageLiveUpdateManifestAndBuildState(f)
   877  	result, err := f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), bs)
   878  	require.NotNil(t, err)
   879  	assert.Contains(t, err.Error(), "Run step \"go install github.com/windmilleng/sancho\" failed with exit code: 123")
   880  
   881  	// one for each container update
   882  	assert.Equal(t, 2, f.docker.CopyCount)
   883  	assert.Equal(t, 2, len(f.docker.ExecCalls))
   884  
   885  	// expect NO image build
   886  	assert.Equal(t, 0, f.docker.BuildCount)
   887  	assert.Equal(t, 0, f.docker.PushCount)
   888  	f.assertK8sUpsertCalled(false)
   889  
   890  	// Make sure we returned the CIDs we LiveUpdated --
   891  	// they contain state now, we'll want to track them
   892  	liveUpdatedCIDs := result.LiveUpdatedContainerIDs()
   893  	expectedCIDs := []container.ID{"sancho-c", "sidecar-c"}
   894  	assert.ElementsMatch(t, expectedCIDs, liveUpdatedCIDs)
   895  }
   896  
   897  func TestLiveUpdateMultipleImagesOneUpdateErrorFallsBackToImageBuild(t *testing.T) {
   898  	f := newBDFixture(t, k8s.EnvDockerDesktop, container.RuntimeDocker)
   899  	defer f.TearDown()
   900  
   901  	// Second LiveUpdate will throw an error
   902  	f.docker.ExecErrorsToThrow = []error{nil, fmt.Errorf("whelp ¯\\_(ツ)_/¯")}
   903  
   904  	manifest, bs := multiImageLiveUpdateManifestAndBuildState(f)
   905  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), bs)
   906  	if err != nil {
   907  		t.Fatal(err)
   908  	}
   909  
   910  	// one for each container update
   911  	assert.Equal(t, 2, f.docker.CopyCount)
   912  	assert.Equal(t, 2, len(f.docker.ExecCalls)) // second one errors
   913  
   914  	// expect image build (2x images) when we fall back from failed LiveUpdate
   915  	assert.Equal(t, 2, f.docker.BuildCount)
   916  	assert.Equal(t, 0, f.docker.PushCount)
   917  	f.assertK8sUpsertCalled(true)
   918  }
   919  
   920  func TestLiveUpdateMultipleImagesOneWithUnsyncedChangeFileFallsBackToImageBuild(t *testing.T) {
   921  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   922  	defer f.TearDown()
   923  
   924  	manifest, bs := multiImageLiveUpdateManifestAndBuildState(f)
   925  	bs[manifest.ImageTargetAt(1).ID()].FilesChangedSet["/not/synced"] = true // changed file not in a sync --> fall back to image build
   926  	_, err := f.bd.BuildAndDeploy(f.ctx, f.st, buildTargets(manifest), bs)
   927  	if err != nil {
   928  		t.Fatal(err)
   929  	}
   930  
   931  	// should NOT have run live update
   932  	assert.Equal(t, 0, f.sCli.UpdateContainerCount)
   933  
   934  	// expect image build (2x images) when we fall back from failed LiveUpdate
   935  	assert.Equal(t, 2, f.docker.BuildCount)
   936  	assert.Equal(t, 2, f.docker.PushCount)
   937  	f.assertK8sUpsertCalled(true)
   938  }
   939  
   940  func TestLocalTargetDeploy(t *testing.T) {
   941  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   942  	defer f.TearDown()
   943  
   944  	lt := model.LocalTarget{Cmd: model.ToShellCmd("echo hello world")}
   945  	res, err := f.bd.BuildAndDeploy(f.ctx, f.st, []model.TargetSpec{lt}, store.BuildStateSet{})
   946  	require.Nil(t, err)
   947  
   948  	assert.Equal(t, 0, f.docker.BuildCount, "should have 0 docker builds")
   949  	assert.Equal(t, 0, f.docker.PushCount, "should have 0 docker pushes")
   950  	assert.Empty(t, f.k8s.Yaml, "should not apply any k8s yaml")
   951  	assert.Len(t, res, 1, "expect exactly one result in result set")
   952  	assert.Contains(t, f.logs.String(), "hello world", "logs should contain cmd output")
   953  }
   954  
   955  func TestLocalTargetFailure(t *testing.T) {
   956  	f := newBDFixture(t, k8s.EnvGKE, container.RuntimeDocker)
   957  	defer f.TearDown()
   958  
   959  	lt := model.LocalTarget{Cmd: model.ToShellCmd("echo oh no; false")}
   960  	res, err := f.bd.BuildAndDeploy(f.ctx, f.st, []model.TargetSpec{lt}, store.BuildStateSet{})
   961  	assert.Empty(t, res, "expect empty result for failed command")
   962  
   963  	require.NotNil(t, err)
   964  	assert.Contains(t, err.Error(), "exit status 1", "error msg should indicate command failure")
   965  	assert.Contains(t, f.logs.String(), "oh no", "logs should contain cmd output")
   966  
   967  	assert.Equal(t, 0, f.docker.BuildCount, "should have 0 docker builds")
   968  	assert.Equal(t, 0, f.docker.PushCount, "should have 0 docker pushes")
   969  	assert.Empty(t, f.k8s.Yaml, "should not apply any k8s yaml")
   970  }
   971  
   972  func multiImageLiveUpdateManifestAndBuildState(f *bdFixture) (model.Manifest, store.BuildStateSet) {
   973  	sanchoTarg := NewSanchoLiveUpdateImageTarget(f)
   974  	sidecarTarg := NewSanchoSidecarLiveUpdateImageTarget(f)
   975  	sanchoRef := container.MustParseNamedTagged(fmt.Sprintf("%s:tilt-123", testyaml.SanchoImage))
   976  	sidecarRef := container.MustParseNamedTagged(fmt.Sprintf("%s:tilt-123", testyaml.SanchoSidecarImage))
   977  	sanchoCInfo := store.ContainerInfo{
   978  		PodID:         testPodID,
   979  		ContainerName: "sancho",
   980  		ContainerID:   "sancho-c",
   981  	}
   982  	sidecarCInfo := store.ContainerInfo{
   983  		PodID:         testPodID,
   984  		ContainerName: "sancho-sidecar",
   985  		ContainerID:   "sidecar-c",
   986  	}
   987  
   988  	manifest := manifestbuilder.New(f, "sancho").
   989  		WithK8sYAML(testyaml.SanchoSidecarYAML).
   990  		WithImageTargets(sanchoTarg, sidecarTarg).
   991  		Build()
   992  
   993  	changed := f.WriteFile("a.txt", "a")
   994  	sanchoState := store.NewBuildState(store.NewImageBuildResult(sanchoTarg.ID(), sanchoRef), []string{changed}).
   995  		WithRunningContainers([]store.ContainerInfo{sanchoCInfo})
   996  	sidecarState := store.NewBuildState(store.NewImageBuildResult(sidecarTarg.ID(), sidecarRef), []string{changed}).
   997  		WithRunningContainers([]store.ContainerInfo{sidecarCInfo})
   998  
   999  	bs := store.BuildStateSet{sanchoTarg.ID(): sanchoState, sidecarTarg.ID(): sidecarState}
  1000  
  1001  	return manifest, bs
  1002  }
  1003  
  1004  // The API boundaries between BuildAndDeployer and the ImageBuilder aren't obvious and
  1005  // are likely to change in the future. So we test them together, using
  1006  // a fake Client and K8sClient
  1007  type bdFixture struct {
  1008  	*tempdir.TempDirFixture
  1009  	ctx    context.Context
  1010  	cancel func()
  1011  	docker *docker.FakeClient
  1012  	k8s    *k8s.FakeK8sClient
  1013  	sCli   *synclet.TestSyncletClient
  1014  	bd     BuildAndDeployer
  1015  	st     *store.Store
  1016  	dcCli  *dockercompose.FakeDCClient
  1017  	logs   *bytes.Buffer
  1018  }
  1019  
  1020  func newBDFixture(t *testing.T, env k8s.Env, runtime container.Runtime) *bdFixture {
  1021  	logs := new(bytes.Buffer)
  1022  	ctx, _, ta := testutils.ForkedCtxAndAnalyticsForTest(logs)
  1023  	ctx, cancel := context.WithCancel(ctx)
  1024  	f := tempdir.NewTempDirFixture(t)
  1025  	dir := dirs.NewWindmillDirAt(f.Path())
  1026  	docker := docker.NewFakeClient()
  1027  	docker.ContainerListOutput = map[string][]types.Container{
  1028  		"pod": []types.Container{
  1029  			types.Container{
  1030  				ID: k8s.MagicTestContainerID,
  1031  			},
  1032  		},
  1033  	}
  1034  	k8s := k8s.NewFakeK8sClient()
  1035  	k8s.Runtime = runtime
  1036  	sCli := synclet.NewTestSyncletClient(docker)
  1037  	mode := UpdateModeFlag(UpdateModeAuto)
  1038  	dcc := dockercompose.NewFakeDockerComposeClient(t, ctx)
  1039  	kp := &fakeKINDPusher{}
  1040  	bd, err := provideBuildAndDeployer(ctx, docker, k8s, dir, env, mode, sCli, dcc, fakeClock{now: time.Unix(1551202573, 0)}, kp, ta)
  1041  	if err != nil {
  1042  		t.Fatal(err)
  1043  	}
  1044  
  1045  	st, _ := store.NewStoreForTesting()
  1046  
  1047  	return &bdFixture{
  1048  		TempDirFixture: f,
  1049  		ctx:            ctx,
  1050  		cancel:         cancel,
  1051  		docker:         docker,
  1052  		k8s:            k8s,
  1053  		sCli:           sCli,
  1054  		bd:             bd,
  1055  		st:             st,
  1056  		dcCli:          dcc,
  1057  		logs:           logs,
  1058  	}
  1059  }
  1060  
  1061  func (f *bdFixture) TearDown() {
  1062  	f.k8s.TearDown()
  1063  	f.cancel()
  1064  	f.TempDirFixture.TearDown()
  1065  }
  1066  
  1067  func (f *bdFixture) NewPathSet(paths ...string) model.PathSet {
  1068  	return model.NewPathSet(paths, f.Path())
  1069  }
  1070  
  1071  func (f *bdFixture) assertContainerRestarts(count int) {
  1072  	// Ensure that MagicTestContainerID was the only container id that saw
  1073  	// restarts, and that it saw the right number of restarts.
  1074  	expected := map[string]int{}
  1075  	if count != 0 {
  1076  		expected[string(k8s.MagicTestContainerID)] = count
  1077  	}
  1078  	assert.Equal(f.T(), expected, f.docker.RestartsByContainer,
  1079  		"checking for expected # of container restarts")
  1080  }
  1081  
  1082  // Total number of restarts, regardless of which container.
  1083  func (f *bdFixture) assertTotalContainerRestarts(count int) {
  1084  	assert.Len(f.T(), f.docker.RestartsByContainer, count,
  1085  		"checking for expected # of container restarts")
  1086  }
  1087  
  1088  func (f *bdFixture) assertK8sUpsertCalled(called bool) {
  1089  	assert.Equal(f.T(), called, f.k8s.Yaml != "",
  1090  		"checking that k8s.Upsert was called")
  1091  }
  1092  
  1093  func (f *bdFixture) createBuildStateSet(manifest model.Manifest, changedFiles []string) store.BuildStateSet {
  1094  	bs := store.BuildStateSet{}
  1095  
  1096  	// If there are no changed files, the test wants a build state where
  1097  	// nothing has ever been built.
  1098  	//
  1099  	// If there are changed files, the test wants a build state where
  1100  	// everything has been built once. The changed files chould be
  1101  	// attached to the appropriate build state.
  1102  	if len(changedFiles) == 0 {
  1103  		return bs
  1104  	}
  1105  
  1106  	consumedFiles := make(map[string]bool)
  1107  	for _, iTarget := range manifest.ImageTargets {
  1108  		filesChangingImage := []string{}
  1109  		for _, file := range changedFiles {
  1110  			fullPath := f.JoinPath(file)
  1111  			inDeps := false
  1112  			for _, dep := range iTarget.Dependencies() {
  1113  				if ospath.IsChild(dep, fullPath) {
  1114  					inDeps = true
  1115  					break
  1116  				}
  1117  			}
  1118  
  1119  			if inDeps {
  1120  				filesChangingImage = append(filesChangingImage, f.WriteFile(file, "blah"))
  1121  				consumedFiles[file] = true
  1122  			}
  1123  		}
  1124  
  1125  		state := store.NewBuildState(alreadyBuilt, filesChangingImage)
  1126  		if manifest.IsImageDeployed(iTarget) {
  1127  			state = state.WithRunningContainers([]store.ContainerInfo{testContainerInfo})
  1128  		}
  1129  		bs[iTarget.ID()] = state
  1130  	}
  1131  
  1132  	if len(consumedFiles) != len(changedFiles) {
  1133  		f.T().Fatalf("testCase has files that weren't consumed by an image. "+
  1134  			"Was that intentional?\nChangedFiles: %v\nConsumedFiles: %v\n",
  1135  			changedFiles, consumedFiles)
  1136  	}
  1137  	return bs
  1138  }
  1139  
  1140  func resultToStateSet(resultSet store.BuildResultSet, files []string, cInfo store.ContainerInfo) store.BuildStateSet {
  1141  	stateSet := store.BuildStateSet{}
  1142  	for id, result := range resultSet {
  1143  		state := store.NewBuildState(result, files).WithRunningContainers([]store.ContainerInfo{cInfo})
  1144  		stateSet[id] = state
  1145  	}
  1146  	return stateSet
  1147  }
  1148  
  1149  type fakeClock struct {
  1150  	now time.Time
  1151  }
  1152  
  1153  func (c fakeClock) Now() time.Time { return c.now }