github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/intercept/command.go (about)

     1  package intercept
     2  
     3  import (
     4  	"strconv"
     5  	"strings"
     6  
     7  	"github.com/spf13/cobra"
     8  
     9  	"github.com/datawire/dlib/dlog"
    10  	"github.com/telepresenceio/telepresence/rpc/v2/connector"
    11  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    12  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect"
    13  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon"
    14  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/flags"
    15  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/output"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/dos"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    18  )
    19  
    20  type Command struct {
    21  	Name           string // Command[0] || `${Command[0]}-${--namespace}` // which depends on a combinationof --workload and --namespace
    22  	AgentName      string // --workload || Command[0] // only valid if !localOnly
    23  	Port           string // --port // only valid if !localOnly
    24  	ServiceName    string // --service // only valid if !localOnly
    25  	Address        string // --address // only valid if !localOnly
    26  	LocalOnly      bool   // --local-only
    27  	LocalMountPort uint16 // --local-mount-port
    28  
    29  	Replace bool // whether --replace was passed
    30  
    31  	EnvFile  string   // --env-file
    32  	EnvJSON  string   // --env-json
    33  	Mount    string   // --mount // "true", "false", or desired mount point // only valid if !localOnly
    34  	MountSet bool     // whether --mount was passed
    35  	ToPod    []string // --to-pod
    36  
    37  	DockerRun          bool     // --docker-run
    38  	DockerBuild        string   // --docker-build DIR | URL
    39  	DockerBuildOptions []string // --docker-build-opt key=value, // Optional flag to docker build can be repeated (but not comma separated)
    40  	DockerDebug        string   // --docker-debug DIR | URL
    41  	DockerMount        string   // --docker-mount // where to mount in a docker container. Defaults to mount unless mount is "true" or "false".
    42  	Cmdline            []string // Command[1:]
    43  
    44  	Mechanism       string // --mechanism tcp
    45  	MechanismArgs   []string
    46  	ExtendedInfo    []byte
    47  	WaitMessage     string // Message printed when a containerized intercept handler is started and waiting for an interrupt
    48  	FormattedOutput bool
    49  	DetailedOutput  bool
    50  	Silent          bool
    51  }
    52  
    53  func (a *Command) AddFlags(cmd *cobra.Command) {
    54  	flagSet := cmd.Flags()
    55  	flagSet.StringVarP(&a.AgentName, "workload", "w", "", "Name of workload (Deployment, ReplicaSet) to intercept, if different from <name>")
    56  	flagSet.StringVarP(&a.Port, "port", "p", "", ``+
    57  		`Local port to forward to. If intercepting a service with multiple ports, `+
    58  		`use <local port>:<svcPortIdentifier>, where the identifier is the port name or port number. `+
    59  		`With --docker-run and a daemon that doesn't run in docker', use <local port>:<container port> or `+
    60  		`<local port>:<container port>:<svcPortIdentifier>.`,
    61  	)
    62  
    63  	flagSet.StringVar(&a.Address, "address", "127.0.0.1", ``+
    64  		`Local address to forward to, Only accepts IP address as a value. `+
    65  		`e.g. '--address 10.0.0.2'`,
    66  	)
    67  
    68  	flagSet.StringVar(&a.ServiceName, "service", "", "Name of service to intercept. If not provided, we will try to auto-detect one")
    69  
    70  	flagSet.BoolVarP(&a.LocalOnly, "local-only", "l", false, ``+
    71  		`Declare a local-only intercept for the purpose of getting direct outbound access to the intercept's namespace`)
    72  
    73  	flagSet.StringVarP(&a.EnvFile, "env-file", "e", "", ``+
    74  		`Also emit the remote environment to an env file in Docker Compose format. `+
    75  		`See https://docs.docker.com/compose/env-file/ for more information on the limitations of this format.`)
    76  
    77  	flagSet.StringVarP(&a.EnvJSON, "env-json", "j", "", `Also emit the remote environment to a file as a JSON blob.`)
    78  
    79  	flagSet.StringVar(&a.Mount, "mount", "true", ``+
    80  		`The absolute path for the root directory where volumes will be mounted, $TELEPRESENCE_ROOT. Use "true" to `+
    81  		`have Telepresence pick a random mount point (default). Use "false" to disable filesystem mounting entirely.`)
    82  
    83  	flagSet.StringSliceVar(&a.ToPod, "to-pod", []string{}, ``+
    84  		`An additional port to forward from the intercepted pod, will be made available at localhost:PORT `+
    85  		`Use this to, for example, access proxy/helper sidecars in the intercepted pod. The default protocol is TCP. `+
    86  		`Use <port>/UDP for UDP ports`)
    87  
    88  	flagSet.BoolVar(&a.DockerRun, "docker-run", false, ``+
    89  		`Run a Docker container with intercepted environment, volume mount, by passing arguments after -- to 'docker run', `+
    90  		`e.g. '--docker-run -- -it --rm ubuntu:20.04 /bin/bash'`)
    91  
    92  	flagSet.StringVar(&a.DockerBuild, "docker-build", "", ``+
    93  		`Build a Docker container from the given docker-context (path or URL), and run it with intercepted environment and volume mounts, `+
    94  		`by passing arguments after -- to 'docker run', e.g. '--docker-build /path/to/docker/context -- -it IMAGE /bin/bash'`)
    95  
    96  	flagSet.StringVar(&a.DockerDebug, "docker-debug", "", ``+
    97  		`Like --docker-build, but allows a debugger to run inside the container with relaxed security`)
    98  
    99  	flagSet.StringArrayVar(&a.DockerBuildOptions, "docker-build-opt", nil,
   100  		`Option to docker-build in the form key=value, e.g. --docker-build-opt tag=mytag. Can be repeated`)
   101  
   102  	flagSet.StringVar(&a.DockerMount, "docker-mount", "", ``+
   103  		`The volume mount point in docker. Defaults to same as "--mount"`)
   104  
   105  	flagSet.StringP("namespace", "n", "", "If present, the namespace scope for this CLI request")
   106  
   107  	flagSet.StringVar(&a.Mechanism, "mechanism", "tcp", "Which extension `mechanism` to use")
   108  
   109  	flagSet.BoolVar(&a.DetailedOutput, "detailed-output", false,
   110  		`Provide very detailed info about the intercept when used together with --output=json or --output=yaml'`)
   111  
   112  	flagSet.Uint16Var(&a.LocalMountPort, "local-mount-port", 0,
   113  		`Do not mount remote directories. Instead, expose this port on localhost to an external mounter`)
   114  
   115  	flagSet.BoolVarP(&a.Replace, "replace", "", false,
   116  		`Indicates if the traffic-agent should replace application containers in workload pods. `+
   117  			`The default behavior is for the agent sidecar to be installed alongside existing containers.`)
   118  
   119  	// Hide these flags. They are still functional but deprecated. Using them will yield a deprecation message.
   120  	flagSet.Lookup("local-only").Hidden = true
   121  	flagSet.Lookup("namespace").Hidden = true
   122  }
   123  
   124  func (a *Command) Validate(cmd *cobra.Command, positional []string) error {
   125  	flags.DeprecationIfChanged(cmd, "local-only", "use telepresence connect to set the namespace")
   126  	flags.DeprecationIfChanged(cmd, "namespace", "use telepresence connect to set the namespace")
   127  	if len(positional) > 1 && cmd.Flags().ArgsLenAtDash() != 1 {
   128  		return errcat.User.New("commands to be run with intercept must come after options")
   129  	}
   130  	a.Name = positional[0]
   131  	a.Cmdline = positional[1:]
   132  	a.FormattedOutput = output.WantsFormatted(cmd)
   133  	if a.LocalOnly {
   134  		// Not actually intercepting anything -- check that the flags make sense for that
   135  		if a.AgentName != "" {
   136  			return errcat.User.New("a local-only intercept cannot have a workload")
   137  		}
   138  		if a.ServiceName != "" {
   139  			return errcat.User.New("a local-only intercept cannot have a service")
   140  		}
   141  		if cmd.Flag("port").Changed {
   142  			return errcat.User.New("a local-only intercept cannot have a port")
   143  		}
   144  		if cmd.Flag("mount").Changed {
   145  			if doMount, _ := a.GetMountPoint(); doMount {
   146  				return errcat.User.New("a local-only intercept cannot have mounts")
   147  			}
   148  		}
   149  		return nil
   150  	}
   151  
   152  	if a.LocalMountPort > 0 && client.GetConfig(cmd.Context()).Intercept().UseFtp {
   153  		return errcat.User.New("only SFTP can be used with --local-mount-port. Client is configured to perform remote mounts using FTP")
   154  	}
   155  
   156  	// Actually intercepting something
   157  	if a.AgentName == "" {
   158  		a.AgentName = a.Name
   159  	}
   160  	if a.Port == "" {
   161  		a.Port = strconv.Itoa(client.GetConfig(cmd.Context()).Intercept().DefaultPort)
   162  	}
   163  	a.MountSet = cmd.Flag("mount").Changed
   164  	drCount := 0
   165  	if a.DockerRun {
   166  		drCount++
   167  	}
   168  	if a.DockerBuild != "" {
   169  		drCount++
   170  	}
   171  	if a.DockerDebug != "" {
   172  		drCount++
   173  	}
   174  	if drCount > 1 {
   175  		return errcat.User.New("only one of --docker-run, --docker-build, or --docker-debug can be used")
   176  	}
   177  	a.DockerRun = drCount == 1
   178  	if a.DockerRun {
   179  		if err := a.ValidateDockerArgs(); err != nil {
   180  			return err
   181  		}
   182  	}
   183  	return nil
   184  }
   185  
   186  func (a *Command) Run(cmd *cobra.Command, positional []string) error {
   187  	if err := a.Validate(cmd, positional); err != nil {
   188  		return err
   189  	}
   190  	if err := connect.InitCommand(cmd); err != nil {
   191  		return err
   192  	}
   193  	ctx := dos.WithStdio(cmd.Context(), cmd)
   194  	_, err := NewState(a).Run(ctx)
   195  	return err
   196  }
   197  
   198  func (a *Command) ValidateDockerArgs() error {
   199  	for _, arg := range a.Cmdline {
   200  		if arg == "-d" || arg == "--detach" {
   201  			return errcat.User.New("running docker container in background using -d or --detach is not supported")
   202  		}
   203  	}
   204  	return nil
   205  }
   206  
   207  func (a *Command) ValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   208  	if len(args) > 0 {
   209  		// Not completing the name of the workload
   210  		return nil, cobra.ShellCompDirectiveNoFileComp
   211  	}
   212  	if err := connect.InitCommand(cmd); err != nil {
   213  		return nil, cobra.ShellCompDirectiveError
   214  	}
   215  	req := connector.ListRequest{
   216  		Filter: connector.ListRequest_INTERCEPTABLE,
   217  	}
   218  	nf := cmd.Flag("namespace")
   219  	if nf.Changed {
   220  		req.Namespace = nf.Value.String()
   221  	}
   222  	ctx := cmd.Context()
   223  
   224  	// Trace level is used here, because we generally don't want to log expansion attempts
   225  	// in the cli.log
   226  	dlog.Tracef(ctx, "ns = %s, toComplete = %s, args = %v", req.Namespace, toComplete, args)
   227  	r, err := daemon.GetUserClient(ctx).List(ctx, &req)
   228  	if err != nil {
   229  		dlog.Debugf(ctx, "unable to get list of interceptable workloads: %v", err)
   230  		return nil, cobra.ShellCompDirectiveError
   231  	}
   232  
   233  	list := make([]string, 0)
   234  	for _, w := range r.Workloads {
   235  		// only suggest strings that start with the string were autocompleting
   236  		if strings.HasPrefix(w.Name, toComplete) {
   237  			list = append(list, w.Name)
   238  		}
   239  	}
   240  
   241  	// TODO(raphaelreyna): This list can be quite large (in the double digits of MB).
   242  	// There probably exists a number that would be a good cutoff limit.
   243  
   244  	return list, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
   245  }
   246  
   247  // GetMountPoint returns a boolean indicating if mounts are enabled or not, and path
   248  // indicating a mount point.
   249  func (a *Command) GetMountPoint() (bool, string) {
   250  	if !a.MountSet {
   251  		// Default is that mount is enabled and the path is unspecified
   252  		return true, ""
   253  	}
   254  	if doMount, err := strconv.ParseBool(a.Mount); err == nil {
   255  		// Boolean flag, path unspecified
   256  		return doMount, ""
   257  	}
   258  	if len(a.Mount) == 0 {
   259  		// Let explicit --mount= have the same meaning as --mount=false
   260  		return false, ""
   261  	}
   262  	return true, a.Mount
   263  }