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 {