github.com/interconnectedcloud/qdr-operator@v0.0.0-20210826174505-576d2b33dac7/test/e2e/framework/kubectl.go (about)

     1  /*
     2  Copyright 2019 The Interconnectedcloud 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 framework
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"net"
    23  	"net/url"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  	"syscall"
    29  	"time"
    30  
    31  	"k8s.io/client-go/tools/clientcmd"
    32  	uexec "k8s.io/utils/exec"
    33  
    34  	e2elog "github.com/interconnectedcloud/qdr-operator/test/e2e/framework/log"
    35  	//"github.com/onsi/gomega"
    36  )
    37  
    38  const (
    39  	// Poll is how often to Poll pods
    40  	Poll = 2 * time.Second
    41  )
    42  
    43  // KubectlCmd runs the kubectl executable through the wrapper script.
    44  func KubectlCmd(args ...string) *exec.Cmd {
    45  	defaultArgs := []string{}
    46  
    47  	// Reference a --server option so tests can run anywhere.
    48  	if TestContext.Host != "" {
    49  		defaultArgs = append(defaultArgs, "--"+clientcmd.FlagAPIServer+"="+TestContext.Host)
    50  	}
    51  	if TestContext.KubeConfig != "" {
    52  		defaultArgs = append(defaultArgs, "--"+clientcmd.RecommendedConfigPathFlag+"="+TestContext.KubeConfig)
    53  
    54  		// Reference the KubeContext
    55  		if TestContext.KubeContext != "" {
    56  			defaultArgs = append(defaultArgs, "--"+clientcmd.FlagContext+"="+TestContext.KubeContext)
    57  		}
    58  
    59  	} else {
    60  		if TestContext.CertDir != "" {
    61  			defaultArgs = append(defaultArgs,
    62  				fmt.Sprintf("--certificate-authority=%s", filepath.Join(TestContext.CertDir, "ca.crt")),
    63  				fmt.Sprintf("--client-certificate=%s", filepath.Join(TestContext.CertDir, "kubecfg.crt")),
    64  				fmt.Sprintf("--client-key=%s", filepath.Join(TestContext.CertDir, "kubecfg.key")))
    65  		}
    66  	}
    67  	kubectlArgs := append(defaultArgs, args...)
    68  
    69  	//We allow users to specify path to kubectl, so you can test either "kubectl" or "cluster/kubectl.sh"
    70  	//and so on.
    71  	cmd := exec.Command(TestContext.KubectlPath, kubectlArgs...)
    72  
    73  	//caller will invoke this and wait on it.
    74  	return cmd
    75  }
    76  
    77  // LookForString looks for the given string in the output of fn, repeatedly calling fn until
    78  // the timeout is reached or the string is found. Returns last log and possibly
    79  // error if the string was not found.
    80  // TODO(alejandrox1): move to pod/ subpkg once kubectl methods are refactored.
    81  func LookForString(expectedString string, timeout time.Duration, fn func() string) (result string, err error) {
    82  	for t := time.Now(); time.Since(t) < timeout; time.Sleep(Poll) {
    83  		result = fn()
    84  		if strings.Contains(result, expectedString) {
    85  			return
    86  		}
    87  	}
    88  	err = fmt.Errorf("Failed to find \"%s\", last result: \"%s\"", expectedString, result)
    89  	return
    90  }
    91  
    92  // LookForStringInLog looks for the given string in the log of a specific pod container
    93  func LookForStringInLog(ns, podName, container, expectedString string, timeout time.Duration) (result string, err error) {
    94  	return LookForString(expectedString, timeout, func() string {
    95  		return RunKubectlOrDie("logs", podName, container, fmt.Sprintf("--namespace=%v", ns))
    96  	})
    97  }
    98  
    99  // LookForRegexp looks for the given regexp in results from given "func() string"
   100  func LookForRegexp(expectedRegexp string, timeout time.Duration, fn func() string) (result string, err error) {
   101  	var expRegexp = regexp.MustCompile(expectedRegexp)
   102  	for t := time.Now(); time.Since(t) < timeout; time.Sleep(Poll) {
   103  		result = fn()
   104  		if expRegexp.MatchString(result) {
   105  			return
   106  		}
   107  	}
   108  	err = fmt.Errorf("Failed to find \"%s\", last result: \"%s\"", expectedRegexp, result)
   109  	return
   110  }
   111  
   112  // LookForRegexpInLog looks for the given regexp in the log of a specific pod container
   113  func LookForRegexpInLog(ns, podName, container, expectedRegexp string, timeout time.Duration) (result string, err error) {
   114  	return LookForRegexp(expectedRegexp, timeout, func() string {
   115  		return RunKubectlOrDie("logs", podName, container, fmt.Sprintf("--namespace=%v", ns))
   116  	})
   117  }
   118  
   119  // KubectlBuilder is used to build, customize and execute a kubectl Command.
   120  // Add more functions to customize the builder as needed.
   121  type KubectlBuilder struct {
   122  	cmd     *exec.Cmd
   123  	timeout <-chan time.Time
   124  }
   125  
   126  // NewKubectlCommand returns a KubectlBuilder for running kubectl.
   127  func NewKubectlCommand(args ...string) *KubectlBuilder {
   128  	return NewKubectlCommandTimeout(Timeout, args...)
   129  }
   130  
   131  // NewKubectlCommandTimeout returns a KubectlBuilder with a timeout defined, for running kubectl.
   132  func NewKubectlCommandTimeout(timeout time.Duration, args ...string) *KubectlBuilder {
   133  	b := new(KubectlBuilder)
   134  	b.cmd = KubectlCmd(args...)
   135  	b.timeout = time.After(timeout)
   136  	return b
   137  }
   138  
   139  // NewKubectlExecCommand returns a KubectlBuilder prepared to execute a given command in a running pod.
   140  func NewKubectlExecCommand(f *Framework, pod string, timeout time.Duration, commandArgs ...string) *KubectlBuilder {
   141  	defaultArgs := []string{}
   142  	defaultArgs = append(defaultArgs, "--namespace", f.Namespace, "exec", pod, "--")
   143  	defaultArgs = append(defaultArgs, commandArgs...)
   144  	return NewKubectlCommandTimeout(timeout, defaultArgs...)
   145  }
   146  
   147  // ExecOrDie runs the kubectl executable or dies if error occurs.
   148  func (b KubectlBuilder) ExecOrDie() string {
   149  	str, err := b.Exec()
   150  	// In case of i/o timeout error, try talking to the apiserver again after 2s before dying.
   151  	// Note that we're still dying after retrying so that we can get visibility to triage it further.
   152  	if isTimeout(err) {
   153  		e2elog.Logf("Hit i/o timeout error, talking to the server 2s later to see if it's temporary.")
   154  		time.Sleep(2 * time.Second)
   155  		retryStr, retryErr := RunKubectl("version")
   156  		e2elog.Logf("stdout: %q", retryStr)
   157  		e2elog.Logf("err: %v", retryErr)
   158  	}
   159  	ExpectNoError(err)
   160  	return str
   161  }
   162  
   163  func isTimeout(err error) bool {
   164  	switch err := err.(type) {
   165  	case net.Error:
   166  		if err.Timeout() {
   167  			return true
   168  		}
   169  	case *url.Error:
   170  		if err, ok := err.Err.(net.Error); ok && err.Timeout() {
   171  			return true
   172  		}
   173  	}
   174  	return false
   175  }
   176  
   177  // Exec runs the kubectl executable.
   178  func (b KubectlBuilder) Exec() (string, error) {
   179  	var stdout, stderr bytes.Buffer
   180  	cmd := b.cmd
   181  	cmd.Stdout, cmd.Stderr = &stdout, &stderr
   182  
   183  	//e2elog.Logf("Running '%s %s'", cmd.Path, strings.Join(cmd.Args[1:], " ")) // skip arg[0] as it is printed separately
   184  	if err := cmd.Start(); err != nil {
   185  		return "", fmt.Errorf("error starting %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v", cmd, cmd.Stdout, cmd.Stderr, err)
   186  	}
   187  	errCh := make(chan error, 1)
   188  	go func() {
   189  		errCh <- cmd.Wait()
   190  	}()
   191  	select {
   192  	case err := <-errCh:
   193  		if err != nil {
   194  			var rc = 127
   195  			if ee, ok := err.(*exec.ExitError); ok {
   196  				rc = int(ee.Sys().(syscall.WaitStatus).ExitStatus())
   197  				e2elog.Logf("rc: %d", rc)
   198  			}
   199  			return "", uexec.CodeExitError{
   200  				Err:  fmt.Errorf("error running %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v", cmd, cmd.Stdout, cmd.Stderr, err),
   201  				Code: rc,
   202  			}
   203  		}
   204  	case <-b.timeout:
   205  		_ = b.cmd.Process.Kill()
   206  		return "", fmt.Errorf("timed out waiting for command %v:\nCommand stdout:\n%v\nstderr:\n%v", cmd, cmd.Stdout, cmd.Stderr)
   207  	}
   208  	// Note: these help to debug
   209  	//e2elog.Logf("stderr: %q", stderr.String())
   210  	//e2elog.Logf("stdout: %q", stdout.String())
   211  	return stdout.String(), nil
   212  }
   213  
   214  // RunKubectlOrDie is a convenience wrapper over kubectlBuilder
   215  func RunKubectlOrDie(args ...string) string {
   216  	return NewKubectlCommand(args...).ExecOrDie()
   217  }
   218  
   219  // RunKubectl is a convenience wrapper over kubectlBuilder
   220  func RunKubectl(args ...string) (string, error) {
   221  	return NewKubectlCommand(args...).Exec()
   222  }