k8s.io/kubernetes@v1.29.3/test/e2e/framework/pod/output/output.go (about)

     1  /*
     2  Copyright 2014 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  package output
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/onsi/ginkgo/v2"
    26  	"github.com/onsi/gomega"
    27  	gomegatypes "github.com/onsi/gomega/types"
    28  
    29  	v1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/labels"
    32  	clientset "k8s.io/client-go/kubernetes"
    33  	"k8s.io/kubectl/pkg/util/podutils"
    34  	"k8s.io/kubernetes/test/e2e/framework"
    35  	e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
    36  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    37  )
    38  
    39  // DEPRECATED constants. Use the timeouts in framework.Framework instead.
    40  const (
    41  	// Poll is how often to Poll pods, nodes and claims.
    42  	Poll = 2 * time.Second
    43  )
    44  
    45  // LookForStringInPodExec looks for the given string in the output of a command
    46  // executed in the first container of specified pod.
    47  func LookForStringInPodExec(ns, podName string, command []string, expectedString string, timeout time.Duration) (result string, err error) {
    48  	return LookForStringInPodExecToContainer(ns, podName, "", command, expectedString, timeout)
    49  }
    50  
    51  // LookForStringInPodExecToContainer looks for the given string in the output of a
    52  // command executed in specified pod container, or first container if not specified.
    53  func LookForStringInPodExecToContainer(ns, podName, containerName string, command []string, expectedString string, timeout time.Duration) (result string, err error) {
    54  	return lookForString(expectedString, timeout, func() string {
    55  		args := []string{"exec", podName, fmt.Sprintf("--namespace=%v", ns)}
    56  		if len(containerName) > 0 {
    57  			args = append(args, fmt.Sprintf("--container=%s", containerName))
    58  		}
    59  		args = append(args, "--")
    60  		args = append(args, command...)
    61  		return e2ekubectl.RunKubectlOrDie(ns, args...)
    62  	})
    63  }
    64  
    65  // lookForString looks for the given string in the output of fn, repeatedly calling fn until
    66  // the timeout is reached or the string is found. Returns last log and possibly
    67  // error if the string was not found.
    68  func lookForString(expectedString string, timeout time.Duration, fn func() string) (result string, err error) {
    69  	for t := time.Now(); time.Since(t) < timeout; time.Sleep(Poll) {
    70  		result = fn()
    71  		if strings.Contains(result, expectedString) {
    72  			return
    73  		}
    74  	}
    75  	err = fmt.Errorf("Failed to find \"%s\", last result: \"%s\"", expectedString, result)
    76  	return
    77  }
    78  
    79  // RunHostCmd runs the given cmd in the context of the given pod using `kubectl exec`
    80  // inside of a shell.
    81  func RunHostCmd(ns, name, cmd string) (string, error) {
    82  	return e2ekubectl.RunKubectl(ns, "exec", name, "--", "/bin/sh", "-x", "-c", cmd)
    83  }
    84  
    85  // RunHostCmdWithFullOutput runs the given cmd in the context of the given pod using `kubectl exec`
    86  // inside of a shell. It will also return the command's stderr.
    87  func RunHostCmdWithFullOutput(ns, name, cmd string) (string, string, error) {
    88  	return e2ekubectl.RunKubectlWithFullOutput(ns, "exec", name, "--", "/bin/sh", "-x", "-c", cmd)
    89  }
    90  
    91  // RunHostCmdOrDie calls RunHostCmd and dies on error.
    92  func RunHostCmdOrDie(ns, name, cmd string) string {
    93  	stdout, err := RunHostCmd(ns, name, cmd)
    94  	framework.Logf("stdout: %v", stdout)
    95  	framework.ExpectNoError(err)
    96  	return stdout
    97  }
    98  
    99  // RunHostCmdWithRetries calls RunHostCmd and retries all errors
   100  // until it succeeds or the specified timeout expires.
   101  // This can be used with idempotent commands to deflake transient Node issues.
   102  func RunHostCmdWithRetries(ns, name, cmd string, interval, timeout time.Duration) (string, error) {
   103  	start := time.Now()
   104  	for {
   105  		out, err := RunHostCmd(ns, name, cmd)
   106  		if err == nil {
   107  			return out, nil
   108  		}
   109  		if elapsed := time.Since(start); elapsed > timeout {
   110  			return out, fmt.Errorf("RunHostCmd still failed after %v: %w", elapsed, err)
   111  		}
   112  		framework.Logf("Waiting %v to retry failed RunHostCmd: %v", interval, err)
   113  		time.Sleep(interval)
   114  	}
   115  }
   116  
   117  // LookForStringInLog looks for the given string in the log of a specific pod container
   118  func LookForStringInLog(ns, podName, container, expectedString string, timeout time.Duration) (result string, err error) {
   119  	return lookForString(expectedString, timeout, func() string {
   120  		return e2ekubectl.RunKubectlOrDie(ns, "logs", podName, container)
   121  	})
   122  }
   123  
   124  // LookForStringInLogWithoutKubectl looks for the given string in the log of a specific pod container
   125  func LookForStringInLogWithoutKubectl(ctx context.Context, client clientset.Interface, ns string, podName string, container string, expectedString string, timeout time.Duration) (result string, err error) {
   126  	return lookForString(expectedString, timeout, func() string {
   127  		podLogs, err := e2epod.GetPodLogs(ctx, client, ns, podName, container)
   128  		framework.ExpectNoError(err)
   129  		return podLogs
   130  	})
   131  }
   132  
   133  // CreateEmptyFileOnPod creates empty file at given path on the pod.
   134  func CreateEmptyFileOnPod(namespace string, podName string, filePath string) error {
   135  	_, err := e2ekubectl.RunKubectl(namespace, "exec", podName, "--", "/bin/sh", "-c", fmt.Sprintf("touch %s", filePath))
   136  	return err
   137  }
   138  
   139  // DumpDebugInfo dumps debug info of tests.
   140  func DumpDebugInfo(ctx context.Context, c clientset.Interface, ns string) {
   141  	sl, _ := c.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{LabelSelector: labels.Everything().String()})
   142  	for _, s := range sl.Items {
   143  		desc, _ := e2ekubectl.RunKubectl(ns, "describe", "po", s.Name)
   144  		framework.Logf("\nOutput of kubectl describe %v:\n%v", s.Name, desc)
   145  
   146  		l, _ := e2ekubectl.RunKubectl(ns, "logs", s.Name, "--tail=100")
   147  		framework.Logf("\nLast 100 log lines of %v:\n%v", s.Name, l)
   148  	}
   149  }
   150  
   151  // MatchContainerOutput creates a pod and waits for all it's containers to exit with success.
   152  // It then tests that the matcher with each expectedOutput matches the output of the specified container.
   153  func MatchContainerOutput(
   154  	ctx context.Context,
   155  	f *framework.Framework,
   156  	pod *v1.Pod,
   157  	containerName string,
   158  	expectedOutput []string,
   159  	matcher func(string, ...interface{}) gomegatypes.GomegaMatcher) error {
   160  	ns := pod.ObjectMeta.Namespace
   161  	if ns == "" {
   162  		ns = f.Namespace.Name
   163  	}
   164  	podClient := e2epod.PodClientNS(f, ns)
   165  
   166  	createdPod := podClient.Create(ctx, pod)
   167  	defer func() {
   168  		ginkgo.By("delete the pod")
   169  		podClient.DeleteSync(ctx, createdPod.Name, metav1.DeleteOptions{}, e2epod.DefaultPodDeletionTimeout)
   170  	}()
   171  
   172  	// Wait for client pod to complete.
   173  	podErr := e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, f.ClientSet, createdPod.Name, ns, f.Timeouts.PodStart)
   174  
   175  	// Grab its logs.  Get host first.
   176  	podStatus, err := podClient.Get(ctx, createdPod.Name, metav1.GetOptions{})
   177  	if err != nil {
   178  		return fmt.Errorf("failed to get pod status: %w", err)
   179  	}
   180  
   181  	if podErr != nil {
   182  		// Pod failed. Dump all logs from all containers to see what's wrong
   183  		_ = podutils.VisitContainers(&podStatus.Spec, podutils.AllContainers, func(c *v1.Container, containerType podutils.ContainerType) bool {
   184  			logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, ns, podStatus.Name, c.Name)
   185  			if err != nil {
   186  				framework.Logf("Failed to get logs from node %q pod %q container %q: %v",
   187  					podStatus.Spec.NodeName, podStatus.Name, c.Name, err)
   188  			} else {
   189  				framework.Logf("Output of node %q pod %q container %q: %s", podStatus.Spec.NodeName, podStatus.Name, c.Name, logs)
   190  			}
   191  			return true
   192  		})
   193  		return fmt.Errorf("expected pod %q success: %v", createdPod.Name, podErr)
   194  	}
   195  
   196  	framework.Logf("Trying to get logs from node %s pod %s container %s: %v",
   197  		podStatus.Spec.NodeName, podStatus.Name, containerName, err)
   198  
   199  	// Sometimes the actual containers take a second to get started, try to get logs for 60s
   200  	logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, ns, podStatus.Name, containerName)
   201  	if err != nil {
   202  		framework.Logf("Failed to get logs from node %q pod %q container %q. %v",
   203  			podStatus.Spec.NodeName, podStatus.Name, containerName, err)
   204  		return fmt.Errorf("failed to get logs from %s for %s: %w", podStatus.Name, containerName, err)
   205  	}
   206  
   207  	for _, expected := range expectedOutput {
   208  		m := matcher(expected)
   209  		matches, err := m.Match(logs)
   210  		if err != nil {
   211  			return fmt.Errorf("expected %q in container output: %w", expected, err)
   212  		} else if !matches {
   213  			return fmt.Errorf("expected %q in container output: %s", expected, m.FailureMessage(logs))
   214  		}
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  // TestContainerOutput runs the given pod in the given namespace and waits
   221  // for all of the containers in the podSpec to move into the 'Success' status, and tests
   222  // the specified container log against the given expected output using a substring matcher.
   223  func TestContainerOutput(ctx context.Context, f *framework.Framework, scenarioName string, pod *v1.Pod, containerIndex int, expectedOutput []string) {
   224  	TestContainerOutputMatcher(ctx, f, scenarioName, pod, containerIndex, expectedOutput, gomega.ContainSubstring)
   225  }
   226  
   227  // TestContainerOutputRegexp runs the given pod in the given namespace and waits
   228  // for all of the containers in the podSpec to move into the 'Success' status, and tests
   229  // the specified container log against the given expected output using a regexp matcher.
   230  func TestContainerOutputRegexp(ctx context.Context, f *framework.Framework, scenarioName string, pod *v1.Pod, containerIndex int, expectedOutput []string) {
   231  	TestContainerOutputMatcher(ctx, f, scenarioName, pod, containerIndex, expectedOutput, gomega.MatchRegexp)
   232  }
   233  
   234  // TestContainerOutputMatcher runs the given pod in the given namespace and waits
   235  // for all of the containers in the podSpec to move into the 'Success' status, and tests
   236  // the specified container log against the given expected output using the given matcher.
   237  func TestContainerOutputMatcher(ctx context.Context, f *framework.Framework,
   238  	scenarioName string,
   239  	pod *v1.Pod,
   240  	containerIndex int,
   241  	expectedOutput []string,
   242  	matcher func(string, ...interface{}) gomegatypes.GomegaMatcher) {
   243  	ginkgo.By(fmt.Sprintf("Creating a pod to test %v", scenarioName))
   244  	if containerIndex < 0 || containerIndex >= len(pod.Spec.Containers) {
   245  		framework.Failf("Invalid container index: %d", containerIndex)
   246  	}
   247  	framework.ExpectNoError(MatchContainerOutput(ctx, f, pod, pod.Spec.Containers[containerIndex].Name, expectedOutput, matcher))
   248  }