Repository: stern/stern
Pull request #132 (2021-05-23 03:19:41)

Make cmd testable
This PR makes `cmd/` testable, and adds some basic tests for `cmd`.


Patch:
diff --git a/cmd/cli.go b/cmd/cli.go
deleted file mode 100644
index 1aad4ad..0000000
--- a/cmd/cli.go
+++ /dev/null
@@ -1,446 +0,0 @@
-//   Copyright 2016 Wercker Holding BV
-//
-//   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 cmd
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"log"
-	"os"
-	"regexp"
-	"sort"
-	"text/template"
-	"time"
-
-	survey "github.com/AlecAivazis/survey/v2"
-	"github.com/fatih/color"
-	"github.com/pkg/errors"
-	"github.com/spf13/cobra"
-	"github.com/spf13/pflag"
-	"github.com/stern/stern/stern"
-	"k8s.io/apimachinery/pkg/fields"
-	"k8s.io/apimachinery/pkg/labels"
-)
-
-var (
-	version = "dev"
-	commit  = ""
-	date    = ""
-)
-
-type Options struct {
-	excludePod          string
-	container           string
-	excludeContainer    string
-	containerStates     []string
-	timestamps          bool
-	timezone            string
-	since               time.Duration
-	context             string
-	namespaces          []string
-	kubeConfig          string
-	exclude             []string
-	include             []string
-	initContainers      bool
-	ephemeralContainers bool
-	allNamespaces       bool
-	selector            string
-	fieldSelector       string
-	tail                int64
-	color               string
-	version             bool
-	completion          string
-	template            string
-	output              string
-	prompt              bool
-}
-
-var opts = &Options{
-	color:               "auto",
-	container:           ".*",
-	containerStates:     []string{"running"},
-	initContainers:      true,
-	ephemeralContainers: true,
-	output:              "default",
-	since:               48 * time.Hour,
-	tail:                -1,
-	template:            "",
-	timestamps:          false,
-	timezone:            "Local",
-	prompt:              false,
-}
-
-func Run() {
-	cmd := &cobra.Command{}
-	cmd.Use = "stern pod-query"
-	cmd.Short = "Tail multiple pods and containers from Kubernetes"
-
-	AddFlags(cmd.Flags())
-
-	// Specify custom bash completion function
-	cmd.BashCompletionFunction = bash_completion_func
-	for name, completion := range bash_completion_flags {
-		if cmd.Flag(name) != nil {
-			if cmd.Flag(name).Annotations == nil {
-				cmd.Flag(name).Annotations = map[string][]string{}
-			}
-			cmd.Flag(name).Annotations[cobra.BashCompCustom] = append(
-				cmd.Flag(name).Annotations[cobra.BashCompCustom],
-				completion,
-			)
-		}
-	}
-
-	cmd.RunE = func(cmd *cobra.Command, args []string) error {
-		if opts.version {
-			fmt.Println(buildVersion(version, commit, date))
-			return nil
-		}
-
-		if opts.completion != "" {
-			return runCompletion(opts.completion, cmd)
-		}
-
-		ctx, cancel := context.WithCancel(context.Background())
-		defer cancel()
-
-		narg := len(args)
-		if (narg > 1) || (narg == 0 && opts.selector == "" && opts.fieldSelector == "") && !opts.prompt {
-			return cmd.Help()
-		}
-		config, err := parseConfig(args)
-		if err != nil {
-			log.Println(err)
-			os.Exit(2)
-		}
-
-		if opts.prompt {
-			if err := promptHandler(ctx, config); err != nil {
-				return err
-			}
-		}
-
-		err = stern.Run(ctx, config)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		return nil
-	}
-
-	if err := cmd.Execute(); err != nil {
-		log.Fatal(err)
-	}
-}
-
-// AddFlags adds all the flags used by stern.
-func AddFlags(fs *pflag.FlagSet) {
-	fs.BoolVarP(&opts.allNamespaces, "all-namespaces", "A", opts.allNamespaces, "If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.")
-	fs.StringVar(&opts.color, "color", opts.color, "Force set color output. 'auto':  colorize if tty attached, 'always': always colorize, 'never': never colorize.")
-	fs.StringVar(&opts.completion, "completion", opts.completion, "Output stern command-line completion code for the specified shell. Can be 'bash' or 'zsh'.")
-	fs.StringVarP(&opts.container, "container", "c", opts.container, "Container name when multiple containers in pod. (regular expression)")
-	fs.StringSliceVar(&opts.containerStates, "container-state", opts.containerStates, "Tail containers with state in running, waiting or terminated. To specify multiple states, repeat this or set comma-separated value.")
-	fs.StringVar(&opts.context, "context", opts.context, "Kubernetes context to use. Default to current context configured in kubeconfig.")
-	fs.StringSliceVarP(&opts.exclude, "exclude", "e", opts.exclude, "Log lines to exclude. (regular expression)")
-	fs.StringVarP(&opts.excludeContainer, "exclude-container", "E", opts.excludeContainer, "Container name to exclude when multiple containers in pod. (regular expression)")
-	fs.StringVar(&opts.excludePod, "exclude-pod", opts.excludePod, "Pod name to exclude. (regular expression)")
-	fs.StringSliceVarP(&opts.include, "include", "i", opts.include, "Log lines to include. (regular expression")
-	fs.BoolVar(&opts.initContainers, "init-containers", opts.initContainers, "Include or exclude init containers.")
-	fs.BoolVar(&opts.ephemeralContainers, "ephemeral-containers", opts.ephemeralContainers, "Include or exclude ephemeral containers.")
-	fs.StringVar(&opts.kubeConfig, "kubeconfig", opts.kubeConfig, "Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.")
-	fs.StringVar(&opts.kubeConfig, "kube-config", opts.kubeConfig, "Path to kubeconfig file to use.")
-	_ = fs.MarkDeprecated("kube-config", "Use --kubeconfig instead.")
-	fs.StringSliceVarP(&opts.namespaces, "namespace", "n", opts.namespaces, "Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.")
-	fs.StringVarP(&opts.output, "output", "o", opts.output, "Specify predefined template. Currently support: [default, raw, json]")
-	fs.BoolVarP(&opts.prompt, "prompt", "p", opts.prompt, "Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.")
-	fs.StringVarP(&opts.selector, "selector", "l", opts.selector, "Selector (label query) to filter on. If present, default to \".*\" for the pod-query.")
-	fs.StringVar(&opts.fieldSelector, "field-selector", opts.fieldSelector, "Selector (field query) to filter on. If present, default to \".*\" for the pod-query.")
-	fs.DurationVarP(&opts.since, "since", "s", opts.since, "Return logs newer than a relative duration like 5s, 2m, or 3h.")
-	fs.Int64Var(&opts.tail, "tail", opts.tail, "The number of lines from the end of the logs to show. Defaults to -1, showing all logs.")
-	fs.StringVar(&opts.template, "template", opts.template, "Template to use for log lines, leave empty to use --output flag.")
-	fs.BoolVarP(&opts.timestamps, "timestamps", "t", opts.timestamps, "Print timestamps.")
-	fs.StringVar(&opts.timezone, "timezone", opts.timezone, "Set timestamps to specific timezone.")
-	fs.BoolVarP(&opts.version, "version", "v", opts.version, "Print the version and exit.")
-}
-
-func parseConfig(args []string) (*stern.Config, error) {
-	var podQuery string
-	if len(args) == 0 {
-		podQuery = ".*"
-	} else {
-		podQuery = args[0]
-	}
-	pod, err := regexp.Compile(podQuery)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to compile regular expression from query")
-	}
-
-	var excludePod *regexp.Regexp
-	if opts.excludePod != "" {
-		excludePod, err = regexp.Compile(opts.excludePod)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to compile regular exression for excluded pod query")
-		}
-	}
-
-	container, err := regexp.Compile(opts.container)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to compile regular expression for container query")
-	}
-
-	var excludeContainer *regexp.Regexp
-	if opts.excludeContainer != "" {
-		excludeContainer, err = regexp.Compile(opts.excludeContainer)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to compile regular expression for exclude container query")
-		}
-	}
-
-	var exclude []*regexp.Regexp
-	for _, ex := range opts.exclude {
-		rex, err := regexp.Compile(ex)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to compile regular expression for exclusion filter")
-		}
-
-		exclude = append(exclude, rex)
-	}
-
-	var include []*regexp.Regexp
-	for _, inc := range opts.include {
-		rin, err := regexp.Compile(inc)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to compile regular expression for inclusion filter")
-		}
-
-		include = append(include, rin)
-	}
-
-	containerStates := []stern.ContainerState{}
-	if opts.containerStates != nil {
-		for _, containerStateStr := range makeUnique(opts.containerStates) {
-			containerState, err := stern.NewContainerState(containerStateStr)
-			if err != nil {
-				return nil, err
-			}
-			containerStates = append(containerStates, containerState)
-		}
-	}
-
-	var labelSelector labels.Selector
-	if opts.selector == "" {
-		labelSelector = labels.Everything()
-	} else {
-		labelSelector, err = labels.Parse(opts.selector)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to parse selector as label selector")
-		}
-	}
-
-	var fieldSelector fields.Selector
-	if opts.fieldSelector == "" {
-		fieldSelector = fields.Everything()
-	} else {
-		fieldSelector, err = fields.ParseSelector(opts.fieldSelector)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to parse selector as field selector")
-		}
-	}
-
-	var tailLines *int64
-	if opts.tail != -1 {
-		tailLines = &opts.tail
-	}
-
-	colorFlag := opts.color
-	if colorFlag == "always" {
-		color.NoColor = false
-	} else if colorFlag == "never" {
-		color.NoColor = true
-	} else if colorFlag != "auto" {
-		return nil, errors.New("color should be one of 'always', 'never', or 'auto'")
-	}
-
-	t := opts.template
-	if t == "" {
-		switch opts.output {
-		case "default":
-			if color.NoColor {
-				t = "{{.PodName}} {{.ContainerName}} {{.Message}}"
-				if opts.allNamespaces || len(opts.namespaces) > 1 {
-					t = fmt.Sprintf("{{.Namespace}} %s", t)
-				}
-			} else {
-				t = "{{color .PodColor .PodName}} {{color .ContainerColor .ContainerName}} {{.Message}}"
-				if opts.allNamespaces || len(opts.namespaces) > 1 {
-					t = fmt.Sprintf("{{color .PodColor .Namespace}} %s", t)
-				}
-
-			}
-		case "raw":
-			t = "{{.Message}}"
-		case "json":
-			t = "{{json .}}"
-		}
-		t += "\n"
-	}
-
-	funs := map[string]interface{}{
-		"json": func(in interface{}) (string, error) {
-			b, err := json.Marshal(in)
-			if err != nil {
-				return "", err
-			}
-			return string(b), nil
-		},
-		"color": func(color color.Color, text string) string {
-			return color.SprintFunc()(text)
-		},
-	}
-	template, err := template.New("log").Funcs(funs).Parse(t)
-	if err != nil {
-		return nil, errors.Wrap(err, "unable to parse template")
-	}
-
-	namespaces := []string{}
-	if opts.namespaces != nil {
-		namespaces = makeUnique(opts.namespaces)
-	}
-
-	// --timezone
-	location, err := time.LoadLocation(opts.timezone)
-	if err != nil {
-		return nil, err
-	}
-
-	return &stern.Config{
-		KubeConfig:            opts.kubeConfig,
-		ContextName:           opts.context,
-		Namespaces:            namespaces,
-		PodQuery:              pod,
-		ExcludePodQuery:       excludePod,
-		Timestamps:            opts.timestamps,
-		Location:              location,
-		ContainerQuery:        container,
-		ExcludeContainerQuery: excludeContainer,
-		ContainerStates:       containerStates,
-		Exclude:               exclude,
-		Include:               include,
-		InitContainers:        opts.initContainers,
-		EphemeralContainers:   opts.ephemeralContainers,
-		Since:                 opts.since,
-		AllNamespaces:         opts.allNamespaces,
-		LabelSelector:         labelSelector,
-		FieldSelector:         fieldSelector,
-		TailLines:             tailLines,
-		Template:              template,
-	}, nil
-}
-
-func buildVersion(version, commit, date string) string {
-	result := fmt.Sprintf("version: %s", version)
-
-	if commit != "" {
-		result = fmt.Sprintf("%s\ncommit: %s", result, commit)
-	}
-
-	if date != "" {
-		result = fmt.Sprintf("%s\nbuilt at: %s", result, date)
-	}
-
-	return result
-}
-
-// makeUnique makes items in string slice unique
-func makeUnique(items []string) []string {
-	result := []string{}
-	m := make(map[string]struct{})
-
-	for _, item := range items {
-		if item == "" {
-			continue
-		}
-
-		if _, ok := m[item]; !ok {
-			m[item] = struct{}{}
-			result = append(result, item)
-		}
-	}
-
-	return result
-}
-
-// promptHandler invokes the interactive prompt and updates config.LabelSelector with the selected value.
-func promptHandler(ctx context.Context, config *stern.Config) error {
-	labelsMap, err := stern.List(ctx, config)
-	if err != nil {
-		return err
-	}
-
-	if len(labelsMap) == 0 {
-		log.Fatal("No matching labels.")
-	}
-
-	var choices []string
-
-	for key := range labelsMap {
-		choices = append(choices, key)
-	}
-
-	sort.Strings(choices)
-
-	choice, err := selectPods(choices)
-	if err != nil {
-		return err
-	}
-
-	selector := fmt.Sprintf("%v=%v", labelsMap[choice], choice)
-
-	fmt.Printf("Selector: %v\n", color.BlueString(selector))
-
-	labelSelector, err := labels.Parse(selector)
-	if err != nil {
-		return err
-	}
-
-	config.LabelSelector = labelSelector
-
-	return nil
-}
-
-// selectPods surfaces an interactive prompt for selecting an app.kubernetes.io/instance.
-func selectPods(pods []string) (string, error) {
-	arrow := survey.WithIcons(func(icons *survey.IconSet) {
-		icons.Question.Text = "❯"
-		icons.SelectFocus.Text = "❯"
-		icons.Question.Format = "blue"
-		icons.SelectFocus.Format = "blue"
-	})
-
-	prompt := &survey.Select{
-		Message: "Select \"app.kubernetes.io/instance\" label value:",
-		Options: pods,
-	}
-
-	var pod string
-
-	if err := survey.AskOne(prompt, &pod, arrow); err != nil {
-		return "", err
-	}
-
-	return pod, nil
-}
diff --git a/cmd/cmd.go b/cmd/cmd.go
new file mode 100644
index 0000000..69def35
--- /dev/null
+++ b/cmd/cmd.go
@@ -0,0 +1,375 @@
+//   Copyright 2016 Wercker Holding BV
+//
+//   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 cmd
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"text/template"
+	"time"
+
+	"github.com/fatih/color"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"github.com/stern/stern/stern"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+)
+
+type options struct {
+	genericclioptions.IOStreams
+
+	excludePod          string
+	container           string
+	excludeContainer    string
+	containerStates     []string
+	timestamps          bool
+	timezone            string
+	since               time.Duration
+	context             string
+	namespaces          []string
+	kubeConfig          string
+	exclude             []string
+	include             []string
+	initContainers      bool
+	ephemeralContainers bool
+	allNamespaces       bool
+	selector            string
+	fieldSelector       string
+	tail                int64
+	color               string
+	version             bool
+	completion          string
+	template            string
+	output              string
+	prompt              bool
+	podQuery            string
+}
+
+func NewOptions(streams genericclioptions.IOStreams) *options {
+	return &options{
+		IOStreams: streams,
+
+		color:               "auto",
+		container:           ".*",
+		containerStates:     []string{"running"},
+		initContainers:      true,
+		ephemeralContainers: true,
+		output:              "default",
+		since:               48 * time.Hour,
+		tail:                -1,
+		template:            "",
+		timestamps:          false,
+		timezone:            "Local",
+		prompt:              false,
+	}
+}
+
+func (o *options) Complete(args []string) error {
+	if len(args) > 0 {
+		o.podQuery = args[0]
+	}
+
+	return nil
+}
+
+func (o *options) Validate() error {
+	if !o.prompt && o.podQuery == "" && o.selector == "" && o.fieldSelector == "" {
+		return errors.New("One of pod-query, --selector, --field-selector or --prompt is required")
+	}
+
+	return nil
+}
+
+func (o *options) Run(cmd *cobra.Command) error {
+	config, err := o.sternConfig()
+	if err != nil {
+		return err
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	if o.prompt {
+		if err := promptHandler(ctx, config, o.Out); err != nil {
+			return err
+		}
+	}
+
+	return stern.Run(ctx, config)
+}
+
+func (o *options) sternConfig() (*stern.Config, error) {
+	pod, err := regexp.Compile(o.podQuery)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to compile regular expression from query")
+	}
+
+	var excludePod *regexp.Regexp
+	if o.excludePod != "" {
+		excludePod, err = regexp.Compile(o.excludePod)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to compile regular exression for excluded pod query")
+		}
+	}
+
+	container, err := regexp.Compile(o.container)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to compile regular expression for container query")
+	}
+
+	var excludeContainer *regexp.Regexp
+	if o.excludeContainer != "" {
+		excludeContainer, err = regexp.Compile(o.excludeContainer)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to compile regular expression for exclude container query")
+		}
+	}
+
+	var exclude []*regexp.Regexp
+	for _, ex := range o.exclude {
+		rex, err := regexp.Compile(ex)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to compile regular expression for exclusion filter")
+		}
+
+		exclude = append(exclude, rex)
+	}
+
+	var include []*regexp.Regexp
+	for _, inc := range o.include {
+		rin, err := regexp.Compile(inc)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to compile regular expression for inclusion filter")
+		}
+
+		include = append(include, rin)
+	}
+
+	containerStates := []stern.ContainerState{}
+	if o.containerStates != nil {
+		for _, containerStateStr := range makeUnique(o.containerStates) {
+			containerState, err := stern.NewContainerState(containerStateStr)
+			if err != nil {
+				return nil, err
+			}
+			containerStates = append(containerStates, containerState)
+		}
+	}
+
+	var labelSelector labels.Selector
+	if o.selector == "" {
+		labelSelector = labels.Everything()
+	} else {
+		labelSelector, err = labels.Parse(o.selector)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to parse selector as label selector")
+		}
+	}
+
+	var fieldSelector fields.Selector
+	if o.fieldSelector == "" {
+		fieldSelector = fields.Everything()
+	} else {
+		fieldSelector, err = fields.ParseSelector(o.fieldSelector)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to parse selector as field selector")
+		}
+	}
+
+	var tailLines *int64
+	if o.tail != -1 {
+		tailLines = &o.tail
+	}
+
+	colorFlag := o.color
+	if colorFlag == "always" {
+		color.NoColor = false
+	} else if colorFlag == "never" {
+		color.NoColor = true
+	} else if colorFlag != "auto" {
+		return nil, errors.New("color should be one of 'always', 'never', or 'auto'")
+	}
+
+	t := o.template
+	if t == "" {
+		switch o.output {
+		case "default":
+			if color.NoColor {
+				t = "{{.PodName}} {{.ContainerName}} {{.Message}}"
+				if o.allNamespaces || len(o.namespaces) > 1 {
+					t = fmt.Sprintf("{{.Namespace}} %s", t)
+				}
+			} else {
+				t = "{{color .PodColor .PodName}} {{color .ContainerColor .ContainerName}} {{.Message}}"
+				if o.allNamespaces || len(o.namespaces) > 1 {
+					t = fmt.Sprintf("{{color .PodColor .Namespace}} %s", t)
+				}
+
+			}
+		case "raw":
+			t = "{{.Message}}"
+		case "json":
+			t = "{{json .}}"
+		}
+		t += "\n"
+	}
+
+	funs := map[string]interface{}{
+		"json": func(in interface{}) (string, error) {
+			b, err := json.Marshal(in)
+			if err != nil {
+				return "", err
+			}
+			return string(b), nil
+		},
+		"color": func(color color.Color, text string) string {
+			return color.SprintFunc()(text)
+		},
+	}
+	template, err := template.New("log").Funcs(funs).Parse(t)
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to parse template")
+	}
+
+	namespaces := []string{}
+	if o.namespaces != nil {
+		namespaces = makeUnique(o.namespaces)
+	}
+
+	// --timezone
+	location, err := time.LoadLocation(o.timezone)
+	if err != nil {
+		return nil, err
+	}
+
+	return &stern.Config{
+		KubeConfig:            o.kubeConfig,
+		ContextName:           o.context,
+		Namespaces:            namespaces,
+		PodQuery:              pod,
+		ExcludePodQuery:       excludePod,
+		Timestamps:            o.timestamps,
+		Location:              location,
+		ContainerQuery:        container,
+		ExcludeContainerQuery: excludeContainer,
+		ContainerStates:       containerStates,
+		Exclude:               exclude,
+		Include:               include,
+		InitContainers:        o.initContainers,
+		EphemeralContainers:   o.ephemeralContainers,
+		Since:                 o.since,
+		AllNamespaces:         o.allNamespaces,
+		LabelSelector:         labelSelector,
+		FieldSelector:         fieldSelector,
+		TailLines:             tailLines,
+		Template:              template,
+
+		Out:    o.Out,
+		ErrOut: o.ErrOut,
+	}, nil
+}
+
+// AddFlags adds all the flags used by stern.
+func (o *options) AddFlags(fs *pflag.FlagSet) {
+	fs.BoolVarP(&o.allNamespaces, "all-namespaces", "A", o.allNamespaces, "If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.")
+	fs.StringVar(&o.color, "color", o.color, "Force set color output. 'auto':  colorize if tty attached, 'always': always colorize, 'never': never colorize.")
+	fs.StringVar(&o.completion, "completion", o.completion, "Output stern command-line completion code for the specified shell. Can be 'bash' or 'zsh'.")
+	fs.StringVarP(&o.container, "container", "c", o.container, "Container name when multiple containers in pod. (regular expression)")
+	fs.StringSliceVar(&o.containerStates, "container-state", o.containerStates, "Tail containers with state in running, waiting or terminated. To specify multiple states, repeat this or set comma-separated value.")
+	fs.StringVar(&o.context, "context", o.context, "Kubernetes context to use. Default to current context configured in kubeconfig.")
+	fs.StringSliceVarP(&o.exclude, "exclude", "e", o.exclude, "Log lines to exclude. (regular expression)")
+	fs.StringVarP(&o.excludeContainer, "exclude-container", "E", o.excludeContainer, "Container name to exclude when multiple containers in pod. (regular expression)")
+	fs.StringVar(&o.excludePod, "exclude-pod", o.excludePod, "Pod name to exclude. (regular expression)")
+	fs.StringSliceVarP(&o.include, "include", "i", o.include, "Log lines to include. (regular expression")
+	fs.BoolVar(&o.initContainers, "init-containers", o.initContainers, "Include or exclude init containers.")
+	fs.BoolVar(&o.ephemeralContainers, "ephemeral-containers", o.ephemeralContainers, "Include or exclude ephemeral containers.")
+	fs.StringVar(&o.kubeConfig, "kubeconfig", o.kubeConfig, "Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.")
+	fs.StringVar(&o.kubeConfig, "kube-config", o.kubeConfig, "Path to kubeconfig file to use.")
+	_ = fs.MarkDeprecated("kube-config", "Use --kubeconfig instead.")
+	fs.StringSliceVarP(&o.namespaces, "namespace", "n", o.namespaces, "Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.")
+	fs.StringVarP(&o.output, "output", "o", o.output, "Specify predefined template. Currently support: [default, raw, json]")
+	fs.BoolVarP(&o.prompt, "prompt", "p", o.prompt, "Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.")
+	fs.StringVarP(&o.selector, "selector", "l", o.selector, "Selector (label query) to filter on. If present, default to \".*\" for the pod-query.")
+	fs.StringVar(&o.fieldSelector, "field-selector", o.fieldSelector, "Selector (field query) to filter on. If present, default to \".*\" for the pod-query.")
+	fs.DurationVarP(&o.since, "since", "s", o.since, "Return logs newer than a relative duration like 5s, 2m, or 3h.")
+	fs.Int64Var(&o.tail, "tail", o.tail, "The number of lines from the end of the logs to show. Defaults to -1, showing all logs.")
+	fs.StringVar(&o.template, "template", o.template, "Template to use for log lines, leave empty to use --output flag.")
+	fs.BoolVarP(&o.timestamps, "timestamps", "t", o.timestamps, "Print timestamps.")
+	fs.StringVar(&o.timezone, "timezone", o.timezone, "Set timestamps to specific timezone.")
+	fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.")
+}
+
+func NewSternCmd(stream genericclioptions.IOStreams) *cobra.Command {
+	o := NewOptions(stream)
+
+	cmd := &cobra.Command{
+		Use:   "stern pod-query",
+		Short: "Tail multiple pods and containers from Kubernetes",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			// Output version information and exit
+			if o.version {
+				outputVersionInfo(o.Out)
+				return nil
+			}
+
+			// Output shell completion code for the specified shell and exit
+			if o.completion != "" {
+				return runCompletion(o.completion, cmd, o.Out)
+			}
+
+			if err := o.Complete(args); err != nil {
+				return err
+			}
+
+			if err := o.Validate(); err != nil {
+				return err
+			}
+
+			cmd.SilenceUsage = true
+
+			return o.Run(cmd)
+		},
+	}
+
+	o.AddFlags(cmd.Flags())
+
+	registerCompletionFunction(cmd)
+
+	return cmd
+}
+
+// makeUnique makes items in string slice unique
+func makeUnique(items []string) []string {
+	result := []string{}
+	m := make(map[string]struct{})
+
+	for _, item := range items {
+		if item == "" {
+			continue
+		}
+
+		if _, ok := m[item]; !ok {
+			m[item] = struct{}{}
+			result = append(result, item)
+		}
+	}
+
+	return result
+}
diff --git a/cmd/completion.go b/cmd/flag_completion.go
similarity index 87%
rename from cmd/completion.go
rename to cmd/flag_completion.go
index 9bbdfb1..d038e8d 100644
--- a/cmd/completion.go
+++ b/cmd/flag_completion.go
@@ -17,7 +17,7 @@ package cmd
 import (
 	"bytes"
 	"fmt"
-	"os"
+	"io"
 
 	"github.com/spf13/cobra"
 )
