github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/containerupdate/exec_updater.go (about) 1 package containerupdate 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "strings" 9 10 "github.com/tilt-dev/tilt/internal/k8s" 11 "github.com/tilt-dev/tilt/internal/store/liveupdates" 12 "github.com/tilt-dev/tilt/pkg/logger" 13 "github.com/tilt-dev/tilt/pkg/model" 14 ) 15 16 type ExecUpdater struct { 17 kCli k8s.Client 18 } 19 20 var _ ContainerUpdater = &ExecUpdater{} 21 22 func NewExecUpdater(kCli k8s.Client) *ExecUpdater { 23 return &ExecUpdater{kCli: kCli} 24 } 25 26 func (cu *ExecUpdater) UpdateContainer(ctx context.Context, cInfo liveupdates.Container, 27 archiveToCopy io.Reader, filesToDelete []string, cmds []model.Cmd, hotReload bool) error { 28 if !hotReload { 29 return fmt.Errorf("ExecUpdater does not support `restart_container()` step. If you ran Tilt " + 30 "with `--updateMode=exec`, omit this flag. If you are using a non-Docker container runtime, " + 31 "see https://github.com/tilt-dev/tilt-extensions/tree/master/restart_process for a workaround") 32 } 33 34 l := logger.Get(ctx) 35 w := logger.Get(ctx).Writer(logger.InfoLvl) 36 37 // delete files (if any) 38 if len(filesToDelete) > 0 { 39 buf := bytes.NewBuffer(nil) 40 rmWriter := io.MultiWriter(w, buf) 41 cmd := model.Cmd{Argv: append([]string{"rm", "-rf"}, filesToDelete...)} 42 err := cu.kCli.Exec(ctx, 43 cInfo.PodID, cInfo.ContainerName, cInfo.Namespace, 44 cmd.Argv, nil, rmWriter, rmWriter) 45 if err != nil { 46 return wrapK8sTarErr(buf, err, cmd, "removing old files") 47 } 48 } 49 50 // copy files to container 51 buf := bytes.NewBuffer(nil) 52 tarWriter := io.MultiWriter(w, buf) 53 tarCmd := tarCmd() 54 err := cu.kCli.Exec(ctx, cInfo.PodID, cInfo.ContainerName, cInfo.Namespace, 55 tarCmd.Argv, archiveToCopy, tarWriter, tarWriter) 56 if err != nil { 57 return wrapK8sTarErr(buf, err, tarCmd, "copying changed files") 58 } 59 60 // run commands 61 for i, c := range cmds { 62 if !c.EchoOff { 63 l.Infof("[CMD %d/%d] %s", i+1, len(cmds), strings.Join(c.Argv, " ")) 64 } 65 err := cu.kCli.Exec(ctx, cInfo.PodID, cInfo.ContainerName, cInfo.Namespace, 66 c.Argv, nil, w, w) 67 if err != nil { 68 return fmt.Errorf( 69 "executing on container %s: %w", 70 cInfo.ContainerID.ShortStr(), 71 wrapRunStepError(wrapK8sGenericExecErr(err, c)), 72 ) 73 } 74 75 } 76 77 return nil 78 } 79 80 // wrapK8sTarErr provides user-friendly diagnostics for common failures when 81 // running `tar` as part of a Live Update. 82 func wrapK8sTarErr(out *bytes.Buffer, err error, cmd model.Cmd, action string) error { 83 if exitCode, ok := ExtractExitCode(err); ok { 84 return wrapTarExecErr(err, cmd, exitCode) 85 } 86 87 // if we didn't get an explicit exit code from the k8s error, look at the 88 // error text + stdout/stderr to see if it's a failure case we understand 89 msg := strings.ToLower(fmt.Sprintf("%s\n%s", out.String(), err.Error())) 90 if strings.Contains(msg, "permission denied") || strings.Contains(msg, "cannot open") { 91 return permissionDeniedErr(err) 92 } 93 if strings.Contains(msg, "executable file not found") { 94 return cannotExecErr(err) 95 } 96 return fmt.Errorf("%s: %w", action, err) 97 } 98 99 // wrapK8sGenericExecErr massages exec errors to be more user-friendly. 100 func wrapK8sGenericExecErr(err error, cmd model.Cmd) error { 101 if exitCode, ok := ExtractExitCode(err); ok { 102 return NewExecError(cmd, exitCode) 103 } 104 105 if strings.Contains(err.Error(), "executable file not found") { 106 return NewExecError(cmd, GenericExitCodeNotFound) 107 } 108 return err 109 }