k8s.io/kubernetes@v1.29.3/test/e2e/framework/kubectl/kubectl_utils.go (about)

     1  /*
     2  Copyright 2019 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 kubectl
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	clientset "k8s.io/client-go/kubernetes"
    31  	"k8s.io/client-go/tools/clientcmd"
    32  	"k8s.io/kubernetes/test/e2e/framework"
    33  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    34  	testutils "k8s.io/kubernetes/test/utils"
    35  
    36  	"github.com/onsi/ginkgo/v2"
    37  )
    38  
    39  const (
    40  	maxKubectlExecRetries = 5
    41  )
    42  
    43  // TestKubeconfig is a struct containing the needed attributes from TestContext and Framework(Namespace).
    44  type TestKubeconfig struct {
    45  	CertDir     string
    46  	Host        string
    47  	KubeConfig  string
    48  	KubeContext string
    49  	KubectlPath string
    50  	Namespace   string // Every test has at least one namespace unless creation is skipped
    51  }
    52  
    53  // NewTestKubeconfig returns a new Kubeconfig struct instance.
    54  func NewTestKubeconfig(certdir, host, kubeconfig, kubecontext, kubectlpath, namespace string) *TestKubeconfig {
    55  	return &TestKubeconfig{
    56  		CertDir:     certdir,
    57  		Host:        host,
    58  		KubeConfig:  kubeconfig,
    59  		KubeContext: kubecontext,
    60  		KubectlPath: kubectlpath,
    61  		Namespace:   namespace,
    62  	}
    63  }
    64  
    65  // KubectlCmd runs the kubectl executable through the wrapper script.
    66  func (tk *TestKubeconfig) KubectlCmd(args ...string) *exec.Cmd {
    67  	defaultArgs := []string{}
    68  
    69  	// Reference a --server option so tests can run anywhere.
    70  	if tk.Host != "" {
    71  		defaultArgs = append(defaultArgs, "--"+clientcmd.FlagAPIServer+"="+tk.Host)
    72  	}
    73  	if tk.KubeConfig != "" {
    74  		defaultArgs = append(defaultArgs, "--"+clientcmd.RecommendedConfigPathFlag+"="+tk.KubeConfig)
    75  
    76  		// Reference the KubeContext
    77  		if tk.KubeContext != "" {
    78  			defaultArgs = append(defaultArgs, "--"+clientcmd.FlagContext+"="+tk.KubeContext)
    79  		}
    80  
    81  	} else {
    82  		if tk.CertDir != "" {
    83  			defaultArgs = append(defaultArgs,
    84  				fmt.Sprintf("--certificate-authority=%s", filepath.Join(tk.CertDir, "ca.crt")),
    85  				fmt.Sprintf("--client-certificate=%s", filepath.Join(tk.CertDir, "kubecfg.crt")),
    86  				fmt.Sprintf("--client-key=%s", filepath.Join(tk.CertDir, "kubecfg.key")))
    87  		}
    88  	}
    89  	if tk.Namespace != "" {
    90  		defaultArgs = append(defaultArgs, fmt.Sprintf("--namespace=%s", tk.Namespace))
    91  	}
    92  	kubectlArgs := append(defaultArgs, args...)
    93  
    94  	//We allow users to specify path to kubectl, so you can test either "kubectl" or "cluster/kubectl.sh"
    95  	//and so on.
    96  	cmd := exec.Command(tk.KubectlPath, kubectlArgs...)
    97  
    98  	//caller will invoke this and wait on it.
    99  	return cmd
   100  }
   101  
   102  // LogFailedContainers runs `kubectl logs` on a failed containers.
   103  func LogFailedContainers(ctx context.Context, c clientset.Interface, ns string, logFunc func(ftm string, args ...interface{})) {
   104  	podList, err := c.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{})
   105  	if err != nil {
   106  		logFunc("Error getting pods in namespace '%s': %v", ns, err)
   107  		return
   108  	}
   109  	logFunc("Running kubectl logs on non-ready containers in %v", ns)
   110  	for _, pod := range podList.Items {
   111  		if res, err := testutils.PodRunningReady(&pod); !res || err != nil {
   112  			kubectlLogPod(ctx, c, pod, "", framework.Logf)
   113  		}
   114  	}
   115  }
   116  
   117  func kubectlLogPod(ctx context.Context, c clientset.Interface, pod v1.Pod, containerNameSubstr string, logFunc func(ftm string, args ...interface{})) {
   118  	for _, container := range pod.Spec.Containers {
   119  		if strings.Contains(container.Name, containerNameSubstr) {
   120  			// Contains() matches all strings if substr is empty
   121  			logs, err := e2epod.GetPodLogs(ctx, c, pod.Namespace, pod.Name, container.Name)
   122  			if err != nil {
   123  				logs, err = e2epod.GetPreviousPodLogs(ctx, c, pod.Namespace, pod.Name, container.Name)
   124  				if err != nil {
   125  					logFunc("Failed to get logs of pod %v, container %v, err: %v", pod.Name, container.Name, err)
   126  				}
   127  			}
   128  			logFunc("Logs of %v/%v:%v on node %v", pod.Namespace, pod.Name, container.Name, pod.Spec.NodeName)
   129  			logFunc("%s : STARTLOG\n%s\nENDLOG for container %v:%v:%v", containerNameSubstr, logs, pod.Namespace, pod.Name, container.Name)
   130  		}
   131  	}
   132  }
   133  
   134  // WriteFileViaContainer writes a file using kubectl exec echo <contents> > <path> via specified container
   135  // because of the primitive technique we're using here, we only allow ASCII alphanumeric characters
   136  func (tk *TestKubeconfig) WriteFileViaContainer(podName, containerName string, path string, contents string) error {
   137  	ginkgo.By("writing a file in the container")
   138  	allowedCharacters := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   139  	for _, c := range contents {
   140  		if !strings.ContainsRune(allowedCharacters, c) {
   141  			return fmt.Errorf("Unsupported character in string to write: %v", c)
   142  		}
   143  	}
   144  	command := fmt.Sprintf("echo '%s' > '%s'; sync", contents, path)
   145  	stdout, stderr, err := tk.kubectlExecWithRetry(tk.Namespace, podName, containerName, "--", "/bin/sh", "-c", command)
   146  	if err != nil {
   147  		framework.Logf("error running kubectl exec to write file: %v\nstdout=%v\nstderr=%v)", err, string(stdout), string(stderr))
   148  	}
   149  	return err
   150  }
   151  
   152  // ReadFileViaContainer reads a file using kubectl exec cat <path>.
   153  func (tk *TestKubeconfig) ReadFileViaContainer(podName, containerName string, path string) (string, error) {
   154  	ginkgo.By("reading a file in the container")
   155  
   156  	stdout, stderr, err := tk.kubectlExecWithRetry(tk.Namespace, podName, containerName, "--", "cat", path)
   157  	if err != nil {
   158  		framework.Logf("error running kubectl exec to read file: %v\nstdout=%v\nstderr=%v)", err, string(stdout), string(stderr))
   159  	}
   160  	return string(stdout), err
   161  }
   162  
   163  func (tk *TestKubeconfig) kubectlExecWithRetry(namespace string, podName, containerName string, args ...string) ([]byte, []byte, error) {
   164  	for numRetries := 0; numRetries < maxKubectlExecRetries; numRetries++ {
   165  		if numRetries > 0 {
   166  			framework.Logf("Retrying kubectl exec (retry count=%v/%v)", numRetries+1, maxKubectlExecRetries)
   167  		}
   168  
   169  		stdOutBytes, stdErrBytes, err := tk.kubectlExec(namespace, podName, containerName, args...)
   170  		if err != nil {
   171  			if strings.Contains(strings.ToLower(string(stdErrBytes)), "i/o timeout") {
   172  				// Retry on "i/o timeout" errors
   173  				framework.Logf("Warning: kubectl exec encountered i/o timeout.\nerr=%v\nstdout=%v\nstderr=%v)", err, string(stdOutBytes), string(stdErrBytes))
   174  				continue
   175  			}
   176  			if strings.Contains(strings.ToLower(string(stdErrBytes)), "container not found") {
   177  				// Retry on "container not found" errors
   178  				framework.Logf("Warning: kubectl exec encountered container not found.\nerr=%v\nstdout=%v\nstderr=%v)", err, string(stdOutBytes), string(stdErrBytes))
   179  				time.Sleep(2 * time.Second)
   180  				continue
   181  			}
   182  		}
   183  
   184  		return stdOutBytes, stdErrBytes, err
   185  	}
   186  	err := fmt.Errorf("Failed: kubectl exec failed %d times with \"i/o timeout\". Giving up", maxKubectlExecRetries)
   187  	return nil, nil, err
   188  }
   189  
   190  func (tk *TestKubeconfig) kubectlExec(namespace string, podName, containerName string, args ...string) ([]byte, []byte, error) {
   191  	var stdout, stderr bytes.Buffer
   192  	cmdArgs := []string{
   193  		"exec",
   194  		fmt.Sprintf("--namespace=%v", namespace),
   195  		podName,
   196  		fmt.Sprintf("-c=%v", containerName),
   197  	}
   198  	cmdArgs = append(cmdArgs, args...)
   199  
   200  	cmd := tk.KubectlCmd(cmdArgs...)
   201  	cmd.Stdout, cmd.Stderr = &stdout, &stderr
   202  
   203  	framework.Logf("Running '%s %s'", cmd.Path, strings.Join(cmdArgs, " "))
   204  	err := cmd.Run()
   205  	return stdout.Bytes(), stderr.Bytes(), err
   206  }