Skip to content

Commit 2fdc298

Browse files
Add support for the config file (#254)
1 parent 23feff7 commit 2fdc298

File tree

9 files changed

+259
-35
lines changed

9 files changed

+259
-35
lines changed

README.md

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -69,38 +69,39 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
6969
### cli flags
7070

7171
<!-- auto generated cli flags begin --->
72-
flag | default | purpose
73-
-----------------------------|-----------|---------
74-
`--all-namespaces`, `-A` | `false` | If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.
75-
`--color` | `auto` | Force set color output. 'auto': colorize if tty attached, 'always': always colorize, 'never': never colorize.
76-
`--completion` | | Output stern command-line completion code for the specified shell. Can be 'bash', 'zsh' or 'fish'.
77-
`--container`, `-c` | `.*` | Container name when multiple containers in pod. (regular expression)
78-
`--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.
79-
`--context` | | Kubernetes context to use. Default to current context configured in kubeconfig.
80-
`--ephemeral-containers` | `true` | Include or exclude ephemeral containers.
81-
`--exclude`, `-e` | `[]` | Log lines to exclude. (regular expression)
82-
`--exclude-container`, `-E` | `[]` | Container name to exclude when multiple containers in pod. (regular expression)
83-
`--exclude-pod` | `[]` | Pod name to exclude. (regular expression)
84-
`--field-selector` | | Selector (field query) to filter on. If present, default to ".*" for the pod-query.
85-
`--include`, `-i` | `[]` | Log lines to include. (regular expression)
86-
`--init-containers` | `true` | Include or exclude init containers.
87-
`--kubeconfig` | | Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.
88-
`--max-log-requests` | `-1` | Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow
89-
`--namespace`, `-n` | | Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.
90-
`--no-follow` | `false` | Exit when all logs have been shown.
91-
`--node` | | Node name to filter on.
92-
`--only-log-lines` | `false` | Print only log lines
93-
`--output`, `-o` | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]
94-
`--prompt`, `-p` | `false` | Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.
95-
`--selector`, `-l` | | Selector (label query) to filter on. If present, default to ".*" for the pod-query.
96-
`--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h.
97-
`--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs.
98-
`--template` | | Template to use for log lines, leave empty to use --output flag.
99-
`--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.
100-
`--timestamps`, `-t` | | Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.
101-
`--timezone` | `Local` | Set timestamps to specific timezone.
102-
`--verbosity` | `0` | Number of the log level verbosity
103-
`--version`, `-v` | `false` | Print the version and exit.
72+
flag | default | purpose
73+
-----------------------------|-------------------------------|---------
74+
`--all-namespaces`, `-A` | `false` | If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.
75+
`--color` | `auto` | Force set color output. 'auto': colorize if tty attached, 'always': always colorize, 'never': never colorize.
76+
`--completion` | | Output stern command-line completion code for the specified shell. Can be 'bash', 'zsh' or 'fish'.
77+
`--config` | `~/.config/stern/config.yaml` | Path to the stern config file
78+
`--container`, `-c` | `.*` | Container name when multiple containers in pod. (regular expression)
79+
`--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.
80+
`--context` | | Kubernetes context to use. Default to current context configured in kubeconfig.
81+
`--ephemeral-containers` | `true` | Include or exclude ephemeral containers.
82+
`--exclude`, `-e` | `[]` | Log lines to exclude. (regular expression)
83+
`--exclude-container`, `-E` | `[]` | Container name to exclude when multiple containers in pod. (regular expression)
84+
`--exclude-pod` | `[]` | Pod name to exclude. (regular expression)
85+
`--field-selector` | | Selector (field query) to filter on. If present, default to ".*" for the pod-query.
86+
`--include`, `-i` | `[]` | Log lines to include. (regular expression)
87+
`--init-containers` | `true` | Include or exclude init containers.
88+
`--kubeconfig` | | Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.
89+
`--max-log-requests` | `-1` | Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow
90+
`--namespace`, `-n` | | Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.
91+
`--no-follow` | `false` | Exit when all logs have been shown.
92+
`--node` | | Node name to filter on.
93+
`--only-log-lines` | `false` | Print only log lines
94+
`--output`, `-o` | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]
95+
`--prompt`, `-p` | `false` | Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.
96+
`--selector`, `-l` | | Selector (label query) to filter on. If present, default to ".*" for the pod-query.
97+
`--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h.
98+
`--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs.
99+
`--template` | | Template to use for log lines, leave empty to use --output flag.
100+
`--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.
101+
`--timestamps`, `-t` | | Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.
102+
`--timezone` | `Local` | Set timestamps to specific timezone.
103+
`--verbosity` | `0` | Number of the log level verbosity
104+
`--version`, `-v` | `false` | Print the version and exit.
104105
<!-- auto generated cli flags end --->
105106

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

113+
## config file
114+
115+
You can use the config file to change the default values of stern options. The default config file path is `~/.config/stern/config.yaml`.
116+
117+
```yaml
118+
# <flag name>: <value>
119+
tail: 10
120+
max-log-requests: 999
121+
timestamps: short
122+
```
123+
124+
You can change the config file path with `--config` flag or `STERNCONFIG` environment variable.
125+
112126
### templates
113127

114128
stern supports outputting custom log messages. There are a few predefined

cmd/cmd.go

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,23 @@ import (
2626
"text/template"
2727
"time"
2828

29-
"k8s.io/klog/v2"
30-
3129
"github.com/fatih/color"
30+
"github.com/mitchellh/go-homedir"
3231
"github.com/pkg/errors"
3332
"github.com/spf13/cast"
3433
"github.com/spf13/cobra"
3534
"github.com/spf13/pflag"
3635
"github.com/stern/stern/stern"
36+
"gopkg.in/yaml.v3"
3737
"k8s.io/apimachinery/pkg/fields"
3838
"k8s.io/apimachinery/pkg/labels"
3939
"k8s.io/cli-runtime/pkg/genericclioptions"
40+
"k8s.io/klog/v2"
4041
)
4142

43+
// Use "~" to avoid exposing the user name in the help message
44+
var defaultConfigFilePath = "~/.config/stern/config.yaml"
45+
4246
type options struct {
4347
genericclioptions.IOStreams
4448

@@ -74,6 +78,7 @@ type options struct {
7478
onlyLogLines bool
7579
maxLogRequests int
7680
node string
81+
configFilePath string
7782
}
7883

7984
func NewOptions(streams genericclioptions.IOStreams) *options {
@@ -95,6 +100,7 @@ func NewOptions(streams genericclioptions.IOStreams) *options {
95100
prompt: false,
96101
noFollow: false,
97102
maxLogRequests: -1,
103+
configFilePath: defaultConfigFilePath,
98104
}
99105
}
100106

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

116+
envVar, ok := os.LookupEnv("STERNCONFIG")
117+
if ok {
118+
o.configFilePath = envVar
119+
}
120+
110121
return nil
111122
}
112123

@@ -289,6 +300,54 @@ func (o *options) setVerbosity() error {
289300
return nil
290301
}
291302

303+
// overrideFlagSetDefaultFromConfig overrides the default value of the flagSets
304+
// from the config file
305+
func (o *options) overrideFlagSetDefaultFromConfig(fs *pflag.FlagSet) error {
306+
expanded, err := homedir.Expand(o.configFilePath)
307+
if err != nil {
308+
return err
309+
}
310+
311+
if o.configFilePath == defaultConfigFilePath {
312+
if _, err := os.Stat(expanded); os.IsNotExist(err) {
313+
return nil
314+
}
315+
}
316+
317+
configFile, err := os.Open(expanded)
318+
if err != nil {
319+
return err
320+
}
321+
322+
data := make(map[string]interface{})
323+
324+
if err := yaml.NewDecoder(configFile).Decode(data); err != nil {
325+
return err
326+
}
327+
328+
for name, value := range data {
329+
flag := fs.Lookup(name)
330+
if flag == nil {
331+
// To avoid command execution failure, we only output a warning
332+
// message instead of exiting with an error if an unknown option is
333+
// specified.
334+
klog.Warningf("Unknown option specified in the config file: %s", name)
335+
continue
336+
}
337+
338+
// flag has higher priority than the config file
339+
if flag.Changed {
340+
continue
341+
}
342+
343+
if err := flag.Value.Set(fmt.Sprint(value)); err != nil {
344+
return fmt.Errorf("invalid value %q for %q in the config file: %v", value, name, err)
345+
}
346+
}
347+
348+
return nil
349+
}
350+
292351
// AddFlags adds all the flags used by stern.
293352
func (o *options) AddFlags(fs *pflag.FlagSet) {
294353
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.")
@@ -321,6 +380,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
321380
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.")
322381
fs.StringVar(&o.timezone, "timezone", o.timezone, "Set timestamps to specific timezone.")
323382
fs.BoolVar(&o.onlyLogLines, "only-log-lines", o.onlyLogLines, "Print only log lines")
383+
fs.StringVar(&o.configFilePath, "config", o.configFilePath, "Path to the stern config file")
324384
fs.IntVar(&o.verbosity, "verbosity", o.verbosity, "Number of the log level verbosity")
325385
fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.")
326386

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

549+
if err := o.overrideFlagSetDefaultFromConfig(cmd.Flags()); err != nil {
550+
return err
551+
}
552+
489553
if err := o.Validate(); err != nil {
490554
return err
491555
}

cmd/cmd_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package cmd
22

33
import (
44
"bytes"
5+
"os"
6+
"path/filepath"
57
"reflect"
68
"regexp"
79
"strings"
810
"testing"
911
"time"
1012

1113
"github.com/fatih/color"
14+
"github.com/spf13/pflag"
1215
"github.com/stern/stern/stern"
1316
"k8s.io/apimachinery/pkg/fields"
1417
"k8s.io/apimachinery/pkg/labels"
@@ -54,6 +57,47 @@ func TestSternCommand(t *testing.T) {
5457
}
5558
}
5659

60+
func TestOptionsComplete(t *testing.T) {
61+
streams := genericclioptions.NewTestIOStreamsDiscard()
62+
63+
tests := []struct {
64+
name string
65+
env map[string]string
66+
args []string
67+
expectedConfigFilePath string
68+
}{
69+
{
70+
name: "No environment variables",
71+
env: map[string]string{},
72+
args: []string{},
73+
expectedConfigFilePath: defaultConfigFilePath,
74+
},
75+
{
76+
name: "Set STERNCONFIG env to ./config.yaml",
77+
env: map[string]string{
78+
"STERNCONFIG": "./config.yaml",
79+
},
80+
args: []string{},
81+
expectedConfigFilePath: "./config.yaml",
82+
},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
for k, v := range tt.env {
88+
t.Setenv(k, v)
89+
}
90+
91+
o := NewOptions(streams)
92+
_ = o.Complete(tt.args)
93+
94+
if tt.expectedConfigFilePath != o.configFilePath {
95+
t.Errorf("expected %s for configFilePath, but got %s", tt.expectedConfigFilePath, o.configFilePath)
96+
}
97+
})
98+
}
99+
}
100+
57101
func TestOptionsValidate(t *testing.T) {
58102
streams := genericclioptions.NewTestIOStreamsDiscard()
59103

@@ -745,3 +789,97 @@ func TestOptionsSternConfig(t *testing.T) {
745789
})
746790
}
747791
}
792+
793+
func TestOptionsOverrideFlagSetDefaultFromConfig(t *testing.T) {
794+
orig := defaultConfigFilePath
795+
defer func() {
796+
defaultConfigFilePath = orig
797+
}()
798+
799+
defaultConfigFilePath = "./config.yaml"
800+
wd, _ := os.Getwd()
801+
802+
tests := []struct {
803+
name string
804+
flagConfigFilePathValue string
805+
flagTailValue string
806+
expectedTailValue int64
807+
wantErr bool
808+
}{
809+
{
810+
name: "--config=testdata/config-tail1.yaml",
811+
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail1.yaml"),
812+
expectedTailValue: 1,
813+
wantErr: false,
814+
},
815+
{
816+
name: "--config=config-not-exist.yaml",
817+
flagConfigFilePathValue: filepath.Join(wd, "config-not-exist.yaml"),
818+
wantErr: true,
819+
},
820+
{
821+
name: "--config=config-invalid.yaml",
822+
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-invalid.yaml"),
823+
wantErr: true,
824+
},
825+
{
826+
name: "--config=config-unknown-option.yaml",
827+
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-unknown-option.yaml"),
828+
expectedTailValue: 1,
829+
wantErr: false,
830+
},
831+
{
832+
name: "--config=config-tail-invalid-value.yaml",
833+
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail-invalid-value.yaml"),
834+
wantErr: true,
835+
},
836+
{
837+
name: "config file path is not specified and config file does not exist",
838+
expectedTailValue: -1,
839+
wantErr: false,
840+
},
841+
{
842+
name: "--config=testdata/config-tail1.yaml and --tail=2",
843+
flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail1.yaml"),
844+
flagTailValue: "2",
845+
expectedTailValue: 2,
846+
wantErr: false,
847+
},
848+
}
849+
850+
for _, tt := range tests {
851+
t.Run(tt.name, func(t *testing.T) {
852+
o := NewOptions(genericclioptions.NewTestIOStreamsDiscard())
853+
fs := pflag.NewFlagSet("", pflag.ExitOnError)
854+
o.AddFlags(fs)
855+
856+
args := []string{}
857+
if tt.flagConfigFilePathValue != "" {
858+
args = append(args, "--config="+tt.flagConfigFilePathValue)
859+
}
860+
if tt.flagTailValue != "" {
861+
args = append(args, "--tail="+tt.flagTailValue)
862+
}
863+
864+
if err := fs.Parse(args); err != nil {
865+
t.Fatal(err)
866+
}
867+
868+
err := o.overrideFlagSetDefaultFromConfig(fs)
869+
if tt.wantErr {
870+
if err == nil {
871+
t.Error("expected err, but got nil")
872+
}
873+
return
874+
}
875+
876+
if err != nil {
877+
t.Errorf("unexpected err: %v", err)
878+
}
879+
880+
if tt.expectedTailValue != o.tail {
881+
t.Errorf("expected %d for tail, but got %d", tt.expectedTailValue, o.tail)
882+
}
883+
})
884+
}
885+
}

cmd/testdata/config-invalid.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this is invalid config file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tail: invalid

cmd/testdata/config-tail1.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tail: 1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
unknown: 999
2+
tail: 1

0 commit comments

Comments
 (0)