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  }