Skip to content

Commit

Permalink
Add generic cli options (#283)
Browse files Browse the repository at this point in the history
  • Loading branch information
superbrothers committed Nov 1, 2023
1 parent 5445cd5 commit f315819
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 151 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
`--config` | `~/.config/stern/config.yaml` | Path to the stern config file
`--container`, `-c` | `.*` | Container name when multiple containers in pod. (regular expression)
`--container-state` | `all` | Tail containers with state in running, waiting, terminated, or all. 'all' matches all container states. To specify multiple states, repeat this or set comma-separated value.
`--context` | | Kubernetes context to use. Default to current context configured in kubeconfig.
`--context` | | The name of the kubeconfig context to use
`--ephemeral-containers` | `true` | Include or exclude ephemeral containers.
`--exclude`, `-e` | `[]` | Log lines to exclude. (regular expression)
`--exclude-container`, `-E` | `[]` | Container name to exclude when multiple containers in pod. (regular expression)
`--exclude-pod` | `[]` | Pod name to exclude. (regular expression)
`--field-selector` | | Selector (field query) to filter on. If present, default to ".*" for the pod-query.
`--include`, `-i` | `[]` | Log lines to include. (regular expression)
`--init-containers` | `true` | Include or exclude init containers.
`--kubeconfig` | | Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.
`--kubeconfig` | | Path to the kubeconfig file to use for CLI requests.
`--max-log-requests` | `-1` | Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow
`--namespace`, `-n` | | Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.
`--no-follow` | `false` | Exit when all logs have been shown.
Expand All @@ -94,6 +94,7 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
`--output`, `-o` | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]
`--prompt`, `-p` | `false` | Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.
`--selector`, `-l` | | Selector (label query) to filter on. If present, default to ".*" for the pod-query.
`--show-hidden-options` | `false` | Print a list of hidden options.
`--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h.
`--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs.
`--template` | | Template to use for log lines, leave empty to use --output flag.
Expand Down
84 changes: 73 additions & 11 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"

// load all auth plugins
_ "k8s.io/client-go/plugin/pkg/client/auth"
)

// Use "~" to avoid exposing the user name in the help message
var defaultConfigFilePath = "~/.config/stern/config.yaml"

