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  }