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 }