type options struct {
configFlags *genericclioptions.ConfigFlags
genericclioptions.IOStreams

excludePod []string
Expand All @@ -54,9 +60,7 @@ type options struct {
timestamps string
timezone string
since time.Duration
context string
namespaces []string
kubeConfig string
exclude []string
include []string
initContainers bool
Expand All @@ -80,11 +84,20 @@ type options struct {
maxLogRequests int
node string
configFilePath string
showHiddenOptions bool

client kubernetes.Interface
clientConfig clientcmd.ClientConfig
}

func NewOptions(streams genericclioptions.IOStreams) *options {
configFlags := genericclioptions.NewConfigFlags(true)
// stern has its own namespace flag, so disable the one in configFlags
configFlags.Namespace = nil

return &options{
IOStreams: streams,
configFlags: configFlags,
IOStreams: streams,

color: "auto",
container: ".*",
Expand Down Expand Up @@ -119,6 +132,23 @@ func (o *options) Complete(args []string) error {
o.configFilePath = envVar
}

o.clientConfig = o.configFlags.ToRawKubeConfigLoader()

restConfig, err := o.configFlags.ToRESTConfig()
if err != nil {
return err
}

o.client = kubernetes.NewForConfigOrDie(restConfig)

if len(o.namespaces) == 0 {
namespace, _, err := o.clientConfig.Namespace()
if err != nil {
return err
}
o.namespaces = []string{namespace}
}

return nil
}

Expand Down Expand Up @@ -147,12 +177,12 @@ func (o *options) Run(cmd *cobra.Command) error {
defer cancel()

if o.prompt {
if err := promptHandler(ctx, config, o.Out); err != nil {
if err := promptHandler(ctx, o.client, config, o.Out); err != nil {
return err
}
}

return stern.Run(ctx, config)
return stern.Run(ctx, o.client, config)
}

func (o *options) sternConfig() (*stern.Config, error) {
Expand Down Expand Up @@ -257,8 +287,6 @@ func (o *options) sternConfig() (*stern.Config, error) {
}

return &stern.Config{
KubeConfig: o.kubeConfig,
ContextName: o.context,
Namespaces: namespaces,
PodQuery: pod,
ExcludePodQuery: excludePod,
Expand Down Expand Up @@ -351,22 +379,20 @@ func (o *options) overrideFlagSetDefaultFromConfig(fs *pflag.FlagSet) error {

// AddFlags adds all the flags used by stern.
func (o *options) AddFlags(fs *pflag.FlagSet) {
o.addKubernetesFlags(fs)

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', 'zsh' or 'fish'.")
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, terminated, or all. 'all' matches all container states. 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.StringArrayVarP(&o.exclude, "exclude", "e", o.exclude, "Log lines to exclude. (regular expression)")
fs.StringArrayVarP(&o.excludeContainer, "exclude-container", "E", o.excludeContainer, "Container name to exclude when multiple containers in pod. (regular expression)")
fs.StringArrayVar(&o.excludePod, "exclude-pod", o.excludePod, "Pod name to exclude. (regular expression)")
fs.BoolVar(&o.noFollow, "no-follow", o.noFollow, "Exit when all logs have been shown.")
fs.StringArrayVarP(&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.StringVar(&o.node, "node", o.node, "Node name to filter on.")
fs.IntVar(&o.maxLogRequests, "max-log-requests", o.maxLogRequests, "Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow")
Expand All @@ -384,10 +410,39 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&o.configFilePath, "config", o.configFilePath, "Path to the stern config file")
fs.IntVar(&o.verbosity, "verbosity", o.verbosity, "Number of the log level verbosity")
fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.")
fs.BoolVar(&o.showHiddenOptions, "show-hidden-options", o.showHiddenOptions, "Print a list of hidden options.")

fs.Lookup("timestamps").NoOptDefVal = "default"
}

func (o *options) addKubernetesFlags(fs *pflag.FlagSet) {
flagset := pflag.NewFlagSet("", pflag.ExitOnError)
o.configFlags.AddFlags(flagset)
flagset.VisitAll(func(f *pflag.Flag) {
// Hide Kubernetes flags except some
if !(f.Name == "kubeconfig" || f.Name == "context") {
f.Hidden = true
}

// `server` flag in configFlags has `s` shorthand, which is used by stern
// as shorthand for `since` flag, so do not use it.
if f.Name == "server" {
f.Shorthand = ""
}
})
fs.AddFlagSet(flagset)
}

func (o *options) outputHiddenOptions() {
fs := pflag.NewFlagSet("", pflag.ExitOnError)
o.AddFlags(fs)
fs.VisitAll(func(f *pflag.Flag) {
f.Hidden = !f.Hidden
})
fmt.Println("The following options can also be used in stern:")
fs.PrintDefaults()
}

func (o *options) generateTemplate() (*template.Template, error) {
t := o.template
if o.templateFile != "" {
Expand Down Expand Up @@ -588,6 +643,11 @@ func NewSternCmd(stream genericclioptions.IOStreams) (*cobra.Command, error) {
return runCompletion(o.completion, cmd, o.Out)
}

if o.showHiddenOptions {
o.outputHiddenOptions()
return nil
}

if err := o.Complete(args); err != nil {
return err
}
Expand All @@ -607,6 +667,8 @@ func NewSternCmd(stream genericclioptions.IOStreams) (*cobra.Command, error) {
ValidArgsFunction: queryCompletionFunc(o),
}

cmd.SetUsageTemplate(cmd.UsageTemplate() + "\nUse \"stern --show-hidden-options\" for a list of hidden command-line options.\n")

o.AddFlags(cmd.Flags())

if err := registerCompletionFuncForFlags(cmd, o); err != nil {
Expand Down
6 changes: 0 additions & 6 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,6 @@ func TestOptionsSternConfig(t *testing.T) {

defaultConfig := func() *stern.Config {
return &stern.Config{
KubeConfig: "",
ContextName: "",
Namespaces: []string{},
PodQuery: re(""),
ExcludePodQuery: nil,
Expand Down Expand Up @@ -495,8 +493,6 @@ func TestOptionsSternConfig(t *testing.T) {
"change all options",
func() *options {
o := NewOptions(streams)
o.kubeConfig = "kubeconfig1"
o.context = "context1"
o.namespaces = []string{"ns1", "ns2"}
o.podQuery = "query1"
o.excludePod = []string{"exp1", "exp2"}
Expand Down Expand Up @@ -524,8 +520,6 @@ func TestOptionsSternConfig(t *testing.T) {
}(),
func() *stern.Config {
c := defaultConfig()
c.KubeConfig = "kubeconfig1"
c.ContextName = "context1"
c.Namespaces = []string{"ns1", "ns2"}
c.PodQuery = re("query1")
c.ExcludePodQuery = []*regexp.Regexp{re("exp1"), re("exp2")}
Expand Down
40 changes: 18 additions & 22 deletions cmd/flag_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ import (

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/stern/stern/kubernetes"
"github.com/stern/stern/stern"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes"
)

var flagChoices = map[string][]string{
Expand Down Expand Up @@ -99,13 +98,11 @@ func registerCompletionFuncForFlags(cmd *cobra.Command, o *options) error {
// that match the toComplete prefix.
func namespaceCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
clientConfig := kubernetes.NewClientConfig(o.kubeConfig, o.context)
clientset, err := kubernetes.NewClientSet(clientConfig)
if err != nil {
if err := o.Complete(nil); err != nil {
return compError(err)
}

namespaceList, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
namespaceList, err := o.client.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return compError(err)
}
Expand All @@ -125,13 +122,11 @@ func namespaceCompletionFunc(o *options) func(cmd *cobra.Command, args []string,
// that match the toComplete prefix.
func nodeCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
clientConfig := kubernetes.NewClientConfig(o.kubeConfig, o.context)
clientset, err := kubernetes.NewClientSet(clientConfig)
if err != nil {
if err := o.Complete(nil); err != nil {
return compError(err)
}

nodeList, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
nodeList, err := o.client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return compError(err)
}
Expand All @@ -151,14 +146,16 @@ func nodeCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toCo
// that match the toComplete prefix.
func contextCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
clientConfig := kubernetes.NewClientConfig(o.kubeConfig, o.context)
config, err := clientConfig.RawConfig()
if err != nil {
if err := o.Complete(nil); err != nil {
return compError(err)
}

var comps []string
for name := range config.Contexts {
kubeConfig, err := o.clientConfig.RawConfig()
if err != nil {
return compError(err)
}
for name := range kubeConfig.Contexts {
if strings.HasPrefix(name, toComplete) {
comps = append(comps, name)
}
Expand All @@ -172,6 +169,10 @@ func contextCompletionFunc(o *options) func(cmd *cobra.Command, args []string, t
// that match the toComplete prefix.
func queryCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if err := o.Complete(nil); err != nil {
return compError(err)
}

var comps []string
parts := strings.Split(toComplete, "/")
if len(parts) != 2 {
Expand All @@ -191,24 +192,19 @@ func queryCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toC
return compError(errors.New("multiple namespaces are not supported"))
}

clientConfig := kubernetes.NewClientConfig(o.kubeConfig, o.context)
clientset, err := kubernetes.NewClientSet(clientConfig)
if err != nil {
return compError(err)
}
var namespace string
if len(uniqueNamespaces) == 1 {
namespace = uniqueNamespaces[0]
} else {
n, _, err := clientConfig.Namespace()
n, _, err := o.clientConfig.Namespace()
if err != nil {
return compError(err)
}
namespace = n
}

kind, name := parts[0], parts[1]
names, err := retrieveNamesFromResource(context.TODO(), clientset, namespace, kind)
names, err := retrieveNamesFromResource(context.TODO(), o.client, namespace, kind)
if err != nil {
return compError(err)
}
Expand All @@ -226,7 +222,7 @@ func compError(err error) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveError
}

func retrieveNamesFromResource(ctx context.Context, client clientset.Interface, namespace, kind string) ([]string, error) {
func retrieveNamesFromResource(ctx context.Context, client kubernetes.Interface, namespace, kind string) ([]string, error) {
opt := metav1.ListOptions{}
var names []string
switch {
Expand Down
5 changes: 3 additions & 2 deletions cmd/flag_prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
"github.com/pkg/errors"
"github.com/stern/stern/stern"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)

// 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)
func promptHandler(ctx context.Context, client kubernetes.Interface, config *stern.Config, out io.Writer) error {
labelsMap, err := stern.List(ctx, client, config)
if err != nil {
return err
}
Expand Down
4 changes: 4 additions & 0 deletions hack/update-readme/update-readme.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func GenerateFlagsMarkdownTable() string {
return
}

if flag.Hidden {
return
}

flagText := ""
if flag.Shorthand != "" {
flagText = fmt.Sprintf(" `--%s`, `-%s` ", flag.Name, flag.Shorthand)
Expand Down

0 comments on commit f315819

Please sign in to comment.