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

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/spf13/cobra"
    10  
    11  	"github.com/datawire/dlib/dlog"
    12  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    13  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon"
    14  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/global"
    15  	"github.com/telepresenceio/telepresence/v2/pkg/client/docker/kubeauth"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/client/logging"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/client/rootd"
    18  	userDaemon "github.com/telepresenceio/telepresence/v2/pkg/client/userd/daemon"
    19  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    20  	"github.com/telepresenceio/telepresence/v2/pkg/maps"
    21  )
    22  
    23  // Telepresence returns the top level "telepresence" CLI command.
    24  func Telepresence(ctx context.Context) *cobra.Command {
    25  	cfg, err := client.LoadConfig(ctx)
    26  	if err != nil {
    27  		fmt.Fprintf(os.Stderr, "Failed to load config: %v", err)
    28  		os.Exit(1)
    29  	}
    30  	ctx = client.WithConfig(ctx, cfg)
    31  	if ctx, err = logging.InitContext(ctx, "cli", logging.RotateDaily, false); err != nil {
    32  		fmt.Fprintln(os.Stderr, err.Error())
    33  		os.Exit(1)
    34  	}
    35  	rootCmd := &cobra.Command{
    36  		Use:  "telepresence",
    37  		Args: perhapsLegacy,
    38  
    39  		Short:              "Connect your workstation to a Kubernetes cluster",
    40  		Long:               help,
    41  		RunE:               RunSubcommands,
    42  		SilenceErrors:      true, // main() will handle it after .ExecuteContext() returns
    43  		SilenceUsage:       true, // our FlagErrorFunc will handle it
    44  		DisableFlagParsing: true, // Bc of the legacyCommand parsing, see legacy_command.go
    45  	}
    46  	rootCmd.SetContext(ctx)
    47  	AddSubCommands(rootCmd)
    48  	rootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
    49  		return errcat.User.New(err)
    50  	})
    51  	return rootCmd
    52  }
    53  
    54  // TelepresenceDaemon returns the top level "telepresence" CLI limited to the subcommands [kubeauth|connector|daemon]-foreground.
    55  func TelepresenceDaemon(ctx context.Context) *cobra.Command {
    56  	cmd := &cobra.Command{
    57  		Use:  "telepresence",
    58  		Args: OnlySubcommands,
    59  		RunE: func(cmd *cobra.Command, args []string) error {
    60  			cmd.SetOut(cmd.ErrOrStderr())
    61  			return nil
    62  		},
    63  		SilenceErrors: true, // main() will handle it after .ExecuteContext() returns
    64  		SilenceUsage:  true, // our FlagErrorFunc will handle it
    65  	}
    66  	cmd.SetContext(ctx)
    67  	AddSubCommands(cmd)
    68  	return cmd
    69  }
    70  
    71  // AddSubCommands adds subcommands to the given command, including the default help, the commands in the
    72  // CommandGroups found in the given command's context, and the completion command. It also replaces
    73  // the standard usage template with a custom template.
    74  func AddSubCommands(cmd *cobra.Command) {
    75  	ctx := cmd.Context()
    76  	commands := getSubCommands(cmd)
    77  	for _, command := range commands {
    78  		if ac := command.Args; ac != nil {
    79  			// Ensure that args errors don't advice the user to look in log files
    80  			command.Args = argsCheck(ac)
    81  		}
    82  		command.SetContext(ctx)
    83  	}
    84  	cmd.AddCommand(commands...)
    85  	cmd.PersistentFlags().AddFlagSet(global.Flags(false))
    86  	addCompletion(cmd)
    87  	cmd.InitDefaultHelpCmd()
    88  	addUsageTemplate(cmd)
    89  	_ = cmd.RegisterFlagCompletionFunc("context", autocompleteContext)
    90  }
    91  
    92  // RunSubcommands is for use as a cobra.interceptCmd.RunE for commands that don't do anything themselves
    93  // but have subcommands.  In such cases, it is important to set RunE even though there's nothing to
    94  // run, because otherwise cobra will treat that as "success", and it shouldn't be "success" if the
    95  // user typos a command and types something invalid.
    96  func RunSubcommands(cmd *cobra.Command, args []string) error {
    97  	// determine if --help was explicitly asked for
    98  	var usedHelpFlag bool
    99  	for _, arg := range args {
   100  		if arg == "--help" || arg == "-h" {
   101  			usedHelpFlag = true
   102  		}
   103  	}
   104  	// If there are no args or --help was used, then it's not a legacy
   105  	// Telepresence command so we return the help text
   106  	if len(args) == 0 || usedHelpFlag {
   107  		cmd.HelpFunc()(cmd, args)
   108  		return nil
   109  	}
   110  	if err := checkLegacy(cmd, args); err != nil {
   111  		return err
   112  	}
   113  	return nil
   114  }
   115  
   116  // OnlySubcommands is a cobra.PositionalArgs that is similar to cobra.NoArgs, but prints a better
   117  // error message.
   118  func OnlySubcommands(cmd *cobra.Command, args []string) error {
   119  	if len(args) == 0 {
   120  		return nil
   121  	}
   122  	if args[0] == "-h" {
   123  		return nil
   124  	}
   125  	err := fmt.Errorf("invalid subcommand %q", args[0])
   126  	if cmd.SuggestionsMinimumDistance <= 0 {
   127  		cmd.SuggestionsMinimumDistance = 2
   128  	}
   129  	if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 {
   130  		err = fmt.Errorf("%w\nDid you mean one of these?\n\t%s", err, strings.Join(suggestions, "\n\t"))
   131  	}
   132  	return cmd.FlagErrorFunc()(cmd, err)
   133  }
   134  
   135  func WithSubCommands(ctx context.Context) context.Context {
   136  	return MergeSubCommands(ctx,
   137  		configCmd(), connectCmd(), currentClusterId(), gatherLogs(), gatherTraces(), genYAML(), helmCmd(),
   138  		interceptCmd(), kubeauthCmd(), leave(), list(), listContexts(), listNamespaces(), loglevel(), quit(), statusCmd(),
   139  		testVPN(), uninstall(), uploadTraces(), version(), listNamespaces(), listContexts(),
   140  	)
   141  }
   142  
   143  func WithDaemonSubCommands(ctx context.Context) context.Context {
   144  	return MergeSubCommands(ctx, kubeauth.Command(), userDaemon.Command(), rootd.Command())
   145  }
   146  
   147  type subCommandsKey struct{}
   148  
   149  func MergeSubCommands(ctx context.Context, commands ...*cobra.Command) context.Context {
   150  	if ecs, ok := ctx.Value(subCommandsKey{}).(*[]*cobra.Command); ok {
   151  		*ecs = mergeCommands(*ecs, commands)
   152  	} else {
   153  		ctx = context.WithValue(ctx, subCommandsKey{}, &commands)
   154  	}
   155  	return ctx
   156  }
   157  
   158  func getSubCommands(cmd *cobra.Command) []*cobra.Command {
   159  	if gs, ok := cmd.Context().Value(subCommandsKey{}).(*[]*cobra.Command); ok {
   160  		return *gs
   161  	}
   162  	return nil
   163  }
   164  
   165  // mergeCommands merges the command slice b into a, replacing commands using the same name
   166  // and returns the resulting slice.
   167  func mergeCommands(a, b []*cobra.Command) []*cobra.Command {
   168  	ac := make(map[string]*cobra.Command, len(a)+len(b))
   169  	for _, c := range a {
   170  		ac[c.Name()] = c
   171  	}
   172  	for _, c := range b {
   173  		ac[c.Name()] = c
   174  	}
   175  	return maps.ToSortedSlice(ac)
   176  }
   177  
   178  // argsCheck wraps an PositionalArgs checker in a function that wraps a potential error
   179  // using errcat.User.
   180  func argsCheck(f cobra.PositionalArgs) cobra.PositionalArgs {
   181  	return func(cmd *cobra.Command, args []string) error {
   182  		if err := f(cmd, args); err != nil {
   183  			return errcat.User.New(err)
   184  		}
   185  		return nil
   186  	}
   187  }
   188  
   189  func autocompleteContext(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   190  	ctx := cmd.Context()
   191  	dlog.Debugf(ctx, "context completion: %q", toComplete)
   192  	cfg, err := daemon.GetKubeStartingConfig(cmd)
   193  	if err != nil {
   194  		dlog.Errorf(ctx, "GetKubeStartingConfig: %v", err)
   195  		return nil, cobra.ShellCompDirectiveError
   196  	}
   197  	cxl := cfg.Contexts
   198  	nss := make([]string, len(cxl))
   199  	i := 0
   200  	for n := range cxl {
   201  		nss[i] = n
   202  		i++
   203  	}
   204  	return nss, cobra.ShellCompDirectiveNoFileComp
   205  }