github.com/dmaizel/tests@v0.0.0-20210728163746-cae6a2d9cee8/integration/docker/docker.go (about)

     1  // Copyright (c) 2018 Intel Corporation
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package docker
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"gopkg.in/yaml.v2"
    20  
    21  	"github.com/kata-containers/tests"
    22  	ginkgoconf "github.com/onsi/ginkgo/config"
    23  )
    24  
    25  const (
    26  	// Docker command
    27  	Docker = "docker"
    28  
    29  	// Image used to run containers
    30  	Image = "busybox"
    31  
    32  	// DebianImage is the debian image
    33  	DebianImage = "debian"
    34  
    35  	// FedoraImage is the fedora image
    36  	FedoraImage = "fedora"
    37  
    38  	// Fedora30Image is the fedora 30 image
    39  	// This Fedora version is used mainly because of https://github.com/kata-containers/tests/issues/2358
    40  	Fedora30Image = "fedora:30"
    41  
    42  	// StressImage is the vish/stress image
    43  	StressImage = "vish/stress"
    44  
    45  	// StressDockerFile is the dockerfile to build vish/stress image
    46  	StressDockerFile = "src/github.com/kata-containers/tests/stress/."
    47  
    48  	// VersionsPath is the path for the versions.yaml
    49  	VersionsPath = "src/github.com/kata-containers/tests/versions.yaml"
    50  )
    51  
    52  // cidDirectory is the directory where container ID files are created.
    53  var cidDirectory string
    54  
    55  // AlpineImage is the alpine image
    56  var AlpineImage string
    57  
    58  var images []string
    59  
    60  // versionDockerImage is the definition in the yaml for the Alpine image
    61  type versionDockerImage struct {
    62  	Description string `yaml:"description"`
    63  	URL         string `yaml:"url"`
    64  	Version     string `yaml:"version"`
    65  }
    66  
    67  // versionDockerImages is the complete information for docker images in the versions yaml
    68  type versionDockerImages struct {
    69  	Description string `yaml:"description"`
    70  	Alpine      versionDockerImage
    71  }
    72  
    73  // Versions will be used to parse the versions yaml
    74  type Versions struct {
    75  	Docker versionDockerImages `yaml:"docker_images"`
    76  }
    77  
    78  func init() {
    79  	var err error
    80  	cidDirectory, err = ioutil.TempDir("", "cid")
    81  	if err != nil {
    82  		log.Fatalf("Could not create cid directory: %v\n", err)
    83  	}
    84  
    85  	// Check versions.yaml
    86  	gopath := os.Getenv("GOPATH")
    87  	entirePath := filepath.Join(gopath, VersionsPath)
    88  
    89  	// Read versions.yaml
    90  	data, err := ioutil.ReadFile(entirePath)
    91  	if err != nil {
    92  		log.Fatalf("Could not read versions.yaml")
    93  	}
    94  
    95  	// Parse versions.yaml
    96  	var versions Versions
    97  	err = yaml.Unmarshal(data, &versions)
    98  	if err != nil {
    99  		log.Fatalf("Could not get alpine version")
   100  	}
   101  
   102  	// Define Alpine image with its proper version
   103  	AlpineImage = "alpine:" + versions.Docker.Alpine.Version
   104  
   105  	images = []string{
   106  		Image,
   107  		AlpineImage,
   108  		DebianImage,
   109  		FedoraImage,
   110  		Fedora30Image,
   111  		CentosImage,
   112  		StressImage,
   113  	}
   114  }
   115  
   116  func cidFilePath(containerName string) string {
   117  	return filepath.Join(cidDirectory, containerName)
   118  }
   119  
   120  func runDockerCommandWithTimeout(timeout time.Duration, command string, args ...string) (string, string, int) {
   121  	return runDockerCommandWithTimeoutAndPipe(nil, timeout, command, args...)
   122  }
   123  
   124  func runDockerCommandWithTimeoutAndPipe(stdin *bytes.Buffer, timeout time.Duration, command string, args ...string) (string, string, int) {
   125  	a := []string{command}
   126  
   127  	// --cidfile must be specified when the container is created (run/create)
   128  	if command == "run" || command == "create" {
   129  		for i := 0; i < len(args); i++ {
   130  			// looks for container name
   131  			if args[i] == "--name" && i+1 < len(args) {
   132  				a = append(a, "--cidfile", cidFilePath(args[i+1]))
   133  			}
   134  		}
   135  	}
   136  
   137  	a = append(a, args...)
   138  
   139  	cmd := tests.NewCommand(Docker, a...)
   140  	cmd.Timeout = timeout
   141  
   142  	return cmd.RunWithPipe(stdin)
   143  }
   144  
   145  func runDockerCommand(command string, args ...string) (string, string, int) {
   146  	return runDockerCommandWithTimeout(time.Duration(tests.Timeout), command, args...)
   147  }
   148  
   149  func runDockerCommandWithPipe(stdin *bytes.Buffer, command string, args ...string) (string, string, int) {
   150  	return runDockerCommandWithTimeoutAndPipe(stdin, time.Duration(tests.Timeout), command, args...)
   151  }
   152  
   153  // LogsDockerContainer returns the container logs
   154  func LogsDockerContainer(name string) (string, error) {
   155  	args := []string{name}
   156  
   157  	stdout, _, exitCode := runDockerCommand("logs", args...)
   158  
   159  	if exitCode != 0 {
   160  		return "", fmt.Errorf("failed to run docker logs command")
   161  	}
   162  
   163  	return strings.TrimSpace(stdout), nil
   164  }
   165  
   166  // StatusDockerContainer returns the container status
   167  func StatusDockerContainer(name string) string {
   168  	args := []string{"-a", "-f", "name=" + name, "--format", "{{.Status}}"}
   169  
   170  	stdout, _, exitCode := runDockerCommand("ps", args...)
   171  
   172  	if exitCode != 0 || stdout == "" {
   173  		return ""
   174  	}
   175  
   176  	state := strings.Split(stdout, " ")
   177  	return state[0]
   178  }
   179  
   180  // hasExitedDockerContainer checks if the container has exited.
   181  func hasExitedDockerContainer(name string) (bool, error) {
   182  	args := []string{"--format={{.State.Status}}", name}
   183  
   184  	stdout, _, exitCode := runDockerCommand("inspect", args...)
   185  
   186  	if exitCode != 0 || stdout == "" {
   187  		return false, fmt.Errorf("failed to run docker inspect command")
   188  	}
   189  
   190  	status := strings.TrimSpace(stdout)
   191  
   192  	if status == "exited" {
   193  		return true, nil
   194  	}
   195  
   196  	return false, nil
   197  }
   198  
   199  // ExitCodeDockerContainer returns the container exit code
   200  func ExitCodeDockerContainer(name string, waitForExit bool) (int, error) {
   201  	// It makes no sense to try to retrieve the exit code of the container
   202  	// if it is still running. That's why this infinite loop takes care of
   203  	// waiting for the status to become "exited" before to ask for the exit
   204  	// code.
   205  	// However, we might want to bypass this check on purpose, that's why
   206  	// we check waitForExit boolean.
   207  	if waitForExit {
   208  		errCh := make(chan error)
   209  		exitCh := make(chan bool)
   210  
   211  		go func() {
   212  			for {
   213  				exited, err := hasExitedDockerContainer(name)
   214  				if err != nil {
   215  					errCh <- err
   216  				}
   217  
   218  				if exited {
   219  					break
   220  				}
   221  
   222  				time.Sleep(time.Second)
   223  			}
   224  
   225  			close(exitCh)
   226  		}()
   227  
   228  		select {
   229  		case <-exitCh:
   230  			break
   231  		case err := <-errCh:
   232  			return -1, err
   233  		case <-time.After(time.Duration(tests.Timeout) * time.Second):
   234  			return -1, fmt.Errorf("Timeout reached after %ds", tests.Timeout)
   235  		}
   236  	}
   237  
   238  	args := []string{"--format={{.State.ExitCode}}", name}
   239  
   240  	stdout, _, exitCode := runDockerCommand("inspect", args...)
   241  
   242  	if exitCode != 0 || stdout == "" {
   243  		return -1, fmt.Errorf("failed to run docker inspect command")
   244  	}
   245  
   246  	return strconv.Atoi(strings.TrimSpace(stdout))
   247  }
   248  
   249  // WaitForRunningDockerContainer verifies if a docker container
   250  // is running for a certain period of time
   251  // returns an error if the timeout is reached.
   252  func WaitForRunningDockerContainer(name string, running bool) error {
   253  	ch := make(chan bool)
   254  	go func() {
   255  		if IsRunningDockerContainer(name) == running {
   256  			close(ch)
   257  			return
   258  		}
   259  
   260  		time.Sleep(time.Second)
   261  	}()
   262  
   263  	select {
   264  	case <-ch:
   265  	case <-time.After(time.Duration(tests.Timeout) * time.Second):
   266  		return fmt.Errorf("Timeout reached after %ds", tests.Timeout)
   267  	}
   268  
   269  	return nil
   270  }
   271  
   272  // IsRunningDockerContainer inspects a container
   273  // returns true if is running
   274  func IsRunningDockerContainer(name string) bool {
   275  	stdout, _, exitCode := runDockerCommand("inspect", "--format={{.State.Running}}", name)
   276  
   277  	if exitCode != 0 {
   278  		return false
   279  	}
   280  
   281  	output := strings.TrimSpace(stdout)
   282  	tests.LogIfFail("container running: " + output)
   283  	return !(output == "false")
   284  }
   285  
   286  // ExistDockerContainer returns true if any of next cases is true:
   287  // - 'docker ps -a' command shows the container
   288  // - the VM is running (qemu)
   289  // - the proxy is running
   290  // - the shim is running
   291  // else false is returned
   292  func ExistDockerContainer(name string) bool {
   293  	if name == "" {
   294  		tests.LogIfFail("Container name is empty")
   295  		return false
   296  	}
   297  
   298  	state := StatusDockerContainer(name)
   299  	if state != "" {
   300  		return true
   301  	}
   302  
   303  	// If we reach this point means that the container doesn't exist in docker,
   304  	// but we have to check that the components (qemu, shim, proxy) are not running.
   305  	// Read container ID from file created by run/create
   306  	path := cidFilePath(name)
   307  	defer os.Remove(path)
   308  	content, err := ioutil.ReadFile(path)
   309  	if err != nil {
   310  		tests.LogIfFail("Could not read container ID file: %v\n", err)
   311  		return false
   312  	}
   313  
   314  	// Use container ID to check if kata components are still running.
   315  	cid := string(content)
   316  	exitCh := make(chan bool)
   317  	go func() {
   318  		for {
   319  			if !tests.HypervisorRunning(cid) &&
   320  				!tests.ProxyRunning(cid) &&
   321  				!tests.ShimRunning(cid) {
   322  				close(exitCh)
   323  				return
   324  			}
   325  			time.Sleep(time.Second)
   326  		}
   327  	}()
   328  
   329  	select {
   330  	case <-exitCh:
   331  		return false
   332  	case <-time.After(time.Duration(tests.Timeout) * time.Second):
   333  		tests.LogIfFail("Timeout reached after %ds", tests.Timeout)
   334  		return true
   335  	}
   336  }
   337  
   338  // RemoveDockerContainer removes a container using docker rm -f
   339  func RemoveDockerContainer(name string) bool {
   340  	_, _, exitCode := dockerRm("-f", name)
   341  	return (exitCode == 0)
   342  }
   343  
   344  // StopDockerContainer stops a container
   345  func StopDockerContainer(name string) bool {
   346  	_, _, exitCode := dockerStop(name)
   347  	return (exitCode == 0)
   348  }
   349  
   350  // KillDockerContainer kills a container
   351  func KillDockerContainer(name string) bool {
   352  	_, _, exitCode := dockerKill(name)
   353  	return (exitCode == 0)
   354  }
   355  
   356  func randomDockerName() string {
   357  	return tests.RandID(29) + fmt.Sprint(ginkgoconf.GinkgoConfig.ParallelNode)
   358  }
   359  
   360  // returns a random and valid repository name
   361  func randomDockerRepoName() string {
   362  	return strings.ToLower(tests.RandID(14)) + fmt.Sprint(ginkgoconf.GinkgoConfig.ParallelNode)
   363  }
   364  
   365  // dockerRm removes a container
   366  func dockerRm(args ...string) (string, string, int) {
   367  	return runDockerCommand("rm", args...)
   368  }
   369  
   370  // dockerStop stops a container
   371  // returns true on success else false
   372  func dockerStop(args ...string) (string, string, int) {
   373  	// docker stop takes ~15 seconds
   374  	return runDockerCommand("stop", args...)
   375  }
   376  
   377  // dockerPull downloads the specific image
   378  func dockerPull(args ...string) (string, string, int) {
   379  	// 10 minutes should be enough to download a image
   380  	return runDockerCommandWithTimeout(600, "pull", args...)
   381  }
   382  
   383  // dockerRun runs a container
   384  func dockerRun(args ...string) (string, string, int) {
   385  	if tests.Runtime != "" {
   386  		args = append(args, []string{"", ""}...)
   387  		copy(args[2:], args[:])
   388  		args[0] = "--runtime"
   389  		args[1] = tests.Runtime
   390  	}
   391  
   392  	return runDockerCommand("run", args...)
   393  }
   394  
   395  // Runs a container with stdin
   396  func dockerRunWithPipe(stdin *bytes.Buffer, args ...string) (string, string, int) {
   397  	if tests.Runtime != "" {
   398  		args = append(args, []string{"", ""}...)
   399  		copy(args[2:], args[:])
   400  		args[0] = "--runtime"
   401  		args[1] = tests.Runtime
   402  	}
   403  
   404  	return runDockerCommandWithPipe(stdin, "run", args...)
   405  }
   406  
   407  // dockerKill kills a container
   408  func dockerKill(args ...string) (string, string, int) {
   409  	return runDockerCommand("kill", args...)
   410  }
   411  
   412  // dockerVolume manages volumes
   413  func dockerVolume(args ...string) (string, string, int) {
   414  	return runDockerCommand("volume", args...)
   415  }
   416  
   417  // dockerAttach attach to a running container
   418  func dockerAttach(args ...string) (string, string, int) {
   419  	return runDockerCommand("attach", args...)
   420  }
   421  
   422  // dockerCommit creates a new image from a container's changes
   423  func dockerCommit(args ...string) (string, string, int) {
   424  	return runDockerCommand("commit", args...)
   425  }
   426  
   427  // dockerImages list images
   428  func dockerImages(args ...string) (string, string, int) {
   429  	return runDockerCommand("images", args...)
   430  }
   431  
   432  // dockerImport imports the contents from a tarball to create a filesystem image
   433  func dockerImport(args ...string) (string, string, int) {
   434  	return runDockerCommand("import", args...)
   435  }
   436  
   437  // dockerRmi removes one or more images
   438  func dockerRmi(args ...string) (string, string, int) {
   439  	// docker takes more than 5 seconds to remove an image, it depends
   440  	// of the image size and this operation does not involve to the
   441  	// runtime
   442  	return runDockerCommand("rmi", args...)
   443  }
   444  
   445  // dockerCp copies files/folders between a container and the local filesystem
   446  func dockerCp(args ...string) (string, string, int) {
   447  	return runDockerCommand("cp", args...)
   448  }
   449  
   450  // dockerExec runs a command in a running container
   451  func dockerExec(args ...string) (string, string, int) {
   452  	return runDockerCommand("exec", args...)
   453  }
   454  
   455  // dockerPs list containers
   456  func dockerPs(args ...string) (string, string, int) {
   457  	return runDockerCommand("ps", args...)
   458  }
   459  
   460  // dockerSearch searches docker hub images
   461  func dockerSearch(args ...string) (string, string, int) {
   462  	return runDockerCommand("search", args...)
   463  }
   464  
   465  // dockerCreate creates a new container
   466  func dockerCreate(args ...string) (string, string, int) {
   467  	return runDockerCommand("create", args...)
   468  }
   469  
   470  // dockerDiff inspect changes to files or directories on a container’s filesystem
   471  func dockerDiff(args ...string) (string, string, int) {
   472  	return runDockerCommand("diff", args...)
   473  }
   474  
   475  // dockerBuild builds an image from a Dockerfile
   476  func dockerBuild(args ...string) (string, string, int) {
   477  	// 10 minutes should be enough to build a image
   478  	return runDockerCommandWithTimeout(600, "build", args...)
   479  }
   480  
   481  // dockerExport will export a container’s filesystem as a tar archive
   482  func dockerExport(args ...string) (string, string, int) {
   483  	return runDockerCommand("export", args...)
   484  }
   485  
   486  // dockerInfo displays system-wide information
   487  func dockerInfo() (string, string, int) {
   488  	return runDockerCommand("info")
   489  }
   490  
   491  // dockerInspect returns low-level information on Docker objects
   492  func dockerInspect(args ...string) (string, string, int) {
   493  	return runDockerCommand("inspect", args...)
   494  }
   495  
   496  // dockerLoad loads a tarred repository
   497  func dockerLoad(args ...string) (string, string, int) {
   498  	return runDockerCommand("load", args...)
   499  }
   500  
   501  // dockerPort starts one or more stopped containers
   502  func dockerPort(args ...string) (string, string, int) {
   503  	return runDockerCommand("port", args...)
   504  }
   505  
   506  // dockerRestart starts one or more stopped containers
   507  func dockerRestart(args ...string) (string, string, int) {
   508  	return runDockerCommand("restart", args...)
   509  }
   510  
   511  // dockerSave saves one or more images
   512  func dockerSave(args ...string) (string, string, int) {
   513  	return runDockerCommand("save", args...)
   514  }
   515  
   516  // dockerPause pauses all processes within one or more containers
   517  func dockerPause(args ...string) (string, string, int) {
   518  	return runDockerCommand("pause", args...)
   519  }
   520  
   521  // dockerUnpause unpauses all processes within one or more containers
   522  func dockerUnpause(args ...string) (string, string, int) {
   523  	return runDockerCommand("unpause", args...)
   524  }
   525  
   526  // dockerTop displays the running processes of a container
   527  //nolint:unused
   528  func dockerTop(args ...string) (string, string, int) {
   529  	return runDockerCommand("top", args...)
   530  }
   531  
   532  // dockerUpdate updates configuration of one or more containers
   533  func dockerUpdate(args ...string) (string, string, int) {
   534  	return runDockerCommand("update", args...)
   535  }
   536  
   537  // createLoopDevice creates a new disk file using 'dd' command, returns the path to disk file and
   538  // its loop device representation
   539  func createLoopDevice() (string, string, error) {
   540  	f, err := ioutil.TempFile("", "dd")
   541  	if err != nil {
   542  		return "", "", err
   543  	}
   544  	defer f.Close()
   545  
   546  	// create disk file
   547  	ddArgs := []string{"if=/dev/zero", fmt.Sprintf("of=%s", f.Name()), "count=1", "bs=50M"}
   548  	ddCmd := tests.NewCommand("dd", ddArgs...)
   549  	if _, stderr, exitCode := ddCmd.Run(); exitCode != 0 {
   550  		return "", "", fmt.Errorf("%s", stderr)
   551  	}
   552  
   553  	// partitioning disk file
   554  	fdiskArgs := []string{"-c", fmt.Sprintf(`printf "g\nn\n\n\n\nw\n" | fdisk %s`, f.Name())}
   555  	fdiskCmd := tests.NewCommand("bash", fdiskArgs...)
   556  	if _, stderr, exitCode := fdiskCmd.Run(); exitCode != 0 {
   557  		return "", "", fmt.Errorf("%s", stderr)
   558  	}
   559  
   560  	// create loop device
   561  	losetupCmd := tests.NewCommand("losetup", "-fP", f.Name())
   562  	if _, stderr, exitCode := losetupCmd.Run(); exitCode != 0 {
   563  		return "", "", fmt.Errorf("%s", stderr)
   564  	}
   565  
   566  	// get loop device path
   567  	getLoopPath := tests.NewCommand("losetup", "-j", f.Name())
   568  	stdout, stderr, exitCode := getLoopPath.Run()
   569  	if exitCode != 0 {
   570  		return "", "", fmt.Errorf("exitCode: %d, stdout: %s, stderr: %s ", exitCode, stdout, stderr)
   571  	}
   572  	re := regexp.MustCompile("/dev/loop[0-9]+")
   573  	loopPath := re.FindStringSubmatch(stdout)
   574  	if len(loopPath) == 0 {
   575  		return "", "", fmt.Errorf("Unable to get loop device path, stdout: %s, stderr: %s", stdout, stderr)
   576  	}
   577  	return f.Name(), loopPath[0], nil
   578  }
   579  
   580  // deleteLoopDevice removes loopdevices
   581  func deleteLoopDevice(loopFile string) error {
   582  	partxCmd := tests.NewCommand("losetup", "-d", loopFile)
   583  	_, stderr, exitCode := partxCmd.Run()
   584  	if exitCode != 0 {
   585  		return fmt.Errorf("%s", stderr)
   586  	}
   587  
   588  	return nil
   589  }