@@ -95,14 +95,14 @@ var bash_completion_flags = map[string]string{
 	"context":   "__stern_kubectl_config_get_contexts",
 }
 
-func runCompletion(shell string, cmd *cobra.Command) error {
+func runCompletion(shell string, cmd *cobra.Command, out io.Writer) error {
 	var err error
 
 	switch shell {
 	case "bash":
-		err = runCompletionBash(cmd)
+		err = runCompletionBash(cmd, out)
 	case "zsh":
-		err = runCompletionZsh(cmd)
+		err = runCompletionZsh(cmd, out)
 	default:
 		err = fmt.Errorf("Unsupported shell type: %q", shell)
 	}
@@ -110,15 +110,15 @@ func runCompletion(shell string, cmd *cobra.Command) error {
 	return err
 }
 
-func runCompletionBash(cmd *cobra.Command) error {
-	return cmd.GenBashCompletion(os.Stdout)
+func runCompletionBash(cmd *cobra.Command, out io.Writer) error {
+	return cmd.GenBashCompletion(out)
 }
 
 // runCompletionZsh is based on `kubectl completion zsh`. This function should
 // be replaced by cobra implementation when cobra itself supports zsh completion.
 // https://github.com/kubernetes/kubernetes/blob/v1.6.1/pkg/kubectl/cmd/completion.go#L136
-func runCompletionZsh(cmd *cobra.Command) error {
-	out := new(bytes.Buffer)
+func runCompletionZsh(cmd *cobra.Command, out io.Writer) error {
+	b := new(bytes.Buffer)
 
 	zshInitialization := `
 __stern_bash_source() {
@@ -245,9 +245,9 @@ __stern_convert_bash_to_zsh() {
     -e "s/\\\$(type${RWORD}/\$(__stern_type/g" \
     <<'BASH_COMPLETION_EOF'
 `
-	out.Write([]byte(zshInitialization))
+	b.Write([]byte(zshInitialization))
 
-	if err := cmd.GenBashCompletion(out); err != nil {
+	if err := cmd.GenBashCompletion(b); err != nil {
 		return err
 	}
 
@@ -256,9 +256,25 @@ BASH_COMPLETION_EOF
 }
 __stern_bash_source <(__stern_convert_bash_to_zsh)
 `
-	out.Write([]byte(zshTail))
+	b.Write([]byte(zshTail))
 
-	fmt.Println(out)
+	fmt.Fprintln(out, b.String())
 
 	return nil
 }
+
+func registerCompletionFunction(cmd *cobra.Command) {
+	// Specify custom bash completion function
+	cmd.BashCompletionFunction = bash_completion_func
+	for name, completion := range bash_completion_flags {
+		if cmd.Flag(name) != nil {
+			if cmd.Flag(name).Annotations == nil {
+				cmd.Flag(name).Annotations = map[string][]string{}
+			}
+			cmd.Flag(name).Annotations[cobra.BashCompCustom] = append(
+				cmd.Flag(name).Annotations[cobra.BashCompCustom],
+				completion,
+			)
+		}
+	}
+}
diff --git a/cmd/flag_prompt.go b/cmd/flag_prompt.go
new file mode 100644
index 0000000..8e39a6f
--- /dev/null
+++ b/cmd/flag_prompt.go
@@ -0,0 +1,75 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"sort"
+
+	"github.com/AlecAivazis/survey/v2"
+	"github.com/fatih/color"
+	"github.com/pkg/errors"
+	"github.com/stern/stern/stern"
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+// promptHandler invokes the interactive prompt and updates config.LabelSelector with the selected value.
+func promptHandler(ctx context.Context, config *stern.Config, out io.Writer) error {
+	labelsMap, err := stern.List(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	if len(labelsMap) == 0 {
+		return errors.New("No matching labels")
+	}
+
+	var choices []string
+
+	for key := range labelsMap {
+		choices = append(choices, key)
+	}
+
+	sort.Strings(choices)
+
+	choice, err := selectPods(choices)
+	if err != nil {
+		return err
+	}
+
+	selector := fmt.Sprintf("%v=%v", labelsMap[choice], choice)
+
+	fmt.Fprintf(out, "Selector: %v\n", color.BlueString(selector))
+
+	labelSelector, err := labels.Parse(selector)
+	if err != nil {
+		return err
+	}
+
+	config.LabelSelector = labelSelector
+
+	return nil
+}
+
+// selectPods surfaces an interactive prompt for selecting an app.kubernetes.io/instance.
+func selectPods(pods []string) (string, error) {
+	arrow := survey.WithIcons(func(icons *survey.IconSet) {
+		icons.Question.Text = "❯"
+		icons.SelectFocus.Text = "❯"
+		icons.Question.Format = "blue"
+		icons.SelectFocus.Format = "blue"
+	})
+
+	prompt := &survey.Select{
+		Message: "Select \"app.kubernetes.io/instance\" label value:",
+		Options: pods,
+	}
+
+	var pod string
+
+	if err := survey.AskOne(prompt, &pod, arrow); err != nil {
+		return "", err
+	}
+
+	return pod, nil
+}
diff --git a/cmd/flag_version.go b/cmd/flag_version.go
new file mode 100644
index 0000000..e2331a9
--- /dev/null
+++ b/cmd/flag_version.go
@@ -0,0 +1,24 @@
+package cmd
+
+import (
+	"fmt"
+	"io"
+)
+
+var (
+	version = "dev"
+	commit  = ""
+	date    = ""
+)
+
+func outputVersionInfo(out io.Writer) {
+	fmt.Fprintf(out, "version: %s\n", version)
+
+	if commit != "" {
+		fmt.Fprintf(out, "commit: %s\n", commit)
+	}
+
+	if date != "" {
+		fmt.Fprintf(out, "built at: %s\n", date)
+	}
+}
diff --git a/go.mod b/go.mod
index 5a608f1..7717254 100644
--- a/go.mod
+++ b/go.mod
@@ -10,5 +10,6 @@ require (
 	github.com/spf13/pflag v1.0.5
 	k8s.io/api v0.21.0
 	k8s.io/apimachinery v0.21.0
+	k8s.io/cli-runtime v0.21.0
 	k8s.io/client-go v0.21.0
 )
diff --git a/go.sum b/go.sum
index f86acca..33b57a9 100644
--- a/go.sum
+++ b/go.sum
@@ -45,13 +45,20 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
 github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
 github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -64,7 +71,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
@@ -75,11 +84,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -89,6 +101,10 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTg
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -98,14 +114,53 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
 github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
 github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
+github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
+github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
+github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
+github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
 github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
 github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
+github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
+github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
+github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
+github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
+github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
 github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw=
+github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
+github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
+github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
+github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
+github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
 github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
+github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
+github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -134,6 +189,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
 github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -152,14 +208,20 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -217,9 +279,17 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
@@ -247,6 +317,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
@@ -261,7 +333,9 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
 github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -285,6 +359,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -294,6 +371,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
 github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@@ -301,9 +379,11 @@ github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzu
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -312,25 +392,38 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI=
+github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc=
+go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -369,6 +462,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -376,9 +470,11 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -417,6 +513,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -426,6 +523,7 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -459,6 +557,7 @@ golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -469,6 +568,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -534,6 +634,7 @@ google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@@ -568,6 +669,7 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@@ -583,6 +685,8 @@ k8s.io/api v0.21.0 h1:gu5iGF4V6tfVCQ/R+8Hc0h7H1JuEhzyEi9S4R5LM8+Y=
 k8s.io/api v0.21.0/go.mod h1:+YbrhBBGgsxbF6o6Kj4KJPJnBmAKuXDeS3E18bgHNVU=
 k8s.io/apimachinery v0.21.0 h1:3Fx+41if+IRavNcKOz09FwEXDBG6ORh6iMsTSelhkMA=
 k8s.io/apimachinery v0.21.0/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY=
+k8s.io/cli-runtime v0.21.0 h1:/V2Kkxtf6x5NI2z+Sd/mIrq4FQyQ8jzZAUD6N5RnN7Y=
+k8s.io/cli-runtime v0.21.0/go.mod h1:XoaHP93mGPF37MkLbjGVYqg3S1MnsFdKtiA/RZzzxOo=
 k8s.io/client-go v0.21.0 h1:n0zzzJsAQmJngpC0IhgFcApZyoGXPrDIAD601HD09ag=
 k8s.io/client-go v0.21.0/go.mod h1:nNBytTF9qPFDEhoqgEPaarobC8QPae13bElIVHzIglA=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
@@ -596,6 +700,10 @@ k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/kustomize/api v0.8.5 h1:bfCXGXDAbFbb/Jv5AhMj2BB8a5VAJuuQ5/KU69WtDjQ=
+sigs.k8s.io/kustomize/api v0.8.5/go.mod h1:M377apnKT5ZHJS++6H4rQoCHmWtt6qTpp3mbe7p6OLY=
+sigs.k8s.io/kustomize/kyaml v0.10.15 h1:dSLgG78KyaxN4HylPXdK+7zB3k7sW6q3IcCmcfKA+aI=
+sigs.k8s.io/kustomize/kyaml v0.10.15/go.mod h1:mlQFagmkm1P+W4lZJbJ/yaxMd8PqMRSC4cPcfUVt5Hg=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
diff --git a/hack/update-readme/update-readme.go b/hack/update-readme/update-readme.go
index d3622b4..9526fdc 100644
--- a/hack/update-readme/update-readme.go
+++ b/hack/update-readme/update-readme.go
@@ -7,9 +7,9 @@ import (
 	"os"
 	"strings"
 
-	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"github.com/stern/stern/cmd"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
 )
 
 const (
@@ -46,13 +46,13 @@ func main() {
 //  `--flag1`, `-f` |                 | This is flag1.
 //  `--flag2`       | `flag2-default` | This is flag2.
 func GenerateFlagsMarkdownTable() string {
-	c := &cobra.Command{}
-	cmd.AddFlags(c.Flags())
-	allFlags := c.NonInheritedFlags()
+	fs := pflag.NewFlagSet("", pflag.ExitOnError)
+	o := cmd.NewOptions(genericclioptions.NewTestIOStreamsDiscard())
+	o.AddFlags(fs)
 
 	flagMaxlen, defaultMaxlen := len(" flag "), len(" default ")
 	allTexts := make([][]string, 0)
-	allFlags.VisitAll(func(flag *pflag.Flag) {
+	fs.VisitAll(func(flag *pflag.Flag) {
 		// won't append deprecated flag
 		if flag.Deprecated != "" {
 			return
@@ -74,7 +74,7 @@ func GenerateFlagsMarkdownTable() string {
 			switch flagTypeName {
 			// convert []string{"aaa", "bbb"} to "aaa,bbb"
 			case "strings":
-				stirngSlice, err := allFlags.GetStringSlice(flag.Name)
+				stirngSlice, err := fs.GetStringSlice(flag.Name)
 				if err != nil {
 					panic(err)
 				}
diff --git a/main.go b/main.go
index 83383ba..05c6fc2 100644
--- a/main.go
+++ b/main.go
@@ -14,8 +14,16 @@
 
 package main
 
-import "github.com/stern/stern/cmd"
+import (
+	"os"
+
+	"github.com/stern/stern/cmd"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+)
 
 func main() {
-	cmd.Run()
+	streams := genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr}
+	if err := cmd.NewSternCmd(streams).Execute(); err != nil {
+		os.Exit(1)
+	}
 }
diff --git a/stern/config.go b/stern/config.go
index e1eadd5..02d0b30 100644
--- a/stern/config.go
+++ b/stern/config.go
@@ -15,6 +15,7 @@
 package stern
 
 import (
+	"io"
 	"regexp"
 	"text/template"
 	"time"
@@ -45,4 +46,7 @@ type Config struct {
 	FieldSelector         fields.Selector
 	TailLines             *int64
 	Template              *template.Template
+
+	Out    io.Writer
+	ErrOut io.Writer
 }
diff --git a/stern/stern.go b/stern/stern.go
index 6aee13a..af6f9f2 100644
--- a/stern/stern.go
+++ b/stern/stern.go
@@ -17,7 +17,6 @@ package stern
 import (
 	"context"
 	"fmt"
-	"os"
 	"sync"
 
 	"github.com/pkg/errors"
@@ -134,7 +133,7 @@ func Run(ctx context.Context, config *Config) error {
 				}
 			}
 
-			tail := NewTail(clientset, p.Node, p.Namespace, p.Pod, p.Container, config.Template, os.Stdout, os.Stderr, &TailOptions{
+			tail := NewTail(clientset, p.Node, p.Namespace, p.Pod, p.Container, config.Template, config.Out, config.ErrOut, &TailOptions{
 				Timestamps:   config.Timestamps,
 				Location:     config.Location,
 				SinceSeconds: int64(config.Since.Seconds()),
@@ -147,7 +146,7 @@ func Run(ctx context.Context, config *Config) error {
 
 			go func(tail *Tail) {
 				if err := tail.Start(ctx); err != nil {
-					fmt.Fprintf(os.Stderr, "unexpected error: %v\n", err)
+					fmt.Fprintf(config.ErrOut, "unexpected error: %v\n", err)
 				}
 			}(tail)
 		}


Test patch:
diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go
new file mode 100644
index 0000000..ffea495
--- /dev/null
+++ b/cmd/cmd_test.go
@@ -0,0 +1,114 @@
+package cmd
+
+import (
+	"strings"
+	"testing"
+
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+)
+
+func TestSternCommand(t *testing.T) {
+	tests := []struct {
+		name string
+		args []string
+		out  string
+	}{
+		{
+			"Output version info with --version",
+			[]string{"--version"},
+			"version: dev",
+		},
+		{
+			"Output completion code for bash with --completion=bash",
+			[]string{"--completion=bash"},
+			"complete -o default -F __start_stern stern",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			streams, _, out, _ := genericclioptions.NewTestIOStreams()
+			cmd := NewSternCmd(streams)
+			cmd.SetArgs(tt.args)
+
+			if err := cmd.Execute(); err != nil {
+				t.Fatal(err)
+			}
+
+			if !strings.Contains(out.String(), tt.out) {
+				t.Errorf("expected to contain %s, but actual %s", tt.out, out.String())
+			}
+		})
+	}
+}
+
+func TestOptionsValidate(t *testing.T) {
+	streams := genericclioptions.NewTestIOStreamsDiscard()
+
+	tests := []struct {
+		name string
+		o    *options
+		err  string
+	}{
+		{
+			"No required options",
+			NewOptions(streams),
+			"One of pod-query, --selector, --field-selector or --prompt is required",
+		},
+		{
+			"Use prompt",
+			func() *options {
+				o := NewOptions(streams)
+				o.prompt = true
+
+				return o
+			}(),
+			"",
+		},
+		{
+			"Specify pod-query",
+			func() *options {
+				o := NewOptions(streams)
+				o.podQuery = "."
+
+				return o
+			}(),
+			"",
+		},
+		{
+			"Specify selector",
+			func() *options {
+				o := NewOptions(streams)
+				o.selector = "app=nginx"
+
+				return o
+			}(),
+			"",
+		},
+		{
+			"Specify fieldSelector",
+			func() *options {
+				o := NewOptions(streams)
+				o.fieldSelector = "spec.nodeName=kind-kind"
+
+				return o
+			}(),
+			"",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.o.Validate()
+			if err == nil {
+				if tt.err != "" {
+					t.Errorf("expected %q err, but actual no err", tt.err)
+				}
+			} else {
+				if tt.err != err.Error() {
+					t.Errorf("expected %q err, but actual %q", tt.err, err)
+				}
+			}
+		})
+	}
+}