Refactor test environment

split all non-cli portions into a new internal/test/environment package

Set a test environment on packages instead of creating new ones.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin
2017-08-25 18:48:36 -04:00
parent 61e7d0595d
commit f85ef42ea5
20 changed files with 478 additions and 558 deletions

View File

@@ -1,198 +0,0 @@
package environment
import (
"regexp"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/gotestyourself/gotestyourself/icmd"
"golang.org/x/net/context"
)
type testingT interface {
logT
Fatalf(string, ...interface{})
}
type logT interface {
Logf(string, ...interface{})
}
// Clean the environment, preserving protected objects (images, containers, ...)
// and removing everything else. It's meant to run after any tests so that they don't
// depend on each others.
func (e *Execution) Clean(t testingT, dockerBinary string) {
cli, err := client.NewEnvClient()
if err != nil {
t.Fatalf("%v", err)
}
defer cli.Close()
if (e.DaemonPlatform() != "windows") || (e.DaemonPlatform() == "windows" && e.Isolation() == "hyperv") {
unpauseAllContainers(t, dockerBinary)
}
deleteAllContainers(t, dockerBinary)
deleteAllImages(t, dockerBinary, e.protectedElements.images)
deleteAllVolumes(t, cli)
deleteAllNetworks(t, cli, e.DaemonPlatform())
if e.DaemonPlatform() == "linux" {
deleteAllPlugins(t, cli, dockerBinary)
}
}
func unpauseAllContainers(t testingT, dockerBinary string) {
containers := getPausedContainers(t, dockerBinary)
if len(containers) > 0 {
icmd.RunCommand(dockerBinary, append([]string{"unpause"}, containers...)...).Assert(t, icmd.Success)
}
}
func getPausedContainers(t testingT, dockerBinary string) []string {
result := icmd.RunCommand(dockerBinary, "ps", "-f", "status=paused", "-q", "-a")
result.Assert(t, icmd.Success)
return strings.Fields(result.Combined())
}
var alreadyExists = regexp.MustCompile(`Error response from daemon: removal of container (\w+) is already in progress`)
func deleteAllContainers(t testingT, dockerBinary string) {
containers := getAllContainers(t, dockerBinary)
if len(containers) > 0 {
result := icmd.RunCommand(dockerBinary, append([]string{"rm", "-fv"}, containers...)...)
if result.Error != nil {
// If the error is "No such container: ..." this means the container doesn't exists anymore,
// or if it is "... removal of container ... is already in progress" it will be removed eventually.
// We can safely ignore those.
if strings.Contains(result.Stderr(), "No such container") || alreadyExists.MatchString(result.Stderr()) {
return
}
t.Fatalf("error removing containers %v : %v (%s)", containers, result.Error, result.Combined())
}
}
}
func getAllContainers(t testingT, dockerBinary string) []string {
result := icmd.RunCommand(dockerBinary, "ps", "-q", "-a")
result.Assert(t, icmd.Success)
return strings.Fields(result.Combined())
}
func deleteAllImages(t testingT, dockerBinary string, protectedImages map[string]struct{}) {
result := icmd.RunCommand(dockerBinary, "images", "--digests")
result.Assert(t, icmd.Success)
lines := strings.Split(string(result.Combined()), "\n")[1:]
imgMap := map[string]struct{}{}
for _, l := range lines {
if l == "" {
continue
}
fields := strings.Fields(l)
imgTag := fields[0] + ":" + fields[1]
if _, ok := protectedImages[imgTag]; !ok {
if fields[0] == "<none>" || fields[1] == "<none>" {
if fields[2] != "<none>" {
imgMap[fields[0]+"@"+fields[2]] = struct{}{}
} else {
imgMap[fields[3]] = struct{}{}
}
// continue
} else {
imgMap[imgTag] = struct{}{}
}
}
}
if len(imgMap) != 0 {
imgs := make([]string, 0, len(imgMap))
for k := range imgMap {
imgs = append(imgs, k)
}
icmd.RunCommand(dockerBinary, append([]string{"rmi", "-f"}, imgs...)...).Assert(t, icmd.Success)
}
}
func deleteAllVolumes(t testingT, c client.APIClient) {
var errs []string
volumes, err := getAllVolumes(c)
if err != nil {
t.Fatalf("%v", err)
}
for _, v := range volumes {
err := c.VolumeRemove(context.Background(), v.Name, true)
if err != nil {
errs = append(errs, err.Error())
continue
}
}
if len(errs) > 0 {
t.Fatalf("%v", strings.Join(errs, "\n"))
}
}
func getAllVolumes(c client.APIClient) ([]*types.Volume, error) {
volumes, err := c.VolumeList(context.Background(), filters.Args{})
if err != nil {
return nil, err
}
return volumes.Volumes, nil
}
func deleteAllNetworks(t testingT, c client.APIClient, daemonPlatform string) {
networks, err := getAllNetworks(c)
if err != nil {
t.Fatalf("%v", err)
}
var errs []string
for _, n := range networks {
if n.Name == "bridge" || n.Name == "none" || n.Name == "host" {
continue
}
if daemonPlatform == "windows" && strings.ToLower(n.Name) == "nat" {
// nat is a pre-defined network on Windows and cannot be removed
continue
}
err := c.NetworkRemove(context.Background(), n.ID)
if err != nil {
errs = append(errs, err.Error())
continue
}
}
if len(errs) > 0 {
t.Fatalf("%v", strings.Join(errs, "\n"))
}
}
func getAllNetworks(c client.APIClient) ([]types.NetworkResource, error) {
networks, err := c.NetworkList(context.Background(), types.NetworkListOptions{})
if err != nil {
return nil, err
}
return networks, nil
}
func deleteAllPlugins(t testingT, c client.APIClient, dockerBinary string) {
plugins, err := getAllPlugins(c)
if err != nil {
t.Fatalf("%v", err)
}
var errs []string
for _, p := range plugins {
err := c.PluginRemove(context.Background(), p.Name, types.PluginRemoveOptions{Force: true})
if err != nil {
errs = append(errs, err.Error())
continue
}
}
if len(errs) > 0 {
t.Fatalf("%v", strings.Join(errs, "\n"))
}
}
func getAllPlugins(c client.APIClient) (types.PluginsListResponse, error) {
plugins, err := c.PluginList(context.Background(), filters.Args{})
if err != nil {
return nil, err
}
return plugins, nil
}

