-
-
Notifications
You must be signed in to change notification settings - Fork 517
/
cmd.go
278 lines (245 loc) · 9.34 KB
/
cmd.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package cli
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"github.com/moby/term"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/cli-runtime/pkg/genericclioptions"
"github.com/telepresenceio/telepresence/v2/pkg/client/cli/intercept"
"github.com/telepresenceio/telepresence/v2/pkg/client/cli/util"
"github.com/telepresenceio/telepresence/v2/pkg/errcat"
)
const (
help = `Telepresence can connect to a cluster and route all outbound traffic from your
workstation to that cluster so that software running locally can communicate
as if it executed remotely, inside the cluster. This is achieved using the
command:
telepresence connect
Telepresence can also intercept traffic intended for a specific service in a
cluster and redirect it to your local workstation:
telepresence intercept <name of service>
Telepresence uses background processes to manage the cluster session. One of
the processes runs with superuser privileges because it modifies the network.
Unless the daemons are already started, an attempt will be made to start them.
This will involve a call to sudo unless this command is run as root (not
recommended) which in turn may result in a password prompt.`
usage = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{flags . | wrappedFlagUsages | trimTrailingWhitespaces}}{{end}}
{{- if hasKubeFlags .}}
Kubernetes flags:
{{kubeFlags | wrappedFlagUsages | trimTrailingWhitespaces}}{{end}}
Global flags:
{{globalFlags | wrappedFlagUsages | trimTrailingWhitespaces}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.
For complete documentation and quick-start guides, check out our website at https://www.telepresence.io{{end}}
`
)
func flagEqual(a, b *pflag.Flag) bool {
if a == b {
return true
}
if a == nil || b == nil {
return false
}
return a.Name == b.Name && a.Usage == b.Usage && a.Hidden == b.Hidden
}
func localFlags(cmd *cobra.Command, exclude ...*pflag.FlagSet) *pflag.FlagSet {
ngFlags := pflag.NewFlagSet("local", pflag.ContinueOnError)
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
for _, ex := range exclude {
if flagEqual(flag, ex.Lookup(flag.Name)) {
return
}
}
ngFlags.AddFlag(flag)
})
return ngFlags
}
func addUsageTemplate(cmd *cobra.Command) {
kubeFlags := pflag.NewFlagSet("Kubernetes flags", 0)
kubeConfig := genericclioptions.NewConfigFlags(false)
kubeConfig.Namespace = nil // "connect", don't take --namespace
kubeConfig.AddFlags(kubeFlags)
globalFlags := GlobalFlags()
cobra.AddTemplateFunc("globalFlags", func() *pflag.FlagSet { return globalFlags })
cobra.AddTemplateFunc("flags", func(cmd *cobra.Command) *pflag.FlagSet { return localFlags(cmd, kubeFlags, globalFlags) })
cobra.AddTemplateFunc("hasKubeFlags", func(cmd *cobra.Command) bool {
yep := true
flags := cmd.Flags()
kubeFlags.VisitAll(func(flag *pflag.Flag) {
if yep && !flagEqual(flag, flags.Lookup(flag.Name)) {
yep = false
}
})
return yep
})
cobra.AddTemplateFunc("kubeFlags", func() *pflag.FlagSet { return kubeFlags })
cobra.AddTemplateFunc("wrappedFlagUsages", func(flags *pflag.FlagSet) string {
// This is based off of what Docker does (github.com/docker/cli/cli/cobra.go), but is
// adjusted
// 1. to take a pflag.FlagSet instead of a cobra.Command, so that we can have flag groups, and
// 2. to correct for the ways that Docker upsets me.
// Obey COLUMNS if the shell or user sets it. (Docker doesn't do this.)
cols, err := strconv.Atoi(os.Getenv("COLUMNS"))
if err != nil {
// Try to detect the size of the stdout file descriptor. (Docker checks stdin, not stdout.)
if ws, err := term.GetWinsize(1); err != nil {
// If stdout is a terminal, but we were unable to get its size (I'm not sure how that can
// happen), then fall back to assuming 80. If stdout isn't a terminal, then we leave cols
// as 0, meaning "don't wrap it". (Docker wraps it even if stdout isn't a terminal.)
if term.IsTerminal(1) {
cols = 80
}
} else {
cols = int(ws.Width)
}
}
return flags.FlagUsagesWrapped(cols)
})
// Set a usage template that is derived from the default but replaces the "Available Commands"
// section with the commandGroups() from the given command
cmd.SetUsageTemplate(usage)
}
// OnlySubcommands is a cobra.PositionalArgs that is similar to cobra.NoArgs, but prints a better
// error message.
func OnlySubcommands(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return nil
}
if args[0] == "-h" {
return nil
}
err := fmt.Errorf("invalid subcommand %q", args[0])
if cmd.SuggestionsMinimumDistance <= 0 {
cmd.SuggestionsMinimumDistance = 2
}
if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 {
err = fmt.Errorf("%w\nDid you mean one of these?\n\t%s", err, strings.Join(suggestions, "\n\t"))
}
return cmd.FlagErrorFunc()(cmd, err)
}
// PerhapsLegacyCommands is like OnlySubcommands but performs some initial check for legacy flags.
func PerhapsLegacyCommands(cmd *cobra.Command, args []string) error {
// If a user is using a flag that is coming from telepresence 1, we try to
// construct the tp2 command based on their input. If the args passed to
// telepresence are one of the flags we recognize, we don't want to error
// out here.
tp1Flags := []string{"--swap-deployment", "-s", "--run", "--run-shell", "--docker-run", "--help"}
for _, v := range args {
for _, flag := range tp1Flags {
if v == flag {
return nil
}
}
}
return OnlySubcommands(cmd, args)
}
// AddSubCommands adds subcommands to the given command, including the default help, the commands in the
// CommandGroups found in the given command's context, and the completion command. It also replaces
// the standard usage template with a custom template.
func AddSubCommands(cmd *cobra.Command) {
ctx := cmd.Context()
commands := util.GetSubCommands(cmd)
for _, command := range commands {
if ac := command.Args; ac != nil {
// Ensure that args errors don't advice the user to look in log files
command.Args = argsCheck(ac)
}
command.SetContext(ctx)
}
cmd.AddCommand(commands...)
cmd.PersistentFlags().AddFlagSet(GlobalFlags())
addCompletionCommand(cmd)
cmd.InitDefaultHelpCmd()
addUsageTemplate(cmd)
}
// RunSubcommands is for use as a cobra.Command.RunE for commands that don't do anything themselves
// but have subcommands. In such cases, it is important to set RunE even though there's nothing to
// run, because otherwise cobra will treat that as "success", and it shouldn't be "success" if the
// user typos a command and types something invalid.
func RunSubcommands(cmd *cobra.Command, args []string) error {
// determine if --help was explicitly asked for
var usedHelpFlag bool
for _, arg := range args {
if arg == "--help" || arg == "-h" {
usedHelpFlag = true
}
}
// If there are no args or --help was used, then it's not a legacy
// Telepresence command so we return the help text
if len(args) == 0 || usedHelpFlag {
cmd.HelpFunc()(cmd, args)
return nil
}
if err := checkLegacyCmd(cmd, args); err != nil {
return err
}
return nil
}
// Command returns the top level "telepresence" CLI command.
func Command(ctx context.Context) *cobra.Command {
rootCmd := &cobra.Command{
Use: "telepresence",
Args: PerhapsLegacyCommands,
Short: "Connect your workstation to a Kubernetes cluster",
Long: help,
RunE: RunSubcommands,
SilenceErrors: true, // main() will handle it after .ExecuteContext() returns
SilenceUsage: true, // our FlagErrorFunc will handle it
DisableFlagParsing: true, // Bc of the legacyCommand parsing, see legacy_command.go
}
rootCmd.SetContext(ctx)
AddSubCommands(rootCmd)
rootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
return errcat.User.New(err)
})
return rootCmd
}
func WithSubCommands(ctx context.Context) context.Context {
return util.AddSubCommands(ctx,
connectCommand(), statusCommand(), quitCommand(),
listCommand(), intercept.LeaveCommand(), intercept.Command(),
helmCommand(), uninstallCommand(),
loglevelCommand(), gatherLogsCommand(),
GatherTracesCommand(), PushTracesCommand(),
versionCommand(), ClusterIdCommand(), genYAMLCommand(), vpnDiagCommand(),
configCommand(),
)
}
// argsCheck wraps an PositionalArgs checker in a function that wraps a potential error
// using errcat.User.
func argsCheck(f cobra.PositionalArgs) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if err := f(cmd, args); err != nil {
return errcat.User.New(err)
}
return nil
}
}
func GlobalFlags() *pflag.FlagSet {
flags := pflag.NewFlagSet("", 0)
flags.Bool(
"no-report", false,
"turn off anonymous crash reports and log submission on failure",
)
flags.String(
"output", "default",
"set the output format, supported values are 'json', 'yaml', and 'default'",
)
return flags
}