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 }