From f85ef42ea538911c82821ab6cc0166d492e9a379 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 25 Aug 2017 18:48:36 -0400 Subject: [PATCH] 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 --- integration-cli/check_test.go | 37 +-- integration-cli/cli/build/fakegit/fakegit.go | 2 + .../cli/build/fakestorage/fixtures.go | 2 +- .../cli/build/fakestorage/storage.go | 42 ++-- integration-cli/cli/cli.go | 27 +-- integration-cli/docker_cli_run_test.go | 4 +- integration-cli/docker_utils_test.go | 4 +- integration-cli/environment/clean.go | 198 ----------------- integration-cli/environment/environment.go | 210 +++--------------- integration-cli/environment/protect.go | 48 ---- integration-cli/fixtures_linux_daemon_test.go | 4 +- integration-cli/requirement/requirement.go | 5 +- integration-cli/requirements_test.go | 56 ++--- integration-cli/requirements_unix_test.go | 2 +- integration/container/main_test.go | 19 +- integration/service/inspect_test.go | 2 +- integration/service/main_test.go | 15 +- internal/test/environment/clean.go | 164 ++++++++++++++ internal/test/environment/environment.go | 117 ++++++++++ internal/test/environment/protect.go | 78 +++++++ 20 files changed, 478 insertions(+), 558 deletions(-) delete mode 100644 integration-cli/environment/clean.go delete mode 100644 integration-cli/environment/protect.go create mode 100644 internal/test/environment/clean.go create mode 100644 internal/test/environment/environment.go create mode 100644 internal/test/environment/protect.go diff --git a/integration-cli/check_test.go b/integration-cli/check_test.go index 6af5229f04..87517e7db7 100644 --- a/integration-cli/check_test.go +++ b/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() { diff --git a/integration-cli/cli/build/fakegit/fakegit.go b/integration-cli/cli/build/fakegit/fakegit.go index 74faffd922..ad028dc723 100644 --- a/integration-cli/cli/build/fakegit/fakegit.go +++ b/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{}) diff --git a/integration-cli/cli/build/fakestorage/fixtures.go b/integration-cli/cli/build/fakestorage/fixtures.go index f6a63dcf03..8a6bb137ad 100644 --- a/integration-cli/cli/build/fakestorage/fixtures.go +++ b/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" } diff --git a/integration-cli/cli/build/fakestorage/storage.go b/integration-cli/cli/build/fakestorage/storage.go index 49f47e4368..25cd872f50 100644 --- a/integration-cli/cli/build/fakestorage/storage.go +++ b/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{ diff --git a/integration-cli/cli/cli.go b/integration-cli/cli/cli.go index d7fadee47d..b8230b2da4 100644 --- a/integration-cli/cli/cli.go +++ b/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 -) +var testEnv *environment.Execution -// 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) - } +// 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 diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 504c659884..340ad4b90f 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/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) } } diff --git a/integration-cli/docker_utils_test.go b/integration-cli/docker_utils_test.go index 79a9e009e8..95d2e93cfe 100644 --- a/integration-cli/docker_utils_test.go +++ b/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() diff --git a/integration-cli/environment/clean.go b/integration-cli/environment/clean.go deleted file mode 100644 index 9df2470153..0000000000 --- a/integration-cli/environment/clean.go +++ /dev/null @@ -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] == "" || fields[1] == "" { - if fields[2] != "" { - 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 -} diff --git a/integration-cli/environment/environment.go b/integration-cli/environment/environment.go index a8a1045901..4e04ba76f3 100644 --- a/integration-cli/environment/environment.go +++ b/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" + "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 } diff --git a/integration-cli/environment/protect.go b/integration-cli/environment/protect.go deleted file mode 100644 index 173fba5425..0000000000 --- a/integration-cli/environment/protect.go +++ /dev/null @@ -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 -} diff --git a/integration-cli/fixtures_linux_daemon_test.go b/integration-cli/fixtures_linux_daemon_test.go index 0011797c0e..1508762060 100644 --- a/integration-cli/fixtures_linux_daemon_test.go +++ b/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 diff --git a/integration-cli/requirement/requirement.go b/integration-cli/requirement/requirement.go index f60917447c..9486c32520 100644 --- a/integration-cli/requirement/requirement.go +++ b/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 { diff --git a/integration-cli/requirements_test.go b/integration-cli/requirements_test.go index d6cc27b1d0..0b10969996 100644 --- a/integration-cli/requirements_test.go +++ b/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...) } diff --git a/integration-cli/requirements_unix_test.go b/integration-cli/requirements_unix_test.go index 2ed04f6e10..6ef900fc18 100644 --- a/integration-cli/requirements_unix_test.go +++ b/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 } diff --git a/integration/container/main_test.go b/integration/container/main_test.go index a54b8d9094..1c4e078400 100644 --- a/integration/container/main_test.go +++ b/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) } } diff --git a/integration/service/inspect_test.go b/integration/service/inspect_test.go index 601e16cd8f..e4459af437 100644 --- a/integration/service/inspect_test.go +++ b/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, diff --git a/integration/service/main_test.go b/integration/service/main_test.go index fac7427819..4d6af81895 100644 --- a/integration/service/main_test.go +++ b/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) } } diff --git a/internal/test/environment/clean.go b/internal/test/environment/clean.go new file mode 100644 index 0000000000..9020a92b7b --- /dev/null +++ b/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) + } +} diff --git a/internal/test/environment/environment.go b/internal/test/environment/environment.go new file mode 100644 index 0000000000..afe8929350 --- /dev/null +++ b/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 +} diff --git a/internal/test/environment/protect.go b/internal/test/environment/protect.go new file mode 100644 index 0000000000..8feaafe816 --- /dev/null +++ b/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 != ":" { + result = append(result, tag) + } + } + for _, digest := range image.RepoDigests { + if digest != "@" { + 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 +}