github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/intercept/docker_run.go (about) 1 package intercept 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "os" 8 "os/signal" 9 "strings" 10 "sync/atomic" 11 "time" 12 13 "github.com/datawire/dlib/dexec" 14 "github.com/datawire/dlib/dlog" 15 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" 16 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags" 17 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" 18 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/spinner" 19 "github.com/telepresenceio/telepresence/v2/pkg/client/docker" 20 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 21 "github.com/telepresenceio/telepresence/v2/pkg/ioutil" 22 "github.com/telepresenceio/telepresence/v2/pkg/proc" 23 ) 24 25 func (s *state) prepareDockerRun(ctx context.Context) error { 26 var buildContext string 27 if s.DockerBuild != "" { 28 buildContext = s.DockerBuild 29 } else if s.DockerDebug != "" { 30 buildContext = s.DockerDebug 31 } 32 imageName, idx := firstDockerArg(s.Cmdline) 33 // Ensure that the image is ready to run before we create the intercept. 34 if buildContext == "" { 35 if idx < 0 { 36 return errcat.User.New(`unable to find the image name. When using --docker-run, the syntax after "--" must be [OPTIONS] IMAGE [COMMAND] [ARG...]`) 37 } 38 return docker.PullImage(ctx, imageName) 39 } 40 41 // DockerBuild will produce an image-ID that must be injected into the docker run argument 42 // list. It must be injected between arguments intended for docker run and arguments intended 43 // for the container, so we require that a placeholder is present. E.g. 44 // 45 // telepresence intercept hello --docker-build ./some/path -- -it --add-host foo IMAGE --port 8080 46 if (idx < 0 || imageName != "IMAGE") && len(s.Cmdline) > 0 { 47 return errcat.User.New(`` + 48 `the string "IMAGE", acting as a placeholder for image ID, must be included after "--" when using "--docker-build", so ` + 49 `that flags intended for docker run can be distinguished from the command and arguments intended for the container.`) 50 } 51 opts := make([]string, len(s.DockerBuildOptions)) 52 for i, opt := range s.DockerBuildOptions { 53 opts[i] = "--" + opt 54 } 55 spin := spinner.New(ctx, "building docker image") 56 imageID, err := docker.BuildImage(ctx, buildContext, opts) 57 if err != nil { 58 return spin.Error(err) 59 } 60 if idx < 0 { 61 s.Cmdline = []string{imageID} 62 } else { 63 s.Cmdline[idx] = imageID 64 } 65 spin.DoneMsg("image built successfully") 66 return nil 67 } 68 69 var dockerBoolFlags = map[string]bool{ //nolint:gochecknoglobals // this is a constant 70 "--detach": true, 71 "--init": true, 72 "--interactive": true, 73 "--no-healthcheck": true, 74 "--oom-kill-disable": true, 75 "--privileged": true, 76 "--publish-all": true, 77 "--quiet": true, 78 "--read-only": true, 79 "--rm": true, 80 "--sig-proxy": true, 81 "--tty": true, 82 } 83 84 // firstDockerArg returns the first argument that isn't an option. This requires knowledge 85 // about boolean docker flags, and if new such flags arrive and are used, this 86 // function might return an incorrect image. 87 func firstDockerArg(args []string) (string, int) { 88 t := len(args) 89 for i := 0; i < t; i++ { 90 arg := args[i] 91 if !strings.HasPrefix(arg, "-") { 92 return arg, i 93 } 94 if strings.IndexByte(arg, '=') > 0 { 95 continue 96 } 97 if strings.HasPrefix(arg, "--") { 98 if !dockerBoolFlags[arg] { 99 i++ 100 } 101 } else if strings.ContainsAny(arg, "ehlmpuvw") { 102 // Shorthand flag that require an argument. Might be prefixed by shorthand booleans, e.g. -itl <label> 103 i++ 104 } 105 } 106 return "", -1 107 } 108 109 type dockerRun struct { 110 cmd *dexec.Cmd 111 err error 112 name string 113 volumes []string 114 } 115 116 func (dr *dockerRun) wait(ctx context.Context) error { 117 if len(dr.volumes) > 0 { 118 defer func() { 119 ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second) 120 docker.StopVolumeMounts(ctx, dr.volumes) 121 cancel() 122 }() 123 } 124 125 if dr.err != nil { 126 return errcat.NoDaemonLogs.New(dr.err) 127 } 128 129 sigCh := make(chan os.Signal, 1) 130 signal.Notify(sigCh, proc.SignalsToForward...) 131 defer func() { 132 signal.Stop(sigCh) 133 }() 134 135 killTimer := time.AfterFunc(math.MaxInt64, func() { 136 _ = dr.cmd.Process.Kill() 137 }) 138 defer killTimer.Stop() 139 140 var signalled atomic.Bool 141 go func() { 142 select { 143 case <-ctx.Done(): 144 case <-sigCh: 145 } 146 signalled.Store(true) 147 // Kill the docker run after a grace period in case it isn't stopped 148 killTimer.Reset(2 * time.Second) 149 ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second) 150 defer cancel() 151 if err := docker.StopContainer(docker.EnableClient(ctx), dr.name); err != nil { 152 dlog.Error(ctx, err) 153 } 154 }() 155 156 err := dr.cmd.Wait() 157 if err != nil { 158 if signalled.Load() { 159 // Errors caused by context or signal termination doesn't count. 160 err = nil 161 } else { 162 err = errcat.NoDaemonLogs.New(err) 163 } 164 } 165 return err 166 } 167 168 func (s *state) getContainerName(args []string) (string, []string, error) { 169 name, err := flags.GetUnparsedValue(args, "--name") 170 if err != nil { 171 return "", args, err 172 } 173 if name == "" { 174 name = fmt.Sprintf("intercept-%s-%d", s.Name(), s.localPort) 175 args = append([]string{"--name", name}, args...) 176 } 177 return name, args, nil 178 } 179 180 func (s *state) startInDocker(ctx context.Context, name, envFile string, args []string) *dockerRun { 181 ourArgs := []string{ 182 "run", 183 "--env-file", envFile, 184 } 185 dr := &dockerRun{name: name} 186 187 if s.DockerDebug != "" { 188 ourArgs = append(ourArgs, "--security-opt", "apparmor=unconfined", "--cap-add", "SYS_PTRACE") 189 } 190 191 ud := daemon.GetUserClient(ctx) 192 if !ud.Containerized() { 193 ourArgs = append(ourArgs, "--dns-search", "tel2-search") 194 if s.dockerPort != 0 { 195 ourArgs = append(ourArgs, "-p", fmt.Sprintf("%d:%d", s.localPort, s.dockerPort)) 196 } 197 dockerMount := "" 198 if s.mountPoint != "" { // do we have a mount point at all? 199 if dockerMount = s.DockerMount; dockerMount == "" { 200 dockerMount = s.mountPoint 201 } 202 } 203 if dockerMount != "" { 204 ourArgs = append(ourArgs, "-v", fmt.Sprintf("%s:%s", s.mountPoint, dockerMount)) 205 } 206 } else { 207 daemonName := ud.DaemonID.ContainerName() 208 ourArgs = append(ourArgs, "--network", "container:"+daemonName) 209 210 // "--rm" is mandatory when using --docker-run against a docker daemon, because without it, the volumes 211 // cannot be removed. 212 _, set, err := flags.GetUnparsedBoolean(args, "--rm") 213 if err != nil { 214 dr.err = err 215 return dr 216 } 217 if !set { 218 ourArgs = append(ourArgs, "--rm") 219 } 220 if !(s.mountDisabled || s.info == nil) { 221 m := s.info.Mount 222 if m != nil { 223 pluginName, err := docker.EnsureVolumePlugin(ctx) 224 if err != nil { 225 ioutil.Printf(output.Err(ctx), "Remote mount disabled: %s\n", err) 226 } 227 container := s.env["TELEPRESENCE_CONTAINER"] 228 dlog.Infof(ctx, "Mounting %v from container %s", m.Mounts, container) 229 dr.volumes, dr.err = docker.StartVolumeMounts(ctx, pluginName, daemonName, container, m.Port, m.Mounts, nil) 230 if dr.err != nil { 231 return dr 232 } 233 for i, vol := range dr.volumes { 234 ourArgs = append(ourArgs, "-v", fmt.Sprintf("%s:%s", vol, m.Mounts[i])) 235 } 236 } 237 } 238 } 239 240 args = append(ourArgs, args...) 241 dr.cmd, dr.err = proc.Start(context.WithoutCancel(ctx), nil, "docker", args...) 242 return dr 243 }