Repository: stern/stern
Pull request #110 (2021-01-04 14:16:02)

Refactor stern/tail.go
- Use corev1client instead all of clientset
- Remove an useless argument from `tail.Start`
- Change `tail.Start` to return an error

superbrothers: Thank you for your review, and happy New Year! 🥳

Patch:
diff --git a/go.sum b/go.sum
index 99d6c3a..48da692 100644
--- a/go.sum
+++ b/go.sum
@@ -80,6 +80,7 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkg
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
@@ -87,6 +88,7 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -191,6 +193,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
@@ -252,8 +255,10 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -550,11 +555,13 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -580,6 +587,7 @@ k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
 k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c=
 k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
 k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw=
 k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
diff --git a/stern/stern.go b/stern/stern.go
index 251adfe..f527543 100644
--- a/stern/stern.go
+++ b/stern/stern.go
@@ -16,10 +16,13 @@ package stern
 
 import (
 	"context"
+	"fmt"
+	"os"
 	"sync"
 
 	"github.com/pkg/errors"
 	"github.com/stern/stern/kubernetes"
+	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 )
 
 var tails = make(map[string]*Tail)
@@ -47,7 +50,12 @@ func clearTail(targetID string) {
 // Run starts the main run loop
 func Run(ctx context.Context, config *Config) error {
 	clientConfig := kubernetes.NewClientConfig(config.KubeConfig, config.ContextName)
-	clientset, err := kubernetes.NewClientSet(clientConfig)
+	cc, err := clientConfig.ClientConfig()
+	if err != nil {
+		return err
+	}
+
+	clientset, err := corev1client.NewForConfig(cc)
 	if err != nil {
 		return err
 	}
@@ -60,6 +68,7 @@ func Run(ctx context.Context, config *Config) error {
 		namespace = config.Namespace
 		if namespace == "" {
 			namespace, _, err = clientConfig.Namespace()
+
 			if err != nil {
 				return errors.Wrap(err, "unable to get default namespace")
 			}
@@ -67,7 +76,7 @@ func Run(ctx context.Context, config *Config) error {
 	}
 
 	added, removed, err := Watch(ctx,
-		clientset.CoreV1().Pods(namespace),
+		clientset.Pods(namespace),
 		config.PodQuery,
 		config.ContainerQuery,
 		config.ExcludeContainerQuery,
@@ -91,7 +100,7 @@ func Run(ctx context.Context, config *Config) error {
 				}
 			}
 
-			tail := NewTail(p.Node, p.Namespace, p.Pod, p.Container, config.Template, &TailOptions{
+			tail := NewTail(clientset, p.Node, p.Namespace, p.Pod, p.Container, config.Template, os.Stdout, os.Stderr, &TailOptions{
 				Timestamps:   config.Timestamps,
 				SinceSeconds: int64(config.Since.Seconds()),
 				Exclude:      config.Exclude,
@@ -101,7 +110,11 @@ func Run(ctx context.Context, config *Config) error {
 			})
 			setTail(targetID, tail)
 
-			tail.Start(ctx, clientset.CoreV1().Pods(p.Namespace))
+			go func(tail *Tail) {
+				if err := tail.Start(ctx); err != nil {
+					fmt.Fprintf(os.Stderr, "unexpected error: %v\n", err)
+				}
+			}(tail)
 		}
 	}()
 
diff --git a/stern/tail.go b/stern/tail.go
index 0f7d209..1902c77 100644
--- a/stern/tail.go
+++ b/stern/tail.go
@@ -18,21 +18,23 @@ import (
 	"bufio"
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"hash/fnv"
 	"io"
-	"os"
 	"regexp"
 	"strings"
 	"text/template"
 
 	"github.com/fatih/color"
 	corev1 "k8s.io/api/core/v1"
-	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 	"k8s.io/client-go/rest"
 )
 
 type Tail struct {
+	clientset corev1client.CoreV1Interface
+
 	NodeName       string
 	Namespace      string
 	PodName        string
@@ -43,6 +45,8 @@ type Tail struct {
 	containerColor *color.Color
 	tmpl           *template.Template
 	active         bool
+	out            io.Writer
+	errOut         io.Writer
 }
 
 type TailOptions struct {
@@ -79,8 +83,9 @@ func (o TailOptions) IsInclude(msg string) bool {
 }
 
 // NewTail returns a new tail for a Kubernetes container inside a pod
-func NewTail(nodeName, namespace, podName, containerName string, tmpl *template.Template, options *TailOptions) *Tail {
+func NewTail(clientset corev1client.CoreV1Interface, nodeName, namespace, podName, containerName string, tmpl *template.Template, out, errOut io.Writer, options *TailOptions) *Tail {
 	return &Tail{
+		clientset:     clientset,
 		NodeName:      nodeName,
 		Namespace:     namespace,
 		PodName:       podName,
@@ -89,6 +94,8 @@ func NewTail(nodeName, namespace, podName, containerName string, tmpl *template.
 		closed:        make(chan struct{}),
 		tmpl:          tmpl,
 		active:        true,
+		out:           out,
+		errOut:        errOut,
 	}
 }
 
@@ -111,7 +118,7 @@ func determineColor(podName string) (podColor, containerColor *color.Color) {
 }
 
 // Start starts tailing
-func (t *Tail) Start(ctx context.Context, i v1.PodInterface) {
+func (t *Tail) Start(ctx context.Context) error {
 	t.podColor, t.containerColor = determineColor(t.PodName)
 
 	ctx, cancel := context.WithCancel(ctx)
@@ -120,31 +127,31 @@ func (t *Tail) Start(ctx context.Context, i v1.PodInterface) {
 		cancel()
 	}()
 
-	go func() {
-		g := color.New(color.FgHiGreen, color.Bold).SprintFunc()
-		p := t.podColor.SprintFunc()
-		c := t.containerColor.SprintFunc()
-		if t.Options.Namespace {
-			fmt.Fprintf(os.Stderr, "%s %s %s › %s\n", g("+"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
-		} else {
-			fmt.Fprintf(os.Stderr, "%s %s › %s\n", g("+"), p(t.PodName), c(t.ContainerName))
-		}
+	g := color.New(color.FgHiGreen, color.Bold).SprintFunc()
+	p := t.podColor.SprintFunc()
+	c := t.containerColor.SprintFunc()
+	if t.Options.Namespace {
+		fmt.Fprintf(t.errOut, "%s %s %s › %s\n", g("+"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
+	} else {
+		fmt.Fprintf(t.errOut, "%s %s › %s\n", g("+"), p(t.PodName), c(t.ContainerName))
+	}
 
-		req := i.GetLogs(t.PodName, &corev1.PodLogOptions{
-			Follow:       true,
-			Timestamps:   t.Options.Timestamps,
-			Container:    t.ContainerName,
-			SinceSeconds: &t.Options.SinceSeconds,
-			TailLines:    t.Options.TailLines,
-		})
-
-		err := t.ConsumeRequest(ctx, req, os.Stdout)
-		if err != nil && err != context.Canceled {
-			fmt.Fprintf(os.Stderr, "unexpected error: %v\n", err)
-		}
+	req := t.clientset.Pods(t.Namespace).GetLogs(t.PodName, &corev1.PodLogOptions{
+		Follow:       true,
+		Timestamps:   t.Options.Timestamps,
+		Container:    t.ContainerName,
+		SinceSeconds: &t.Options.SinceSeconds,
+		TailLines:    t.Options.TailLines,
+	})
 
-		t.active = false
-	}()
+	err := t.ConsumeRequest(ctx, req)
+	t.active = false
+
+	if errors.Is(err, context.Canceled) {
+		return nil
+	}
+
+	return err
 }
 
 // Close stops tailing
@@ -153,9 +160,9 @@ func (t *Tail) Close() {
 	p := t.podColor.SprintFunc()
 	c := t.containerColor.SprintFunc()
 	if t.Options.Namespace {
-		fmt.Fprintf(os.Stderr, "%s %s %s › %s\n", r("-"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
+		fmt.Fprintf(t.errOut, "%s %s %s › %s\n", r("-"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
 	} else {
-		fmt.Fprintf(os.Stderr, "%s %s › %s\n", r("-"), p(t.PodName), c(t.ContainerName))
+		fmt.Fprintf(t.errOut, "%s %s › %s\n", r("-"), p(t.PodName), c(t.ContainerName))
 	}
 
 	close(t.closed)
@@ -163,7 +170,7 @@ func (t *Tail) Close() {
 
 // ConsumeRequest reads the data from request and writes into the out
 // writer.
-func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper, out io.Writer) error {
+func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper) error {
 	stream, err := request.Stream(ctx)
 	if err != nil {
 		return err
@@ -179,7 +186,7 @@ func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper,
 			msg = strings.TrimSuffix(msg, "\n")
 
 			if !t.Options.IsExclude(msg) && t.Options.IsInclude(msg) {
-				t.Print(msg, out)
+				t.Print(msg)
 			}
 		}
 
@@ -193,7 +200,7 @@ func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper,
 }
 
 // Print prints a color coded log message with the pod and container names
-func (t *Tail) Print(msg string, out io.Writer) {
+func (t *Tail) Print(msg string) {
 	vm := Log{
 		Message:        msg,
 		NodeName:       t.NodeName,
@@ -206,11 +213,11 @@ func (t *Tail) Print(msg string, out io.Writer) {
 
 	var buf bytes.Buffer
 	if err := t.tmpl.Execute(&buf, vm); err != nil {
-		fmt.Fprintf(os.Stderr, "expanding template failed: %s\n", err)
+		fmt.Fprintf(t.errOut, "expanding template failed: %s\n", err)
 		return
 	}
 
-	fmt.Fprint(out, buf.String())
+	fmt.Fprint(t.out, buf.String())
 }
 
 // isActive returns false if the log stream is closed.


Test patch:
diff --git a/stern/tail_test.go b/stern/tail_test.go
index 6f4459f..ffaf331 100644
--- a/stern/tail_test.go
+++ b/stern/tail_test.go
@@ -10,6 +10,7 @@ import (
 	"testing"
 	"text/template"
 
+	"k8s.io/client-go/kubernetes/fake"
 	"k8s.io/client-go/rest"
 )
 
@@ -83,11 +84,12 @@ line 4 (my-node/my-namespace/my-pod/my-container)
 		},
 	}
 
+	clientset := fake.NewSimpleClientset()
 	for i, tt := range tests {
-		tail := NewTail("my-node", "my-namespace", "my-pod", "my-container", tt.tmpl, &TailOptions{})
 		out := new(bytes.Buffer)
+		tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", tt.tmpl, out, ioutil.Discard, &TailOptions{})
 
-		if err := tail.ConsumeRequest(context.TODO(), tt.request, out); err != nil {
+		if err := tail.ConsumeRequest(context.TODO(), tt.request); err != nil {
 			t.Fatalf("%d: unexpected err %v", i, err)
 		}