Repository: stern/stern
Pull request #204 (2023-01-13 04:57:58)

Add --no-follow flag to exit when all logs have been shown
Fixes https://github.com/stern/stern/issues/15

This PR adds `--no-follow` flag to exit when all logs have been shown.

The name of the flag could be `--follow=false`. But it will be used with `false` in most cases, so I chose `--no-follow` as the auto-completion can work in one shot.



Patch:
diff --git a/README.md b/README.md
index 266d071..612c01d 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,7 @@ The `pod` query is a regular expression so you could provide `"web-\w"` to tail
  `--init-containers`         | `true`    | Include or exclude init containers.
  `--kubeconfig`              |           | Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.
  `--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.
  `--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.
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 186c243..4a77ecc 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -61,6 +61,7 @@ type options struct {
 	output              string
 	prompt              bool
 	podQuery            string
+	noFollow            bool
 }
 
 func NewOptions(streams genericclioptions.IOStreams) *options {
@@ -79,6 +80,7 @@ func NewOptions(streams genericclioptions.IOStreams) *options {
 		timestamps:          false,
 		timezone:            "Local",
 		prompt:              false,
+		noFollow:            false,
 	}
 }
 
@@ -310,7 +312,7 @@ func (o *options) sternConfig() (*stern.Config, error) {
 		FieldSelector:         fieldSelector,
 		TailLines:             tailLines,
 		Template:              template,
-		Follow:                true,
+		Follow:                !o.noFollow,
 
 		Out:    o.Out,
 		ErrOut: o.ErrOut,
@@ -328,6 +330,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
 	fs.StringArrayVarP(&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.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.")
diff --git a/go.mod b/go.mod
index d978c39..5b49f86 100644
--- a/go.mod
+++ b/go.mod
@@ -61,6 +61,7 @@ require (
 	golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 // indirect
 	golang.org/x/net v0.0.0-20220923203811-8be639271d50 // indirect
 	golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
+	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
 	golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
 	golang.org/x/text v0.3.7 // indirect
diff --git a/go.sum b/go.sum
index 03ecf95..02b7f33 100644
--- a/go.sum
+++ b/go.sum
@@ -241,6 +241,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 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=
diff --git a/stern/list.go b/stern/list.go
index 16f8451..30e2f82 100644
--- a/stern/list.go
+++ b/stern/list.go
@@ -21,6 +21,8 @@ import (
 	"github.com/pkg/errors"
 	"github.com/stern/stern/kubernetes"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/labels"
 	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 )
 
@@ -87,3 +89,18 @@ func List(ctx context.Context, config *Config) (map[string]string, error) {
 
 	return labels, nil
 }
+
+// ListTargets returns targets by listing and filtering pods
+func ListTargets(ctx context.Context, i corev1client.PodInterface, labelSelector labels.Selector, fieldSelector fields.Selector, filter *targetFilter) ([]*Target, error) {
+	list, err := i.List(ctx, metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()})
+	if err != nil {
+		return nil, err
+	}
+	var targets []*Target
+	for i := range list.Items {
+		filter.visit(&list.Items[i], func(t *Target, containerStateMatched bool) {
+			targets = append(targets, t)
+		})
+	}
+	return targets, nil
+}
diff --git a/stern/stern.go b/stern/stern.go
index 1967772..c6355f8 100644
--- a/stern/stern.go
+++ b/stern/stern.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/pkg/errors"
 	"github.com/stern/stern/kubernetes"
+	"golang.org/x/sync/errgroup"
 	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 )
 
@@ -74,6 +75,52 @@ func Run(ctx context.Context, config *Config) error {
 		}
 	}
 
