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 }