From 534b68b54b7d85f70b2b30bc49f7d693dcd9ddc8 Mon Sep 17 00:00:00 2001 From: Matthias Diester Date: Thu, 1 Jul 2021 14:00:26 +0200 Subject: [PATCH] Fix `KUBECONFIG` default behaviour So far, the `havener` CLI did not behave the same way as `kubectl`, when it comes to the Kubernetes configuration usage. If the `KUBECONFIG` environment variable is set, this should always take precedence over the command line flag or the default system location for the Kubernetes configuration. Unify usage of `havener.NewHavener` function call. Introduce options to `NewHavener` function to be able to override settings. Add check for `KUBECONFIG` environment variable. --- internal/cmd/events.go | 25 +++++++--------------- internal/cmd/logs.go | 14 ++++++------- internal/cmd/nexec.go | 15 +++++++------- internal/cmd/pexec.go | 15 +++++++------- internal/cmd/root.go | 12 ++++++----- internal/cmd/top.go | 2 +- internal/cmd/watch.go | 2 +- pkg/havener/common.go | 40 ++++++++++++++--------------------- pkg/havener/havener.go | 47 +++++++++++++++++++++++++++--------------- 9 files changed, 85 insertions(+), 87 deletions(-) diff --git a/internal/cmd/events.go b/internal/cmd/events.go index c08d81d1..3676f5c1 100644 --- a/internal/cmd/events.go +++ b/internal/cmd/events.go @@ -26,7 +26,6 @@ import ( "time" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/gonvenience/bunt" "github.com/gonvenience/wrap" @@ -56,15 +55,12 @@ var eventsCmd = &cobra.Command{ SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { - kubeConfig := viper.GetString("kubeconfig") - switch { - case len(kubeConfig) > 0: - return retrieveClusterEvents(kubeConfig) - - default: - cmd.Usage() + hvnr, err := havener.NewHavener(havener.KubeConfig(kubeConfig)) + if err != nil { + return wrap.Error(err, "unable to get access to cluster") } - return nil + + return retrieveClusterEvents(hvnr) }, } @@ -74,13 +70,8 @@ func init() { eventsCmd.PersistentFlags().StringVarP(&namespaceFilter, "namespace", "n", "", "Filter events for specific namespace") } -func retrieveClusterEvents(kubeConfig string) error { - client, _, err := havener.OutOfClusterAuthentication(kubeConfig) - if err != nil { - return wrap.Error(err, "failed to access cluster") - } - - namespaces, err := havener.ListNamespaces(client) +func retrieveClusterEvents(hvnr havener.Havener) error { + namespaces, err := havener.ListNamespaces(hvnr.Client()) if err != nil { return wrap.Error(err, "failed to get a list of namespaces") } @@ -96,7 +87,7 @@ func retrieveClusterEvents(kubeConfig string) error { } go func() error { - watcher, err := client.CoreV1().Events(namespace).Watch(context.TODO(), metav1.ListOptions{}) + watcher, err := hvnr.Client().CoreV1().Events(namespace).Watch(context.TODO(), metav1.ListOptions{}) if err != nil { return wrap.Error(err, "failed to setup event watcher") } diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index 8df2e8b5..40d94519 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -54,7 +54,12 @@ The download includes all deployment YAMLs of the pods and the describe output.` SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { - return retrieveClusterLogs() + hvnr, err := havener.NewHavener(havener.KubeConfig(kubeConfig)) + if err != nil { + return wrap.Error(err, "unable to get access to cluster") + } + + return retrieveClusterLogs(hvnr) }, } @@ -67,12 +72,7 @@ func init() { logsCmd.PersistentFlags().IntVar(¶llelDownloads, "parallel", 64, "number of parallel download jobs") } -func retrieveClusterLogs() error { - hvnr, err := havener.NewHavener() - if err != nil { - return wrap.Error(err, "unable to get access to cluster") - } - +func retrieveClusterLogs(hvnr havener.Havener) error { var commonText string if excludeConfigFiles { commonText = "log files" diff --git a/internal/cmd/nexec.go b/internal/cmd/nexec.go index 75c0d6dc..ab45ce41 100644 --- a/internal/cmd/nexec.go +++ b/internal/cmd/nexec.go @@ -81,7 +81,12 @@ all nodes automatically. SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { - return execInClusterNodes(args) + hvnr, err := havener.NewHavener(havener.KubeConfig(kubeConfig)) + if err != nil { + return wrap.Error(err, "unable to get access to cluster") + } + + return execInClusterNodes(hvnr, args) }, } @@ -95,16 +100,12 @@ func init() { nodeExecCmd.PersistentFlags().IntVar(&nodeExecMaxParallel, "max-parallel", 0, "number of parallel executions (defaults to number of nodes)") } -func execInClusterNodes(args []string) error { - hvnr, err := havener.NewHavener() - if err != nil { - return wrap.Error(err, "unable to get access to cluster") - } - +func execInClusterNodes(hvnr havener.Havener, args []string) error { var ( nodes []corev1.Node input string command []string + err error ) switch { diff --git a/internal/cmd/pexec.go b/internal/cmd/pexec.go index 4f8a7b5d..f3670a41 100644 --- a/internal/cmd/pexec.go +++ b/internal/cmd/pexec.go @@ -76,7 +76,12 @@ all pods in all namespaces automatically. SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { - return execInClusterPods(args) + hvnr, err := havener.NewHavener(havener.KubeConfig(kubeConfig)) + if err != nil { + return wrap.Error(err, "unable to get access to cluster") + } + + return execInClusterPods(hvnr, args) }, } @@ -87,17 +92,13 @@ func init() { podExecCmd.PersistentFlags().BoolVar(&podExecBlock, "block", false, "show distributed shell output as block for each pod") } -func execInClusterPods(args []string) error { - hvnr, err := havener.NewHavener() - if err != nil { - return wrap.Error(err, "unable to get access to cluster") - } - +func execInClusterPods(hvnr havener.Havener, args []string) error { var ( podMap map[*corev1.Pod][]string countContainers int input string command []string + err error ) switch { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 5cc21fbf..8b472a7d 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -25,10 +25,10 @@ import ( "fmt" "net/url" "os" - "path/filepath" "runtime/debug" "strings" + "github.com/homeport/havener/pkg/havener" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -38,6 +38,8 @@ import ( "github.com/gonvenience/wrap" ) +var kubeConfig string + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "havener", @@ -96,15 +98,16 @@ func Execute() { } func init() { - home, err := os.UserHomeDir() + kubeConfigDefault, err := havener.KubeConfigDefault() if err != nil { - panic(wrap.Error(err, "unable to get home directory")) + panic(err) } rootCmd.Flags().SortFlags = false rootCmd.PersistentFlags().SortFlags = false - rootCmd.PersistentFlags().String("kubeconfig", filepath.Join(home, ".kube", "config"), "Kubernetes configuration file") + rootCmd.PersistentFlags().StringVar(&kubeConfig, "kubeconfig", kubeConfigDefault, "Kubernetes configuration file") + rootCmd.PersistentFlags().Int("terminal-width", -1, "disable autodetection and specify an explicit terminal width") rootCmd.PersistentFlags().Int("terminal-height", -1, "disable autodetection and specify an explicit terminal height") @@ -116,7 +119,6 @@ func init() { rootCmd.PersistentFlags().Bool("trace", false, "trace output - level 6") // Bind environment variables to CLI flags - viper.BindPFlag("kubeconfig", rootCmd.PersistentFlags().Lookup("kubeconfig")) viper.BindPFlag("TERMINAL_WIDTH", rootCmd.PersistentFlags().Lookup("terminal-width")) viper.BindPFlag("TERMINAL_HEIGHT", rootCmd.PersistentFlags().Lookup("terminal-height")) diff --git a/internal/cmd/top.go b/internal/cmd/top.go index 07c52443..51ea7f35 100644 --- a/internal/cmd/top.go +++ b/internal/cmd/top.go @@ -70,7 +70,7 @@ cluster as well as a list per node. topCmdSettings.cycles = 1 } - hvnr, err := havener.NewHavener() + hvnr, err := havener.NewHavener(havener.KubeConfig(kubeConfig)) if err != nil { return err } diff --git a/internal/cmd/watch.go b/internal/cmd/watch.go index 80ae9203..afb835d6 100644 --- a/internal/cmd/watch.go +++ b/internal/cmd/watch.go @@ -48,7 +48,7 @@ var watchCmd = &cobra.Command{ Short: "Watch status of all pods in all namespaces", Long: `Continuesly creates a list of all pods in all namespaces.`, RunE: func(cmd *cobra.Command, args []string) error { - hvnr, err := havener.NewHavener() + hvnr, err := havener.NewHavener(havener.KubeConfig(kubeConfig)) if err != nil { return err } diff --git a/pkg/havener/common.go b/pkg/havener/common.go index b8ee47ee..06a158c2 100644 --- a/pkg/havener/common.go +++ b/pkg/havener/common.go @@ -21,44 +21,42 @@ package havener import ( - "flag" "fmt" "io/ioutil" "os" + "path/filepath" "strings" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" //from https://github.com/kubernetes/client-go/issues/345 - "github.com/spf13/viper" + "github.com/gonvenience/wrap" "gopkg.in/yaml.v3" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) -var kubeconfig *string - -func getKubeConfig() string { - if kubeconfig == nil { - if home := HomeDir(); home != "" { - kubeconfig = flag.String("kubeconfig", viper.GetString("kubeconfig"), "(optional) absolute path to the kubeconfig file") - } - flag.Parse() +// KubeConfigDefault returns assumed default locaation of the Kubernetes +// configuration, which is expected to be `$HOME/.kube/config`. +func KubeConfigDefault() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", wrap.Error(err, "unable to get home directory") } - return *kubeconfig + return filepath.Join(home, ".kube", "config"), nil } -//OutOfClusterAuthentication for kube authentication from the outside -func OutOfClusterAuthentication(kubeConfig string) (*kubernetes.Clientset, *rest.Config, error) { - logf(Verbose, "Connecting to Kubernetes cluster...") - +// outOfClusterAuthentication for kube authentication from the outside +func outOfClusterAuthentication(kubeConfig string) (*kubernetes.Clientset, *rest.Config, error) { if kubeConfig == "" { - kubeConfig = getKubeConfig() + return nil, nil, fmt.Errorf("no kube config supplied") } + logf(Verbose, "Connecting to Kubernetes cluster...") + // BuildConfigFromFlags is a helper function that builds configs from a master // url or a kubeconfig filepath. config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) @@ -73,14 +71,6 @@ func OutOfClusterAuthentication(kubeConfig string) (*kubernetes.Clientset, *rest return clientset, config, err } -// HomeDir returns the HOME env key -func HomeDir() string { - if h := os.Getenv("HOME"); h != "" { - return h - } - return os.Getenv("USERPROFILE") // windows -} - func isSystemNamespace(namespace string) bool { switch namespace { case "default", "kube-system", "ibm-system": diff --git a/pkg/havener/havener.go b/pkg/havener/havener.go index 87f03a2a..cbad4327 100644 --- a/pkg/havener/havener.go +++ b/pkg/havener/havener.go @@ -32,6 +32,7 @@ package havener import ( "fmt" "io" + "os" "time" "github.com/gonvenience/wrap" @@ -98,6 +99,16 @@ type Havener interface { NodeExec(node corev1.Node, containerImage string, timeoutSeconds int, command []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, tty bool) error } +// Option provides a way to set specific settings for creating the Havener setup +type Option func(*Hvnr) + +// KubeConfig is an option to set a specific Kubernetes configuration path +func KubeConfig(kubeConfig string) Option { + return func(h *Hvnr) { + h.kubeConfigPath = kubeConfig + } +} + // NewHavenerFromFields returns a new Havener handle using the provided // input arguments (use for unit testing only) func NewHavenerFromFields(client kubernetes.Interface, restconfig *rest.Config, clusterName string, kubeConfigPath string) *Hvnr { @@ -110,35 +121,37 @@ func NewHavenerFromFields(client kubernetes.Interface, restconfig *rest.Config, } // NewHavener returns a new Havener handle to perform cluster actions -func NewHavener(kubeConfigs ...string) (*Hvnr, error) { - var kubeConfigPath string - switch len(kubeConfigs) { - case 0: - kubeConfigPath = getKubeConfig() +func NewHavener(opts ...Option) (hvnr *Hvnr, err error) { + hvnr = &Hvnr{} + for _, opt := range opts { + opt(hvnr) + } - case 1: - kubeConfigPath = kubeConfigs[0] + // In case `KUBECONFIG` environment variable is set, this will take + // precedence over command line flag or default value + if value, ok := os.LookupEnv("KUBECONFIG"); ok { + hvnr.kubeConfigPath = value + } - default: - return nil, fmt.Errorf("multiple Kubernetes configurations are currently not supported") + // In case there is no Kubernetes configuration set, use the default + if hvnr.kubeConfigPath == "" { + hvnr.kubeConfigPath, err = KubeConfigDefault() + if err != nil { + return nil, wrap.Error(err, "failed to look-up default kube config") + } } - client, restconfig, err := OutOfClusterAuthentication(kubeConfigPath) + hvnr.client, hvnr.restconfig, err = outOfClusterAuthentication(hvnr.kubeConfigPath) if err != nil { return nil, wrap.Error(err, "unable to get access to cluster") } - clusterName, err := clusterName(kubeConfigPath) + hvnr.clusterName, err = clusterName(hvnr.kubeConfigPath) if err != nil { return nil, wrap.Error(err, "unable to get cluster name") } - return &Hvnr{ - client: client, - restconfig: restconfig, - clusterName: clusterName, - kubeConfigPath: kubeConfigPath, - }, nil + return hvnr, nil } // ClusterName returns the name of the currently configured cluster