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  }