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  }