+	filter := &targetFilter{
+		podFilter:              config.PodQuery,
+		excludePodFilter:       config.ExcludePodQuery,
+		containerFilter:        config.ContainerQuery,
+		containerExcludeFilter: config.ExcludeContainerQuery,
+		initContainers:         config.InitContainers,
+		ephemeralContainers:    config.EphemeralContainers,
+		containerStates:        config.ContainerStates,
+	}
+	newTail := func(t *Target) *Tail {
+		return NewTail(clientset, t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, &TailOptions{
+			Timestamps:   config.Timestamps,
+			Location:     config.Location,
+			SinceSeconds: int64(config.Since.Seconds()),
+			Exclude:      config.Exclude,
+			Include:      config.Include,
+			Namespace:    config.AllNamespaces || len(namespaces) > 1,
+			TailLines:    config.TailLines,
+			Follow:       config.Follow,
+		})
+	}
+
+	if !config.Follow {
+		var eg errgroup.Group
+		for _, n := range namespaces {
+			targets, err := ListTargets(ctx,
+				clientset.Pods(n),
+				config.LabelSelector,
+				config.FieldSelector,
+				filter,
+			)
+			if err != nil {
+				return err
+			}
+			for _, t := range targets {
+				t := t
+				eg.Go(func() error {
+					tail := newTail(t)
+					defer tail.Close()
+					return tail.Start(ctx)
+				})
+			}
+		}
+		return eg.Wait()
+	}
+
 	added := make(chan *Target)
 	removed := make(chan *Target)
 	errCh := make(chan error)
