github.phpd.cn/cilium/cilium@v1.6.12/test/helpers/utils.go (about)

     1  // Copyright 2017-2019 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package helpers
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"html/template"
    22  	"io"
    23  	"io/ioutil"
    24  	"math/rand"
    25  	"os"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  	"syscall"
    30  	"time"
    31  
    32  	"github.com/cilium/cilium/pkg/versioncheck"
    33  	"github.com/cilium/cilium/test/config"
    34  	"github.com/cilium/cilium/test/ginkgo-ext"
    35  
    36  	go_version "github.com/hashicorp/go-version"
    37  	"github.com/onsi/ginkgo"
    38  	. "github.com/onsi/gomega"
    39  )
    40  
    41  func init() {
    42  	// ensure that our random numbers are seeded differently on each run
    43  	rand.Seed(time.Now().UnixNano())
    44  }
    45  
    46  // IsRunningOnJenkins detects if the currently running Ginkgo application is
    47  // most likely running in a Jenkins environment. Returns true if certain
    48  // environment variables that are present in Jenkins jobs are set, false
    49  // otherwise.
    50  func IsRunningOnJenkins() bool {
    51  	result := true
    52  
    53  	env := []string{"JENKINS_HOME", "NODE_NAME"}
    54  
    55  	for _, varName := range env {
    56  		if val := os.Getenv(varName); val == "" {
    57  			result = false
    58  			log.Infof("build is not running on Jenkins; environment variable '%v' is not set", varName)
    59  		}
    60  	}
    61  	return result
    62  }
    63  
    64  // Sleep sleeps for the specified duration in seconds
    65  func Sleep(delay time.Duration) {
    66  	time.Sleep(delay * time.Second)
    67  }
    68  
    69  // CountValues returns the count of the occurrences of key in data, as well as
    70  // the length of data.
    71  func CountValues(key string, data []string) (int, int) {
    72  	var result int
    73  
    74  	for _, x := range data {
    75  		if x == key {
    76  			result++
    77  		}
    78  	}
    79  	return result, len(data)
    80  }
    81  
    82  // MakeUID returns a randomly generated string.
    83  func MakeUID() string {
    84  	return fmt.Sprintf("%08x", rand.Uint32())
    85  }
    86  
    87  // RenderTemplateToFile renders a text/template string into a target filename
    88  // with specific persmisions. Returns eturn an error if the template cannot be
    89  // validated or the file cannot be created.
    90  func RenderTemplateToFile(filename string, tmplt string, perm os.FileMode) error {
    91  	t, err := template.New("").Parse(tmplt)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	content := new(bytes.Buffer)
    96  	err = t.Execute(content, nil)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	err = ioutil.WriteFile(filename, content.Bytes(), perm)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	return nil
   106  }
   107  
   108  // TimeoutConfig represents the configuration for the timeout of a command.
   109  type TimeoutConfig struct {
   110  	Ticker  time.Duration // Check interval
   111  	Timeout time.Duration // Limit for how long to spend in the command
   112  }
   113  
   114  // Validate ensuires that the parameters for the TimeoutConfig are reasonable
   115  // for running in tests.
   116  func (c *TimeoutConfig) Validate() error {
   117  	if c.Timeout < 10*time.Second {
   118  		return fmt.Errorf("Timeout too short (must be at least 10 seconds): %v", c.Timeout)
   119  	}
   120  	if c.Ticker == 0 {
   121  		c.Ticker = 5 * time.Second
   122  	} else if c.Ticker < time.Second {
   123  		return fmt.Errorf("Timeout config Ticker interval too short (must be at least 1 second): %v", c.Ticker)
   124  	}
   125  	return nil
   126  }
   127  
   128  // WithTimeout executes body using the time interval specified in config until
   129  // the timeout in config is reached. Returns an error if the timeout is
   130  // exceeded for body to execute successfully.
   131  func WithTimeout(body func() bool, msg string, config *TimeoutConfig) error {
   132  	if err := config.Validate(); err != nil {
   133  		return err
   134  	}
   135  
   136  	bodyChan := make(chan bool, 1)
   137  
   138  	asyncBody := func(ch chan bool) {
   139  		defer ginkgo.GinkgoRecover()
   140  		success := body()
   141  		ch <- success
   142  		if success {
   143  			close(ch)
   144  		}
   145  	}
   146  
   147  	go asyncBody(bodyChan)
   148  
   149  	done := time.After(config.Timeout)
   150  	ticker := time.NewTicker(config.Ticker)
   151  	defer ticker.Stop()
   152  	for {
   153  		select {
   154  		case success := <-bodyChan:
   155  			if success {
   156  				return nil
   157  			}
   158  			// Provide some form of rate-limiting here before running next
   159  			// execution in case body() returns at a fast rate.
   160  			select {
   161  			case <-ticker.C:
   162  				go asyncBody(bodyChan)
   163  			}
   164  		case <-done:
   165  			return fmt.Errorf("Timeout reached: %s", msg)
   166  		}
   167  	}
   168  }
   169  
   170  // WithContext executes body with the given frequency. The function
   171  // f is executed until bool returns true or the given context signalizes Done.
   172  // `f` should stop if context is canceled.
   173  func WithContext(ctx context.Context, f func(ctx context.Context) (bool, error), freq time.Duration) error {
   174  	ticker := time.NewTicker(freq)
   175  	defer ticker.Stop()
   176  	for {
   177  		select {
   178  		case <-ctx.Done():
   179  			return ctx.Err()
   180  		case <-ticker.C:
   181  			stop, err := f(ctx)
   182  			if err != nil {
   183  				select {
   184  				case <-ctx.Done():
   185  					return ctx.Err()
   186  				default:
   187  					return err
   188  				}
   189  			}
   190  			if stop {
   191  				select {
   192  				case <-ctx.Done():
   193  					return ctx.Err()
   194  				default:
   195  					return nil
   196  				}
   197  			}
   198  		}
   199  	}
   200  }
   201  
   202  // GetAppPods fetches app pod names for a namespace.
   203  // For Http based tests, we identify pods with format id=<pod_name>, while
   204  // for Kafka based tests, we identify pods with the format app=<pod_name>.
   205  func GetAppPods(apps []string, namespace string, kubectl *Kubectl, appFmt string) map[string]string {
   206  	appPods := make(map[string]string)
   207  	for _, v := range apps {
   208  		res, err := kubectl.GetPodNames(namespace, fmt.Sprintf("%s=%s", appFmt, v))
   209  		Expect(err).Should(BeNil())
   210  		Expect(res).Should(Not(BeNil()))
   211  		appPods[v] = res[0]
   212  		log.Infof("GetAppPods: pod=%q assigned to %q", res[0], v)
   213  	}
   214  	return appPods
   215  }
   216  
   217  // HoldEnvironment prints the current test status, then pauses the test
   218  // execution. Developers who are writing tests may wish to invoke this function
   219  // directly from test code to assist troubleshooting and test development.
   220  func HoldEnvironment(description ...string) {
   221  	test := ginkgo.CurrentGinkgoTestDescription()
   222  	pid := syscall.Getpid()
   223  
   224  	fmt.Fprintf(os.Stdout, "\n---\n%s", test.FullTestText)
   225  	fmt.Fprintf(os.Stdout, "\nat %s:%d", test.FileName, test.LineNumber)
   226  	fmt.Fprintf(os.Stdout, "\n\n%s", description)
   227  	fmt.Fprintf(os.Stdout, "\n\nPausing test for debug, use vagrant to access test setup.")
   228  	fmt.Fprintf(os.Stdout, "\nRun \"kill -SIGCONT %d\" to continue.\n", pid)
   229  	syscall.Kill(pid, syscall.SIGSTOP)
   230  }
   231  
   232  // Fail is a Ginkgo failure handler which raises a SIGSTOP for the test process
   233  // when there is a failure, so that developers can debug the live environment.
   234  // It is only triggered if the developer provides a commandline flag.
   235  func Fail(description string, callerSkip ...int) {
   236  	if len(callerSkip) > 0 {
   237  		callerSkip[0]++
   238  	} else {
   239  		callerSkip = []int{1}
   240  	}
   241  
   242  	if config.CiliumTestConfig.HoldEnvironment {
   243  		HoldEnvironment(description)
   244  	}
   245  	ginkgoext.Fail(description, callerSkip...)
   246  }
   247  
   248  // CreateReportDirectory creates and returns the directory path to export all report
   249  // commands that need to be run in the case that a test has failed.
   250  // If the directory cannot be created it'll return an error
   251  func CreateReportDirectory() (string, error) {
   252  	prefix := ""
   253  	testName := ginkgoext.GetTestName()
   254  	if strings.HasPrefix(strings.ToLower(testName), K8s) {
   255  		prefix = fmt.Sprintf("%s-", strings.Replace(GetCurrentK8SEnv(), ".", "", -1))
   256  	}
   257  
   258  	testPath := filepath.Join(
   259  		TestResultsPath,
   260  		prefix,
   261  		testName)
   262  	if _, err := os.Stat(testPath); err == nil {
   263  		return testPath, nil
   264  	}
   265  	err := os.MkdirAll(testPath, os.ModePerm)
   266  	return testPath, err
   267  }
   268  
   269  // CreateLogFile creates the ReportDirectory if it is not present, writes the
   270  // given data to the given filename.
   271  func CreateLogFile(filename string, data []byte) error {
   272  	path, err := CreateReportDirectory()
   273  	if err != nil {
   274  		log.WithError(err).Errorf("ReportDirectory cannot be created")
   275  		return err
   276  	}
   277  
   278  	finalPath := filepath.Join(path, filename)
   279  	err = ioutil.WriteFile(finalPath, data, LogPerm)
   280  	return err
   281  }
   282  
   283  // reportMap saves the output of the given commands to the specified filename.
   284  // Function needs a directory path where the files are going to be written and
   285  // a *SSHMeta instance to execute the commands
   286  func reportMap(path string, reportCmds map[string]string, node *SSHMeta) {
   287  	ctx, cancel := context.WithCancel(context.Background())
   288  	defer cancel()
   289  	reportMapContext(ctx, path, reportCmds, node)
   290  }
   291  
   292  // reportMap saves the output of the given commands to the specified filename.
   293  // Function needs a directory path where the files are going to be written and
   294  // a *SSHMeta instance to execute the commands
   295  func reportMapContext(ctx context.Context, path string, reportCmds map[string]string, node *SSHMeta) {
   296  	if node == nil {
   297  		log.Errorf("cannot execute reportMap due invalid node instance")
   298  		return
   299  	}
   300  
   301  	for cmd, logfile := range reportCmds {
   302  		res := node.ExecContext(ctx, cmd, ExecOptions{SkipLog: true})
   303  		err := ioutil.WriteFile(
   304  			fmt.Sprintf("%s/%s", path, logfile),
   305  			res.CombineOutput().Bytes(),
   306  			LogPerm)
   307  		if err != nil {
   308  			log.WithError(err).Errorf("cannot create test results for command '%s'", cmd)
   309  		}
   310  	}
   311  }
   312  
   313  // ManifestGet returns the full path of the given manifest corresponding to the
   314  // Kubernetes version being tested, if such a manifest exists, if not it
   315  // returns the global manifest file.
   316  // The paths are checked in order:
   317  // 1- base_path/integration/filename
   318  // 2- base_path/k8s_version/integration/filename
   319  // 3- base_path/k8s_version/filename
   320  // 4- base_path/filename
   321  func ManifestGet(manifestFilename string) string {
   322  	// Try dependent integration file only if we have one configured. This is
   323  	// needed since no integration is "" and that causes us to find the
   324  	// base_path/filename before we check the base_path/k8s_version/filename
   325  	if integration := GetCurrentIntegration(); integration != "" {
   326  		fullPath := filepath.Join(manifestsPath, integration, manifestFilename)
   327  		_, err := os.Stat(fullPath)
   328  		if err == nil {
   329  			return filepath.Join(BasePath, fullPath)
   330  		}
   331  
   332  		// try dependent k8s version and integration file
   333  		fullPath = filepath.Join(manifestsPath, GetCurrentK8SEnv(), integration, manifestFilename)
   334  		_, err = os.Stat(fullPath)
   335  		if err == nil {
   336  			return filepath.Join(BasePath, fullPath)
   337  		}
   338  	}
   339  
   340  	// try dependent k8s version
   341  	fullPath := filepath.Join(manifestsPath, GetCurrentK8SEnv(), manifestFilename)
   342  	_, err := os.Stat(fullPath)
   343  	if err == nil {
   344  		return filepath.Join(BasePath, fullPath)
   345  	}
   346  	return filepath.Join(BasePath, "k8sT", "manifests", manifestFilename)
   347  }
   348  
   349  // WriteOrAppendToFile writes data to a file named by filename.
   350  // If the file does not exist, WriteFile creates it with permissions perm;
   351  // otherwise WriteFile appends the data to the file
   352  func WriteOrAppendToFile(filename string, data []byte, perm os.FileMode) error {
   353  	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, perm)
   354  	if err != nil {
   355  		return err
   356  	}
   357  	n, err := f.Write(data)
   358  	if err == nil && n < len(data) {
   359  		err = io.ErrShortWrite
   360  	}
   361  	if err1 := f.Close(); err == nil {
   362  		err = err1
   363  	}
   364  	return err
   365  }
   366  
   367  // DNSDeployment returns the manifest to install dns engine on the server.
   368  func DNSDeployment() string {
   369  	var DNSEngine = "coredns"
   370  	k8sVersion := GetCurrentK8SEnv()
   371  	switch k8sVersion {
   372  	case "1.7", "1.8", "1.9", "1.10":
   373  		DNSEngine = "kubedns"
   374  	}
   375  	fullPath := filepath.Join("provision", "manifest", k8sVersion, DNSEngine+"_deployment.yaml")
   376  	_, err := os.Stat(fullPath)
   377  	if err == nil {
   378  		return filepath.Join(BasePath, fullPath)
   379  	}
   380  	return filepath.Join(BasePath, "provision", "manifest", DNSEngine+"_deployment.yaml")
   381  }
   382  
   383  // getK8sSupportedConstraints returns the Kubernetes versions supported by
   384  // a specific Cilium version.
   385  func getK8sSupportedConstraints(ciliumVersion string) (go_version.Constraints, error) {
   386  	cst, err := go_version.NewVersion(ciliumVersion)
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  	// Make pre-releases part of the official release
   391  	strSegments := make([]string, len(cst.Segments()))
   392  	if cst.Prerelease() != "" {
   393  		for i, segment := range cst.Segments() {
   394  			strSegments[i] = strconv.Itoa(segment)
   395  		}
   396  		ciliumVersion = strings.Join(strSegments, ".")
   397  		cst, err = go_version.NewVersion(ciliumVersion)
   398  		if err != nil {
   399  			return nil, err
   400  		}
   401  	}
   402  	switch {
   403  	case CiliumV1_5.Check(cst):
   404  		return versioncheck.MustCompile(">= 1.8, <1.16"), nil
   405  	case CiliumV1_6.Check(cst):
   406  		return versioncheck.MustCompile(">= 1.8, <1.16"), nil
   407  	default:
   408  		return nil, fmt.Errorf("unrecognized version '%s'", ciliumVersion)
   409  	}
   410  }
   411  
   412  // CanRunK8sVersion returns true if the givel ciliumVersion can run in the given
   413  // Kubernetes version. If any version is unparsable, an error is returned.
   414  func CanRunK8sVersion(ciliumVersion, k8sVersionStr string) (bool, error) {
   415  	k8sVersion, err := go_version.NewVersion(k8sVersionStr)
   416  	if err != nil {
   417  		return false, err
   418  	}
   419  	constraint, err := getK8sSupportedConstraints(ciliumVersion)
   420  	if err != nil {
   421  		return false, err
   422  	}
   423  	return constraint.Check(k8sVersion), nil
   424  }
   425  
   426  // failIfContainsBadLogMsg makes a test case to fail if any message from
   427  // given log messages contains an entry from badLogMessages (map key) AND
   428  // does not contain ignore messages (map value).
   429  func failIfContainsBadLogMsg(logs string) {
   430  	for _, msg := range strings.Split(logs, "\n") {
   431  		for fail, ignoreMessages := range badLogMessages {
   432  			if strings.Contains(msg, fail) {
   433  				ok := false
   434  				for _, ignore := range ignoreMessages {
   435  					if strings.Contains(msg, ignore) {
   436  						ok = true
   437  						break
   438  					}
   439  				}
   440  				if !ok {
   441  					fmt.Fprintf(CheckLogs, "⚠️  Found a %q in logs\n", fail)
   442  					ginkgoext.Fail(fmt.Sprintf("Found a %q in Cilium Logs", fail))
   443  				}
   444  			}
   445  		}
   446  	}
   447  }
   448  
   449  // RunsOnNetNext checks whether a test case is running on the net next machine
   450  // which means running on the latest (probably) unreleased kernel
   451  func RunsOnNetNext() bool {
   452  	return os.Getenv("NETNEXT") == "true"
   453  }
   454  
   455  // DoesNotRunOnNetNext is the inverse function of RunsOnNetNext.
   456  func DoesNotRunOnNetNext() bool {
   457  	return !RunsOnNetNext()
   458  }