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 }