@@ -83,17 +130,12 @@ func Run(ctx context.Context, config *Config) error {
 	defer close(errCh)
 
 	for _, n := range namespaces {
-		a, r, err := Watch(ctx,
+		a, r, err := WatchTargets(ctx,
 			clientset.Pods(n),
-			config.PodQuery,
-			config.ExcludePodQuery,
-			config.ContainerQuery,
-			config.ExcludeContainerQuery,
-			config.InitContainers,
-			config.EphemeralContainers,
-			config.ContainerStates,
 			config.LabelSelector,
-			config.FieldSelector)
+			config.FieldSelector,
+			filter,
+		)
 		if err != nil {
 			return errors.Wrap(err, "failed to set up watch")
 		}
@@ -133,16 +175,7 @@ func Run(ctx context.Context, config *Config) error {
 				}
 			}
 
-			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()),
-				Exclude:      config.Exclude,
-				Include:      config.Include,
-				Namespace:    config.AllNamespaces || len(namespaces) > 1,
-				TailLines:    config.TailLines,
-				Follow:       config.Follow,
-			})
+			tail := newTail(p)
 			setTail(targetID, tail)
 
 			go func(tail *Tail) {
diff --git a/stern/target.go b/stern/target.go
new file mode 100644
index 0000000..0cc563d
--- /dev/null
+++ b/stern/target.go
@@ -0,0 +1,91 @@
+//   Copyright 2017 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 stern
+
+import (
+	"fmt"
+	"regexp"
+
+	corev1 "k8s.io/api/core/v1"
+)
+
+// Target is a target to watch
+type Target struct {
+	Node      string
+	Namespace string
+	Pod       string
+	Container string
+}
+
+// GetID returns the ID of the object
+func (t *Target) GetID() string {
+	return fmt.Sprintf("%s-%s-%s", t.Namespace, t.Pod, t.Container)
+}
+
+// targetFilter is a filter of Target
+type targetFilter struct {
+	podFilter              *regexp.Regexp
+	excludePodFilter       *regexp.Regexp
+	containerFilter        *regexp.Regexp
+	containerExcludeFilter *regexp.Regexp
+	initContainers         bool
+	ephemeralContainers    bool
+	containerStates        []ContainerState
+}
+
+// visit passes filtered Targets to the visitor function
+func (f *targetFilter) visit(pod *corev1.Pod, visitor func(t *Target, containerStateMatched bool)) {
+	// filter by pod
+	if !f.podFilter.MatchString(pod.Name) {
+		return
+	}
+	if f.excludePodFilter != nil && f.excludePodFilter.MatchString(pod.Name) {
+		return
+	}
+
+	// filter by container statuses
+	var statuses []corev1.ContainerStatus
+	statuses = append(statuses, pod.Status.ContainerStatuses...)
+	if f.initContainers {
+		statuses = append(statuses, pod.Status.InitContainerStatuses...)
+	}
+	if f.ephemeralContainers {
+		statuses = append(statuses, pod.Status.EphemeralContainerStatuses...)
+	}
+	for _, c := range statuses {
+		if !f.containerFilter.MatchString(c.Name) {
+			continue
+		}
+		if f.containerExcludeFilter != nil && f.containerExcludeFilter.MatchString(c.Name) {
+			continue
+		}
+		t := &Target{
+			Node:      pod.Spec.NodeName,
+			Namespace: pod.Namespace,
+			Pod:       pod.Name,
+			Container: c.Name,
+		}
+		visitor(t, f.matchContainerState(c.State))
+	}
+}
+
+func (f *targetFilter) matchContainerState(state corev1.ContainerState) bool {
+	for _, containerState := range f.containerStates {
+		if containerState.Match(state) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/stern/watch.go b/stern/watch.go
index cf0541a..a68c395 100644
--- a/stern/watch.go
+++ b/stern/watch.go
@@ -16,8 +16,6 @@ package stern
 
 import (
 	"context"
-	"fmt"
-	"regexp"
 
 	"github.com/pkg/errors"
 
@@ -31,23 +29,10 @@ import (
 	watchtools "k8s.io/client-go/tools/watch"
 )
 
-// Target is a target to watch
-type Target struct {
-	Node      string
-	Namespace string
-	Pod       string
-	Container string
-}
-
-// GetID returns the ID of the object
-func (t *Target) GetID() string {
-	return fmt.Sprintf("%s-%s-%s", t.Namespace, t.Pod, t.Container)
-}
-
 // Watch starts listening to Kubernetes events and emits modified
 // containers/pods. The first result is targets added, the second is targets
 // removed
-func Watch(ctx context.Context, i v1.PodInterface, podFilter *regexp.Regexp, excludePodFilter *regexp.Regexp, containerFilter *regexp.Regexp, containerExcludeFilter *regexp.Regexp, initContainers bool, ephemeralContainers bool, containerStates []ContainerState, labelSelector labels.Selector, fieldSelector fields.Selector) (chan *Target, chan *Target, error) {
+func WatchTargets(ctx context.Context, i v1.PodInterface, labelSelector labels.Selector, fieldSelector fields.Selector, filter *targetFilter) (chan *Target, chan *Target, error) {
 	// RetryWatcher will make sure that in case the underlying watcher is
 	// closed (e.g. due to API timeout or etcd timeout) it will get restarted
 	// from the last point without the consumer even knowing about it.
@@ -79,79 +64,19 @@ func Watch(ctx context.Context, i v1.PodInterface, podFilter *regexp.Regexp, exc
 					continue
 				}
 
-				if !podFilter.MatchString(pod.Name) {
-					continue
-				}
-
-				if excludePodFilter != nil && excludePodFilter.MatchString(pod.Name) {
-					continue
-				}
-
 				switch e.Type {
 				case watch.Added, watch.Modified:
-					var statuses []corev1.ContainerStatus
-					statuses = append(statuses, pod.Status.ContainerStatuses...)
-					if initContainers {
-						statuses = append(statuses, pod.Status.InitContainerStatuses...)
-					}
-
-					if ephemeralContainers {
-						statuses = append(statuses, pod.Status.EphemeralContainerStatuses...)
-					}
-
-					for _, c := range statuses {
-						if !containerFilter.MatchString(c.Name) {
-							continue
-						}
-						if containerExcludeFilter != nil && containerExcludeFilter.MatchString(c.Name) {
-							continue
-						}
-						t := &Target{
-							Node:      pod.Spec.NodeName,
-							Namespace: pod.Namespace,
-							Pod:       pod.Name,
-							Container: c.Name,
-						}
-
-						containerStateMatched := false
-						for _, containerState := range containerStates {
-							if containerState.Match(c.State) {
-								containerStateMatched = true
-								break
-							}
-						}
+					filter.visit(pod, func(t *Target, containerStateMatched bool) {
 						if containerStateMatched {
 							added <- t
 						} else {
 							removed <- t
 						}
-					}
+					})
 				case watch.Deleted:
-					var containerNames []string
-					containerNames = append(containerNames, getContainerNames(pod.Spec.Containers)...)
-					if initContainers {
-						containerNames = append(containerNames, getContainerNames(pod.Spec.InitContainers)...)
-					}
-
-					if ephemeralContainers {
-						containerNames = append(containerNames, getEphemeralContainerNames(pod.Spec.EphemeralContainers)...)
-					}
-
-					for _, cn := range containerNames {
-						if !containerFilter.MatchString(cn) {
-							continue
-						}
-						if containerExcludeFilter != nil && containerExcludeFilter.MatchString(cn) {
-							continue
-						}
-
-						removed <- &Target{
-							Node:      pod.Spec.NodeName,
-							Namespace: pod.Namespace,
-							Pod:       pod.Name,
-							Container: cn,
-						}
-					}
+					filter.visit(pod, func(t *Target, containerStateMatched bool) {
+						removed <- t
+					})
 				}
 			case <-ctx.Done():
 				watcher.Stop()
@@ -164,21 +89,3 @@ func Watch(ctx context.Context, i v1.PodInterface, podFilter *regexp.Regexp, exc
 
 	return added, removed, nil
 }
-
-// getContainerNames returns names of containers
-func getContainerNames(containers []corev1.Container) []string {
-	var result []string
-	for _, c := range containers {
-		result = append(result, c.Name)
-	}
-	return result
-}
-
-// getContainerNames returns names of ephemeral containers
-func getEphemeralContainerNames(containers []corev1.EphemeralContainer) []string {
-	var result []string
-	for _, c := range containers {
-		result = append(result, c.Name)
-	}
-	return result
-}


Test patch:
diff --git a/stern/target_test.go b/stern/target_test.go
new file mode 100644
index 0000000..fbaaa52
--- /dev/null
+++ b/stern/target_test.go
@@ -0,0 +1,227 @@
+package stern
+
+import (
+	"reflect"
+	"regexp"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestTargetFilter(t *testing.T) {
+	running := corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}
+	terminated := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}}
+	waiting := corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{}}
+
+	pod := &corev1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns1",
+			Name:      "pod1",
+		},
+		Spec: corev1.PodSpec{
+			NodeName: "node1",
+		},
+		Status: corev1.PodStatus{
+			ContainerStatuses: []corev1.ContainerStatus{
+				{Name: "container1-running", State: running},
+				{Name: "container2-terminated", State: terminated},
+				{Name: "container3-waiting", State: waiting},
+			},
+			InitContainerStatuses: []corev1.ContainerStatus{
+				{Name: "init-container1-running", State: running},
+				{Name: "init-container2-terminated", State: terminated},
+				{Name: "init-container3-waiting", State: waiting},
+			},
+			EphemeralContainerStatuses: []corev1.ContainerStatus{
+				{Name: "ephemeral-container1-running", State: running},
+				{Name: "ephemeral-container2-terminated", State: terminated},
+				{Name: "ephemeral-container3-waiting", State: waiting},
+			},
+		},
+	}
+
+	type targetMatch struct {
+		target                Target
+		containerStateMatched bool
+	}
+	genTargetMatch := func(container string, matched bool) targetMatch {
+		return targetMatch{
+			target: Target{
+				Node:      "node1",
+				Namespace: "ns1",
+				Pod:       "pod1",
+				Container: container,
+			},
+			containerStateMatched: matched,
+		}
+	}
+
+	tests := []struct {
+		name     string
+		filter   targetFilter
+		expected []targetMatch
+	}{
+		{
+			name: "match all",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`.*`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: nil,
+				initContainers:         true,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{
+				genTargetMatch("container1-running", true),
+				genTargetMatch("container2-terminated", true),
+				genTargetMatch("container3-waiting", true),
+				genTargetMatch("init-container1-running", true),
+				genTargetMatch("init-container2-terminated", true),
+				genTargetMatch("init-container3-waiting", true),
+				genTargetMatch("ephemeral-container1-running", true),
+				genTargetMatch("ephemeral-container2-terminated", true),
+				genTargetMatch("ephemeral-container3-waiting", true),
+			},
+		},
+		{
+			name: "filter by podFilter",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`not-matched`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: nil,
+				initContainers:         true,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{},
+		},
+		{
+			name: "filter by excludePodFilter",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(``),
+				excludePodFilter:       regexp.MustCompile(`pod1`),
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: nil,
+				initContainers:         true,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{},
+		},
+		{
+			name: "filter by containerFilter",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`.*`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*container1.*`),
+				containerExcludeFilter: nil,
+				initContainers:         true,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{
+				genTargetMatch("container1-running", true),
+				genTargetMatch("init-container1-running", true),
+				genTargetMatch("ephemeral-container1-running", true),
+			},
+		},
+		{
+			name: "filter by containerExcludeFilter",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`.*`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: regexp.MustCompile(`.*container1.*`),
+				initContainers:         true,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{
+				genTargetMatch("container2-terminated", true),
+				genTargetMatch("container3-waiting", true),
+				genTargetMatch("init-container2-terminated", true),
+				genTargetMatch("init-container3-waiting", true),
+				genTargetMatch("ephemeral-container2-terminated", true),
+				genTargetMatch("ephemeral-container3-waiting", true),
+			},
+		},
+		{
+			name: "dot not include initContainers",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`.*`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: nil,
+				initContainers:         false,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{
+				genTargetMatch("container1-running", true),
+				genTargetMatch("container2-terminated", true),
+				genTargetMatch("container3-waiting", true),
+				genTargetMatch("ephemeral-container1-running", true),
+				genTargetMatch("ephemeral-container2-terminated", true),
+				genTargetMatch("ephemeral-container3-waiting", true),
+			},
+		},
+		{
+			name: "dot not include ephemeralContainers",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`.*`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: nil,
+				initContainers:         true,
+				ephemeralContainers:    false,
+				containerStates:        []ContainerState{RUNNING, TERMINATED, WAITING},
+			},
+			expected: []targetMatch{
+				genTargetMatch("container1-running", true),
+				genTargetMatch("container2-terminated", true),
+				genTargetMatch("container3-waiting", true),
+				genTargetMatch("init-container1-running", true),
+				genTargetMatch("init-container2-terminated", true),
+				genTargetMatch("init-container3-waiting", true),
+			},
+		},
+		{
+			name: "match running states",
+			filter: targetFilter{
+				podFilter:              regexp.MustCompile(`.*`),
+				excludePodFilter:       nil,
+				containerFilter:        regexp.MustCompile(`.*`),
+				containerExcludeFilter: nil,
+				initContainers:         true,
+				ephemeralContainers:    true,
+				containerStates:        []ContainerState{RUNNING},
+			},
+			expected: []targetMatch{
+				genTargetMatch("container1-running", true),
+				genTargetMatch("container2-terminated", false),
+				genTargetMatch("container3-waiting", false),
+				genTargetMatch("init-container1-running", true),
+				genTargetMatch("init-container2-terminated", false),
+				genTargetMatch("init-container3-waiting", false),
+				genTargetMatch("ephemeral-container1-running", true),
+				genTargetMatch("ephemeral-container2-terminated", false),
+				genTargetMatch("ephemeral-container3-waiting", false),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := []targetMatch{}
+			tt.filter.visit(pod, func(target *Target, containerStateMatched bool) {
+				actual = append(actual, targetMatch{target: *target, containerStateMatched: containerStateMatched})
+			})
+			if !reflect.DeepEqual(tt.expected, actual) {
+				t.Errorf("expected %v, but actual %v", tt.expected, actual)
+			}
+		})
+	}
+}