github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/containerupdate/docker_container_updater.go (about) 1 package containerupdate 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "strings" 9 10 "github.com/pkg/errors" 11 12 "github.com/tilt-dev/tilt/internal/container" 13 "github.com/tilt-dev/tilt/internal/docker" 14 "github.com/tilt-dev/tilt/internal/k8s" 15 "github.com/tilt-dev/tilt/internal/store/liveupdates" 16 "github.com/tilt-dev/tilt/pkg/logger" 17 "github.com/tilt-dev/tilt/pkg/model" 18 ) 19 20 type DockerUpdater struct { 21 dCli docker.Client 22 } 23 24 var _ ContainerUpdater = &DockerUpdater{} 25 26 func NewDockerUpdater(dCli docker.Client) *DockerUpdater { 27 return &DockerUpdater{dCli: dCli} 28 } 29 30 func (cu *DockerUpdater) WillBuildToKubeContext(kctx k8s.KubeContext) bool { 31 return cu.dCli.Env().WillBuildToKubeContext(kctx) 32 } 33 34 func (cu *DockerUpdater) UpdateContainer(ctx context.Context, cInfo liveupdates.Container, 35 archiveToCopy io.Reader, filesToDelete []string, cmds []model.Cmd, hotReload bool) error { 36 l := logger.Get(ctx) 37 38 err := cu.rmPathsFromContainer(ctx, cInfo.ContainerID, filesToDelete) 39 if err != nil { 40 return errors.Wrap(err, "rmPathsFromContainer") 41 } 42 43 // Use `tar` to unpack the files into the container. 44 // 45 // Although docker has a copy API, it's buggy and not well-maintained 46 // (whereas the Exec API is part of the CRI and much more battle-tested). 47 // Discussion: 48 // https://github.com/tilt-dev/tilt/issues/3708 49 tarCmd := tarCmd() 50 err = cu.dCli.ExecInContainer(ctx, cInfo.ContainerID, tarCmd, archiveToCopy, l.Writer(logger.InfoLvl)) 51 if err != nil { 52 if exitCode, ok := ExtractExitCode(err); ok { 53 return wrapTarExecErr(err, tarCmd, exitCode) 54 } 55 return fmt.Errorf("copying changed files: %w", err) 56 } 57 58 // Exec run's on container 59 for i, cmd := range cmds { 60 if !cmd.EchoOff { 61 l.Infof("[CMD %d/%d] %s", i+1, len(cmds), strings.Join(cmd.Argv, " ")) 62 } 63 err = cu.dCli.ExecInContainer(ctx, cInfo.ContainerID, cmd, nil, l.Writer(logger.InfoLvl)) 64 if err != nil { 65 return fmt.Errorf( 66 "executing on container %s: %w", 67 cInfo.ContainerID.ShortStr(), 68 wrapRunStepError(wrapDockerGenericExecErr(cmd, err)), 69 ) 70 } 71 } 72 73 if hotReload { 74 l.Debugf("Hot reload on, skipping container restart: %s", cInfo.DisplayName()) 75 return nil 76 } 77 78 // Restart container so that entrypoint restarts with the updated files etc. 79 l.Debugf("Restarting container: %s", cInfo.DisplayName()) 80 err = cu.dCli.ContainerRestartNoWait(ctx, cInfo.ContainerID.String()) 81 if err != nil { 82 return errors.Wrap(err, "ContainerRestart") 83 } 84 return nil 85 } 86 87 func (cu *DockerUpdater) rmPathsFromContainer(ctx context.Context, cID container.ID, paths []string) error { 88 if len(paths) == 0 { 89 return nil 90 } 91 92 out := bytes.NewBuffer(nil) 93 err := cu.dCli.ExecInContainer(ctx, cID, model.Cmd{Argv: makeRmCmd(paths)}, nil, out) 94 if err != nil { 95 if docker.IsExitError(err) { 96 return fmt.Errorf("Error deleting files from container: %s", out.String()) 97 } 98 return errors.Wrap(err, "Error deleting files from container") 99 } 100 return nil 101 } 102 103 func makeRmCmd(paths []string) []string { 104 cmd := []string{"rm", "-rf"} 105 cmd = append(cmd, paths...) 106 return cmd 107 } 108 109 func wrapDockerGenericExecErr(cmd model.Cmd, err error) error { 110 if exitCode, ok := ExtractExitCode(err); ok { 111 return NewExecError(cmd, exitCode) 112 } 113 return err 114 }