Skip to content

Commit a5e581d

Browse files
author
Takashi Kusumi
authored
Add --no-follow flag to exit when all logs have been shown (#204)
* Extract filtering logic to targetFilter * Add --no-follow flag to exit when all logs have been shown
1 parent 80a68a9 commit a5e581d

File tree

9 files changed

+401
-119
lines changed

9 files changed

+401
-119
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ The `pod` query is a regular expression so you could provide `"web-\w"` to tail
7878
`--init-containers` | `true` | Include or exclude init containers.
7979
`--kubeconfig` | | Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.
8080
`--namespace`, `-n` | | Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.
81+
`--no-follow` | `false` | Exit when all logs have been shown.
8182
`--output`, `-o` | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]
8283
`--prompt`, `-p` | `false` | Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.
8384
`--selector`, `-l` | | Selector (label query) to filter on. If present, default to ".*" for the pod-query.

cmd/cmd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type options struct {
6161
output string
6262
prompt bool
6363
podQuery string
64+
noFollow bool
6465
}
6566

6667
func NewOptions(streams genericclioptions.IOStreams) *options {
@@ -79,6 +80,7 @@ func NewOptions(streams genericclioptions.IOStreams) *options {
7980
timestamps: false,
8081
timezone: "Local",
8182
prompt: false,
83+
noFollow: false,
8284
}
8385
}
8486

@@ -310,7 +312,7 @@ func (o *options) sternConfig() (*stern.Config, error) {
310312
FieldSelector: fieldSelector,
311313
TailLines: tailLines,
312314
Template: template,
313-
Follow: true,
315+
Follow: !o.noFollow,
314316

315317
Out: o.Out,
316318
ErrOut: o.ErrOut,
@@ -328,6 +330,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
328330
fs.StringArrayVarP(&o.exclude, "exclude", "e", o.exclude, "Log lines to exclude. (regular expression)")
329331
fs.StringVarP(&o.excludeContainer, "exclude-container", "E", o.excludeContainer, "Container name to exclude when multiple containers in pod. (regular expression)")
330332
fs.StringVar(&o.excludePod, "exclude-pod", o.excludePod, "Pod name to exclude. (regular expression)")
333+
fs.BoolVar(&o.noFollow, "no-follow", o.noFollow, "Exit when all logs have been shown.")
331334
fs.StringArrayVarP(&o.include, "include", "i", o.include, "Log lines to include. (regular expression)")
332335
fs.BoolVar(&o.initContainers, "init-containers", o.initContainers, "Include or exclude init containers.")
333336
fs.BoolVar(&o.ephemeralContainers, "ephemeral-containers", o.ephemeralContainers, "Include or exclude ephemeral containers.")

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ require (
6161
golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 // indirect
6262
golang.org/x/net v0.0.0-20220923203811-8be639271d50 // indirect
6363
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
64+
golang.org/x/sync v0.1.0 // indirect
6465
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
6566
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
6667
golang.org/x/text v0.3.7 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
241241
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
242242
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
243243
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
244+
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
245+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
244246
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
245247
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
246248
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

stern/list.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"github.com/pkg/errors"
2222
"github.com/stern/stern/kubernetes"
2323
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/fields"
25+
"k8s.io/apimachinery/pkg/labels"
2426
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
2527
)
2628

@@ -87,3 +89,18 @@ func List(ctx context.Context, config *Config) (map[string]string, error) {
8789

8890
return labels, nil
8991
}
92+
93+
// ListTargets returns targets by listing and filtering pods
94+
func ListTargets(ctx context.Context, i corev1client.PodInterface, labelSelector labels.Selector, fieldSelector fields.Selector, filter *targetFilter) ([]*Target, error) {
95+
list, err := i.List(ctx, metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()})
96+
if err != nil {
97+
return nil, err
98+
}
99+
var targets []*Target
100+
for i := range list.Items {
101+
filter.visit(&list.Items[i], func(t *Target, containerStateMatched bool) {
102+
targets = append(targets, t)
103+
})
104+
}
105+
return targets, nil
106+
}

stern/stern.go

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/pkg/errors"
2323
"github.com/stern/stern/kubernetes"
24+
"golang.org/x/sync/errgroup"
2425
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
2526
)
2627

@@ -74,6 +75,52 @@ func Run(ctx context.Context, config *Config) error {
7475
}
7576
}
7677

