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  }