Repository: stern/stern (go)

Issue:
use --template with labels
**What would you like to be added**:
Use label in template. Like
```
stern -l part-of=microapp --tail 10 --template '{{.ContainerName}} {{.PodName}} {{.Labels.app}} {{.Labels.version}} {{.Message}}'
```
**Why is this needed**:
label is helpful than ContainerName or PodName

Pull request:
Add support for labels & annotations in templates
Fixes #335 

This is two commits, the first is the minimum needed to support labels & annotations in templates. The second avoids the re-lookup of a pod to get annotations and labels by passing around `Pod` inside `Target`. This should also make it easier to add more fields in templates later as the entire pod is available when `Log` is created.

Patch:
diff --git a/README.md b/README.md
index d491257..caa589c 100644
--- a/README.md
+++ b/README.md
@@ -147,13 +147,15 @@ It accepts a custom template through the `--template` flag, which will be
 compiled to a Go template and then used for every log message. This Go template
 will receive the following struct:
 
-| property        | type   | description                                 |
-|-----------------|--------|---------------------------------------------|
-| `Message`       | string | The log message itself                      |
-| `NodeName`      | string | The node name where the pod is scheduled on |
-| `Namespace`     | string | The namespace of the pod                    |
-| `PodName`       | string | The name of the pod                         |
-| `ContainerName` | string | The name of the container                   |
+| property        | type              | description                                 |
+|-----------------|-------------------|---------------------------------------------|
+| `Message`       | string            | The log message itself                      |
+| `NodeName`      | string            | The node name where the pod is scheduled on |
+| `Namespace`     | string            | The namespace of the pod                    |
+| `PodName`       | string            | The name of the pod                         |
+| `ContainerName` | string            | The name of the container                   |
+| `Labels`        | map[string]string | The labels of the pod                       |
+| `Annotations`   | map[string]string | The annotations of the pod                  |
 
 The following functions are available within the template (besides the [builtin
 functions](https://golang.org/pkg/text/template/#hdr-Functions)):
diff --git a/stern/stern.go b/stern/stern.go
index 77b7f28..e3259f4 100644
--- a/stern/stern.go
+++ b/stern/stern.go
@@ -66,7 +66,7 @@ func Run(ctx context.Context, client kubernetes.Interface, config *Config) error
 		}
 	}
 	newTail := func(t *Target) *Tail {
-		return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, newTailOptions(), config.DiffContainer)
+		return NewTail(client.CoreV1(), t.Pod, t.Container, config.Template, config.Out, config.ErrOut, newTailOptions(), config.DiffContainer)
 	}
 
 	if config.Stdin {
diff --git a/stern/tail.go b/stern/tail.go
index 997d49f..555874d 100644
--- a/stern/tail.go
+++ b/stern/tail.go
@@ -43,10 +43,9 @@ const TimestampFormatShort = "01-02 15:04:05"
 type Tail struct {
 	clientset corev1client.CoreV1Interface
 
-	NodeName       string
-	Namespace      string
-	PodName        string
-	ContainerName  string
+	Pod           *corev1.Pod
+	ContainerName string
+
 	Options        *TailOptions
 	closed         chan struct{}
 	podColor       *color.Color
@@ -67,14 +66,12 @@ type ResumeRequest struct {
 }
 
 // NewTail returns a new tail for a Kubernetes container inside a pod
-func NewTail(clientset corev1client.CoreV1Interface, nodeName, namespace, podName, containerName string, tmpl *template.Template, out, errOut io.Writer, options *TailOptions, diffContainer bool) *Tail {
-	podColor, containerColor := determineColor(podName, containerName, diffContainer)
+func NewTail(clientset corev1client.CoreV1Interface, pod *corev1.Pod, containerName string, tmpl *template.Template, out, errOut io.Writer, options *TailOptions, diffContainer bool) *Tail {
+	podColor, containerColor := determineColor(pod.Name, containerName, diffContainer)
 
 	return &Tail{
 		clientset:      clientset,
-		NodeName:       nodeName,
-		Namespace:      namespace,
-		PodName:        podName,
+		Pod:            pod,
 		ContainerName:  containerName,
 		Options:        options,
 		closed:         make(chan struct{}),
@@ -111,7 +108,7 @@ func (t *Tail) Start(ctx context.Context) error {
 
 	t.printStarting()
 
-	req := t.clientset.Pods(t.Namespace).GetLogs(t.PodName, &corev1.PodLogOptions{
+	req := t.clientset.Pods(t.Pod.Namespace).GetLogs(t.Pod.Name, &corev1.PodLogOptions{
 		Follow:       t.Options.Follow,
 		Timestamps:   true,
 		Container:    t.ContainerName,
@@ -155,9 +152,9 @@ func (t *Tail) printStarting() {
 		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))
+			fmt.Fprintf(t.errOut, "%s %s %s › %s\n", g("+"), p(t.Pod.Namespace), p(t.Pod.Name), c(t.ContainerName))
 		} else {
-			fmt.Fprintf(t.errOut, "%s %s › %s\n", g("+"), p(t.PodName), c(t.ContainerName))
+			fmt.Fprintf(t.errOut, "%s %s › %s\n", g("+"), p(t.Pod.Name), c(t.ContainerName))
 		}
 	}
 }
@@ -168,9 +165,9 @@ func (t *Tail) printStopping() {
 		p := t.podColor.SprintFunc()
 		c := t.containerColor.SprintFunc()
 		if t.Options.Namespace {
-			fmt.Fprintf(t.errOut, "%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.Pod.Namespace), p(t.Pod.Name), c(t.ContainerName))
 		} else {
-			fmt.Fprintf(t.errOut, "%s %s › %s\n", r("-"), p(t.PodName), c(t.ContainerName))
+			fmt.Fprintf(t.errOut, "%s %s › %s\n", r("-"), p(t.Pod.Name), c(t.ContainerName))
 		}
 	}
 }
@@ -204,10 +201,12 @@ func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper)
 func (t *Tail) Print(msg string) {
 	vm := Log{
 		Message:        msg,
-		NodeName:       t.NodeName,
-		Namespace:      t.Namespace,
-		PodName:        t.PodName,
+		NodeName:       t.Pod.Spec.NodeName,
+		Namespace:      t.Pod.Namespace,
+		PodName:        t.Pod.Name,
 		ContainerName:  t.ContainerName,
+		Labels:         t.Pod.Labels,
+		Annotations:    t.Pod.Annotations,
 		PodColor:       t.podColor,
 		ContainerColor: t.containerColor,
 	}
diff --git a/stern/tail_utils.go b/stern/tail_utils.go
index 08b77b1..6b9f8c0 100644
--- a/stern/tail_utils.go
+++ b/stern/tail_utils.go
@@ -29,6 +29,12 @@ type Log struct {
 	// ContainerName of the container
 	ContainerName string `json:"containerName"`
 
+	// Labels of the pod
+	Labels map[string]string `json:"labels"`
+
+	// Annotations of the pod
+	Annotations map[string]string `json:"annotations"`
+
 	PodColor       *color.Color `json:"-"`
 	ContainerColor *color.Color `json:"-"`
 }
diff --git a/stern/target.go b/stern/target.go
index cbe2148..648d67a 100644
--- a/stern/target.go
+++ b/stern/target.go
@@ -25,15 +25,13 @@ import (
 
 // Target is a target to watch
 type Target struct {
-	Node      string
-	Namespace string
-	Pod       string
+	Pod       *corev1.Pod
 	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)
+	return fmt.Sprintf("%s-%s-%s", t.Pod.Namespace, t.Pod.Name, t.Container)
 }
 
 // targetState holds a last shown container ID
@@ -112,9 +110,7 @@ OUTER:
 		}
 
 		t := &Target{
-			Node:      pod.Spec.NodeName,
-			Namespace: pod.Namespace,
-			Pod:       pod.Name,
+			Pod:       pod,
 			Container: c.Name,
 		}
 


Test patch:
diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go
index 44de0b6..edf19fb 100644
--- a/cmd/cmd_test.go
+++ b/cmd/cmd_test.go
@@ -271,7 +271,7 @@ func TestOptionsGenerateTemplate(t *testing.T) {
 				return o
 			}(),
 			"json message",
-			`{"message":"json message","nodeName":"node1","namespace":"ns1","podName":"pod1","containerName":"container1"}
+			`{"message":"json message","nodeName":"node1","namespace":"ns1","podName":"pod1","containerName":"container1","labels":{"app":"nginx","env":"prod"},"annotations":{"version":"1.23.4"}}
 `,
 			false,
 		},
@@ -432,6 +432,72 @@ func TestOptionsGenerateTemplate(t *testing.T) {
 			`Jan 01 2024 05:00 UTC`,
 			false,
 		},
+		{
+			"template with specific label",
+			func() *options {
+				o := NewOptions(streams)
+				o.template = "{{.PodName}} [{{.Labels.app}}] {{.Message}}"
+				return o
+			}(),
+			"message with app label",
+			"pod1 [nginx] message with app label",
+			false,
+		},
+		{
+			"template with specific annotation",
+			func() *options {
+				o := NewOptions(streams)
+				o.template = "{{.PodName}} [{{.Annotations.version}}] {{.Message}}"
+				return o
+			}(),
+			"message with version annotation",
+			"pod1 [1.23.4] message with version annotation",
+			false,
+		},
+		{
+			"template with labels as json",
+			func() *options {
+				o := NewOptions(streams)
+				o.template = "{{.PodName}} {{json .Labels}} {{.Message}}"
+				return o
+			}(),
+			"message with json labels",
+			"pod1 {\"app\":\"nginx\",\"env\":\"prod\"} message with json labels",
+			false,
+		},
+		{
+			"template with annotations as json",
+			func() *options {
+				o := NewOptions(streams)
+				o.template = "{{.PodName}} {{json .Annotations}} {{.Message}}"
+				return o
+			}(),
+			"message with json annotations",
+			"pod1 {\"version\":\"1.23.4\"} message with json annotations",
+			false,
+		},
+		{
+			"template with missing label",
+			func() *options {
+				o := NewOptions(streams)
+				o.template = "{{.PodName}} [{{.Labels.missing}}] {{.Message}}"
+				return o
+			}(),
+			"message with missing label",
+			"pod1 [<no value>] message with missing label",
+			false,
+		},
+		{
+			"template with missing annotation",
+			func() *options {
+				o := NewOptions(streams)
+				o.template = "{{.PodName}} [{{.Annotations.missing}}] {{.Message}}"
+				return o
+			}(),
+			"message with missing annotation",
+			"pod1 [<no value>] message with missing annotation",
+			false,
+		},
 	}
 
 	for _, tt := range tests {
@@ -442,6 +508,8 @@ func TestOptionsGenerateTemplate(t *testing.T) {
 				Namespace:      "ns1",
 				PodName:        "pod1",
 				ContainerName:  "container1",
+				Labels:         map[string]string{"app": "nginx", "env": "prod"},
+				Annotations:    map[string]string{"version": "1.23.4"},
 				PodColor:       color.New(color.FgRed),
 				ContainerColor: color.New(color.FgBlue),
 			}
diff --git a/stern/tail_test.go b/stern/tail_test.go
index f8a9f86..b60d785 100644
--- a/stern/tail_test.go
+++ b/stern/tail_test.go
@@ -8,6 +8,8 @@ import (
 	"testing"
 	"text/template"
 
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes/fake"
 )
 
@@ -103,7 +105,16 @@ line 4 (my-node/my-namespace/my-pod/my-container)
 	for i, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			out := new(bytes.Buffer)
-			tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", tmpl, out, io.Discard, &TailOptions{}, false)
+			pod := &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "my-namespace",
+					Name:      "my-pod",
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "my-node",
+				},
+			}
+			tail := NewTail(clientset.CoreV1(), pod, "my-container", tmpl, out, io.Discard, &TailOptions{}, false)
 			tail.resumeRequest = tt.resumeReq
 			if err := tail.ConsumeRequest(context.TODO(), &responseWrapperMock{data: bytes.NewBufferString(logLines)}); err != nil {
 				t.Fatalf("%d: unexpected err %v", i, err)
@@ -162,7 +173,13 @@ func TestPrintStarting(t *testing.T) {
 	clientset := fake.NewSimpleClientset()
 	for i, tt := range tests {
 		errOut := new(bytes.Buffer)
-		tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options, false)
+		pod := &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "my-namespace",
+				Name:      "my-pod",
+			},
+		}
+		tail := NewTail(clientset.CoreV1(), pod, "my-container", nil, io.Discard, errOut, tt.options, false)
 		tail.printStarting()
 
 		if !bytes.Equal(tt.expected, errOut.Bytes()) {
@@ -204,7 +221,13 @@ func TestPrintStopping(t *testing.T) {
 	clientset := fake.NewSimpleClientset()
 	for i, tt := range tests {
 		errOut := new(bytes.Buffer)
-		tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options, false)
+		pod := &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "my-namespace",
+				Name:      "my-pod",
+			},
+		}
+		tail := NewTail(clientset.CoreV1(), pod, "my-container", nil, io.Discard, errOut, tt.options, false)
 		tail.printStopping()
 
 		if !bytes.Equal(tt.expected, errOut.Bytes()) {
diff --git a/stern/target_test.go b/stern/target_test.go
index 36fb7a1..2045807 100644
--- a/stern/target_test.go
+++ b/stern/target_test.go
@@ -49,10 +49,9 @@ func TestTargetFilter(t *testing.T) {
 	}
 
 	genTarget := func(node, pod, container string) Target {
+		p := createPod(node, pod)
 		return Target{
-			Namespace: "ns1",
-			Node:      node,
-			Pod:       pod,
+			Pod:       p,
 			Container: container,
 		}
 	}
@@ -346,12 +345,17 @@ func TestTargetFilterShouldAdd(t *testing.T) {
 			},
 		}
 	}
-	genTarget := func(container string) Target {
+	genTarget := func(containerName, containerID string) Target {
+		pod := createPod(corev1.ContainerStatus{
+			Name:        containerName,
+			ContainerID: containerID,
+			State: corev1.ContainerState{
+				Running: &corev1.ContainerStateRunning{},
+			},
+		})
 		return Target{
-			Namespace: "ns1",
-			Node:      "node1",
-			Pod:       "pod1",
-			Container: container,
+			Pod:       pod,
+			Container: containerName,
 		}
 	}
 	tests := []struct {
@@ -374,7 +378,7 @@ func TestTargetFilterShouldAdd(t *testing.T) {
 					Running: &corev1.ContainerStateRunning{},
 				},
 			},
-			expected: []Target{genTarget("c1")},
+			expected: []Target{genTarget("c1", "cid1")},
 		},
 		{
 			name: "same container ID should be ignored",
@@ -396,7 +400,7 @@ func TestTargetFilterShouldAdd(t *testing.T) {
 					Running: &corev1.ContainerStateRunning{},
 				},
 			},
-			expected: []Target{genTarget("c1")},
+			expected: []Target{genTarget("c1", "cid2")},
 		},
 		{
 			name:   "forget() allows the same ID ",
@@ -408,7 +412,7 @@ func TestTargetFilterShouldAdd(t *testing.T) {
 					Running: &corev1.ContainerStateRunning{},
 				},
 			},
-			expected: []Target{genTarget("c1")},
+			expected: []Target{genTarget("c1", "cid2")},
 		},
 	}
 	for _, tt := range tests {