Jelajahi Sumber

Merge pull request #34656 from dnephin/move-testenv-to-internal

Move integration-cli/environment to an internal package
Yong Tang 7 tahun lalu
induk
melakukan
184cea5ff7

+ 23 - 14
integration-cli/check_test.go

@@ -2,10 +2,12 @@ package main
 
 import (
 	"fmt"
+	"io/ioutil"
 	"net/http/httptest"
 	"os"
 	"path"
 	"path/filepath"
+	"strconv"
 	"sync"
 	"syscall"
 	"testing"
@@ -20,6 +22,7 @@ import (
 	"github.com/docker/docker/integration-cli/environment"
 	"github.com/docker/docker/integration-cli/fixtures/plugin"
 	"github.com/docker/docker/integration-cli/registry"
+	ienv "github.com/docker/docker/internal/test/environment"
 	"github.com/docker/docker/pkg/reexec"
 	"github.com/go-check/check"
 	"golang.org/x/net/context"
@@ -57,20 +60,14 @@ func init() {
 
 func TestMain(m *testing.M) {
 	dockerBinary = testEnv.DockerBinary()
-
-	if testEnv.LocalDaemon() {
-		fmt.Println("INFO: Testing against a local daemon")
-	} else {
-		fmt.Println("INFO: Testing against a remote daemon")
-	}
-	exitCode := m.Run()
-	os.Exit(exitCode)
+	testEnv.Print()
+	os.Exit(m.Run())
 }
 
 func Test(t *testing.T) {
-	cli.EnsureTestEnvIsLoaded(t)
-	fakestorage.EnsureTestEnvIsLoaded(t)
-	environment.ProtectImages(t, testEnv)
+	cli.SetTestEnvironment(testEnv)
+	fakestorage.SetTestEnvironment(&testEnv.Execution)
+	ienv.ProtectImages(t, &testEnv.Execution)
 	check.TestingT(t)
 }
 
@@ -82,13 +79,25 @@ type DockerSuite struct {
 }
 
 func (s *DockerSuite) OnTimeout(c *check.C) {
-	if testEnv.DaemonPID() > 0 && testEnv.LocalDaemon() {
-		daemon.SignalDaemonDump(testEnv.DaemonPID())
+	path := filepath.Join(os.Getenv("DEST"), "docker.pid")
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		c.Fatalf("Failed to get daemon PID from %s\n", path)
+	}
+
+	rawPid, err := strconv.ParseInt(string(b), 10, 32)
+	if err != nil {
+		c.Fatalf("Failed to parse pid from %s: %s\n", path, err)
+	}
+
+	daemonPid := int(rawPid)
+	if daemonPid > 0 && testEnv.IsLocalDaemon() {
+		daemon.SignalDaemonDump(daemonPid)
 	}
 }
 
 func (s *DockerSuite) TearDownTest(c *check.C) {
-	testEnv.Clean(c, dockerBinary)
+	testEnv.Clean(c)
 }
 
 func init() {

+ 2 - 0
integration-cli/cli/build/fakegit/fakegit.go

@@ -11,9 +11,11 @@ import (
 
 	"github.com/docker/docker/integration-cli/cli/build/fakecontext"
 	"github.com/docker/docker/integration-cli/cli/build/fakestorage"
+	"github.com/stretchr/testify/require"
 )
 
 type testingT interface {
+	require.TestingT
 	logT
 	Fatal(args ...interface{})
 	Fatalf(string, ...interface{})

+ 1 - 1
integration-cli/cli/build/fakestorage/fixtures.go

@@ -30,7 +30,7 @@ func ensureHTTPServerImage(t testingT) {
 	}
 	defer os.RemoveAll(tmp)
 
-	goos := testEnv.DaemonPlatform()
+	goos := testEnv.DaemonInfo.OSType
 	if goos == "" {
 		goos = "linux"
 	}

+ 16 - 26
integration-cli/cli/build/fakestorage/storage.go

@@ -8,39 +8,20 @@ import (
 	"net/url"
 	"os"
 	"strings"
-	"sync"
 
 	"github.com/docker/docker/integration-cli/cli"
 	"github.com/docker/docker/integration-cli/cli/build"
 	"github.com/docker/docker/integration-cli/cli/build/fakecontext"
-	"github.com/docker/docker/integration-cli/environment"
 	"github.com/docker/docker/integration-cli/request"
+	"github.com/docker/docker/internal/test/environment"
 	"github.com/docker/docker/pkg/stringutils"
+	"github.com/stretchr/testify/require"
 )
 
-var (
-	testEnv  *environment.Execution
-	onlyOnce sync.Once
-)
-
-// EnsureTestEnvIsLoaded make sure the test environment is loaded for this package
-func EnsureTestEnvIsLoaded(t testingT) {
-	var doIt bool
-	var err error
-	onlyOnce.Do(func() {
-		doIt = true
-	})
-
-	if !doIt {
-		return
-	}
-	testEnv, err = environment.New()
-	if err != nil {
-		t.Fatalf("error loading testenv : %v", err)
-	}
-}
+var testEnv *environment.Execution
 
 type testingT interface {
+	require.TestingT
 	logT
 	Fatal(args ...interface{})
 	Fatalf(string, ...interface{})
@@ -58,11 +39,20 @@ type Fake interface {
 	CtxDir() string
 }
 
+// SetTestEnvironment sets a static test environment
+// TODO: decouple this package from environment
+func SetTestEnvironment(env *environment.Execution) {
+	testEnv = env
+}
+
 // New returns a static file server that will be use as build context.
 func New(t testingT, dir string, modifiers ...func(*fakecontext.Fake) error) Fake {
+	if testEnv == nil {
+		t.Fatal("fakstorage package requires SetTestEnvironment() to be called before use.")
+	}
 	ctx := fakecontext.New(t, dir, modifiers...)
-	if testEnv.LocalDaemon() {
-		return newLocalFakeStorage(t, ctx)
+	if testEnv.IsLocalDaemon() {
+		return newLocalFakeStorage(ctx)
 	}
 	return newRemoteFileServer(t, ctx)
 }
@@ -86,7 +76,7 @@ func (s *localFileStorage) Close() error {
 	return s.Fake.Close()
 }
 
-func newLocalFakeStorage(t testingT, ctx *fakecontext.Fake) *localFileStorage {
+func newLocalFakeStorage(ctx *fakecontext.Fake) *localFileStorage {
 	handler := http.FileServer(http.Dir(ctx.Dir))
 	server := httptest.NewServer(handler)
 	return &localFileStorage{

+ 6 - 21
integration-cli/cli/cli.go

@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"io"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/docker/docker/integration-cli/daemon"
@@ -13,26 +12,12 @@ import (
 	"github.com/pkg/errors"
 )
 
-var (
-	testEnv  *environment.Execution
-	onlyOnce sync.Once
-)
-
-// EnsureTestEnvIsLoaded make sure the test environment is loaded for this package
-func EnsureTestEnvIsLoaded(t testingT) {
-	var doIt bool
-	var err error
-	onlyOnce.Do(func() {
-		doIt = true
-	})
+var testEnv *environment.Execution
 
-	if !doIt {
-		return
-	}
-	testEnv, err = environment.New()
-	if err != nil {
-		t.Fatalf("error loading testenv : %v", err)
-	}
+// SetTestEnvironment sets a static test environment
+// TODO: decouple this package from environment
+func SetTestEnvironment(env *environment.Execution) {
+	testEnv = env
 }
 
 // CmdOperator defines functions that can modify a command
@@ -130,7 +115,7 @@ func Docker(cmd icmd.Cmd, cmdOperators ...CmdOperator) *icmd.Result {
 // validateArgs is a checker to ensure tests are not running commands which are
 // not supported on platforms. Specifically on Windows this is 'busybox top'.
 func validateArgs(args ...string) error {
-	if testEnv.DaemonPlatform() != "windows" {
+	if testEnv.DaemonInfo.OSType != "windows" {
 		return nil
 	}
 	foundBusybox := -1

+ 2 - 2
integration-cli/docker_cli_run_test.go

@@ -2215,7 +2215,7 @@ func (s *DockerSuite) TestRunVolumesCleanPaths(c *check.C) {
 
 	out, err = inspectMountSourceField("dark_helmet", prefix+slash+`foo`)
 	c.Assert(err, check.IsNil)
-	if !strings.Contains(strings.ToLower(out), strings.ToLower(testEnv.VolumesConfigPath())) {
+	if !strings.Contains(strings.ToLower(out), strings.ToLower(testEnv.PlatformDefaults.VolumesConfigPath)) {
 		c.Fatalf("Volume was not defined for %s/foo\n%q", prefix, out)
 	}
 
@@ -2226,7 +2226,7 @@ func (s *DockerSuite) TestRunVolumesCleanPaths(c *check.C) {
 
 	out, err = inspectMountSourceField("dark_helmet", prefix+slash+"bar")
 	c.Assert(err, check.IsNil)
-	if !strings.Contains(strings.ToLower(out), strings.ToLower(testEnv.VolumesConfigPath())) {
+	if !strings.Contains(strings.ToLower(out), strings.ToLower(testEnv.PlatformDefaults.VolumesConfigPath)) {
 		c.Fatalf("Volume was not defined for %s/bar\n%q", prefix, out)
 	}
 }

+ 2 - 2
integration-cli/docker_utils_test.go

@@ -234,7 +234,7 @@ func readFile(src string, c *check.C) (content string) {
 }
 
 func containerStorageFile(containerID, basename string) string {
-	return filepath.Join(testEnv.ContainerStoragePath(), containerID, basename)
+	return filepath.Join(testEnv.PlatformDefaults.ContainerStoragePath, containerID, basename)
 }
 
 // docker commands that use this function must be run with the '-d' switch.
@@ -266,7 +266,7 @@ func readContainerFileWithExec(c *check.C, containerID, filename string) []byte
 
 // daemonTime provides the current time on the daemon host
 func daemonTime(c *check.C) time.Time {
-	if testEnv.LocalDaemon() {
+	if testEnv.IsLocalDaemon() {
 		return time.Now()
 	}
 	cli, err := client.NewEnvClient()

+ 0 - 198
integration-cli/environment/clean.go

@@ -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
-}

+ 28 - 178
integration-cli/environment/environment.go

@@ -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"
+	"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
+// DockerBinary returns the docker binary for this testing environment
+func (e *Execution) DockerBinary() string {
+	return e.dockerBinary
 }
 
-// New creates a new Execution struct
+// 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
 }

+ 0 - 48
integration-cli/environment/protect.go

@@ -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
-}

+ 50 - 31
integration-cli/fixtures/load/frozen.go

@@ -9,21 +9,27 @@ import (
 	"strings"
 	"sync"
 
+	"context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
+	"github.com/docker/docker/pkg/jsonmessage"
+	"github.com/docker/docker/pkg/term"
 	"github.com/pkg/errors"
 )
 
-var frozenImgDir = "/docker-frozen-images"
+const frozenImgDir = "/docker-frozen-images"
 
 // FrozenImagesLinux loads the frozen image set for the integration suite
 // If the images are not available locally it will download them
 // TODO: This loads whatever is in the frozen image dir, regardless of what
 // images were passed in. If the images need to be downloaded, then it will respect
 // the passed in images
-func FrozenImagesLinux(dockerBinary string, images ...string) error {
+func FrozenImagesLinux(client client.APIClient, images ...string) error {
 	imgNS := os.Getenv("TEST_IMAGE_NAMESPACE")
 	var loadImages []struct{ srcName, destName string }
 	for _, img := range images {
-		if err := exec.Command(dockerBinary, "inspect", "--type=image", img).Run(); err != nil {
+		if !imageExists(client, img) {
 			srcName := img
 			// hello-world:latest gets re-tagged as hello-world:frozen
 			// there are some tests that use hello-world:latest specifically so it pulls
@@ -46,35 +52,41 @@ func FrozenImagesLinux(dockerBinary string, images ...string) error {
 		return nil
 	}
 
+	ctx := context.Background()
 	fi, err := os.Stat(frozenImgDir)
 	if err != nil || !fi.IsDir() {
 		srcImages := make([]string, 0, len(loadImages))
 		for _, img := range loadImages {
 			srcImages = append(srcImages, img.srcName)
 		}
-		if err := pullImages(dockerBinary, srcImages); err != nil {
+		if err := pullImages(ctx, client, srcImages); err != nil {
 			return errors.Wrap(err, "error pulling image list")
 		}
 	} else {
-		if err := loadFrozenImages(dockerBinary); err != nil {
+		if err := loadFrozenImages(ctx, client); err != nil {
 			return err
 		}
 	}
 
 	for _, img := range loadImages {
 		if img.srcName != img.destName {
-			if out, err := exec.Command(dockerBinary, "tag", img.srcName, img.destName).CombinedOutput(); err != nil {
-				return errors.Errorf("%v: %s", err, string(out))
+			if err := client.ImageTag(ctx, img.srcName, img.destName); err != nil {
+				return errors.Wrapf(err, "failed to tag %s as %s", img.srcName, img.destName)
 			}
-			if out, err := exec.Command(dockerBinary, "rmi", img.srcName).CombinedOutput(); err != nil {
-				return errors.Errorf("%v: %s", err, string(out))
+			if _, err := client.ImageRemove(ctx, img.srcName, types.ImageRemoveOptions{}); err != nil {
+				return errors.Wrapf(err, "failed to remove %s", img.srcName)
 			}
 		}
 	}
 	return nil
 }
 
-func loadFrozenImages(dockerBinary string) error {
+func imageExists(client client.APIClient, name string) bool {
+	_, _, err := client.ImageInspectWithRaw(context.Background(), name)
+	return err == nil
+}
+
+func loadFrozenImages(ctx context.Context, client client.APIClient) error {
 	tar, err := exec.LookPath("tar")
 	if err != nil {
 		return errors.Wrap(err, "could not find tar binary")
@@ -90,15 +102,16 @@ func loadFrozenImages(dockerBinary string) error {
 	tarCmd.Start()
 	defer tarCmd.Wait()
 
-	cmd := exec.Command(dockerBinary, "load")
-	cmd.Stdin = out
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return errors.Errorf("%v: %s", err, string(out))
+	resp, err := client.ImageLoad(ctx, out, true)
+	if err != nil {
+		return errors.Wrap(err, "failed to load frozen images")
 	}
-	return nil
+	defer resp.Body.Close()
+	fd, isTerminal := term.GetFdInfo(os.Stdout)
+	return jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stdout, fd, isTerminal, nil)
 }
 
-func pullImages(dockerBinary string, images []string) error {
+func pullImages(ctx context.Context, client client.APIClient, images []string) error {
 	cwd, err := os.Getwd()
 	if err != nil {
 		return errors.Wrap(err, "error getting path to dockerfile")
@@ -119,16 +132,8 @@ func pullImages(dockerBinary string, images []string) error {
 		wg.Add(1)
 		go func(tag, ref string) {
 			defer wg.Done()
-			if out, err := exec.Command(dockerBinary, "pull", ref).CombinedOutput(); err != nil {
-				chErr <- errors.Errorf("%v: %s", string(out), err)
-				return
-			}
-			if out, err := exec.Command(dockerBinary, "tag", ref, tag).CombinedOutput(); err != nil {
-				chErr <- errors.Errorf("%v: %s", string(out), err)
-				return
-			}
-			if out, err := exec.Command(dockerBinary, "rmi", ref).CombinedOutput(); err != nil {
-				chErr <- errors.Errorf("%v: %s", string(out), err)
+			if err := pullTagAndRemove(ctx, client, ref, tag); err != nil {
+				chErr <- err
 				return
 			}
 		}(tag, ref)
@@ -138,6 +143,25 @@ func pullImages(dockerBinary string, images []string) error {
 	return <-chErr
 }
 
+func pullTagAndRemove(ctx context.Context, client client.APIClient, ref string, tag string) error {
+	resp, err := client.ImagePull(ctx, ref, types.ImagePullOptions{})
+	if err != nil {
+		return errors.Wrapf(err, "failed to pull %s", ref)
+	}
+	defer resp.Close()
+	fd, isTerminal := term.GetFdInfo(os.Stdout)
+	if err := jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, fd, isTerminal, nil); err != nil {
+		return err
+	}
+
+	if err := client.ImageTag(ctx, ref, tag); err != nil {
+		return errors.Wrapf(err, "failed to tag %s as %s", ref, tag)
+	}
+	_, err = client.ImageRemove(ctx, ref, types.ImageRemoveOptions{})
+	return errors.Wrapf(err, "failed to remove %s", ref)
+
+}
+
 func readFrozenImageList(dockerfilePath string, images []string) (map[string]string, error) {
 	f, err := os.Open(dockerfilePath)
 	if err != nil {
@@ -156,11 +180,6 @@ func readFrozenImageList(dockerfilePath string, images []string) (map[string]str
 			continue
 		}
 
-		frozenImgDir = line[2]
-		if line[2] == frozenImgDir {
-			frozenImgDir = filepath.Join(os.Getenv("DEST"), "frozen-images")
-		}
-
 		for scanner.Scan() {
 			img := strings.TrimSpace(scanner.Text())
 			img = strings.TrimSuffix(img, "\\")

+ 2 - 2
integration-cli/fixtures_linux_daemon_test.go

@@ -79,7 +79,7 @@ func ensureSyscallTest(c *check.C) {
 }
 
 func ensureSyscallTestBuild(c *check.C) {
-	err := load.FrozenImagesLinux(dockerBinary, "buildpack-deps:jessie")
+	err := load.FrozenImagesLinux(testEnv.APIClient(), "buildpack-deps:jessie")
 	c.Assert(err, checker.IsNil)
 
 	var buildArgs []string
@@ -126,7 +126,7 @@ func ensureNNPTest(c *check.C) {
 }
 
 func ensureNNPTestBuild(c *check.C) {
-	err := load.FrozenImagesLinux(dockerBinary, "buildpack-deps:jessie")
+	err := load.FrozenImagesLinux(testEnv.APIClient(), "buildpack-deps:jessie")
 	c.Assert(err, checker.IsNil)
 
 	var buildArgs []string

+ 3 - 2
integration-cli/requirement/requirement.go

@@ -8,7 +8,8 @@ import (
 	"strings"
 )
 
-type skipT interface {
+// SkipT is the interface required to skip tests
+type SkipT interface {
 	Skip(reason string)
 }
 
@@ -17,7 +18,7 @@ type Test func() bool
 
 // Is checks if the environment satisfies the requirements
 // for the test to run or skips the tests.
-func Is(s skipT, requirements ...Test) {
+func Is(s SkipT, requirements ...Test) {
 	for _, r := range requirements {
 		isValid := r()
 		if !isValid {

+ 21 - 35
integration-cli/requirements_test.go

@@ -6,57 +6,47 @@ import (
 	"net/http"
 	"os"
 	"os/exec"
+	"strconv"
 	"strings"
 	"time"
 
 	"github.com/docker/docker/integration-cli/requirement"
-	"github.com/go-check/check"
 )
 
-func PlatformIs(platform string) bool {
-	return testEnv.DaemonPlatform() == platform
-}
-
-func ArchitectureIs(arch string) bool {
-	return os.Getenv("DOCKER_ENGINE_GOARCH") == arch
-}
-
 func ArchitectureIsNot(arch string) bool {
 	return os.Getenv("DOCKER_ENGINE_GOARCH") != arch
 }
 
-func StorageDriverIs(storageDriver string) bool {
-	return strings.HasPrefix(testEnv.DaemonStorageDriver(), storageDriver)
-}
-
-func StorageDriverIsNot(storageDriver string) bool {
-	return !strings.HasPrefix(testEnv.DaemonStorageDriver(), storageDriver)
-}
-
 func DaemonIsWindows() bool {
-	return PlatformIs("windows")
+	return testEnv.DaemonInfo.OSType == "windows"
 }
 
 func DaemonIsWindowsAtLeastBuild(buildNumber int) func() bool {
 	return func() bool {
-		return DaemonIsWindows() && testEnv.DaemonKernelVersionNumeric() >= buildNumber
+		if testEnv.DaemonInfo.OSType != "windows" {
+			return false
+		}
+		version := testEnv.DaemonInfo.KernelVersion
+		numVersion, _ := strconv.Atoi(strings.Split(version, " ")[1])
+		return numVersion >= buildNumber
 	}
 }
 
 func DaemonIsLinux() bool {
-	return PlatformIs("linux")
+	return testEnv.DaemonInfo.OSType == "linux"
 }
 
+// Deprecated: use skip.IfCondition(t, !testEnv.DaemonInfo.ExperimentalBuild)
 func ExperimentalDaemon() bool {
-	return testEnv.ExperimentalDaemon()
+	return testEnv.DaemonInfo.ExperimentalBuild
 }
 
 func NotExperimentalDaemon() bool {
-	return !testEnv.ExperimentalDaemon()
+	return !testEnv.DaemonInfo.ExperimentalBuild
 }
 
 func IsAmd64() bool {
-	return ArchitectureIs("amd64")
+	return os.Getenv("DOCKER_ENGINE_GOARCH") == "amd64"
 }
 
 func NotArm() bool {
@@ -76,7 +66,7 @@ func NotS390X() bool {
 }
 
 func SameHostDaemon() bool {
-	return testEnv.LocalDaemon()
+	return testEnv.IsLocalDaemon()
 }
 
 func UnixCli() bool {
@@ -127,12 +117,8 @@ func NotaryServerHosting() bool {
 	return err == nil
 }
 
-func NotOverlay() bool {
-	return StorageDriverIsNot("overlay")
-}
-
 func Devicemapper() bool {
-	return StorageDriverIs("devicemapper")
+	return strings.HasPrefix(testEnv.DaemonInfo.Driver, "devicemapper")
 }
 
 func IPv6() bool {
@@ -177,21 +163,21 @@ func UserNamespaceInKernel() bool {
 }
 
 func IsPausable() bool {
-	if testEnv.DaemonPlatform() == "windows" {
-		return testEnv.Isolation() == "hyperv"
+	if testEnv.DaemonInfo.OSType == "windows" {
+		return testEnv.DaemonInfo.Isolation == "hyperv"
 	}
 	return true
 }
 
 func NotPausable() bool {
-	if testEnv.DaemonPlatform() == "windows" {
-		return testEnv.Isolation() == "process"
+	if testEnv.DaemonInfo.OSType == "windows" {
+		return testEnv.DaemonInfo.Isolation == "process"
 	}
 	return false
 }
 
 func IsolationIs(expectedIsolation string) bool {
-	return testEnv.DaemonPlatform() == "windows" && string(testEnv.Isolation()) == expectedIsolation
+	return testEnv.DaemonInfo.OSType == "windows" && string(testEnv.DaemonInfo.Isolation) == expectedIsolation
 }
 
 func IsolationIsHyperv() bool {
@@ -204,6 +190,6 @@ func IsolationIsProcess() bool {
 
 // testRequires checks if the environment satisfies the requirements
 // for the test to run or skips the tests.
-func testRequires(c *check.C, requirements ...requirement.Test) {
+func testRequires(c requirement.SkipT, requirements ...requirement.Test) {
 	requirement.Is(c, requirements...)
 }

+ 1 - 1
integration-cli/requirements_unix_test.go

@@ -101,7 +101,7 @@ func overlay2Supported() bool {
 		return false
 	}
 
-	daemonV, err := kernel.ParseRelease(testEnv.DaemonKernelVersion())
+	daemonV, err := kernel.ParseRelease(testEnv.DaemonInfo.KernelVersion)
 	if err != nil {
 		return false
 	}

+ 5 - 14
integration/container/main_test.go

@@ -5,12 +5,10 @@ import (
 	"os"
 	"testing"
 
-	"github.com/docker/docker/integration-cli/environment"
+	"github.com/docker/docker/internal/test/environment"
 )
 
-var (
-	testEnv *environment.Execution
-)
+var testEnv *environment.Execution
 
 func TestMain(m *testing.M) {
 	var err error
@@ -20,18 +18,11 @@ func TestMain(m *testing.M) {
 		os.Exit(1)
 	}
 
-	// TODO: replace this with `testEnv.Print()` to print the full env
-	if testEnv.LocalDaemon() {
-		fmt.Println("INFO: Testing against a local daemon")
-	} else {
-		fmt.Println("INFO: Testing against a remote daemon")
-	}
-
-	res := m.Run()
-	os.Exit(res)
+	testEnv.Print()
+	os.Exit(m.Run())
 }
 
 func setupTest(t *testing.T) func() {
 	environment.ProtectImages(t, testEnv)
-	return func() { testEnv.Clean(t, testEnv.DockerBinary()) }
+	return func() { testEnv.Clean(t) }
 }

+ 1 - 1
integration/service/inspect_test.go

@@ -110,7 +110,7 @@ const defaultSwarmPort = 2477
 func newSwarm(t *testing.T) *daemon.Swarm {
 	d := &daemon.Swarm{
 		Daemon: daemon.New(t, "", dockerdBinary, daemon.Config{
-			Experimental: testEnv.ExperimentalDaemon(),
+			Experimental: testEnv.DaemonInfo.ExperimentalBuild,
 		}),
 		// TODO: better method of finding an unused port
 		Port: defaultSwarmPort,

+ 4 - 11
integration/service/main_test.go

@@ -5,7 +5,7 @@ import (
 	"os"
 	"testing"
 
-	"github.com/docker/docker/integration-cli/environment"
+	"github.com/docker/docker/internal/test/environment"
 )
 
 var testEnv *environment.Execution
@@ -20,18 +20,11 @@ func TestMain(m *testing.M) {
 		os.Exit(1)
 	}
 
-	// TODO: replace this with `testEnv.Print()` to print the full env
-	if testEnv.LocalDaemon() {
-		fmt.Println("INFO: Testing against a local daemon")
-	} else {
-		fmt.Println("INFO: Testing against a remote daemon")
-	}
-
-	res := m.Run()
-	os.Exit(res)
+	testEnv.Print()
+	os.Exit(m.Run())
 }
 
 func setupTest(t *testing.T) func() {
 	environment.ProtectImages(t, testEnv)
-	return func() { testEnv.Clean(t, testEnv.DockerBinary()) }
+	return func() { testEnv.Clean(t) }
 }

+ 164 - 0
internal/test/environment/clean.go

@@ -0,0 +1,164 @@
+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/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/net/context"
+)
+
+type testingT interface {
+	require.TestingT
+	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) {
+	client := e.APIClient()
+
+	platform := e.DaemonInfo.OSType
+	if (platform != "windows") || (platform == "windows" && e.DaemonInfo.Isolation == "hyperv") {
+		unpauseAllContainers(t, client)
+	}
+	deleteAllContainers(t, client)
+	deleteAllImages(t, client, e.protectedElements.images)
+	deleteAllVolumes(t, client)
+	deleteAllNetworks(t, client, platform)
+	if platform == "linux" {
+		deleteAllPlugins(t, client)
+	}
+}
+
+func unpauseAllContainers(t testingT, client client.ContainerAPIClient) {
+	ctx := context.Background()
+	containers := getPausedContainers(ctx, t, client)
+	if len(containers) > 0 {
+		for _, container := range containers {
+			err := client.ContainerUnpause(ctx, container.ID)
+			assert.NoError(t, err, "failed to unpause container %s", container.ID)
+		}
+	}
+}
+
+func getPausedContainers(ctx context.Context, t testingT, client client.ContainerAPIClient) []types.Container {
+	filter := filters.NewArgs()
+	filter.Add("status", "paused")
+	containers, err := client.ContainerList(ctx, types.ContainerListOptions{
+		Filters: filter,
+		Quiet:   true,
+		All:     true,
+	})
+	assert.NoError(t, err, "failed to list containers")
+	return containers
+}
+
+var alreadyExists = regexp.MustCompile(`Error response from daemon: removal of container (\w+) is already in progress`)
+
+func deleteAllContainers(t testingT, apiclient client.ContainerAPIClient) {
+	ctx := context.Background()
+	containers := getAllContainers(ctx, t, apiclient)
+	if len(containers) == 0 {
+		return
+	}
+
+	for _, container := range containers {
+		err := apiclient.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{
+			Force:         true,
+			RemoveVolumes: true,
+		})
+		if err == nil || client.IsErrNotFound(err) || alreadyExists.MatchString(err.Error()) {
+			continue
+		}
+		assert.NoError(t, err, "failed to remove %s", container.ID)
+	}
+}
+
+func getAllContainers(ctx context.Context, t testingT, client client.ContainerAPIClient) []types.Container {
+	containers, err := client.ContainerList(ctx, types.ContainerListOptions{
+		Quiet: true,
+		All:   true,
+	})
+	assert.NoError(t, err, "failed to list containers")
+	return containers
+}
+
+func deleteAllImages(t testingT, apiclient client.ImageAPIClient, protectedImages map[string]struct{}) {
+	images, err := apiclient.ImageList(context.Background(), types.ImageListOptions{})
+	assert.NoError(t, err, "failed to list images")
+
+	ctx := context.Background()
+	for _, image := range images {
+		tags := tagsFromImageSummary(image)
+		if len(tags) == 0 {
+			t.Logf("Removing image %s", image.ID)
+			removeImage(ctx, t, apiclient, image.ID)
+			continue
+		}
+		for _, tag := range tags {
+			if _, ok := protectedImages[tag]; !ok {
+				t.Logf("Removing image %s", tag)
+				removeImage(ctx, t, apiclient, tag)
+				continue
+			}
+		}
+	}
+}
+
+func removeImage(ctx context.Context, t testingT, apiclient client.ImageAPIClient, ref string) {
+	_, err := apiclient.ImageRemove(ctx, ref, types.ImageRemoveOptions{
+		Force: true,
+	})
+	if client.IsErrNotFound(err) {
+		return
+	}
+	assert.NoError(t, err, "failed to remove image %s", ref)
+}
+
+func deleteAllVolumes(t testingT, c client.VolumeAPIClient) {
+	volumes, err := c.VolumeList(context.Background(), filters.Args{})
+	assert.NoError(t, err, "failed to list volumes")
+
+	for _, v := range volumes.Volumes {
+		err := c.VolumeRemove(context.Background(), v.Name, true)
+		assert.NoError(t, err, "failed to remove volume %s", v.Name)
+	}
+}
+
+func deleteAllNetworks(t testingT, c client.NetworkAPIClient, daemonPlatform string) {
+	networks, err := c.NetworkList(context.Background(), types.NetworkListOptions{})
+	assert.NoError(t, err, "failed to list networks")
+
+	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)
+		assert.NoError(t, err, "failed to remove network %s", n.ID)
+	}
+}
+
+func deleteAllPlugins(t testingT, c client.PluginAPIClient) {
+	plugins, err := c.PluginList(context.Background(), filters.Args{})
+	assert.NoError(t, err, "failed to list plugins")
+
+	for _, p := range plugins {
+		err := c.PluginRemove(context.Background(), p.Name, types.PluginRemoveOptions{Force: true})
+		assert.NoError(t, err, "failed to remove plugin %s", p.ID)
+	}
+}

+ 117 - 0
internal/test/environment/environment.go

@@ -0,0 +1,117 @@
+package environment
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
+	"github.com/pkg/errors"
+	"golang.org/x/net/context"
+)
+
+// Execution contains information about the current test execution and daemon
+// under test
+type Execution struct {
+	client            client.APIClient
+	DaemonInfo        types.Info
+	PlatformDefaults  PlatformDefaults
+	protectedElements protectedElements
+}
+
+// PlatformDefaults are defaults values for the platform of the daemon under test
+type PlatformDefaults struct {
+	BaseImage            string
+	VolumesConfigPath    string
+	ContainerStoragePath string
+}
+
+// New creates a new Execution struct
+func New() (*Execution, error) {
+	client, err := client.NewEnvClient()
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create client")
+	}
+
+	info, err := client.Info(context.Background())
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get info from daemon")
+	}
+
+	return &Execution{
+		client:            client,
+		DaemonInfo:        info,
+		PlatformDefaults:  getPlatformDefaults(info),
+		protectedElements: newProtectedElements(),
+	}, nil
+}
+
+func getPlatformDefaults(info types.Info) PlatformDefaults {
+	volumesPath := filepath.Join(info.DockerRootDir, "volumes")
+	containersPath := filepath.Join(info.DockerRootDir, "containers")
+
+	switch info.OSType {
+	case "linux":
+		return PlatformDefaults{
+			BaseImage:            "scratch",
+			VolumesConfigPath:    toSlash(volumesPath),
+			ContainerStoragePath: toSlash(containersPath),
+		}
+	case "windows":
+		baseImage := "microsoft/windowsservercore"
+		if override := os.Getenv("WINDOWS_BASE_IMAGE"); override != "" {
+			baseImage = override
+			fmt.Println("INFO: Windows Base image is ", baseImage)
+		}
+		return PlatformDefaults{
+			BaseImage:            baseImage,
+			VolumesConfigPath:    filepath.FromSlash(volumesPath),
+			ContainerStoragePath: filepath.FromSlash(containersPath),
+		}
+	default:
+		panic(fmt.Sprintf("unknown info.OSType for daemon: %s", info.OSType))
+	}
+}
+
+// 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.
+func toSlash(path string) string {
+	return strings.Replace(path, `\`, `/`, -1)
+}
+
+// IsLocalDaemon is true if the daemon under test is on the same
+// host as the CLI.
+//
+// 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.
+func (e *Execution) IsLocalDaemon() bool {
+	return os.Getenv("DOCKER_REMOTE_DAEMON") == ""
+}
+
+// Print the execution details to stdout
+// TODO: print everything
+func (e *Execution) Print() {
+	if e.IsLocalDaemon() {
+		fmt.Println("INFO: Testing against a local daemon")
+	} else {
+		fmt.Println("INFO: Testing against a remote daemon")
+	}
+}
+
+// APIClient returns an APIClient connected to the daemon under test
+func (e *Execution) APIClient() client.APIClient {
+	return e.client
+}

+ 78 - 0
internal/test/environment/protect.go

@@ -0,0 +1,78 @@
+package environment
+
+import (
+	"context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/integration-cli/fixtures/load"
+	"github.com/stretchr/testify/require"
+)
+
+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{}{}
+	}
+}
+
+func newProtectedElements() protectedElements {
+	return protectedElements{
+		images: map[string]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.DaemonInfo.OSType == "linux" {
+		images = append(images, ensureFrozenImagesLinux(t, testEnv)...)
+	}
+	testEnv.ProtectImage(t, images...)
+}
+
+func getExistingImages(t testingT, testEnv *Execution) []string {
+	client := testEnv.APIClient()
+	filter := filters.NewArgs()
+	filter.Add("dangling", "false")
+	imageList, err := client.ImageList(context.Background(), types.ImageListOptions{
+		Filters: filter,
+	})
+	require.NoError(t, err, "failed to list images")
+
+	images := []string{}
+	for _, image := range imageList {
+		images = append(images, tagsFromImageSummary(image)...)
+	}
+	return images
+}
+
+func tagsFromImageSummary(image types.ImageSummary) []string {
+	result := []string{}
+	for _, tag := range image.RepoTags {
+		if tag != "<none>:<none>" {
+			result = append(result, tag)
+		}
+	}
+	for _, digest := range image.RepoDigests {
+		if digest != "<none>@<none>" {
+			result = append(result, digest)
+		}
+	}
+	return result
+}
+
+func ensureFrozenImagesLinux(t testingT, testEnv *Execution) []string {
+	images := []string{"busybox:latest", "hello-world:frozen", "debian:jessie"}
+	err := load.FrozenImagesLinux(testEnv.APIClient(), images...)
+	if err != nil {
+		t.Fatalf("Failed to load frozen images: %s", err)
+	}
+	return images
+}