github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/containerupdate/exec_updater_test.go (about) 1 package containerupdate 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "strings" 10 "testing" 11 12 "github.com/stretchr/testify/assert" 13 "k8s.io/client-go/util/exec" 14 15 "github.com/tilt-dev/tilt/internal/build" 16 "github.com/tilt-dev/tilt/internal/sliceutils" 17 18 "github.com/tilt-dev/tilt/internal/k8s" 19 "github.com/tilt-dev/tilt/internal/testutils" 20 "github.com/tilt-dev/tilt/pkg/model" 21 ) 22 23 var toDelete = []string{"/foo/delete_me", "/bar/me_too"} 24 var ( 25 cmdA = model.Cmd{Argv: []string{"a"}} 26 cmdB = model.Cmd{Argv: []string{"b", "bar", "baz"}} 27 ) 28 var cmds = []model.Cmd{cmdA, cmdB} 29 30 func TestUpdateContainerDoesntSupportRestart(t *testing.T) { 31 f := newExecFixture(t) 32 33 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("boop"), toDelete, cmds, false) 34 if assert.NotNil(t, err, "expect Exec UpdateContainer to fail if !hotReload") { 35 assert.Contains(t, err.Error(), "ExecUpdater does not support `restart_container()` step") 36 } 37 } 38 39 func TestUpdateContainerDeletesFiles(t *testing.T) { 40 f := newExecFixture(t) 41 42 // No files to delete 43 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("boop"), nil, cmds, true) 44 if err != nil { 45 t.Fatal(err) 46 } 47 48 for _, call := range f.kCli.ExecCalls { 49 if sliceutils.StringSliceStartsWith(call.Cmd, "rm") { 50 t.Fatal("found kubernetes exec `rm` call, expected none b/c no files to delete") 51 } 52 } 53 54 // Two files to delete 55 err = f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("boop"), toDelete, cmds, true) 56 if err != nil { 57 t.Fatal(err) 58 } 59 var rmCmd []string 60 for _, call := range f.kCli.ExecCalls { 61 if sliceutils.StringSliceStartsWith(call.Cmd, "rm") { 62 if len(rmCmd) != 0 { 63 t.Fatalf(`found two rm commands, expected one. 64 cmd 1: %v 65 cmd 2: %v`, rmCmd, call.Cmd) 66 } 67 rmCmd = call.Cmd 68 } 69 } 70 if len(rmCmd) == 0 { 71 t.Fatal("no `rm` cmd found, expected one b/c we specified files to delete") 72 } 73 74 expectedRmCmd := []string{"rm", "-rf", "/foo/delete_me", "/bar/me_too"} 75 assert.Equal(t, expectedRmCmd, rmCmd) 76 } 77 78 func TestUpdateContainerTarsArchive(t *testing.T) { 79 f := newExecFixture(t) 80 81 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("hello world"), nil, nil, true) 82 if err != nil { 83 t.Fatal(err) 84 } 85 86 expectedCmd := []string{"tar", "-C", "/", "-x", "-f", "-"} 87 if assert.Len(t, f.kCli.ExecCalls, 1, "expect exactly 1 k8s exec call") { 88 call := f.kCli.ExecCalls[0] 89 assert.Equal(t, expectedCmd, call.Cmd) 90 assert.Equal(t, []byte("hello world"), call.Stdin) 91 } 92 } 93 94 func TestUpdateContainerRunsCommands(t *testing.T) { 95 f := newExecFixture(t) 96 97 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("hello world"), nil, cmds, true) 98 if err != nil { 99 t.Fatal(err) 100 } 101 102 if assert.Len(t, f.kCli.ExecCalls, 3, "expect exactly 3 k8s exec calls") { 103 // second and third calls should be our cmd runs 104 assert.Equal(t, cmdA.Argv, f.kCli.ExecCalls[1].Cmd) 105 assert.Equal(t, cmdB.Argv, f.kCli.ExecCalls[2].Cmd) 106 } 107 } 108 109 func TestUpdateContainerRunsFailure(t *testing.T) { 110 f := newExecFixture(t) 111 112 // The first exec() call is a copy, so won't trigger a RunStepFailure 113 f.kCli.ExecErrors = []error{ 114 nil, 115 exec.CodeExitError{Err: fmt.Errorf("Compile error"), Code: 1234}, 116 } 117 118 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("hello world"), nil, cmds, true) 119 if assert.True(t, build.IsRunStepFailure(err)) { 120 assert.Equal(t, `executing on container test_conta: command "a" failed with exit code: 1234`, err.Error()) 121 } 122 assert.Equal(t, 2, len(f.kCli.ExecCalls)) 123 } 124 125 func TestUpdateContainerMissingTarFailure(t *testing.T) { 126 f := newExecFixture(t) 127 128 f.kCli.ExecErrors = []error{ 129 errors.New("opaque Kubernetes error that includes the phrase 'executable file not found' in it"), 130 } 131 132 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("hello world"), nil, cmds, true) 133 if assert.Error(t, err) { 134 assert.Contains(t, err.Error(), "Please check that the container image includes `tar` in $PATH.") 135 } 136 assert.Equal(t, 1, len(f.kCli.ExecCalls)) 137 } 138 139 func TestUpdateContainerPermissionDenied(t *testing.T) { 140 f := newExecFixture(t) 141 142 f.kCli.ExecOutputs = []io.Reader{strings.NewReader("tar: app/index.js: Cannot open: File exists\n")} 143 f.kCli.ExecErrors = []error{exec.CodeExitError{Err: fmt.Errorf("command terminated with exit code 2"), Code: 2}} 144 145 err := f.ecu.UpdateContainer(f.ctx, TestContainerInfo, newReader("hello world"), nil, cmds, true) 146 if assert.Error(t, err) { 147 assert.Contains(t, err.Error(), "container filesystem denied access") 148 } 149 assert.Equal(t, 1, len(f.kCli.ExecCalls)) 150 } 151 152 type execUpdaterFixture struct { 153 t testing.TB 154 ctx context.Context 155 kCli *k8s.FakeK8sClient 156 ecu *ExecUpdater 157 } 158 159 func newExecFixture(t testing.TB) *execUpdaterFixture { 160 fakeCli := k8s.NewFakeK8sClient(t) 161 cu := &ExecUpdater{ 162 kCli: fakeCli, 163 } 164 ctx, _, _ := testutils.CtxAndAnalyticsForTest() 165 166 return &execUpdaterFixture{ 167 t: t, 168 ctx: ctx, 169 kCli: fakeCli, 170 ecu: cu, 171 } 172 } 173 174 func newReader(contents string) io.Reader { 175 return bytes.NewBuffer([]byte(contents)) 176 }