Skip to content

Commit

Permalink
Add support for the config file (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
superbrothers committed Apr 8, 2023
1 parent 23feff7 commit 2fdc298
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 35 deletions.
78 changes: 46 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,38 +69,39 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
### cli flags

<!-- auto generated cli flags begin --->
flag | default | purpose
-----------------------------|-----------|---------
`--all-namespaces`, `-A` | `false` | If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.
`--color` | `auto` | Force set color output. 'auto': colorize if tty attached, 'always': always colorize, 'never': never colorize.
`--completion` | | Output stern command-line completion code for the specified shell. Can be 'bash', 'zsh' or 'fish'.
`--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.
`--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.
`--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.
`--node` | | Node name to filter on.
`--only-log-lines` | `false` | Print only log lines
`--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.
`--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.
`--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.
`--timestamps`, `-t` | | Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.
`--timezone` | `Local` | Set timestamps to specific timezone.
`--verbosity` | `0` | Number of the log level verbosity
`--version`, `-v` | `false` | Print the version and exit.
flag | default | purpose
-----------------------------|-------------------------------|---------
`--all-namespaces`, `-A` | `false` | If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.
`--color` | `auto` | Force set color output. 'auto': colorize if tty attached, 'always': always colorize, 'never': never colorize.
`--completion` | | Output stern command-line completion code for the specified shell. Can be 'bash', 'zsh' or 'fish'.
`--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.
`--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.
`--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.
`--node` | | Node name to filter on.
`--only-log-lines` | `false` | Print only log lines
`--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.
`--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.
`--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.
`--timestamps`, `-t` | | Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.
`--timezone` | `Local` | Set timestamps to specific timezone.
`--verbosity` | `0` | Number of the log level verbosity
`--version`, `-v` | `false` | Print the version and exit.
<!-- auto generated cli flags end --->

See `stern --help` for details
Expand All @@ -109,6 +110,19 @@ Stern will use the `$KUBECONFIG` environment variable if set. If both the
environment variable and `--kubeconfig` flag are passed the cli flag will be
used.

## config file

You can use the config file to change the default values of stern options. The default config file path is `~/.config/stern/config.yaml`.

```yaml
# <flag name>: <value>
tail: 10
max-log-requests: 999
timestamps: short
```

You can change the config file path with `--config` flag or `STERNCONFIG` environment variable.

### templates

stern supports outputting custom log messages. There are a few predefined
Expand Down
68 changes: 66 additions & 2 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,23 @@ import (
"text/template"
"time"

"k8s.io/klog/v2"

"github.com/fatih/color"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stern/stern/stern"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/klog/v2"
)

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

type options struct {
genericclioptions.IOStreams

Expand Down Expand Up @@ -74,6 +78,7 @@ type options struct {
onlyLogLines bool
maxLogRequests int
node string
configFilePath string
}

func NewOptions(streams genericclioptions.IOStreams) *options {
Expand All @@ -95,6 +100,7 @@ func NewOptions(streams genericclioptions.IOStreams) *options {
prompt: false,
noFollow: false,
maxLogRequests: -1,
configFilePath: defaultConfigFilePath,
}
}

Expand All @@ -107,6 +113,11 @@ func (o *options) Complete(args []string) error {
}
}

envVar, ok := os.LookupEnv("STERNCONFIG")
if ok {
o.configFilePath = envVar
}

return nil
}

Expand Down Expand Up @@ -289,6 +300,54 @@ func (o *options) setVerbosity() error {
return nil
}

