github.com/containers/podman/v5@v5.1.0-rc1/test/utils/utils.go (about) 1 package utils 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "fmt" 7 "math/rand" 8 "os" 9 "os/exec" 10 "runtime" 11 "strings" 12 "time" 13 14 crypto_rand "crypto/rand" 15 "crypto/rsa" 16 "crypto/x509" 17 "encoding/pem" 18 19 "github.com/sirupsen/logrus" 20 21 "github.com/containers/storage/pkg/parsers/kernel" 22 . "github.com/onsi/ginkgo/v2" //nolint:revive,stylecheck 23 . "github.com/onsi/gomega" //nolint:revive,stylecheck 24 . "github.com/onsi/gomega/gexec" //nolint:revive,stylecheck 25 ) 26 27 type NetworkBackend int 28 29 const ( 30 // Container Networking backend 31 CNI NetworkBackend = iota 32 // Netavark network backend 33 Netavark NetworkBackend = iota 34 // Env variable for creating time files. 35 EnvTimeDir = "_PODMAN_TIME_DIR" 36 ) 37 38 func (n NetworkBackend) ToString() string { 39 switch n { 40 case CNI: 41 return "cni" 42 case Netavark: 43 return "netavark" 44 } 45 logrus.Errorf("unknown network backend: %q", n) 46 return "" 47 } 48 49 var ( 50 DefaultWaitTimeout = 90 51 OSReleasePath = "/etc/os-release" 52 ProcessOneCgroupPath = "/proc/1/cgroup" 53 ) 54 55 // PodmanTestCommon contains common functions will be updated later in 56 // the inheritance structs 57 type PodmanTestCommon interface { 58 MakeOptions(args []string, noEvents, noCache bool) []string 59 WaitForContainer() bool 60 WaitContainerReady(id string, expStr string, timeout int, step int) bool 61 } 62 63 // PodmanTest struct for command line options 64 type PodmanTest struct { 65 ImageCacheDir string 66 ImageCacheFS string 67 NetworkBackend NetworkBackend 68 DatabaseBackend string 69 PodmanBinary string 70 PodmanMakeOptions func(args []string, noEvents, noCache bool) []string 71 RemoteCommand *exec.Cmd 72 RemotePodmanBinary string 73 RemoteSession *os.Process 74 RemoteSocket string 75 RemoteSocketLock string // If not "", should be removed _after_ RemoteSocket is removed 76 RemoteTest bool 77 TempDir string 78 } 79 80 // PodmanSession wraps the gexec.session so we can extend it 81 type PodmanSession struct { 82 *Session 83 } 84 85 // HostOS is a simple struct for the test os 86 type HostOS struct { 87 Distribution string 88 Version string 89 Arch string 90 } 91 92 // MakeOptions assembles all podman options 93 func (p *PodmanTest) MakeOptions(args []string, noEvents, noCache bool) []string { 94 return p.PodmanMakeOptions(args, noEvents, noCache) 95 } 96 97 // PodmanAsUserBase exec podman as user. uid and gid is set for credentials usage. env is used 98 // to record the env for debugging 99 func (p *PodmanTest) PodmanAsUserBase(args []string, uid, gid uint32, cwd string, env []string, noEvents, noCache bool, wrapper []string, extraFiles []*os.File) *PodmanSession { 100 var command *exec.Cmd 101 podmanOptions := p.MakeOptions(args, noEvents, noCache) 102 podmanBinary := p.PodmanBinary 103 if p.RemoteTest { 104 podmanBinary = p.RemotePodmanBinary 105 } 106 107 if timeDir := os.Getenv(EnvTimeDir); timeDir != "" { 108 timeFile, err := os.CreateTemp(timeDir, ".time") 109 if err != nil { 110 Fail(fmt.Sprintf("Error creating time file: %v", err)) 111 } 112 timeArgs := []string{"-f", "%M", "-o", timeFile.Name()} 113 timeCmd := append([]string{"/usr/bin/time"}, timeArgs...) 114 wrapper = append(timeCmd, wrapper...) 115 } 116 runCmd := wrapper 117 runCmd = append(runCmd, podmanBinary) 118 119 if env == nil { 120 GinkgoWriter.Printf("Running: %s %s\n", strings.Join(runCmd, " "), strings.Join(podmanOptions, " ")) 121 } else { 122 GinkgoWriter.Printf("Running: (env: %v) %s %s\n", env, strings.Join(runCmd, " "), strings.Join(podmanOptions, " ")) 123 } 124 if uid != 0 || gid != 0 { 125 pythonCmd := fmt.Sprintf("import os; import sys; uid = %d; gid = %d; cwd = '%s'; os.setgid(gid); os.setuid(uid); os.chdir(cwd) if len(cwd)>0 else True; os.execv(sys.argv[1], sys.argv[1:])", gid, uid, cwd) 126 runCmd = append(runCmd, podmanOptions...) 127 nsEnterOpts := append([]string{"-c", pythonCmd}, runCmd...) 128 command = exec.Command("python", nsEnterOpts...) 129 } else { 130 runCmd = append(runCmd, podmanOptions...) 131 command = exec.Command(runCmd[0], runCmd[1:]...) 132 } 133 if env != nil { 134 command.Env = env 135 } 136 if cwd != "" { 137 command.Dir = cwd 138 } 139 140 command.ExtraFiles = extraFiles 141 142 session, err := Start(command, GinkgoWriter, GinkgoWriter) 143 if err != nil { 144 Fail(fmt.Sprintf("unable to run podman command: %s\n%v", strings.Join(podmanOptions, " "), err)) 145 } 146 return &PodmanSession{session} 147 } 148 149 // PodmanBase exec podman with default env. 150 func (p *PodmanTest) PodmanBase(args []string, noEvents, noCache bool) *PodmanSession { 151 return p.PodmanAsUserBase(args, 0, 0, "", nil, noEvents, noCache, nil, nil) 152 } 153 154 // WaitForContainer waits on a started container 155 func (p *PodmanTest) WaitForContainer() bool { 156 for i := 0; i < 10; i++ { 157 if p.NumberOfContainersRunning() > 0 { 158 return true 159 } 160 time.Sleep(1 * time.Second) 161 } 162 GinkgoWriter.Printf("WaitForContainer(): timed out\n") 163 return false 164 } 165 166 // NumberOfContainersRunning returns an int of how many 167 // containers are currently running. 168 func (p *PodmanTest) NumberOfContainersRunning() int { 169 var containers []string 170 ps := p.PodmanBase([]string{"ps", "-q"}, false, true) 171 ps.WaitWithDefaultTimeout() 172 Expect(ps).Should(Exit(0)) 173 for _, i := range ps.OutputToStringArray() { 174 if i != "" { 175 containers = append(containers, i) 176 } 177 } 178 return len(containers) 179 } 180 181 // NumberOfContainers returns an int of how many 182 // containers are currently defined. 183 func (p *PodmanTest) NumberOfContainers() int { 184 var containers []string 185 ps := p.PodmanBase([]string{"ps", "-aq"}, false, true) 186 ps.WaitWithDefaultTimeout() 187 Expect(ps.ExitCode()).To(Equal(0)) 188 for _, i := range ps.OutputToStringArray() { 189 if i != "" { 190 containers = append(containers, i) 191 } 192 } 193 return len(containers) 194 } 195 196 // NumberOfPods returns an int of how many 197 // pods are currently defined. 198 func (p *PodmanTest) NumberOfPods() int { 199 var pods []string 200 ps := p.PodmanBase([]string{"pod", "ps", "-q"}, false, true) 201 ps.WaitWithDefaultTimeout() 202 Expect(ps.ExitCode()).To(Equal(0)) 203 for _, i := range ps.OutputToStringArray() { 204 if i != "" { 205 pods = append(pods, i) 206 } 207 } 208 return len(pods) 209 } 210 211 // GetContainerStatus returns the containers state. 212 // This function assumes only one container is active. 213 func (p *PodmanTest) GetContainerStatus() string { 214 var podmanArgs = []string{"ps"} 215 podmanArgs = append(podmanArgs, "--all", "--format={{.Status}}") 216 session := p.PodmanBase(podmanArgs, false, true) 217 session.WaitWithDefaultTimeout() 218 return session.OutputToString() 219 } 220 221 // WaitContainerReady waits process or service inside container start, and ready to be used. 222 func (p *PodmanTest) WaitContainerReady(id string, expStr string, timeout int, step int) bool { 223 startTime := time.Now() 224 s := p.PodmanBase([]string{"logs", id}, false, true) 225 s.WaitWithDefaultTimeout() 226 227 for { 228 if strings.Contains(s.OutputToString(), expStr) || strings.Contains(s.ErrorToString(), expStr) { 229 return true 230 } 231 232 if time.Since(startTime) >= time.Duration(timeout)*time.Second { 233 GinkgoWriter.Printf("Container %s is not ready in %ds", id, timeout) 234 return false 235 } 236 time.Sleep(time.Duration(step) * time.Second) 237 s = p.PodmanBase([]string{"logs", id}, false, true) 238 s.WaitWithDefaultTimeout() 239 } 240 } 241 242 // WaitForContainer is a wrapper function for accept inheritance PodmanTest struct. 243 func WaitForContainer(p PodmanTestCommon) bool { 244 return p.WaitForContainer() 245 } 246 247 // WaitForContainerReady is a wrapper function for accept inheritance PodmanTest struct. 248 func WaitContainerReady(p PodmanTestCommon, id string, expStr string, timeout int, step int) bool { 249 return p.WaitContainerReady(id, expStr, timeout, step) 250 } 251 252 // OutputToString formats session output to string 253 func (s *PodmanSession) OutputToString() string { 254 if s == nil || s.Out == nil || s.Out.Contents() == nil { 255 return "" 256 } 257 258 fields := strings.Fields(string(s.Out.Contents())) 259 return strings.Join(fields, " ") 260 } 261 262 // OutputToStringArray returns the output as a []string 263 // where each array item is a line split by newline 264 func (s *PodmanSession) OutputToStringArray() []string { 265 var results []string 266 output := string(s.Out.Contents()) 267 for _, line := range strings.Split(output, "\n") { 268 if line != "" { 269 results = append(results, line) 270 } 271 } 272 return results 273 } 274 275 // ErrorToString formats session stderr to string 276 func (s *PodmanSession) ErrorToString() string { 277 fields := strings.Fields(string(s.Err.Contents())) 278 return strings.Join(fields, " ") 279 } 280 281 // ErrorToStringArray returns the stderr output as a []string 282 // where each array item is a line split by newline 283 func (s *PodmanSession) ErrorToStringArray() []string { 284 output := string(s.Err.Contents()) 285 return strings.Split(output, "\n") 286 } 287 288 // GrepString takes session output and behaves like grep. it returns a bool 289 // if successful and an array of strings on positive matches 290 func (s *PodmanSession) GrepString(term string) (bool, []string) { 291 var ( 292 greps []string 293 matches bool 294 ) 295 296 for _, line := range s.OutputToStringArray() { 297 if strings.Contains(line, term) { 298 matches = true 299 greps = append(greps, line) 300 } 301 } 302 return matches, greps 303 } 304 305 // ErrorGrepString takes session stderr output and behaves like grep. it returns a bool 306 // if successful and an array of strings on positive matches 307 func (s *PodmanSession) ErrorGrepString(term string) (bool, []string) { 308 var ( 309 greps []string 310 matches bool 311 ) 312 313 for _, line := range s.ErrorToStringArray() { 314 if strings.Contains(line, term) { 315 matches = true 316 greps = append(greps, line) 317 } 318 } 319 return matches, greps 320 } 321 322 // LineInOutputStartsWith returns true if a line in a 323 // session output starts with the supplied string 324 func (s *PodmanSession) LineInOutputStartsWith(term string) bool { 325 for _, i := range s.OutputToStringArray() { 326 if strings.HasPrefix(i, term) { 327 return true 328 } 329 } 330 return false 331 } 332 333 // LineInOutputContains returns true if a line in a 334 // session output contains the supplied string 335 func (s *PodmanSession) LineInOutputContains(term string) bool { 336 for _, i := range s.OutputToStringArray() { 337 if strings.Contains(i, term) { 338 return true 339 } 340 } 341 return false 342 } 343 344 // LineInOutputContainsTag returns true if a line in the 345 // session's output contains the repo-tag pair as returned 346 // by podman-images(1). 347 func (s *PodmanSession) LineInOutputContainsTag(repo, tag string) bool { 348 tagMap := tagOutputToMap(s.OutputToStringArray()) 349 return tagMap[repo][tag] 350 } 351 352 // IsJSONOutputValid attempts to unmarshal the session buffer 353 // and if successful, returns true, else false 354 func (s *PodmanSession) IsJSONOutputValid() bool { 355 var i interface{} 356 if err := json.Unmarshal(s.Out.Contents(), &i); err != nil { 357 GinkgoWriter.Println(err) 358 return false 359 } 360 return true 361 } 362 363 // WaitWithDefaultTimeout waits for process finished with DefaultWaitTimeout 364 func (s *PodmanSession) WaitWithDefaultTimeout() { 365 s.WaitWithTimeout(DefaultWaitTimeout) 366 } 367 368 // WaitWithTimeout waits for process finished with DefaultWaitTimeout 369 func (s *PodmanSession) WaitWithTimeout(timeout int) { 370 Eventually(s, timeout).Should(Exit(), func() string { 371 // in case of timeouts show output 372 return fmt.Sprintf("command timed out after %ds: %v\nSTDOUT: %s\nSTDERR: %s", 373 timeout, s.Command.Args, string(s.Out.Contents()), string(s.Err.Contents())) 374 }) 375 os.Stdout.Sync() 376 os.Stderr.Sync() 377 } 378 379 // SystemExec is used to exec a system command to check its exit code or output 380 func SystemExec(command string, args []string) *PodmanSession { 381 c := exec.Command(command, args...) 382 GinkgoWriter.Println("Execing " + c.String() + "\n") 383 session, err := Start(c, GinkgoWriter, GinkgoWriter) 384 if err != nil { 385 Fail(fmt.Sprintf("unable to run command: %s %s", command, strings.Join(args, " "))) 386 } 387 session.Wait(DefaultWaitTimeout) 388 return &PodmanSession{session} 389 } 390 391 // StartSystemExec is used to start exec a system command 392 func StartSystemExec(command string, args []string) *PodmanSession { 393 c := exec.Command(command, args...) 394 GinkgoWriter.Println("Execing " + c.String() + "\n") 395 session, err := Start(c, GinkgoWriter, GinkgoWriter) 396 if err != nil { 397 Fail(fmt.Sprintf("unable to run command: %s %s", command, strings.Join(args, " "))) 398 } 399 return &PodmanSession{session} 400 } 401 402 // tagOutPutToMap parses each string in imagesOutput and returns 403 // a map whose key is a repo, and value is another map whose keys 404 // are the tags found for that repo. Notice, the first array item will 405 // be skipped as it's considered to be the header. 406 func tagOutputToMap(imagesOutput []string) map[string]map[string]bool { 407 m := make(map[string]map[string]bool) 408 // iterate over output but skip the header 409 for _, i := range imagesOutput[1:] { 410 tmp := []string{} 411 for _, x := range strings.Split(i, " ") { 412 if x != "" { 413 tmp = append(tmp, x) 414 } 415 } 416 // podman-images(1) return a list like output 417 // in the format of "Repository Tag [...]" 418 if len(tmp) < 2 { 419 continue 420 } 421 if m[tmp[0]] == nil { 422 m[tmp[0]] = map[string]bool{} 423 } 424 m[tmp[0]][tmp[1]] = true 425 } 426 return m 427 } 428 429 // GetHostDistributionInfo returns a struct with its distribution Name and version 430 func GetHostDistributionInfo() HostOS { 431 f, err := os.Open(OSReleasePath) 432 if err != nil { 433 return HostOS{} 434 } 435 defer f.Close() 436 437 l := bufio.NewScanner(f) 438 host := HostOS{} 439 host.Arch = runtime.GOARCH 440 for l.Scan() { 441 if strings.HasPrefix(l.Text(), "ID=") { 442 host.Distribution = strings.ReplaceAll(strings.TrimSpace(strings.Join(strings.Split(l.Text(), "=")[1:], "")), "\"", "") 443 } 444 if strings.HasPrefix(l.Text(), "VERSION_ID=") { 445 host.Version = strings.ReplaceAll(strings.TrimSpace(strings.Join(strings.Split(l.Text(), "=")[1:], "")), "\"", "") 446 } 447 } 448 return host 449 } 450 451 // IsKernelNewerThan compares the current kernel version to one provided. If 452 // the kernel is equal to or greater, returns true 453 func IsKernelNewerThan(version string) (bool, error) { 454 inputVersion, err := kernel.ParseRelease(version) 455 if err != nil { 456 return false, err 457 } 458 kv, err := kernel.GetKernelVersion() 459 if err != nil { 460 return false, err 461 } 462 463 // CompareKernelVersion compares two kernel.VersionInfo structs. 464 // Returns -1 if a < b, 0 if a == b, 1 it a > b 465 result := kernel.CompareKernelVersion(*kv, *inputVersion) 466 if result >= 0 { 467 return true, nil 468 } 469 return false, nil 470 } 471 472 // IsCommandAvailable check if command exist 473 func IsCommandAvailable(command string) bool { 474 check := exec.Command("bash", "-c", strings.Join([]string{"command -v", command}, " ")) 475 err := check.Run() 476 return err == nil 477 } 478 479 // WriteJSONFile write json format data to a json file 480 func WriteJSONFile(data []byte, filePath string) error { 481 var jsonData map[string]interface{} 482 if err := json.Unmarshal(data, &jsonData); err != nil { 483 return err 484 } 485 formatJSON, err := json.MarshalIndent(jsonData, "", " ") 486 if err != nil { 487 return err 488 } 489 return os.WriteFile(filePath, formatJSON, 0644) 490 } 491 492 // Containerized check the podman command run inside container 493 func Containerized() bool { 494 container := os.Getenv("container") 495 if container != "" { 496 return true 497 } 498 b, err := os.ReadFile(ProcessOneCgroupPath) 499 if err != nil { 500 // shrug, if we cannot read that file, return false 501 return false 502 } 503 return strings.Contains(string(b), "docker") 504 } 505 506 var randomLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 507 508 // RandomString returns a string of given length composed of random characters 509 func RandomString(n int) string { 510 b := make([]rune, n) 511 for i := range b { 512 b[i] = randomLetters[rand.Intn(len(randomLetters))] 513 } 514 return string(b) 515 } 516 517 // Encode *rsa.PublicKey and store it in a file. 518 // Adds appropriate extension to the fileName, and returns the complete fileName of 519 // the file storing the public key. 520 func savePublicKey(fileName string, publicKey *rsa.PublicKey) (string, error) { 521 // Encode public key to PKIX, ASN.1 DER form 522 pubBytes, err := x509.MarshalPKIXPublicKey(publicKey) 523 if err != nil { 524 return "", err 525 } 526 527 pubPEM := pem.EncodeToMemory( 528 &pem.Block{ 529 Type: "RSA PUBLIC KEY", 530 Bytes: pubBytes, 531 }, 532 ) 533 534 // Write public key to file 535 publicKeyFileName := fileName + ".rsa.pub" 536 if err := os.WriteFile(publicKeyFileName, pubPEM, 0600); err != nil { 537 return "", err 538 } 539 540 return publicKeyFileName, nil 541 } 542 543 // Encode *rsa.PrivateKey and store it in a file. 544 // Adds appropriate extension to the fileName, and returns the complete fileName of 545 // the file storing the private key. 546 func savePrivateKey(fileName string, privateKey *rsa.PrivateKey) (string, error) { 547 // Encode private key to PKCS#1, ASN.1 DER form 548 privBytes := x509.MarshalPKCS1PrivateKey(privateKey) 549 keyPEM := pem.EncodeToMemory( 550 &pem.Block{ 551 Type: "RSA PRIVATE KEY", 552 Bytes: privBytes, 553 }, 554 ) 555 556 // Write private key to file 557 privateKeyFileName := fileName + ".rsa" 558 if err := os.WriteFile(privateKeyFileName, keyPEM, 0600); err != nil { 559 return "", err 560 } 561 562 return privateKeyFileName, nil 563 } 564 565 // Generate RSA key pair of specified bit size and write them to files. 566 // Adds appropriate extension to the fileName, and returns the complete fileName of 567 // the files storing the public and private key respectively. 568 func WriteRSAKeyPair(fileName string, bitSize int) (string, string, error) { 569 // Generate RSA key 570 privateKey, err := rsa.GenerateKey(crypto_rand.Reader, bitSize) 571 if err != nil { 572 return "", "", err 573 } 574 575 publicKey := privateKey.Public().(*rsa.PublicKey) 576 577 publicKeyFileName, err := savePublicKey(fileName, publicKey) 578 if err != nil { 579 return "", "", err 580 } 581 582 privateKeyFileName, err := savePrivateKey(fileName, privateKey) 583 if err != nil { 584 return "", "", err 585 } 586 587 return publicKeyFileName, privateKeyFileName, nil 588 }