k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/kubectl/logs.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // OWNER = sig/cli
    18  
    19  package kubectl
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/onsi/ginkgo/v2"
    29  	"github.com/onsi/gomega"
    30  
    31  	appsv1 "k8s.io/api/apps/v1"
    32  	v1 "k8s.io/api/core/v1"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/util/uuid"
    35  	clientset "k8s.io/client-go/kubernetes"
    36  	"k8s.io/kubectl/pkg/cmd/util/podcmd"
    37  	"k8s.io/kubernetes/test/e2e/framework"
    38  	e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment"
    39  	e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
    40  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    41  	e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
    42  	imageutils "k8s.io/kubernetes/test/utils/image"
    43  	admissionapi "k8s.io/pod-security-admission/api"
    44  )
    45  
    46  func testingDeployment(name, ns string, numberOfPods int32) appsv1.Deployment {
    47  	return appsv1.Deployment{
    48  		ObjectMeta: metav1.ObjectMeta{
    49  			Name:      name,
    50  			Namespace: ns,
    51  			Labels: map[string]string{
    52  				"name": name,
    53  			},
    54  		},
    55  		Spec: appsv1.DeploymentSpec{
    56  			Replicas: &numberOfPods,
    57  			Selector: &metav1.LabelSelector{
    58  				MatchLabels: map[string]string{
    59  					"name": name,
    60  				},
    61  			},
    62  			Template: v1.PodTemplateSpec{
    63  				ObjectMeta: metav1.ObjectMeta{
    64  					Labels: map[string]string{
    65  						"name": name,
    66  					},
    67  					Annotations: map[string]string{
    68  						podcmd.DefaultContainerAnnotationName: "container-2",
    69  					},
    70  				},
    71  				Spec: v1.PodSpec{
    72  					Containers: []v1.Container{
    73  						{
    74  							Name:  "container-1",
    75  							Image: imageutils.GetE2EImage(imageutils.Agnhost),
    76  							Args:  []string{"logs-generator", "--log-lines-total", "10", "--run-duration", "5s"},
    77  						},
    78  						{
    79  							Name:  "container-2",
    80  							Image: imageutils.GetE2EImage(imageutils.Agnhost),
    81  							Args:  []string{"logs-generator", "--log-lines-total", "20", "--run-duration", "5s"},
    82  						},
    83  					},
    84  					RestartPolicy: v1.RestartPolicyAlways,
    85  				},
    86  			},
    87  		},
    88  	}
    89  }
    90  
    91  func testingPod(name, value, defaultContainerName string) v1.Pod {
    92  	return v1.Pod{
    93  		ObjectMeta: metav1.ObjectMeta{
    94  			Name: name,
    95  			Labels: map[string]string{
    96  				"name": "foo",
    97  				"time": value,
    98  			},
    99  			Annotations: map[string]string{
   100  				podcmd.DefaultContainerAnnotationName: defaultContainerName,
   101  			},
   102  		},
   103  		Spec: v1.PodSpec{
   104  			Containers: []v1.Container{
   105  				{
   106  					Name:  "container-1",
   107  					Image: imageutils.GetE2EImage(imageutils.Agnhost),
   108  					Args:  []string{"logs-generator", "--log-lines-total", "10", "--run-duration", "5s"},
   109  				},
   110  				{
   111  					Name:  defaultContainerName,
   112  					Image: imageutils.GetE2EImage(imageutils.Agnhost),
   113  					Args:  []string{"logs-generator", "--log-lines-total", "20", "--run-duration", "5s"},
   114  				},
   115  			},
   116  			RestartPolicy: v1.RestartPolicyNever,
   117  		},
   118  	}
   119  }
   120  
   121  var _ = SIGDescribe("Kubectl logs", func() {
   122  	f := framework.NewDefaultFramework("kubectl-logs")
   123  	f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
   124  	defer ginkgo.GinkgoRecover()
   125  
   126  	var c clientset.Interface
   127  	var ns string
   128  	ginkgo.BeforeEach(func() {
   129  		c = f.ClientSet
   130  		ns = f.Namespace.Name
   131  	})
   132  
   133  	// Split("something\n", "\n") returns ["something", ""], so
   134  	// strip trailing newline first
   135  	lines := func(out string) []string {
   136  		return strings.Split(strings.TrimRight(out, "\n"), "\n")
   137  	}
   138  
   139  	ginkgo.Describe("logs", func() {
   140  
   141  		podName := "logs-generator"
   142  		containerName := "logs-generator"
   143  		ginkgo.BeforeEach(func() {
   144  			ginkgo.By("creating a pod")
   145  			// Agnhost image generates logs for a total of 100 lines over 20s.
   146  			e2ekubectl.RunKubectlOrDie(ns, "run", podName, "--image="+imageutils.GetE2EImage(imageutils.Agnhost), "--restart=Never", podRunningTimeoutArg, "--", "logs-generator", "--log-lines-total", "100", "--run-duration", "20s")
   147  		})
   148  		ginkgo.AfterEach(func() {
   149  			e2ekubectl.RunKubectlOrDie(ns, "delete", "pod", podName)
   150  		})
   151  
   152  		/*
   153  			Release: v1.9
   154  			Testname: Kubectl, logs
   155  			Description: When a Pod is running then it MUST generate logs.
   156  			Starting a Pod should have a expected log line. Also log command options MUST work as expected and described below.
   157  				'kubectl logs -tail=1' should generate a output of one line, the last line in the log.
   158  				'kubectl --limit-bytes=1' should generate a single byte output.
   159  				'kubectl --tail=1 --timestamp should generate one line with timestamp in RFC3339 format
   160  				'kubectl --since=1s' should output logs that are only 1 second older from now
   161  				'kubectl --since=24h' should output logs that are only 1 day older from now
   162  		*/
   163  		framework.ConformanceIt("should be able to retrieve and filter logs", func(ctx context.Context) {
   164  
   165  			ginkgo.By("Waiting for log generator to start.")
   166  			if !e2epod.CheckPodsRunningReadyOrSucceeded(ctx, c, ns, []string{podName}, framework.PodStartTimeout) {
   167  				framework.Failf("Pod %s was not ready", podName)
   168  			}
   169  
   170  			ginkgo.By("checking for a matching strings")
   171  			_, err := e2eoutput.LookForStringInLog(ns, podName, containerName, "/api/v1/namespaces/kube-system", framework.PodStartTimeout)
   172  			framework.ExpectNoError(err)
   173  
   174  			ginkgo.By("limiting log lines")
   175  			out := e2ekubectl.RunKubectlOrDie(ns, "logs", podName, containerName, "--tail=1")
   176  			framework.Logf("got output %q", out)
   177  			gomega.Expect(out).NotTo(gomega.BeEmpty())
   178  			gomega.Expect(lines(out)).To(gomega.HaveLen(1))
   179  
   180  			ginkgo.By("limiting log bytes")
   181  			out = e2ekubectl.RunKubectlOrDie(ns, "logs", podName, containerName, "--limit-bytes=1")
   182  			framework.Logf("got output %q", out)
   183  			gomega.Expect(lines(out)).To(gomega.HaveLen(1))
   184  			gomega.Expect(out).To(gomega.HaveLen(1))
   185  
   186  			ginkgo.By("exposing timestamps")
   187  			out = e2ekubectl.RunKubectlOrDie(ns, "logs", podName, containerName, "--tail=1", "--timestamps")
   188  			framework.Logf("got output %q", out)
   189  			l := lines(out)
   190  			gomega.Expect(l).To(gomega.HaveLen(1))
   191  			words := strings.Split(l[0], " ")
   192  			gomega.Expect(len(words)).To(gomega.BeNumerically(">", 1))
   193  			if _, err := time.Parse(time.RFC3339Nano, words[0]); err != nil {
   194  				if _, err := time.Parse(time.RFC3339, words[0]); err != nil {
   195  					framework.Failf("expected %q to be RFC3339 or RFC3339Nano", words[0])
   196  				}
   197  			}
   198  
   199  			ginkgo.By("restricting to a time range")
   200  			// Note: we must wait at least two seconds,
   201  			// because the granularity is only 1 second and
   202  			// it could end up rounding the wrong way.
   203  			time.Sleep(2500 * time.Millisecond) // ensure that startup logs on the node are seen as older than 1s
   204  			recentOut := e2ekubectl.RunKubectlOrDie(ns, "logs", podName, containerName, "--since=1s")
   205  			recent := len(strings.Split(recentOut, "\n"))
   206  			olderOut := e2ekubectl.RunKubectlOrDie(ns, "logs", podName, containerName, "--since=24h")
   207  			older := len(strings.Split(olderOut, "\n"))
   208  			gomega.Expect(recent).To(gomega.BeNumerically("<", older), "expected recent(%v) to be less than older(%v)\nrecent lines:\n%v\nolder lines:\n%v\n", recent, older, recentOut, olderOut)
   209  		})
   210  	})
   211  
   212  	ginkgo.Describe("default container logs", func() {
   213  		ginkgo.Describe("the second container is the default-container by annotation", func() {
   214  			var pod *v1.Pod
   215  			podName := "pod" + string(uuid.NewUUID())
   216  			defaultContainerName := "container-2"
   217  			ginkgo.BeforeEach(func(ctx context.Context) {
   218  				podClient := f.ClientSet.CoreV1().Pods(ns)
   219  				ginkgo.By("constructing the pod")
   220  				value := strconv.Itoa(time.Now().Nanosecond())
   221  				podCopy := testingPod(podName, value, defaultContainerName)
   222  				pod = &podCopy
   223  				ginkgo.By("creating the pod")
   224  				_, err := podClient.Create(ctx, pod, metav1.CreateOptions{})
   225  				if err != nil {
   226  					framework.Failf("Failed to create pod: %v", err)
   227  				}
   228  			})
   229  			ginkgo.AfterEach(func() {
   230  				e2ekubectl.RunKubectlOrDie(ns, "delete", "pod", podName)
   231  			})
   232  
   233  			ginkgo.It("should log default container if not specified", func(ctx context.Context) {
   234  				ginkgo.By("Waiting for log generator to start.")
   235  				// we need to wait for pod completion, to check the generated number of lines
   236  				if err := e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, c, podName, ns, framework.PodStartTimeout); err != nil {
   237  					framework.Failf("Pod %s did not finish: %v", podName, err)
   238  				}
   239  
   240  				ginkgo.By("specified container log lines")
   241  				out := e2ekubectl.RunKubectlOrDie(ns, "logs", podName, "-c", "container-1")
   242  				framework.Logf("got output %q", out)
   243  				gomega.Expect(out).NotTo(gomega.BeEmpty())
   244  				gomega.Expect(lines(out)).To(gomega.HaveLen(10))
   245  
   246  				ginkgo.By("log all containers log lines")
   247  				out = e2ekubectl.RunKubectlOrDie(ns, "logs", podName, "--all-containers")
   248  				framework.Logf("got output %q", out)
   249  				gomega.Expect(out).NotTo(gomega.BeEmpty())
   250  				gomega.Expect(lines(out)).To(gomega.HaveLen(30))
   251  
   252  				ginkgo.By("default container logs")
   253  				out = e2ekubectl.RunKubectlOrDie(ns, "logs", podName)
   254  				framework.Logf("got output %q", out)
   255  				gomega.Expect(lines(out)).To(gomega.HaveLen(20))
   256  			})
   257  		})
   258  	})
   259  
   260  	ginkgo.Describe("all pod logs", func() {
   261  		ginkgo.Describe("the Deployment has 2 replicas and each pod has 2 containers", func() {
   262  			var deploy *appsv1.Deployment
   263  			deployName := "deploy-" + string(uuid.NewUUID())
   264  			numberReplicas := int32(2)
   265  			ginkgo.BeforeEach(func(ctx context.Context) {
   266  				deployClient := c.AppsV1().Deployments(ns)
   267  				ginkgo.By("constructing the Deployment")
   268  				deployCopy := testingDeployment(deployName, ns, numberReplicas)
   269  				deploy = &deployCopy
   270  				ginkgo.By("creating the Deployment")
   271  				var err error
   272  				deploy, err = deployClient.Create(ctx, deploy, metav1.CreateOptions{})
   273  				if err != nil {
   274  					framework.Failf("Failed to create Deployment: %v", err)
   275  				}
   276  
   277  				if err = e2edeployment.WaitForDeploymentComplete(c, deploy); err != nil {
   278  					framework.Failf("Failed to wait for Deployment to complete: %v", err)
   279  				}
   280  
   281  			})
   282  
   283  			ginkgo.AfterEach(func() {
   284  				e2ekubectl.RunKubectlOrDie(ns, "delete", "deploy", deployName)
   285  			})
   286  
   287  			ginkgo.It("should get logs from all pods based on default container", func(ctx context.Context) {
   288  				ginkgo.By("Waiting for Deployment pods to be running.")
   289  
   290  				// get the pod names
   291  				pods, err := e2edeployment.GetPodsForDeployment(ctx, c, deploy)
   292  				if err != nil {
   293  					framework.Failf("Failed to get pods for Deployment: %v", err)
   294  				}
   295  
   296  				podOne := pods.Items[0].GetName()
   297  				podTwo := pods.Items[1].GetName()
   298  
   299  				ginkgo.By("expecting logs from both replicas in Deployment")
   300  				out := e2ekubectl.RunKubectlOrDie(ns, "logs", fmt.Sprintf("deploy/%s", deployName), "--all-pods")
   301  				framework.Logf("got output %q", out)
   302  				logLines := strings.Split(out, "\n")
   303  				logFound := false
   304  				for _, line := range logLines {
   305  					var deployPod bool
   306  					if line != "" {
   307  						if strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podTwo)) {
   308  							logFound = true
   309  							deployPod = true
   310  						}
   311  						gomega.Expect(deployPod).To(gomega.BeTrueBecause("each log should be from the default container from each pod in the Deployment"))
   312  					}
   313  
   314  				}
   315  				gomega.Expect(logFound).To(gomega.BeTrueBecause("log should be present"))
   316  			})
   317  
   318  			ginkgo.It("should get logs from each pod and each container in Deployment", func(ctx context.Context) {
   319  				ginkgo.By("Waiting for Deployment pods to be running.")
   320  
   321  				pods, err := e2edeployment.GetPodsForDeployment(ctx, c, deploy)
   322  				if err != nil {
   323  					framework.Failf("Failed to get pods for Deployment: %v", err)
   324  				}
   325  
   326  				podOne := pods.Items[0].GetName()
   327  				podTwo := pods.Items[1].GetName()
   328  
   329  				ginkgo.By("all containers and all containers")
   330  				out := e2ekubectl.RunKubectlOrDie(ns, "logs", fmt.Sprintf("deploy/%s", deployName), "--all-pods", "--all-containers")
   331  				framework.Logf("got output %q", out)
   332  				logLines := strings.Split(out, "\n")
   333  				logFound := false
   334  				for _, line := range logLines {
   335  					if line != "" {
   336  						var deployPodContainer bool
   337  						if strings.Contains(line, fmt.Sprintf("[pod/%s/container-1]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-1]", podTwo)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podTwo)) {
   338  							logFound = true
   339  							deployPodContainer = true
   340  						}
   341  						gomega.Expect(deployPodContainer).To(gomega.BeTrueBecause("each log should be from all containers from all pods in the Deployment"))
   342  					}
   343  				}
   344  				gomega.Expect(logFound).To(gomega.BeTrueBecause("log should be present"))
   345  				gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-1]", podOne))).To(gomega.BeTrueBecause("pod 1 container 1 log should be present"))
   346  				gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-2]", podOne))).To(gomega.BeTrueBecause("pod 1 container 2 log should be present"))
   347  				gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-1]", podTwo))).To(gomega.BeTrueBecause("pod 2 container 1 log should be present"))
   348  				gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-2]", podTwo))).To(gomega.BeTrueBecause("pod 2 container 2 log should be present"))
   349  				gomega.Expect(out).NotTo(gomega.BeEmpty())
   350  
   351  			})
   352  
   353  		})
   354  	})
   355  
   356  })