View File

@@ -1,19 +1,11 @@
package environment
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/opts"
"golang.org/x/net/context"
"os/exec"
"github.com/docker/docker/internal/test/environment"
)
var (
@@ -23,89 +15,28 @@ var (
func init() {
if DefaultClientBinary == "" {
// TODO: to be removed once we no longer depend on the docker cli for integration tests
//panic("TEST_CLIENT_BINARY must be set")
DefaultClientBinary = "docker"
}
}
// Execution holds informations about the test execution environment.
// Execution contains information about the current test execution and daemon
// under test
type Execution struct {
daemonPlatform string
localDaemon bool
experimentalDaemon bool
daemonStorageDriver string
isolation container.Isolation
daemonPid int
daemonKernelVersion string
// For a local daemon on Linux, these values will be used for testing
// user namespace support as the standard graph path(s) will be
// appended with the root remapped uid.gid prefix
dockerBasePath string
volumesConfigPath string
containerStoragePath string
// baseImage is the name of the base image for testing
// Environment variable WINDOWS_BASE_IMAGE can override this
baseImage string
environment.Execution
dockerBinary string
protectedElements protectedElements
}
// New creates a new Execution struct
// DockerBinary returns the docker binary for this testing environment
func (e *Execution) DockerBinary() string {
return e.dockerBinary
}
// New returns details about the testing environment
func New() (*Execution, error) {
localDaemon := true
// Deterministically working out the environment in which CI is running
// to evaluate whether the daemon is local or remote is not possible through
// a build tag.
//
// For example Windows to Linux CI under Jenkins tests the 64-bit
// Windows binary build with the daemon build tag, but calls a remote
// Linux daemon.
//
// We can't just say if Windows then assume the daemon is local as at
// some point, we will be testing the Windows CLI against a Windows daemon.
//
// Similarly, it will be perfectly valid to also run CLI tests from
// a Linux CLI (built with the daemon tag) against a Windows daemon.
if len(os.Getenv("DOCKER_REMOTE_DAEMON")) > 0 {
localDaemon = false
}
info, err := getDaemonDockerInfo()
env, err := environment.New()
if err != nil {
return nil, err
}
daemonPlatform := info.OSType
if daemonPlatform != "linux" && daemonPlatform != "windows" {
return nil, fmt.Errorf("Cannot run tests against platform: %s", daemonPlatform)
}
baseImage := "scratch"
volumesConfigPath := filepath.Join(info.DockerRootDir, "volumes")
containerStoragePath := filepath.Join(info.DockerRootDir, "containers")
// Make sure in context of daemon, not the local platform. Note we can't
// use filepath.FromSlash or ToSlash here as they are a no-op on Unix.
if daemonPlatform == "windows" {
volumesConfigPath = strings.Replace(volumesConfigPath, `/`, `\`, -1)
containerStoragePath = strings.Replace(containerStoragePath, `/`, `\`, -1)
baseImage = "microsoft/windowsservercore"
if len(os.Getenv("WINDOWS_BASE_IMAGE")) > 0 {
baseImage = os.Getenv("WINDOWS_BASE_IMAGE")
fmt.Println("INFO: Windows Base image is ", baseImage)
}
} else {
volumesConfigPath = strings.Replace(volumesConfigPath, `\`, `/`, -1)
containerStoragePath = strings.Replace(containerStoragePath, `\`, `/`, -1)
}
var daemonPid int
dest := os.Getenv("DEST")
b, err := ioutil.ReadFile(filepath.Join(dest, "docker.pid"))
if err == nil {
if p, err := strconv.ParseInt(string(b), 10, 32); err == nil {
daemonPid = int(p)
}
}
dockerBinary, err := exec.LookPath(DefaultClientBinary)
if err != nil {
@@ -113,117 +44,36 @@ func New() (*Execution, error) {
}
return &Execution{
localDaemon: localDaemon,
daemonPlatform: daemonPlatform,
daemonStorageDriver: info.Driver,
daemonKernelVersion: info.KernelVersion,
dockerBasePath: info.DockerRootDir,
volumesConfigPath: volumesConfigPath,
containerStoragePath: containerStoragePath,
isolation: info.Isolation,
daemonPid: daemonPid,
experimentalDaemon: info.ExperimentalBuild,
baseImage: baseImage,
dockerBinary: dockerBinary,
protectedElements: protectedElements{
images: map[string]struct{}{},
},
Execution: *env,
dockerBinary: dockerBinary,
}, nil
}
func getDaemonDockerInfo() (types.Info, error) {
// FIXME(vdemeester) should be safe to use as is
client, err := client.NewEnvClient()
if err != nil {
return types.Info{}, err
}
return client.Info(context.Background())
// DockerBasePath is the base path of the docker folder (by default it is -/var/run/docker)
// TODO: remove
// Deprecated: use Execution.DaemonInfo.DockerRootDir
func (e *Execution) DockerBasePath() string {
return e.DaemonInfo.DockerRootDir
}
// LocalDaemon is true if the daemon under test is on the same
// host as the CLI.
func (e *Execution) LocalDaemon() bool {
return e.localDaemon
// ExperimentalDaemon tell whether the main daemon has
// experimental features enabled or not
// Deprecated: use DaemonInfo.ExperimentalBuild
func (e *Execution) ExperimentalDaemon() bool {
return e.DaemonInfo.ExperimentalBuild
}
// DaemonPlatform is held globally so that tests can make intelligent
// decisions on how to configure themselves according to the platform
// of the daemon. This is initialized in docker_utils by sending
// a version call to the daemon and examining the response header.
// Deprecated: use Execution.DaemonInfo.OSType
func (e *Execution) DaemonPlatform() string {
return e.daemonPlatform
}
// DockerBasePath is the base path of the docker folder (by default it is -/var/run/docker)
func (e *Execution) DockerBasePath() string {
return e.dockerBasePath
}
// VolumesConfigPath is the path of the volume configuration for the testing daemon
func (e *Execution) VolumesConfigPath() string {
return e.volumesConfigPath
}
// ContainerStoragePath is the path where the container are stored for the testing daemon
func (e *Execution) ContainerStoragePath() string {
return e.containerStoragePath
}
// DaemonStorageDriver is held globally so that tests can know the storage
// driver of the daemon. This is initialized in docker_utils by sending
// a version call to the daemon and examining the response header.
func (e *Execution) DaemonStorageDriver() string {
return e.daemonStorageDriver
}
// Isolation is the isolation mode of the daemon under test
func (e *Execution) Isolation() container.Isolation {
return e.isolation
}
// DaemonPID is the pid of the main test daemon
func (e *Execution) DaemonPID() int {
return e.daemonPid
}
// ExperimentalDaemon tell whether the main daemon has
// experimental features enabled or not
func (e *Execution) ExperimentalDaemon() bool {
return e.experimentalDaemon
return e.DaemonInfo.OSType
}
// MinimalBaseImage is the image used for minimal builds (it depends on the platform)
// Deprecated: use Execution.PlatformDefaults.BaseImage
func (e *Execution) MinimalBaseImage() string {
return e.baseImage
}
// DaemonKernelVersion is the kernel version of the daemon as a string, as returned
// by an INFO call to the daemon.
func (e *Execution) DaemonKernelVersion() string {
return e.daemonKernelVersion
}
// DaemonKernelVersionNumeric is the kernel version of the daemon as an integer.
// Mostly useful on Windows where DaemonKernelVersion holds the full string such
// as `10.0 14393 (14393.447.amd64fre.rs1_release_inmarket.161102-0100)`, but
// integration tests really only need the `14393` piece to make decisions.
func (e *Execution) DaemonKernelVersionNumeric() int {
if e.daemonPlatform != "windows" {
return -1
}
v, _ := strconv.Atoi(strings.Split(e.daemonKernelVersion, " ")[1])
return v
}
// DockerBinary returns the docker binary for this testing environment
func (e *Execution) DockerBinary() string {
return e.dockerBinary
}
// DaemonHost return the daemon host string for this test execution
func DaemonHost() string {
daemonURLStr := "unix://" + opts.DefaultUnixSocket
if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" {
daemonURLStr = daemonHostVar
}
return daemonURLStr
return e.PlatformDefaults.BaseImage
}

View File

@@ -1,48 +0,0 @@
package environment
import (
"strings"
"github.com/docker/docker/integration-cli/fixtures/load"
"github.com/gotestyourself/gotestyourself/icmd"
)
type protectedElements struct {
images map[string]struct{}
}
// ProtectImage adds the specified image(s) to be protected in case of clean
func (e *Execution) ProtectImage(t testingT, images ...string) {
for _, image := range images {
e.protectedElements.images[image] = struct{}{}
}
}
// ProtectImages protects existing images and on linux frozen images from being
// cleaned up at the end of test runs
func ProtectImages(t testingT, testEnv *Execution) {
images := getExistingImages(t, testEnv)
if testEnv.DaemonPlatform() == "linux" {
images = append(images, ensureFrozenImagesLinux(t, testEnv)...)
}
testEnv.ProtectImage(t, images...)
}
func getExistingImages(t testingT, testEnv *Execution) []string {
// TODO: use API instead of cli
result := icmd.RunCommand(testEnv.dockerBinary, "images", "-f", "dangling=false", "--format", "{{.Repository}}:{{.Tag}}")
result.Assert(t, icmd.Success)
return strings.Split(strings.TrimSpace(result.Stdout()), "\n")
}
func ensureFrozenImagesLinux(t testingT, testEnv *Execution) []string {
images := []string{"busybox:latest", "hello-world:frozen", "debian:jessie"}
err := load.FrozenImagesLinux(testEnv.DockerBinary(), images...)
if err != nil {
result := icmd.RunCommand(testEnv.DockerBinary(), "image", "ls")
t.Logf(result.String())
t.Fatalf("%+v", err)
}
return images
}