Refactor test environment

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

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

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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