github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/cmd/legacy_command.go (about) 1 package cmd 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/spf13/cobra" 8 "github.com/spf13/pflag" 9 "k8s.io/cli-runtime/pkg/genericclioptions" 10 11 "github.com/telepresenceio/telepresence/v2/pkg/client/scout" 12 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 13 ) 14 15 // Here we handle parsing legacy commands, as well as generating Telepresence 16 // commands from them. This will make it easier for users to migrate from 17 // legacy Telepresence. Note: This isn't exhaustive, but should capture the major 18 // flags that were used and have a correlated command in Telepresence. 19 20 type legacyCommand struct { 21 swapDeployment string 22 newDeployment bool 23 method bool 24 expose string 25 run bool 26 dockerRun bool 27 runShell bool 28 processCmd string 29 mount string 30 dockerMount string 31 envFile string 32 envJSON string 33 34 // kubectl-related flags 35 context string 36 namespace string 37 38 globalFlags []string 39 unsupportedFlags []string 40 } 41 42 // Unfortunately we have to do our own flag parsing if we see legacy telepresence 43 // flags because the run command might include something that cobra might detect 44 // as a flag e.g. --run python3 -m http.server. In python this was handled by 45 // using argparse.REMAINDER and there is no similar functionality within cobra. 46 // There is an open ticket to pass unknown flags to the command: 47 // https://github.com/spf13/cobra/issues/739 48 // but until that is addressed, we'll do the flag parsing ourselves (which isn't 49 // the worst because it's a legacy command so the flags won't be growing). 50 func parseLegacy(args []string) *legacyCommand { 51 lc := &legacyCommand{} 52 53 // We don't want to over-index in case somebody has a command that has a 54 // flag but doesn't put the value after it. So we have this helper function 55 // to ensure we don't do that. It may mean the telepresence command at the 56 // end fails, but then they'll see the Telepresence error messge and can 57 // fix it from there. 58 getArg := func(i int) string { 59 if len(args) > i { 60 return args[i] 61 } 62 return "" 63 } 64 kubeFlags := pflag.NewFlagSet("Kubernetes flags", 0) 65 kubeConfig := genericclioptions.NewConfigFlags(false) 66 kubeConfig.Namespace = nil // "connect", don't take --namespace 67 kubeConfig.Context = nil // --context is global 68 kubeConfig.AddFlags(kubeFlags) 69 Parsing: 70 for i, v := range args { 71 switch { 72 case v == "--swap-deployment" || v == "-s": 73 lc.swapDeployment = getArg(i + 1) 74 case v == "--new-deployment" || v == "-n": 75 lc.newDeployment = true 76 case v == "--method" || v == "-m": 77 lc.method = true 78 case v == "--expose": 79 lc.expose = getArg(i + 1) 80 case v == "--mount": 81 lc.mount = getArg(i + 1) 82 case v == "--docker-mount": 83 lc.dockerMount = getArg(i + 1) 84 case v == "--env-json": 85 lc.envJSON = getArg(i + 1) 86 case v == "--env-file": 87 lc.envFile = getArg(i + 1) 88 case v == "--namespace": 89 lc.namespace = getArg(i + 1) 90 // The three run commands are terminal so we break 91 // out of the loop after encountering them. 92 // This also means if somebody uses --run and --docker-run 93 // in the same command, whichever is first will be used. I don't 94 // think this is terrible because I'm not sure how much we need to 95 // correct *incorrect* tp1 commands here, but it could be improved 96 case v == "--run": 97 lc.run = true 98 if nxtArg := getArg(i + 1); nxtArg != "" { 99 lc.processCmd = strings.Join(args[i+1:], " ") 100 } 101 break Parsing 102 case v == "--docker-run": 103 lc.dockerRun = true 104 if nxtArg := getArg(i + 1); nxtArg != "" { 105 lc.processCmd = strings.Join(args[i+1:], " ") 106 } 107 break Parsing 108 case v == "--run-shell": 109 lc.runShell = true 110 break Parsing 111 case len(v) > 2 && strings.HasPrefix(v, "--"): 112 g := v[2:] 113 if gf := kubeFlags.Lookup(g); gf != nil { 114 lc.globalFlags = append(lc.globalFlags, v) 115 if gv := getArg(i + 1); gv != "" && !strings.HasPrefix(gv, "-") { 116 lc.globalFlags = append(lc.globalFlags, gv) 117 } 118 continue Parsing 119 } 120 lc.unsupportedFlags = append(lc.unsupportedFlags, v) 121 } 122 } 123 return lc 124 } 125 126 // genTPCommand constructs a Telepresence command based on 127 // the values that are set in the legacyCommand struct. 128 func (lc *legacyCommand) genTPCommand() (string, error) { 129 var cmdSlice []string 130 switch { 131 // if swapDeployment isn't empty, then our translation is 132 // an intercept subcommand 133 case lc.swapDeployment != "": 134 cmdSlice = append(cmdSlice, "intercept", lc.swapDeployment) 135 if lc.expose != "" { 136 cmdSlice = append(cmdSlice, "--port", lc.expose) 137 } 138 139 if lc.envFile != "" { 140 cmdSlice = append(cmdSlice, "--env-file", lc.envFile) 141 } 142 143 if lc.envJSON != "" { 144 cmdSlice = append(cmdSlice, "--env-json", lc.envJSON) 145 } 146 147 if lc.context != "" { 148 cmdSlice = append(cmdSlice, "--context", lc.context) 149 } 150 151 if lc.namespace != "" { 152 cmdSlice = append(cmdSlice, "--namespace", lc.namespace) 153 } 154 155 // This should be impossible based on how we currently parse commands. 156 // Just putting it here just in case the impossible happens. 157 if lc.run && lc.dockerRun { 158 return "", errcat.User.New("--run and --docker-run are mutually exclusive") 159 } 160 161 if lc.run { 162 if lc.mount != "" { 163 cmdSlice = append(cmdSlice, "--mount", lc.mount) 164 } 165 } 166 167 if lc.dockerRun { 168 if lc.dockerMount != "" { 169 cmdSlice = append(cmdSlice, "--docker-mount", lc.dockerMount) 170 } 171 cmdSlice = append(cmdSlice, "--docker-run") 172 } 173 cmdSlice = append(cmdSlice, lc.globalFlags...) 174 175 if lc.processCmd != "" { 176 cmdSlice = append(cmdSlice, "--", lc.processCmd) 177 } 178 179 if lc.runShell { 180 cmdSlice = append(cmdSlice, "--", "bash") 181 } 182 // If we have a run of some kind without a swapDeployment, then 183 // we translate to a connect 184 case lc.runShell: 185 cmdSlice = append(cmdSlice, "connect") 186 cmdSlice = append(cmdSlice, lc.globalFlags...) 187 cmdSlice = append(cmdSlice, "--", "bash") 188 case lc.run: 189 cmdSlice = append(cmdSlice, "connect") 190 cmdSlice = append(cmdSlice, lc.globalFlags...) 191 cmdSlice = append(cmdSlice, "--", lc.processCmd) 192 // Either not a legacyCommand or we don't know how to translate it to Telepresence 193 default: 194 return "", nil 195 } 196 197 return strings.Join(cmdSlice, " "), nil 198 } 199 200 // translateLegacy tries to detect if a legacy Telepresence command was used 201 // and constructs a Telepresence command from that. 202 func translateLegacy(args []string) (string, string, *legacyCommand, error) { 203 lc := parseLegacy(args) 204 tpCmd, err := lc.genTPCommand() 205 if err != nil { 206 return "", "", lc, err 207 } 208 209 // There are certain elements of the telepresence 1 cli that we either 210 // don't have a perfect mapping for or want to explicitly let users know 211 // about changed behavior. 212 msg := "" 213 if len(lc.unsupportedFlags) > 0 { 214 msg += fmt.Sprintf("The following flags used don't have a direct translation to Telepresence: %s\n", 215 strings.Join(lc.unsupportedFlags, " ")) 216 } 217 if lc.method { 218 msg += "Telepresence doesn't have proxying methods. You can use --docker-run for container, otherwise it works similarly to vpn-tcp\n" 219 } 220 221 if lc.newDeployment { 222 msg += "This flag is ignored since Telepresence uses one traffic-manager deployed in the ambassador namespace.\n" 223 } 224 return tpCmd, msg, lc, nil 225 } 226 227 // perhapsLegacy is like OnlySubcommands but performs some initial check for legacy flags. 228 func perhapsLegacy(cmd *cobra.Command, args []string) error { 229 // If a user is using a flag that is coming from telepresence 1, we try to 230 // construct the tp2 command based on their input. If the args passed to 231 // telepresence are one of the flags we recognize, we don't want to error 232 // out here. 233 tp1Flags := []string{"--swap-deployment", "-s", "--run", "--run-shell", "--docker-run", "--help"} 234 for _, v := range args { 235 for _, flag := range tp1Flags { 236 if v == flag { 237 return nil 238 } 239 } 240 } 241 return OnlySubcommands(cmd, args) 242 } 243 244 // checkLegacy is mostly a wrapper around translateLegacy. The latter 245 // is separate to make for easier testing. 246 func checkLegacy(cmd *cobra.Command, args []string) error { 247 if len(args) == 0 { 248 return nil 249 } 250 tpCmd, msg, lc, err := translateLegacy(args) 251 if err != nil { 252 return err 253 } 254 255 ctx := cmd.Context() 256 ctx = scout.NewReporter(ctx, "cli") 257 scout.Start(ctx) 258 defer scout.Close(ctx) 259 260 // Add metadata for the main legacy Telepresence commands so we can 261 // track usage and see what legacy commands people are still using. 262 if lc.swapDeployment != "" { 263 scout.SetMetadatum(ctx, "swap_deployment", true) 264 } 265 if lc.run { 266 scout.SetMetadatum(ctx, "run", true) 267 } 268 if lc.dockerRun { 269 scout.SetMetadatum(ctx, "docker_run", true) 270 } 271 if lc.runShell { 272 scout.SetMetadatum(ctx, "run_shell", true) 273 } 274 if lc.unsupportedFlags != nil { 275 scout.SetMetadatum(ctx, "unsupported_flags", lc.unsupportedFlags) 276 } 277 scout.Report(ctx, "Used legacy syntax") 278 279 // Generate output to user letting them know legacy Telepresence was used, 280 // what the Telepresence command is, and runs it. 281 if tpCmd != "" { 282 fmt.Fprintf(cmd.OutOrStderr(), "Legacy Telepresence command used\n") 283 284 if msg != "" { 285 fmt.Fprintln(cmd.OutOrStderr(), msg) 286 } 287 288 fmt.Fprintf(cmd.OutOrStderr(), "Command roughly translates to the following in Telepresence:\ntelepresence %s\n", tpCmd) 289 ctx := cmd.Context() 290 fmt.Fprintln(cmd.OutOrStderr(), "running...") 291 newCmd := Telepresence(ctx) 292 newCmd.SetArgs(strings.Split(tpCmd, " ")) 293 newCmd.SetOut(cmd.OutOrStderr()) 294 newCmd.SetErr(cmd.OutOrStderr()) 295 if err := newCmd.ExecuteContext(ctx); err != nil { 296 fmt.Fprintln(cmd.ErrOrStderr(), err) 297 } 298 } 299 return nil 300 }