// overrideFlagSetDefaultFromConfig overrides the default value of the flagSets
// from the config file
func (o *options) overrideFlagSetDefaultFromConfig(fs *pflag.FlagSet) error {
expanded, err := homedir.Expand(o.configFilePath)
if err != nil {
return err
}

if o.configFilePath == defaultConfigFilePath {
if _, err := os.Stat(expanded); os.IsNotExist(err) {
return nil
}
}

configFile, err := os.Open(expanded)
if err != nil {
return err
}

data := make(map[string]interface{})

if err := yaml.NewDecoder(configFile).Decode(data); err != nil {
return err
}

for name, value := range data {
flag := fs.Lookup(name)
if flag == nil {
// To avoid command execution failure, we only output a warning
// message instead of exiting with an error if an unknown option is
// specified.
klog.Warningf("Unknown option specified in the config file: %s", name)
continue
}

// flag has higher priority than the config file
if flag.Changed {
continue
}

if err := flag.Value.Set(fmt.Sprint(value)); err != nil {
return fmt.Errorf("invalid value %q for %q in the config file: %v", value, name, err)
}
}

return 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.")
Expand Down Expand Up @@ -321,6 +380,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
fs.StringVarP(&o.timestamps, "timestamps", "t", o.timestamps, "Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.")
fs.StringVar(&o.timezone, "timezone", o.timezone, "Set timestamps to specific timezone.")
fs.BoolVar(&o.onlyLogLines, "only-log-lines", o.onlyLogLines, "Print only log lines")
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.")

Expand Down Expand Up @@ -486,6 +546,10 @@ func NewSternCmd(stream genericclioptions.IOStreams) (*cobra.Command, error) {
return err
}

if err := o.overrideFlagSetDefaultFromConfig(cmd.Flags()); err != nil {
return err
}

if err := o.Validate(); err != nil {
return err
}
Expand Down
138 changes: 138 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package cmd

import (
"bytes"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"time"

"github.com/fatih/color"
"github.com/spf13/pflag"
"github.com/stern/stern/stern"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -54,6 +57,47 @@ func TestSternCommand(t *testing.T) {
}
}

func TestOptionsComplete(t *testing.T) {
streams := genericclioptions.NewTestIOStreamsDiscard()

tests := []struct {
name string
env map[string]string
args []string
expectedConfigFilePath string
}{
{
name: "No environment variables",
env: map[string]string{},
args: []string{},
expectedConfigFilePath: defaultConfigFilePath,
},
{
name: "Set STERNCONFIG env to ./config.yaml",
env: map[string]string{
"STERNCONFIG": "./config.yaml",
},
args: []string{},
expectedConfigFilePath: "./config.yaml",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.env {
t.Setenv(k, v)
}

o := NewOptions(streams)
_ = o.Complete(tt.args)

if tt.expectedConfigFilePath != o.configFilePath {
t.Errorf("expected %s for configFilePath, but got %s", tt.expectedConfigFilePath, o.configFilePath)
}
})
}
}

func TestOptionsValidate(t *testing.T) {
streams := genericclioptions.NewTestIOStreamsDiscard()

Expand Down Expand Up @@ -745,3 +789,97 @@ func TestOptionsSternConfig(t *testing.T) {
})
}
}

func TestOptionsOverrideFlagSetDefaultFromConfig(t *testing.T) {
orig := defaultConfigFilePath
defer func() {
defaultConfigFilePath = orig
}()

defaultConfigFilePath = "./config.yaml"
wd, _ := os.Getwd()

tests := []struct {
name string
flagConfigFilePathValue string
flagTailValue string
expectedTailValue int64
wantErr bool
}{
{
name: "--config=testdata/config-tail1.yaml",
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail1.yaml"),
expectedTailValue: 1,
wantErr: false,
},
{
name: "--config=config-not-exist.yaml",
flagConfigFilePathValue: filepath.Join(wd, "config-not-exist.yaml"),
wantErr: true,
},
{
name: "--config=config-invalid.yaml",
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-invalid.yaml"),
wantErr: true,
},
{
name: "--config=config-unknown-option.yaml",
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-unknown-option.yaml"),
expectedTailValue: 1,
wantErr: false,
},
{
name: "--config=config-tail-invalid-value.yaml",
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail-invalid-value.yaml"),
wantErr: true,
},
{
name: "config file path is not specified and config file does not exist",
expectedTailValue: -1,
wantErr: false,
},
{
name: "--config=testdata/config-tail1.yaml and --tail=2",
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail1.yaml"),
flagTailValue: "2",
expectedTailValue: 2,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := NewOptions(genericclioptions.NewTestIOStreamsDiscard())
fs := pflag.NewFlagSet("", pflag.ExitOnError)
o.AddFlags(fs)

args := []string{}
if tt.flagConfigFilePathValue != "" {
args = append(args, "--config="+tt.flagConfigFilePathValue)
}
if tt.flagTailValue != "" {
args = append(args, "--tail="+tt.flagTailValue)
}

if err := fs.Parse(args); err != nil {
t.Fatal(err)
}

err := o.overrideFlagSetDefaultFromConfig(fs)
if tt.wantErr {
if err == nil {
t.Error("expected err, but got nil")
}
return
}

if err != nil {
t.Errorf("unexpected err: %v", err)
}

if tt.expectedTailValue != o.tail {
t.Errorf("expected %d for tail, but got %d", tt.expectedTailValue, o.tail)
}
})
}
}
1 change: 1 addition & 0 deletions cmd/testdata/config-invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is invalid config file
1 change: 1 addition & 0 deletions cmd/testdata/config-tail-invalid-value.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tail: invalid
1 change: 1 addition & 0 deletions cmd/testdata/config-tail1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tail: 1
2 changes: 2 additions & 0 deletions cmd/testdata/config-unknown-option.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
unknown: 999
tail: 1

0 comments on commit 2fdc298

Please sign in to comment.