From 843522353acc8311c7741309110b1b18ef48f8a8 Mon Sep 17 00:00:00 2001 From: Robin Hahling Date: Thu, 3 Dec 2020 16:19:13 +0100 Subject: [PATCH] cmd: improve command usage message by grouping related flags I wanted to create multiple commits but it turns out to be complicated. Let's summarize what this commit does: - Add `cmd/common/template` where a `Usage()` can be used by commands to generate a custom template based on the provided `pflag.FlagSets`. This allows grouping related flags when displaying help/usage of a command or subcommand. - With the help of `Usage()` described above, the custom usage function for the `observer` subcommand can be dropped. It was quite hacky and easy to get out of sync by adding a flag to the `observe` command and forgetting to update the usage formatting function to indicate to which flag group the newly added flag belongs. The `observe` command now has several defines several `pflag.FlagSet` for each flag group (selectors, filters, formatting, others). - Global flags (ie flags that are persisted from the root command) have been split into to `pflag.FlagSet` in `cmd/common/config`: `GlobalFlags` and `ServerFlags`. The former contains all global flags (ie: debug and config) and the latter all flags related to connecting to a Hubble server instance. - The global flags section is always printed for each command or subcommand as it apply to any command. However, the new `ServerFlags` flag set needs to be added to the help message by any command or subcommand that makes use of it. For the user, this is really convenient as these flags appear in the help message of a command only when relevant instead of always (eg: the `completion` command does not use the server flags and this does not display help for these when running `hubble completion -h`). The `ServerFlags` are nonetheless global in the sense that they are persistent flags added to the root command. This is done for 2 reasons: not breaking the existing behavior and allowing users to use these flags in any order (e.g. `hubble --server foo observe` also works). - Instead of using hardcoded strings for every configuration key in every command where they need to be used, constants have been defined in `cmd/common/config` and shall be used instead. - Defaults for the configuration directory, fallback configuration directory and configuration file have been moved to `pkg/defaults`. Signed-off-by: Robin Hahling --- cmd/common/config/flags.go | 95 +++++++++ cmd/common/config/viper.go | 46 ++++ cmd/common/template/usage.go | 63 ++++++ cmd/common/template/usage_test.go | 86 ++++++++ cmd/config/config.go | 1 + cmd/node/list.go | 15 +- cmd/node/node.go | 13 +- cmd/observe/observe.go | 334 ++++++++++++++++-------------- cmd/observe/observe_usage.go | 118 ----------- cmd/observe/observe_usage_test.go | 57 ----- cmd/peer/peer.go | 65 +----- cmd/peer/watch.go | 72 +++++++ cmd/reflect/reflect.go | 7 + cmd/root.go | 124 ++--------- cmd/status/status.go | 7 + pkg/defaults/defaults.go | 31 +++ 16 files changed, 638 insertions(+), 496 deletions(-) create mode 100644 cmd/common/config/flags.go create mode 100644 cmd/common/config/viper.go create mode 100644 cmd/common/template/usage.go create mode 100644 cmd/common/template/usage_test.go delete mode 100644 cmd/observe/observe_usage.go delete mode 100644 cmd/observe/observe_usage_test.go create mode 100644 cmd/peer/watch.go diff --git a/cmd/common/config/flags.go b/cmd/common/config/flags.go new file mode 100644 index 000000000..16b4eb31a --- /dev/null +++ b/cmd/common/config/flags.go @@ -0,0 +1,95 @@ +// Copyright 2020 Authors of Hubble +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "github.com/cilium/hubble/pkg/defaults" + "github.com/spf13/pflag" +) + +// Keys can be used to retrieve values from GlobalFlags and ServerFlags (e.g. +// when bound to a viper instance). +const ( + // GlobalFlags keys. + KeyConfig = "config" + KeyDebug = "debug" + + // ServerFlags keys. + KeyServer = "server" + KeyTLS = "tls" + KeyTLSAllowInsecure = "tls-allow-insecure" + KeyTLSCACertFiles = "tls-ca-cert-files" + KeyTLSClientCertFile = "tls-client-cert-file" + KeyTLSClientKeyFile = "tls-client-key-file" + KeyTLSServerName = "tls-server-name" + KeyTimeout = "timeout" +) + +// GlobalFlags are flags that apply to any command. +var GlobalFlags = pflag.NewFlagSet("global", pflag.ContinueOnError) + +// ServerFlags are flags that configure how to connect to a Hubble server. +var ServerFlags = pflag.NewFlagSet("server", pflag.ContinueOnError) + +func init() { + initGlobalFlags() + initServerFlags() +} + +func initGlobalFlags() { + GlobalFlags.String(KeyConfig, defaults.ConfigFile, "Optional config file") + GlobalFlags.BoolP(KeyDebug, "D", false, "Enable debug messages") +} + +func initServerFlags() { + ServerFlags.String(KeyServer, defaults.GetSocketPath(), "Address of a Hubble server") + ServerFlags.Duration(KeyTimeout, defaults.DialTimeout, "Hubble server dialing timeout") + ServerFlags.Bool( + KeyTLS, + false, + "Specify that TLS must be used when establishing a connection to a Hubble server.\r\n"+ + "By default, TLS is only enabled if the server address starts with 'tls://'.", + ) + ServerFlags.Bool( + KeyTLSAllowInsecure, + false, + "Allows the client to skip verifying the server's certificate chain and host name.\r\n"+ + "This option is NOT recommended as, in this mode, TLS is susceptible to machine-in-the-middle attacks.\r\n"+ + "See also the 'tls-server-name' option which allows setting the server name.", + ) + ServerFlags.StringSlice( + KeyTLSCACertFiles, + nil, + "Paths to custom Certificate Authority (CA) certificate files."+ + "The files must contain PEM encoded data.", + ) + ServerFlags.String( + KeyTLSClientCertFile, + "", + "Path to the public key file for the client certificate to connect to a Hubble server (implies TLS).\r\n"+ + "The file must contain PEM encoded data.", + ) + ServerFlags.String( + KeyTLSClientKeyFile, + "", + "Path to the private key file for the client certificate to connect a Hubble server (implies TLS).\r\n"+ + "The file must contain PEM encoded data.", + ) + ServerFlags.String( + KeyTLSServerName, + "", + "Specify a server name to verify the hostname on the returned certificate (eg: 'instance.hubble-relay.cilium.io').", + ) +} diff --git a/cmd/common/config/viper.go b/cmd/common/config/viper.go new file mode 100644 index 000000000..8bf520e2b --- /dev/null +++ b/cmd/common/config/viper.go @@ -0,0 +1,46 @@ +// Copyright 2020 Authors of Hubble +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "strings" + + "github.com/cilium/hubble/pkg/defaults" + "github.com/spf13/viper" +) + +// NewViper creates a new viper instance configured for Hubble. +func NewViper() *viper.Viper { + vp := viper.New() + + // read config from a file + vp.SetConfigName("config") // name of config file (without extension) + vp.SetConfigType("yaml") // useful if the given config file does not have the extension in the name + vp.AddConfigPath(".") // look for a config in the working directory first + if defaults.ConfigDir != "" { + vp.AddConfigPath(defaults.ConfigDir) + } + if defaults.ConfigDirFallback != "" { + vp.AddConfigPath(defaults.ConfigDirFallback) + } + + // read config from environment variables + vp.SetEnvPrefix("hubble") // env var must start with HUBBLE_ + // replace - by _ for environment variable names + // (eg: the env var for tls-server-name is TLS_SERVER_NAME) + vp.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + vp.AutomaticEnv() // read in environment variables that match + return vp +} diff --git a/cmd/common/template/usage.go b/cmd/common/template/usage.go new file mode 100644 index 000000000..294284ed1 --- /dev/null +++ b/cmd/common/template/usage.go @@ -0,0 +1,63 @@ +// Copyright 2020 Authors of Hubble +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "fmt" + "strings" + + "github.com/cilium/hubble/cmd/common/config" + "github.com/spf13/pflag" +) + +const ( + header = `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}} + +` + footer = `{{- 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.{{end}} +` +) + +// Usage returns a usage template string with the given sets of flags. +// Each flag set is separated in a new flags section with the flagset name as +// section title. +// When used with cobra commands, the resulting template may be passed as a +// parameter to command.SetUsageTemplate(). +func Usage(flagSets ...*pflag.FlagSet) string { + var b strings.Builder + b.WriteString(header) + for _, fs := range append(flagSets, config.GlobalFlags) { + fmt.Fprintf(&b, "%s Flags:\n", strings.Title(fs.Name())) + fmt.Fprintln(&b, fs.FlagUsages()) + } + // treat the special --help flag separately + b.WriteString("Get help:\n -h, --help Help for any command or subcommand") + b.WriteString(footer) + return b.String() +} diff --git a/cmd/common/template/usage_test.go b/cmd/common/template/usage_test.go new file mode 100644 index 000000000..b9627a4db --- /dev/null +++ b/cmd/common/template/usage_test.go @@ -0,0 +1,86 @@ +// Copyright 2020 Authors of Hubble +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "strings" + "testing" + + "github.com/cilium/hubble/pkg/defaults" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" +) + +func TestUsage(t *testing.T) { + cmd := &cobra.Command{ + Use: "cmd", + Aliases: []string{"and", "conquer"}, + Example: "I'm afraid this is not a good an example.", + Short: "Do foo with bar", + Long: "Do foo with bar and pay attention to baz and more.", + Run: func(_ *cobra.Command, _ []string) { + // noop + }, + } + flags := pflag.NewFlagSet("bar", pflag.ContinueOnError) + flags.String("baz", "", "baz usage") + cmd.Flags().AddFlagSet(flags) + cmd.SetUsageTemplate(Usage(flags)) + + subCmd := &cobra.Command{ + Use: "subcmd", + Run: func(_ *cobra.Command, _ []string) { + // noop + }, + } + cmd.AddCommand(subCmd) + + var out strings.Builder + cmd.SetOut(&out) + cmd.Usage() + + var expect strings.Builder + expect.WriteString(`Usage: + cmd [flags] + cmd [command] + +Aliases: + cmd, and, conquer + +Examples: +I'm afraid this is not a good an example. + +Available Commands: + subcmd + +Bar Flags: + --baz string baz usage + +Global Flags: + --config string Optional config file (default "`) + + expect.WriteString(defaults.ConfigFile) + expect.WriteString(`") + -D, --debug Enable debug messages + +Get help: + -h, --help Help for any command or subcommand + +Use "cmd [command] --help" for more information about a command. +`) + + require.Equal(t, expect.String(), out.String()) +} diff --git a/cmd/config/config.go b/cmd/config/config.go index 13f0e0b43..6327c4c31 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -52,6 +52,7 @@ HUBBLE_TLS_ALLOW_INSECURE and so on.`, return cmd.Help() }, } + configCmd.AddCommand( newGetCommand(vp), newResetCommand(vp), diff --git a/cmd/node/list.go b/cmd/node/list.go index 12fcac566..3675d4802 100644 --- a/cmd/node/list.go +++ b/cmd/node/list.go @@ -25,8 +25,11 @@ import ( observerpb "github.com/cilium/cilium/api/v1/observer" relaypb "github.com/cilium/cilium/api/v1/relay" + "github.com/cilium/hubble/cmd/common/config" "github.com/cilium/hubble/cmd/common/conn" + "github.com/cilium/hubble/cmd/common/template" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" "google.golang.org/grpc" ) @@ -38,7 +41,7 @@ var listOpts struct { } func newListCommand(vp *viper.Viper) *cobra.Command { - cmd := &cobra.Command{ + listCmd := &cobra.Command{ Use: "list", Short: "List Hubble nodes", RunE: func(cmd *cobra.Command, _ []string) error { @@ -53,20 +56,24 @@ func newListCommand(vp *viper.Viper) *cobra.Command { }, } - cmd.Flags().StringVarP( + formattingFlags := pflag.NewFlagSet("Formatting", pflag.ContinueOnError) + formattingFlags.StringVarP( &listOpts.output, "output", "o", "table", `Specify the output format, one of: json: JSON encoding table: Tab-aligned columns wide: Tab-aligned columns with additional information`) - cmd.RegisterFlagCompletionFunc("output", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + listCmd.RegisterFlagCompletionFunc("output", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{ "json", "table", "wide", }, cobra.ShellCompDirectiveDefault }) - return cmd + listCmd.Flags().AddFlagSet(formattingFlags) + + listCmd.SetUsageTemplate(template.Usage(formattingFlags, config.ServerFlags)) + return listCmd } func runList(ctx context.Context, cmd *cobra.Command, conn *grpc.ClientConn) error { diff --git a/cmd/node/node.go b/cmd/node/node.go index 75ee84ee5..a29bc303f 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -15,20 +15,27 @@ package node import ( + "github.com/cilium/hubble/cmd/common/config" + "github.com/cilium/hubble/cmd/common/template" "github.com/spf13/cobra" "github.com/spf13/viper" ) // New creates a new hidden peer command. func New(vp *viper.Viper) *cobra.Command { - cmd := &cobra.Command{ + nodeCmd := &cobra.Command{ Use: "nodes", Aliases: []string{"node"}, Short: "Get information about Hubble nodes", Long: `Get information about Hubble nodes.`, } - cmd.AddCommand( + + // add config.ServerFlags to the help template as these flags are used by + // this command + nodeCmd.SetUsageTemplate(template.Usage(config.ServerFlags)) + + nodeCmd.AddCommand( newListCommand(vp), ) - return cmd + return nodeCmd } diff --git a/cmd/observe/observe.go b/cmd/observe/observe.go index f6a20abd0..61f0f5324 100644 --- a/cmd/observe/observe.go +++ b/cmd/observe/observe.go @@ -27,13 +27,16 @@ import ( pb "github.com/cilium/cilium/api/v1/flow" "github.com/cilium/cilium/api/v1/observer" monitorAPI "github.com/cilium/cilium/pkg/monitor/api" + "github.com/cilium/hubble/cmd/common/config" "github.com/cilium/hubble/cmd/common/conn" + "github.com/cilium/hubble/cmd/common/template" "github.com/cilium/hubble/pkg/defaults" hubprinter "github.com/cilium/hubble/pkg/printer" hubtime "github.com/cilium/hubble/pkg/time" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/timestamp" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -41,22 +44,29 @@ import ( ) var ( - last uint64 - all bool - sinceVar, untilVar string - - jsonOutput bool - compactOutput bool - dictOutput bool - output string - follow bool - ignoreStderr bool - enableIPTranslation bool - nodeName bool + selectorOpts struct { + all bool + last uint64 + since, until string + follow bool + } - printer *hubprinter.Printer + formattingOpts struct { + jsonOutput bool + compactOutput bool + dictOutput bool + output string + + enableIPTranslation bool + nodeName bool + numeric bool + } - numeric bool + otherOpts struct { + ignoreStderr bool + } + + printer *hubprinter.Printer ) var verdicts = []string{ @@ -78,7 +88,7 @@ func New(vp *viper.Viper) *cobra.Command { } func newObserveCmd(vp *viper.Viper, ofilter *observeFilter) *cobra.Command { - observerCmd := &cobra.Command{ + observeCmd := &cobra.Command{ Use: "observe", Short: "Observe flows of a Hubble server", Long: `Observe provides visibility into flow information on the network and @@ -108,96 +118,173 @@ more.`, return nil }, } - observerCmd.Flags().VarP(filterVarP( - "type", "t", ofilter, []string{}, - fmt.Sprintf("Filter by event types TYPE[:SUBTYPE] (%v)", eventTypes()))) - observerCmd.RegisterFlagCompletionFunc("type", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return eventTypes(), cobra.ShellCompDirectiveDefault - }) - - observerCmd.Flags().Uint64Var(&last, "last", 0, fmt.Sprintf("Get last N flows stored in Hubble's buffer (default %d)", defaults.FlowPrintCount)) - observerCmd.Flags().BoolVar(&all, "all", false, "Get all flows stored in Hubble's buffer") - observerCmd.Flags().BoolVarP(&follow, "follow", "f", false, "Follow flows output") - observerCmd.Flags().StringVar(&sinceVar, "since", "", "Filter flows since a specific date (relative or RFC3339)") - observerCmd.Flags().StringVar(&untilVar, "until", "", "Filter flows until a specific date (relative or RFC3339)") - observerCmd.Flags().Var(filterVar( + // selector flags + selectorFlags := pflag.NewFlagSet("selectors", pflag.ContinueOnError) + selectorFlags.BoolVar(&selectorOpts.all, "all", false, "Get all flows stored in Hubble's buffer") + selectorFlags.Uint64Var(&selectorOpts.last, "last", 0, fmt.Sprintf("Get last N flows stored in Hubble's buffer (default %d)", defaults.FlowPrintCount)) + selectorFlags.StringVar(&selectorOpts.since, "since", "", "Filter flows since a specific date (relative or RFC3339)") + selectorFlags.StringVar(&selectorOpts.until, "until", "", "Filter flows until a specific date (relative or RFC3339)") + selectorFlags.BoolVarP(&selectorOpts.follow, "follow", "f", false, "Follow flows output") + observeCmd.Flags().AddFlagSet(selectorFlags) + + // filter flags + filterFlags := pflag.NewFlagSet("filters", pflag.ContinueOnError) + filterFlags.Var(filterVar( "not", ofilter, "Reverses the next filter to be blacklist i.e. --not --from-ip 2.2.2.2")) - observerCmd.Flags().Lookup("not").NoOptDefVal = "true" + filterFlags.Var(filterVar( + "node-name", ofilter, + `Show all flows which match the given node names (e.g. "k8s*", "test-cluster/*.company.com")`)) + filterFlags.Var(filterVar( + "protocol", ofilter, + `Show only flows which match the given L4/L7 flow protocol (e.g. "udp", "http")`)) + filterFlags.VarP(filterVarP( + "type", "t", ofilter, []string{}, + fmt.Sprintf("Filter by event types TYPE[:SUBTYPE] (%v)", eventTypes()))) + filterFlags.Var(filterVar( + "verdict", ofilter, + fmt.Sprintf("Show only flows with this verdict [%s]", strings.Join(verdicts, ", ")), + )) + + filterFlags.Var(filterVar( + "http-status", ofilter, + `Show only flows which match this HTTP status code prefix (e.g. "404", "5+")`)) + filterFlags.Var(filterVar( + "http-method", ofilter, + `Show only flows which match this HTTP method (e.g. "get", "post")`)) + filterFlags.Var(filterVar( + "http-path", ofilter, + `Show only flows which match this HTTP path regular expressions (e.g. "/page/\\d+")`)) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "from-fqdn", ofilter, `Show all flows originating at the given fully qualified domain name (e.g. "*.cilium.io").`)) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "fqdn", ofilter, `Show all flows related to the given fully qualified domain name (e.g. "*.cilium.io").`)) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "to-fqdn", ofilter, `Show all flows terminating at the given fully qualified domain name (e.g. "*.cilium.io").`)) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "from-ip", ofilter, "Show all flows originating at the given IP address.")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "ip", ofilter, "Show all flows related to the given IP address.")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "to-ip", ofilter, "Show all flows terminating at the given IP address.")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "from-pod", ofilter, "Show all flows originating in the given pod name ([namespace/]). If namespace is not provided, 'default' is used")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "pod", ofilter, "Show all flows related to the given pod name ([namespace/]). If namespace is not provided, 'default' is used")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "to-pod", ofilter, "Show all flows terminating in the given pod name ([namespace/]). If namespace is not provided, 'default' is used")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "from-namespace", ofilter, "Show all flows originating in the given Kubernetes namespace.")) - observerCmd.Flags().VarP(filterVarP( + filterFlags.VarP(filterVarP( "namespace", "n", ofilter, nil, "Show all flows related to the given Kubernetes namespace.")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "to-namespace", ofilter, "Show all flows terminating in the given Kubernetes namespace.")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "from-label", ofilter, `Show only flows originating in an endpoint with the given labels (e.g. "key1=value1", "reserved:world")`)) - observerCmd.Flags().VarP(filterVarP( + filterFlags.VarP(filterVarP( "label", "l", ofilter, nil, `Show only flows related to an endpoint with the given labels (e.g. "key1=value1", "reserved:world")`)) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "to-label", ofilter, `Show only flows terminating in an endpoint with given labels (e.g. "key1=value1", "reserved:world")`)) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "from-service", ofilter, "Show all flows originating in the given service ([namespace/]). If namespace is not provided, 'default' is used")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "service", ofilter, "Show all flows related to the given service ([namespace/]). If namespace is not provided, 'default' is used")) - observerCmd.Flags().Var(filterVar( + filterFlags.Var(filterVar( "to-service", ofilter, "Show all flows terminating in the given service ([namespace/]). If namespace is not provided, 'default' is used")) - observerCmd.Flags().Var(filterVar( - "verdict", ofilter, - fmt.Sprintf("Show only flows with this verdict [%s]", strings.Join(verdicts, ", ")), - )) - observerCmd.RegisterFlagCompletionFunc("verdict", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + filterFlags.Var(filterVar( + "from-port", ofilter, + "Show only flows with the given source port (e.g. 8080)")) + filterFlags.Var(filterVar( + "port", ofilter, + "Show only flows with given port in either source or destination (e.g. 8080)")) + filterFlags.Var(filterVar( + "to-port", ofilter, + "Show only flows with the given destination port (e.g. 8080)")) + + filterFlags.Var(filterVar( + "from-identity", ofilter, + "Show all flows originating at an endpoint with the given security identity")) + filterFlags.Var(filterVar( + "identity", ofilter, + "Show all flows related to an endpoint with the given security identity")) + filterFlags.Var(filterVar( + "to-identity", ofilter, + "Show all flows terminating at an endpoint with the given security identity")) + observeCmd.Flags().AddFlagSet(filterFlags) + + formattingFlags := pflag.NewFlagSet("Formatting", pflag.ContinueOnError) + formattingFlags.BoolVarP( + &formattingOpts.jsonOutput, "json", "j", false, "Deprecated. Use '--output json' instead.", + ) + formattingFlags.BoolVar( + &formattingOpts.compactOutput, "compact", false, "Deprecated. Use '--output compact' instead.", + ) + formattingFlags.BoolVar( + &formattingOpts.dictOutput, "dict", false, "Deprecated. Use '--output dict' instead.", + ) + formattingFlags.StringVarP( + &formattingOpts.output, "output", "o", "", + `Specify the output format, one of: + compact: Compact output + dict: Each flow is shown as KEY:VALUE pair + json: JSON encoding + jsonpb: Output each GetFlowResponse according to proto3's JSON mapping + table: Tab-aligned columns`) + formattingFlags.BoolVar( + &formattingOpts.numeric, + "numeric", + false, + "Display all information in numeric form", + ) + formattingFlags.BoolVar( + &formattingOpts.enableIPTranslation, + "ip-translation", + true, + "Translate IP addresses to logical names such as pod name, FQDN, ...", + ) + formattingFlags.BoolVarP(&formattingOpts.nodeName, "print-node-name", "", false, "Print node name in output") + observeCmd.Flags().AddFlagSet(formattingFlags) + + // other flags + otherFlags := pflag.NewFlagSet("other", pflag.ContinueOnError) + otherFlags.BoolVarP( + &otherOpts.ignoreStderr, "silent-errors", "s", false, "Silently ignores errors and warnings") + observeCmd.Flags().AddFlagSet(otherFlags) + + // advanced completion for flags + observeCmd.RegisterFlagCompletionFunc("type", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return eventTypes(), cobra.ShellCompDirectiveDefault + }) + observeCmd.RegisterFlagCompletionFunc("verdict", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return verdicts, cobra.ShellCompDirectiveDefault }) - - observerCmd.Flags().Var(filterVar( - "http-status", ofilter, - `Show only flows which match this HTTP status code prefix (e.g. "404", "5+")`)) - observerCmd.RegisterFlagCompletionFunc("http-status", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + observeCmd.RegisterFlagCompletionFunc("http-status", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { httpStatus := []string{ "100", "101", "102", "103", "200", "201", "202", "203", "204", "205", "206", "207", "208", @@ -213,10 +300,7 @@ more.`, } return httpStatus, cobra.ShellCompDirectiveDefault }) - observerCmd.Flags().Var(filterVar( - "http-method", ofilter, - `Show only flows which match this HTTP method (e.g. "get", "post")`)) - observerCmd.RegisterFlagCompletionFunc("http-method", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + observeCmd.RegisterFlagCompletionFunc("http-method", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{ http.MethodConnect, http.MethodDelete, @@ -229,56 +313,7 @@ more.`, http.MethodTrace, }, cobra.ShellCompDirectiveDefault }) - observerCmd.Flags().Var(filterVar( - "http-path", ofilter, - `Show only flows which match this HTTP path regular expressions (e.g. "/page/\\d+")`)) - - observerCmd.Flags().Var(filterVar( - "protocol", ofilter, - `Show only flows which match the given L4/L7 flow protocol (e.g. "udp", "http")`)) - - observerCmd.Flags().Var(filterVar( - "from-port", ofilter, - "Show only flows with the given source port (e.g. 8080)")) - observerCmd.Flags().Var(filterVar( - "port", ofilter, - "Show only flows with given port in either source or destination (e.g. 8080)")) - observerCmd.Flags().Var(filterVar( - "to-port", ofilter, - "Show only flows with the given destination port (e.g. 8080)")) - - observerCmd.Flags().Var(filterVar( - "from-identity", ofilter, - "Show all flows originating at an endpoint with the given security identity")) - observerCmd.Flags().Var(filterVar( - "identity", ofilter, - "Show all flows related to an endpoint with the given security identity")) - observerCmd.Flags().Var(filterVar( - "to-identity", ofilter, - "Show all flows terminating at an endpoint with the given security identity")) - - observerCmd.Flags().Var(filterVar( - "node-name", ofilter, - `Show all flows which match the given node names (e.g. "k8s*", "test-cluster/*.company.com")`)) - - observerCmd.Flags().BoolVarP( - &jsonOutput, "json", "j", false, "Deprecated. Use '--output json' instead.", - ) - observerCmd.Flags().BoolVar( - &compactOutput, "compact", false, "Deprecated. Use '--output compact' instead.", - ) - observerCmd.Flags().BoolVar( - &dictOutput, "dict", false, "Deprecated. Use '--output dict' instead.", - ) - observerCmd.Flags().StringVarP( - &output, "output", "o", "", - `Specify the output format, one of: - compact: Compact output - dict: Each flow is shown as KEY:VALUE pair - json: JSON encoding - jsonpb: Output each GetFlowResponse according to proto3's JSON mapping - table: Tab-aligned columns`) - observerCmd.RegisterFlagCompletionFunc("output", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + observeCmd.RegisterFlagCompletionFunc("output", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{ "compact", "dict", @@ -287,28 +322,13 @@ more.`, "table", }, cobra.ShellCompDirectiveDefault }) - observerCmd.Flags().BoolVarP( - &ignoreStderr, "silent-errors", "s", false, "Silently ignores errors and warnings") - - observerCmd.Flags().BoolVar( - &numeric, - "numeric", - false, - "Display all information in numeric form", - ) - - observerCmd.Flags().BoolVar( - &enableIPTranslation, - "ip-translation", - true, - "Translate IP addresses to logical names such as pod name, FQDN, ...", - ) - observerCmd.Flags().BoolVarP(&nodeName, "print-node-name", "", false, "Print node name in output") + // default value for when the flag is on the command line without any options + observeCmd.Flags().Lookup("not").NoOptDefVal = "true" - customObserverHelp(observerCmd) + observeCmd.SetUsageTemplate(template.Usage(selectorFlags, filterFlags, formattingFlags, config.ServerFlags, otherFlags)) - return observerCmd + return observeCmd } func handleArgs(ofilter *observeFilter, debug bool) (err error) { @@ -319,17 +339,17 @@ func handleArgs(ofilter *observeFilter, debug bool) (err error) { // initialize the printer with any options that were passed in var opts []hubprinter.Option - if output == "" { // support deprecated output flags if provided - if jsonOutput { - output = "json" - } else if dictOutput { - output = "dict" - } else if compactOutput { - output = "compact" + if formattingOpts.output == "" { // support deprecated output flags if provided + if formattingOpts.jsonOutput { + formattingOpts.output = "json" + } else if formattingOpts.dictOutput { + formattingOpts.output = "dict" + } else if formattingOpts.compactOutput { + formattingOpts.output = "compact" } } - switch output { + switch formattingOpts.output { case "compact": opts = append(opts, hubprinter.Compact()) case "dict": @@ -339,34 +359,34 @@ func handleArgs(ofilter *observeFilter, debug bool) (err error) { case "jsonpb": opts = append(opts, hubprinter.JSONPB()) case "tab", "table": - if follow { + if selectorOpts.follow { return fmt.Errorf("table output format is not compatible with follow mode") } opts = append(opts, hubprinter.Tab()) case "": // no format specified, choose most appropriate format based on // user provided flags - if follow { + if selectorOpts.follow { opts = append(opts, hubprinter.Compact()) } else { opts = append(opts, hubprinter.Tab()) } default: - return fmt.Errorf("invalid output format: %s", output) + return fmt.Errorf("invalid output format: %s", formattingOpts.output) } - if ignoreStderr { + if otherOpts.ignoreStderr { opts = append(opts, hubprinter.IgnoreStderr()) } - if numeric { - enableIPTranslation = false + if formattingOpts.numeric { + formattingOpts.enableIPTranslation = false } - if enableIPTranslation { + if formattingOpts.enableIPTranslation { opts = append(opts, hubprinter.WithIPTranslation()) } if debug { opts = append(opts, hubprinter.WithDebug()) } - if nodeName { + if formattingOpts.nodeName { opts = append(opts, hubprinter.WithNodeName()) } printer = hubprinter.New(opts...) @@ -374,10 +394,10 @@ func handleArgs(ofilter *observeFilter, debug bool) (err error) { } func runObserve(conn *grpc.ClientConn, ofilter *observeFilter) error { - // convert sinceVar into a param for GetFlows + // convert selectorOpts.since into a param for GetFlows var since, until *timestamp.Timestamp - if sinceVar != "" { - st, err := hubtime.FromString(sinceVar) + if selectorOpts.since != "" { + st, err := hubtime.FromString(selectorOpts.since) if err != nil { return fmt.Errorf("failed to parse the since time: %v", err) } @@ -389,8 +409,8 @@ func runObserve(conn *grpc.ClientConn, ofilter *observeFilter) error { // Set the until field if both --since and --until options are specified and --follow // is not specified. If --since is specified but --until is not, the server sets the // --until option to the current timestamp. - if untilVar != "" && !follow { - ut, err := hubtime.FromString(untilVar) + if selectorOpts.until != "" && !selectorOpts.follow { + ut, err := hubtime.FromString(selectorOpts.until) if err != nil { return fmt.Errorf("failed to parse the until time: %v", err) } @@ -403,12 +423,12 @@ func runObserve(conn *grpc.ClientConn, ofilter *observeFilter) error { if since == nil && until == nil { switch { - case all: + case selectorOpts.all: // all is an alias for last=uint64_max - last = ^uint64(0) - case last == 0: + selectorOpts.last = ^uint64(0) + case selectorOpts.last == 0: // no specific parameters were provided, just a vanilla `hubble observe` - last = defaults.FlowPrintCount + selectorOpts.last = defaults.FlowPrintCount } } @@ -425,8 +445,8 @@ func runObserve(conn *grpc.ClientConn, ofilter *observeFilter) error { client := observer.NewObserverClient(conn) req := &observer.GetFlowsRequest{ - Number: last, - Follow: follow, + Number: selectorOpts.last, + Follow: selectorOpts.follow, Whitelist: wl, Blacklist: bl, Since: since, diff --git a/cmd/observe/observe_usage.go b/cmd/observe/observe_usage.go deleted file mode 100644 index f80850462..000000000 --- a/cmd/observe/observe_usage.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2019 Authors of Hubble -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package observe - -import ( - "bytes" - "fmt" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type flagsSection struct { - name string // short one-liner to describe the section - desc string // optional paragraph to preface and deeper explain a section - flags []string // names of flags to include in the section -} - -// customObserverHelp is a function which modifies the usage template -// **specifically** for the `hubble observe` command by providing separation of -// sections for the `Flags:`. -func customObserverHelp(observerCmd *cobra.Command) { - origTpl := observerCmd.UsageTemplate() - observerCmd.SetUsageTemplate(modifyTemplate(origTpl, observerCmd)) -} - -func modifyTemplate(orig string, cmd *cobra.Command) string { - // prepare to take out the `Flags:` section completely - fi := strings.Index(orig, "Flags:") - gfi := strings.Index(orig, "Global Flags:") - - sections := []flagsSection{ - { - name: "Selectors (retrieve data from hubble)", - flags: []string{ - "all", "last", "since", "until", "follow", - }, - }, - { - name: "Filters (limit result set, not all are compatible with each other)", - flags: []string{ - "not", - "ip", "to-ip", "from-ip", - "pod", "to-pod", "from-pod", - "fqdn", "to-fqdn", "from-fqdn", - "label", "to-label", "from-label", - "namespace", "to-namespace", "from-namespace", - "service", "to-service", "from-service", - "port", "to-port", "from-port", - "type", "verdict", "http-status", "http-method", "http-path", "protocol", - "identity", "to-identity", "from-identity", - "node-name", - }, - }, - } - - var b bytes.Buffer - var seen []string // what flags have already been processed - - // go through all sections defined in the config in order - for _, s := range sections { - fmt.Fprintf(&b, "%s:\n", s.name) - if s.desc != "" { - fmt.Fprintf(&b, "\n%s\n\n", s.desc) - } - - fs := &pflag.FlagSet{SortFlags: true} - for _, f := range s.flags { - // extract the actual command flag by name and add to the set - flag := cmd.Flags().Lookup(f) - if flag == nil { - continue - } - fs.AddFlag(flag) - seen = append(seen, f) - } - - // print the usages in the section - fmt.Fprintln(&b, fs.FlagUsages()) - } - - haveSeen := func(f string) bool { - for _, s := range seen { - if s == f { - return true - } - } - return false - } - - // go through the rest of the flags and include them down at the bottom - rest := &pflag.FlagSet{SortFlags: true} - cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { - if haveSeen(f.Name) { - return // ignore seen flags - } - rest.AddFlag(f) - }) - if rest.HasFlags() { - fmt.Fprintln(&b, "Other Flags:") - fmt.Fprintln(&b, rest.FlagUsages()) - } - - return orig[:fi] + b.String() + orig[gfi:] -} diff --git a/cmd/observe/observe_usage_test.go b/cmd/observe/observe_usage_test.go deleted file mode 100644 index 1d4bb5b25..000000000 --- a/cmd/observe/observe_usage_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 Authors of Hubble -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package observe - -import ( - "bytes" - "strings" - "testing" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/require" -) - -func TestObserveUsage(t *testing.T) { - cmd := &cobra.Command{ - Use: "cmd subcmd [foo]", - Run: func(cmd *cobra.Command, args []string) { - // noop - }, - } - cmd.Flags().String("last", "", "last selector") - cmd.Flags().String("to-fqdn", "", "to-fqdn usage") - cmd.Flags().String("verdict", "", "verdict filter") - cmd.Flags().String("something-else", "", "some other flag") - customObserverHelp(cmd) - - var b bytes.Buffer - cmd.SetOut(&b) - cmd.Help() - - require.Equal(t, strings.TrimSpace(`Usage: - cmd subcmd [foo] [flags] - -Selectors (retrieve data from hubble): - --last string last selector - -Filters (limit result set, not all are compatible with each other): - --to-fqdn string to-fqdn usage - --verdict string verdict filter - -Other Flags: - --something-else string some other flag - -Global Flags:`), strings.TrimSpace(b.String())) -} diff --git a/cmd/peer/peer.go b/cmd/peer/peer.go index b42665972..d96a75210 100644 --- a/cmd/peer/peer.go +++ b/cmd/peer/peer.go @@ -15,73 +15,28 @@ package peer import ( - "context" - "fmt" - "io" - - peerpb "github.com/cilium/cilium/api/v1/peer" - "github.com/cilium/hubble/cmd/common/conn" + "github.com/cilium/hubble/cmd/common/config" + "github.com/cilium/hubble/cmd/common/template" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // New creates a new hidden peer command. func New(vp *viper.Viper) *cobra.Command { - cmd := &cobra.Command{ + peerCmd := &cobra.Command{ Use: "peers", Aliases: []string{"peer"}, Short: "Get information about Hubble peers", Long: `Get information about Hubble peers.`, Hidden: true, // this command is only useful for development/debugging purposes } - cmd.AddCommand( - newWatchCommand(vp), - ) - return cmd -} -func newWatchCommand(vp *viper.Viper) *cobra.Command { - return &cobra.Command{ - Use: "watch", - Aliases: []string{"w"}, - Short: "Watch for Hubble peers updates", - Long: `Watch for Hubble peers updates.`, - RunE: func(_ *cobra.Command, _ []string) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hubbleConn, err := conn.New(ctx, vp.GetString("server"), vp.GetDuration("timeout")) - if err != nil { - return err - } - defer hubbleConn.Close() - return runWatch(ctx, peerpb.NewPeerClient(hubbleConn)) - }, - } -} + // add config.ServerFlags to the help template as these flags are used by + // this command + peerCmd.SetUsageTemplate(template.Usage(config.ServerFlags)) -func runWatch(ctx context.Context, client peerpb.PeerClient) error { - b, err := client.Notify(ctx, &peerpb.NotifyRequest{}) - if err != nil { - return err - } - for { - resp, err := b.Recv() - switch err { - case io.EOF, context.Canceled: - return nil - case nil: - tlsServerName := "" - if tls := resp.GetTls(); tls != nil { - tlsServerName = fmt.Sprintf(" (TLS.ServerName: %s)", tls.GetServerName()) - } - fmt.Printf("%-12s %s %s%s\n", resp.GetType(), resp.GetAddress(), resp.GetName(), tlsServerName) - default: - if status.Code(err) == codes.Canceled { - return nil - } - return err - } - } + peerCmd.AddCommand( + newWatchCommand(vp), + ) + return peerCmd } diff --git a/cmd/peer/watch.go b/cmd/peer/watch.go new file mode 100644 index 000000000..ee2fdf256 --- /dev/null +++ b/cmd/peer/watch.go @@ -0,0 +1,72 @@ +// Copyright 2020 Authors of Hubble +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package peer + +import ( + "context" + "fmt" + "io" + + peerpb "github.com/cilium/cilium/api/v1/peer" + "github.com/cilium/hubble/cmd/common/conn" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func newWatchCommand(vp *viper.Viper) *cobra.Command { + return &cobra.Command{ + Use: "watch", + Aliases: []string{"w"}, + Short: "Watch for Hubble peers updates", + Long: `Watch for Hubble peers updates.`, + RunE: func(_ *cobra.Command, _ []string) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hubbleConn, err := conn.New(ctx, vp.GetString("server"), vp.GetDuration("timeout")) + if err != nil { + return err + } + defer hubbleConn.Close() + return runWatch(ctx, peerpb.NewPeerClient(hubbleConn)) + }, + } +} + +func runWatch(ctx context.Context, client peerpb.PeerClient) error { + b, err := client.Notify(ctx, &peerpb.NotifyRequest{}) + if err != nil { + return err + } + for { + resp, err := b.Recv() + switch err { + case io.EOF, context.Canceled: + return nil + case nil: + tlsServerName := "" + if tls := resp.GetTls(); tls != nil { + tlsServerName = fmt.Sprintf(" (TLS.ServerName: %s)", tls.GetServerName()) + } + fmt.Printf("%-12s %s %s%s\n", resp.GetType(), resp.GetAddress(), resp.GetName(), tlsServerName) + default: + if status.Code(err) == codes.Canceled { + return nil + } + return err + } + } +} diff --git a/cmd/reflect/reflect.go b/cmd/reflect/reflect.go index b87c81f52..9c25c6998 100644 --- a/cmd/reflect/reflect.go +++ b/cmd/reflect/reflect.go @@ -19,7 +19,9 @@ import ( "encoding/json" "fmt" + "github.com/cilium/hubble/cmd/common/config" "github.com/cilium/hubble/cmd/common/conn" + "github.com/cilium/hubble/cmd/common/template" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/spf13/cobra" @@ -45,6 +47,11 @@ func New(vp *viper.Viper) *cobra.Command { }, Hidden: true, } + + // add config.ServerFlags to the help template as these flags are used by + // this command + reflectCmd.SetUsageTemplate(template.Usage(config.ServerFlags)) + return reflectCmd } diff --git a/cmd/root.go b/cmd/root.go index 8434f690a..2f5869a08 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,13 +17,13 @@ package cmd import ( "fmt" "os" - "path/filepath" - "strings" + "github.com/cilium/hubble/cmd/common/config" "github.com/cilium/hubble/cmd/common/conn" + "github.com/cilium/hubble/cmd/common/template" "github.com/cilium/hubble/cmd/common/validate" "github.com/cilium/hubble/cmd/completion" - "github.com/cilium/hubble/cmd/config" + cmdConfig "github.com/cilium/hubble/cmd/config" "github.com/cilium/hubble/cmd/node" "github.com/cilium/hubble/cmd/observe" "github.com/cilium/hubble/cmd/peer" @@ -31,45 +31,18 @@ import ( "github.com/cilium/hubble/cmd/status" "github.com/cilium/hubble/cmd/version" "github.com/cilium/hubble/pkg" - "github.com/cilium/hubble/pkg/defaults" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var ( - // defaultConfigDir is the default directory path to store Hubble - // configuration files. - defaultConfigDir string - // fallbackConfigDir is the directory path to store Hubble configuration - // files if defaultConfigDir is unset. Note that it might also be unset. - fallbackConfigDir string - // defaultConfigFile is the path to an optional configuration file. - // It might be unset. - defaultConfigFile string -) - -func init() { - // honor user config dir - if dir, err := os.UserConfigDir(); err == nil { - defaultConfigDir = filepath.Join(dir, "hubble") - } - // fallback to home directory - if dir, err := os.UserHomeDir(); err == nil { - fallbackConfigDir = filepath.Join(dir, ".hubble") - } - - switch { - case defaultConfigDir != "": - defaultConfigFile = filepath.Join(defaultConfigDir, "config.yaml") - case fallbackConfigDir != "": - defaultConfigFile = filepath.Join(fallbackConfigDir, "config.yaml") - } -} - // New create a new root command. func New() *cobra.Command { - vp := newViper() + return NewWithViper(config.NewViper()) +} + +// NewWithViper creates a new root command with the given viper. +func NewWithViper(vp *viper.Viper) *cobra.Command { rootCmd := &cobra.Command{ Use: "hubble", Short: "CLI", @@ -86,64 +59,35 @@ func New() *cobra.Command { } cobra.OnInitialize(func() { - if cfg := vp.GetString("config"); cfg != "" { // enable ability to specify config file via flag + if cfg := vp.GetString(config.KeyConfig); cfg != "" { // enable ability to specify config file via flag vp.SetConfigFile(cfg) } // if a config file is found, read it in. - if err := vp.ReadInConfig(); err == nil && vp.GetBool("debug") { + if err := vp.ReadInConfig(); err == nil && vp.GetBool(config.KeyDebug) { fmt.Fprintln(rootCmd.ErrOrStderr(), "Using config file:", vp.ConfigFileUsed()) } }) flags := rootCmd.PersistentFlags() - flags.String("config", defaultConfigFile, "Optional config file") - flags.BoolP("debug", "D", false, "Enable debug messages") - flags.String("server", defaults.GetSocketPath(), "Address of a Hubble server") - flags.Duration("timeout", defaults.DialTimeout, "Hubble server dialing timeout") - flags.Bool( - "tls", - false, - "Specify that TLS must be used when establishing a connection to a Hubble server.\r\n"+ - "By default, TLS is only enabled if the server address starts with 'tls://'.", - ) - flags.Bool( - "tls-allow-insecure", - false, - "Allows the client to skip verifying the server's certificate chain and host name.\r\n"+ - "This option is NOT recommended as, in this mode, TLS is susceptible to machine-in-the-middle attacks.\r\n"+ - "See also the 'tls-server-name' option which allows setting the server name.", - ) - flags.StringSlice( - "tls-ca-cert-files", - nil, - "Paths to custom Certificate Authority (CA) certificate files."+ - "The files must contain PEM encoded data.", - ) - flags.String( - "tls-client-cert-file", - "", - "Path to the public key file for the client certificate to connect to a Hubble server (implies TLS).\r\n"+ - "The file must contain PEM encoded data.", - ) - flags.String( - "tls-client-key-file", - "", - "Path to the private key file for the client certificate to connect a Hubble server (implies TLS).\r\n"+ - "The file must contain PEM encoded data.", - ) - flags.String( - "tls-server-name", - "", - "Specify a server name to verify the hostname on the returned certificate (eg: 'instance.hubble-relay.cilium.io').", - ) + // config.GlobalFlags can be used with any command + flags.AddFlagSet(config.GlobalFlags) + // config.ServerFlags is added to the root command's persistent flags + // so that "hubble --server foo observe" still works + flags.AddFlagSet(config.ServerFlags) vp.BindPFlags(flags) + // config.ServerFlags is only useful to a subset of commands so do not + // add it by default in the help template + // config.GlobalFlags is always added to the help template as it's global + // to all commands + rootCmd.SetUsageTemplate(template.Usage()) + rootCmd.SetErr(os.Stderr) rootCmd.SetVersionTemplate("{{with .Name}}{{printf \"%s \" .}}{{end}}{{printf \"v%s\" .Version}}\r\n") rootCmd.AddCommand( + cmdConfig.New(vp), completion.New(), - config.New(vp), node.New(vp), observe.New(vp), peer.New(vp), @@ -158,27 +102,3 @@ func New() *cobra.Command { func Execute() error { return New().Execute() } - -// newViper creates a new viper instance configured for Hubble. -func newViper() *viper.Viper { - vp := viper.New() - - // read config from a file - vp.SetConfigName("config") // name of config file (without extension) - vp.SetConfigType("yaml") // useful if the given config file does not have the extension in the name - vp.AddConfigPath(".") // look for a config in the working directory first - if defaultConfigDir != "" { - vp.AddConfigPath(defaultConfigDir) - } - if fallbackConfigDir != "" { - vp.AddConfigPath(fallbackConfigDir) - } - - // read config from environment variables - vp.SetEnvPrefix("hubble") // env var must start with HUBBLE_ - // replace - by _ for environment variable names - // (eg: the env var for tls-server-name is TLS_SERVER_NAME) - vp.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - vp.AutomaticEnv() // read in environment variables that match - return vp -} diff --git a/cmd/status/status.go b/cmd/status/status.go index eb2bac4d4..1418ebc0e 100644 --- a/cmd/status/status.go +++ b/cmd/status/status.go @@ -24,7 +24,9 @@ import ( "github.com/cilium/cilium/api/v1/observer" v1 "github.com/cilium/cilium/pkg/hubble/api/v1" + "github.com/cilium/hubble/cmd/common/config" "github.com/cilium/hubble/cmd/common/conn" + "github.com/cilium/hubble/cmd/common/template" "github.com/cilium/hubble/pkg/defaults" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -50,6 +52,11 @@ connectivity health check.`, return runStatus(hubbleConn) }, } + + // add config.ServerFlags to the help template as these flags are used by + // this command + statusCmd.SetUsageTemplate(template.Usage(config.ServerFlags)) + return statusCmd } diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index 764f129ff..3751c1729 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -16,6 +16,7 @@ package defaults import ( "os" + "path/filepath" "time" ) @@ -43,6 +44,36 @@ const ( socketPath = "unix:///var/run/cilium/hubble.sock" ) +var ( + // ConfigDir is the default directory path to store Hubble + // configuration files. It may be unset. + ConfigDir string + // ConfigDirFallback is the directory path to store Hubble configuration + // files if defaultConfigDir is unset. Note that it may also be unset. + ConfigDirFallback string + // ConfigFile is the path to an optional configuration file. + // It may be unset. + ConfigFile string +) + +func init() { + // honor user config dir + if dir, err := os.UserConfigDir(); err == nil { + ConfigDir = filepath.Join(dir, "hubble") + } + // fallback to home directory + if dir, err := os.UserHomeDir(); err == nil { + ConfigDirFallback = filepath.Join(dir, ".hubble") + } + + switch { + case ConfigDir != "": + ConfigFile = filepath.Join(ConfigDir, "config.yaml") + case ConfigDirFallback != "": + ConfigFile = filepath.Join(ConfigDirFallback, "config.yaml") + } +} + // GetSocketPath returns the default server for status and observe command. func GetSocketPath() string { if path, ok := os.LookupEnv(socketPathKey); ok {