78+
filter := &targetFilter{
79+
podFilter: config.PodQuery,
80+
excludePodFilter: config.ExcludePodQuery,
81+
containerFilter: config.ContainerQuery,
82+
containerExcludeFilter: config.ExcludeContainerQuery,
83+
initContainers: config.InitContainers,
84+
ephemeralContainers: config.EphemeralContainers,
85+
containerStates: config.ContainerStates,
86+
}
87+
newTail := func(t *Target) *Tail {
88+
return NewTail(clientset, t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, &TailOptions{
89+
Timestamps: config.Timestamps,
90+
Location: config.Location,
91+
SinceSeconds: int64(config.Since.Seconds()),
92+
Exclude: config.Exclude,
93+
Include: config.Include,
94+
Namespace: config.AllNamespaces || len(namespaces) > 1,
95+
TailLines: config.TailLines,
96+
Follow: config.Follow,
97+
})
98+
}
99+
100+
if !config.Follow {
101+
var eg errgroup.Group
102+
for _, n := range namespaces {
103+
targets, err := ListTargets(ctx,
104+
clientset.Pods(n),
105+
config.LabelSelector,
106+
config.FieldSelector,
107+
filter,
108+
)
109+
if err != nil {
110+
return err
111+
}
112+
for _, t := range targets {
113+
t := t
114+
eg.Go(func() error {
115+
tail := newTail(t)
116+
defer tail.Close()
117+
return tail.Start(ctx)
118+
})
119+
}
120+
}
121+
return eg.Wait()
122+
}
123+
77124
added := make(chan *Target)
78125
removed := make(chan *Target)
79126
errCh := make(chan error)
@@ -83,17 +130,12 @@ func Run(ctx context.Context, config *Config) error {
83130
defer close(errCh)
84131

85132
for _, n := range namespaces {
86-
a, r, err := Watch(ctx,
133+
a, r, err := WatchTargets(ctx,
87134
clientset.Pods(n),
88-
config.PodQuery,
89-
config.ExcludePodQuery,
90-
config.ContainerQuery,
91-
config.ExcludeContainerQuery,
92-
config.InitContainers,
93-
config.EphemeralContainers,
94-
config.ContainerStates,
95135
config.LabelSelector,
96-
config.FieldSelector)
136+
config.FieldSelector,
137+
filter,
138+
)
97139
if err != nil {
98140
return errors.Wrap(err, "failed to set up watch")
99141
}
@@ -133,16 +175,7 @@ func Run(ctx context.Context, config *Config) error {
133175
}
134176
}
135177

136-
tail := NewTail(clientset, p.Node, p.Namespace, p.Pod, p.Container, config.Template, config.Out, config.ErrOut, &TailOptions{
137-
Timestamps: config.Timestamps,
138-
Location: config.Location,
139-
SinceSeconds: int64(config.Since.Seconds()),
140-
Exclude: config.Exclude,
141-
Include: config.Include,
142-
Namespace: config.AllNamespaces || len(namespaces) > 1,
143-
TailLines: config.TailLines,
144-
Follow: config.Follow,
145-
})
178+
tail := newTail(p)
146179
setTail(targetID, tail)
147180

148181
go func(tail *Tail) {

stern/target.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2017 Wercker Holding BV
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package stern
16+
17+
import (
18+
"fmt"
19+
"regexp"
20+
21+
corev1 "k8s.io/api/core/v1"
22+
)
23+
24+
// Target is a target to watch
25+
type Target struct {
26+
Node string
27+
Namespace string
28+
Pod string
29+
Container string
30+
}
31+
32+
// GetID returns the ID of the object
33+
func (t *Target) GetID() string {
34+
return fmt.Sprintf("%s-%s-%s", t.Namespace, t.Pod, t.Container)
35+
}
36+
37+
// targetFilter is a filter of Target
38+
type targetFilter struct {
39+
podFilter *regexp.Regexp
40+
excludePodFilter *regexp.Regexp
41+
containerFilter *regexp.Regexp
42+
containerExcludeFilter *regexp.Regexp
43+
initContainers bool
44+
ephemeralContainers bool
45+
containerStates []ContainerState
46+
}
47+
48+
// visit passes filtered Targets to the visitor function
49+
func (f *targetFilter) visit(pod *corev1.Pod, visitor func(t *Target, containerStateMatched bool)) {
50+
// filter by pod
51+
if !f.podFilter.MatchString(pod.Name) {
52+
return
53+
}
54+
if f.excludePodFilter != nil && f.excludePodFilter.MatchString(pod.Name) {
55+
return
56+
}
57+
58+
// filter by container statuses
59+
var statuses []corev1.ContainerStatus
60+
statuses = append(statuses, pod.Status.ContainerStatuses...)
61+
if f.initContainers {
62+
statuses = append(statuses, pod.Status.InitContainerStatuses...)
63+
}
64+
if f.ephemeralContainers {
65+
statuses = append(statuses, pod.Status.EphemeralContainerStatuses...)
66+
}
67+
for _, c := range statuses {
68+
if !f.containerFilter.MatchString(c.Name) {
69+
continue
70+
}
71+
if f.containerExcludeFilter != nil && f.containerExcludeFilter.MatchString(c.Name) {
72+
continue
73+
}
74+
t := &Target{
75+
Node: pod.Spec.NodeName,
76+
Namespace: pod.Namespace,
77+
Pod: pod.Name,
78+
Container: c.Name,
79+
}
80+
visitor(t, f.matchContainerState(c.State))
81+
}
82+
}
83+
84+
func (f *targetFilter) matchContainerState(state corev1.ContainerState) bool {
85+
for _, containerState := range f.containerStates {
86+
if containerState.Match(state) {
87+
return true
88+
}
89+
}
90+
return false
91+
}

0 commit comments

Comments
 (0)