diff --git a/integration-cli/daemon/daemon.go b/integration-cli/daemon/daemon.go index 54f3a2f388..06bf504fa6 100644 --- a/integration-cli/daemon/daemon.go +++ b/integration-cli/daemon/daemon.go @@ -33,6 +33,7 @@ import ( ) type testingT interface { + require.TestingT logT Fatalf(string, ...interface{}) } @@ -329,9 +330,7 @@ func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error { // then save the busybox image from the main daemon and load it into this Daemon instance. func (d *Daemon) StartWithBusybox(t testingT, arg ...string) { d.Start(t, arg...) - if err := d.LoadBusybox(); err != nil { - t.Fatalf("Error loading busybox image to current daemon: %s\n%v", d.id, err) - } + d.LoadBusybox(t) } // Kill will send a SIGKILL to the daemon @@ -493,27 +492,24 @@ func (d *Daemon) handleUserns() { } } -// LoadBusybox will load the stored busybox into a newly started daemon -func (d *Daemon) LoadBusybox() error { - bb := filepath.Join(d.Folder, "busybox.tar") - if _, err := os.Stat(bb); err != nil { - if !os.IsNotExist(err) { - return errors.Errorf("unexpected error on busybox.tar stat: %v", err) - } - // saving busybox image from main daemon - if out, err := exec.Command(d.dockerBinary, "save", "--output", bb, "busybox:latest").CombinedOutput(); err != nil { - imagesOut, _ := exec.Command(d.dockerBinary, "images", "--format", "{{ .Repository }}:{{ .Tag }}").CombinedOutput() - return errors.Errorf("could not save busybox image: %s\n%s", string(out), strings.TrimSpace(string(imagesOut))) - } - } - // loading busybox image to this daemon - if out, err := d.Cmd("load", "--input", bb); err != nil { - return errors.Errorf("could not load busybox image: %s", out) - } - if err := os.Remove(bb); err != nil { - return err - } - return nil +// LoadBusybox image into the daemon +func (d *Daemon) LoadBusybox(t testingT) { + clientHost, err := client.NewEnvClient() + require.NoError(t, err, "failed to create client") + defer clientHost.Close() + + ctx := context.Background() + reader, err := clientHost.ImageSave(ctx, []string{"busybox:latest"}) + require.NoError(t, err, "failed to download busybox") + defer reader.Close() + + client, err := d.NewClient() + require.NoError(t, err, "failed to create client") + defer client.Close() + + resp, err := client.ImageLoad(ctx, reader, true) + require.NoError(t, err, "failed to load busybox") + defer resp.Body.Close() } func (d *Daemon) queryRootDir() (string, error) { diff --git a/integration-cli/docker_api_swarm_service_test.go b/integration-cli/docker_api_swarm_service_test.go index 68b78b6453..a9563f2de1 100644 --- a/integration-cli/docker_api_swarm_service_test.go +++ b/integration-cli/docker_api_swarm_service_test.go @@ -69,9 +69,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesCreate(c *check.C) { c.Assert(err, checker.IsNil) defer cli.Close() - options := types.ServiceInspectOptions{ - InsertDefaults: true, - } + options := types.ServiceInspectOptions{InsertDefaults: true} // insertDefaults inserts UpdateConfig when service is fetched by ID resp, _, err := cli.ServiceInspectWithRaw(context.Background(), id, options) diff --git a/integration-cli/docker_cli_authz_plugin_v2_test.go b/integration-cli/docker_cli_authz_plugin_v2_test.go index 0143f1c0fb..b8bbcc75e1 100644 --- a/integration-cli/docker_cli_authz_plugin_v2_test.go +++ b/integration-cli/docker_cli_authz_plugin_v2_test.go @@ -53,7 +53,7 @@ func (s *DockerAuthzV2Suite) TestAuthZPluginAllowNonVolumeRequest(c *check.C) { // start the daemon with the plugin and load busybox, --net=none build fails otherwise // because it needs to pull busybox s.d.Restart(c, "--authorization-plugin="+authzPluginNameWithTag) - c.Assert(s.d.LoadBusybox(), check.IsNil) + s.d.LoadBusybox(c) // defer disabling the plugin defer func() { @@ -83,7 +83,7 @@ func (s *DockerAuthzV2Suite) TestAuthZPluginDisable(c *check.C) { // start the daemon with the plugin and load busybox, --net=none build fails otherwise // because it needs to pull busybox s.d.Restart(c, "--authorization-plugin="+authzPluginNameWithTag) - c.Assert(s.d.LoadBusybox(), check.IsNil) + s.d.LoadBusybox(c) // defer removing the plugin defer func() { diff --git a/integration-cli/docker_cli_authz_unix_test.go b/integration-cli/docker_cli_authz_unix_test.go index 959292f053..f46ab806ca 100644 --- a/integration-cli/docker_cli_authz_unix_test.go +++ b/integration-cli/docker_cli_authz_unix_test.go @@ -209,7 +209,7 @@ func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) { s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) s.ctrl.reqRes.Allow = true s.ctrl.resRes.Allow = true - c.Assert(s.d.LoadBusybox(), check.IsNil) + s.d.LoadBusybox(c) // Ensure command successful out, err := s.d.Cmd("run", "-d", "busybox", "top") @@ -322,7 +322,7 @@ func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) { s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) s.ctrl.reqRes.Allow = true s.ctrl.resRes.Allow = true - c.Assert(s.d.LoadBusybox(), check.IsNil) + s.d.LoadBusybox(c) startTime := strconv.FormatInt(daemonTime(c).Unix(), 10) // Add another command to to enable event pipelining @@ -418,7 +418,7 @@ func (s *DockerAuthzSuite) TestAuthZPluginEnsureLoadImportWorking(c *check.C) { s.d.Start(c, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) s.ctrl.reqRes.Allow = true s.ctrl.resRes.Allow = true - c.Assert(s.d.LoadBusybox(), check.IsNil) + s.d.LoadBusybox(c) tmp, err := ioutil.TempDir("", "test-authz-load-import") c.Assert(err, check.IsNil) @@ -445,7 +445,7 @@ func (s *DockerAuthzSuite) TestAuthZPluginHeader(c *check.C) { s.d.Start(c, "--debug", "--authorization-plugin="+testAuthZPlugin) s.ctrl.reqRes.Allow = true s.ctrl.resRes.Allow = true - c.Assert(s.d.LoadBusybox(), check.IsNil) + s.d.LoadBusybox(c) daemonURL, err := url.Parse(s.d.Sock()) diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index e7dd5bfd52..5ecb010b29 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -1534,22 +1534,6 @@ func (s *DockerSwarmSuite) TestSwarmManagerAddress(c *check.C) { c.Assert(out, checker.Contains, expectedOutput) } -func (s *DockerSwarmSuite) TestSwarmServiceInspectPretty(c *check.C) { - d := s.AddDaemon(c, true, true) - - name := "top" - out, err := d.Cmd("service", "create", "--no-resolve-image", "--name", name, "--limit-cpu=0.5", "busybox", "top") - c.Assert(err, checker.IsNil, check.Commentf(out)) - - expectedOutput := ` -Resources: - Limits: - CPU: 0.5` - out, err = d.Cmd("service", "inspect", "--pretty", name) - c.Assert(err, checker.IsNil, check.Commentf(out)) - c.Assert(out, checker.Contains, expectedOutput, check.Commentf(out)) -} - func (s *DockerSwarmSuite) TestSwarmNetworkIPAMOptions(c *check.C) { d := s.AddDaemon(c, true, true) @@ -1691,76 +1675,6 @@ func (s *DockerSwarmSuite) TestSwarmNetworkCreateDup(c *check.C) { } } -func (s *DockerSwarmSuite) TestSwarmServicePsMultipleServiceIDs(c *check.C) { - d := s.AddDaemon(c, true, true) - - name1 := "top1" - out, err := d.Cmd("service", "create", "--no-resolve-image", "--detach=true", "--name", name1, "--replicas=3", "busybox", "top") - c.Assert(err, checker.IsNil) - c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") - id1 := strings.TrimSpace(out) - - name2 := "top2" - out, err = d.Cmd("service", "create", "--no-resolve-image", "--detach=true", "--name", name2, "--replicas=3", "busybox", "top") - c.Assert(err, checker.IsNil) - c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") - id2 := strings.TrimSpace(out) - - // make sure task has been deployed. - waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 6) - - out, err = d.Cmd("service", "ps", name1) - c.Assert(err, checker.IsNil) - c.Assert(out, checker.Contains, name1+".1") - c.Assert(out, checker.Contains, name1+".2") - c.Assert(out, checker.Contains, name1+".3") - c.Assert(out, checker.Not(checker.Contains), name2+".1") - c.Assert(out, checker.Not(checker.Contains), name2+".2") - c.Assert(out, checker.Not(checker.Contains), name2+".3") - - out, err = d.Cmd("service", "ps", name1, name2) - c.Assert(err, checker.IsNil) - c.Assert(out, checker.Contains, name1+".1") - c.Assert(out, checker.Contains, name1+".2") - c.Assert(out, checker.Contains, name1+".3") - c.Assert(out, checker.Contains, name2+".1") - c.Assert(out, checker.Contains, name2+".2") - c.Assert(out, checker.Contains, name2+".3") - - // Name Prefix - out, err = d.Cmd("service", "ps", "to") - c.Assert(err, checker.IsNil) - c.Assert(out, checker.Contains, name1+".1") - c.Assert(out, checker.Contains, name1+".2") - c.Assert(out, checker.Contains, name1+".3") - c.Assert(out, checker.Contains, name2+".1") - c.Assert(out, checker.Contains, name2+".2") - c.Assert(out, checker.Contains, name2+".3") - - // Name Prefix (no hit) - out, err = d.Cmd("service", "ps", "noname") - c.Assert(err, checker.NotNil) - c.Assert(out, checker.Contains, "no such services: noname") - - out, err = d.Cmd("service", "ps", id1) - c.Assert(err, checker.IsNil) - c.Assert(out, checker.Contains, name1+".1") - c.Assert(out, checker.Contains, name1+".2") - c.Assert(out, checker.Contains, name1+".3") - c.Assert(out, checker.Not(checker.Contains), name2+".1") - c.Assert(out, checker.Not(checker.Contains), name2+".2") - c.Assert(out, checker.Not(checker.Contains), name2+".3") - - out, err = d.Cmd("service", "ps", id1, id2) - c.Assert(err, checker.IsNil) - c.Assert(out, checker.Contains, name1+".1") - c.Assert(out, checker.Contains, name1+".2") - c.Assert(out, checker.Contains, name1+".3") - c.Assert(out, checker.Contains, name2+".1") - c.Assert(out, checker.Contains, name2+".2") - c.Assert(out, checker.Contains, name2+".3") -} - func (s *DockerSwarmSuite) TestSwarmPublishDuplicatePorts(c *check.C) { d := s.AddDaemon(c, true, true) diff --git a/integration/service/inspect_test.go b/integration/service/inspect_test.go new file mode 100644 index 0000000000..601e16cd8f --- /dev/null +++ b/integration/service/inspect_test.go @@ -0,0 +1,145 @@ +package service + +import ( + "fmt" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "github.com/docker/docker/integration-cli/daemon" + "github.com/docker/docker/integration-cli/request" + "github.com/gotestyourself/gotestyourself/poll" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" +) + +func TestInspect(t *testing.T) { + d := newSwarm(t) + defer d.Stop(t) + client, err := request.NewClientForHost(d.Sock()) + require.NoError(t, err) + + var before = time.Now() + var instances uint64 = 2 + serviceSpec := fullSwarmServiceSpec("test-service-inspect", instances) + + ctx := context.Background() + resp, err := client.ServiceCreate(ctx, serviceSpec, types.ServiceCreateOptions{ + QueryRegistry: false, + }) + require.NoError(t, err) + + id := resp.ID + poll.WaitOn(t, serviceContainerCount(client, id, instances)) + + service, _, err := client.ServiceInspectWithRaw(ctx, id, types.ServiceInspectOptions{}) + require.NoError(t, err) + assert.Equal(t, serviceSpec, service.Spec) + assert.Equal(t, uint64(11), service.Meta.Version.Index) + assert.Equal(t, id, service.ID) + assert.WithinDuration(t, before, service.CreatedAt, 30*time.Second) + assert.WithinDuration(t, before, service.UpdatedAt, 30*time.Second) +} + +func fullSwarmServiceSpec(name string, replicas uint64) swarm.ServiceSpec { + restartDelay := 100 * time.Millisecond + maxAttempts := uint64(4) + + return swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: map[string]string{ + "service-label": "service-label-value", + }, + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: &swarm.ContainerSpec{ + Image: "busybox:latest", + Labels: map[string]string{"container-label": "container-value"}, + Command: []string{"/bin/top"}, + Args: []string{"-u", "root"}, + Hostname: "hostname", + Env: []string{"envvar=envvalue"}, + Dir: "/work", + User: "root", + StopSignal: "SIGINT", + StopGracePeriod: &restartDelay, + Hosts: []string{"8.8.8.8 google"}, + DNSConfig: &swarm.DNSConfig{ + Nameservers: []string{"8.8.8.8"}, + Search: []string{"somedomain"}, + }, + }, + RestartPolicy: &swarm.RestartPolicy{ + Delay: &restartDelay, + Condition: swarm.RestartPolicyConditionOnFailure, + MaxAttempts: &maxAttempts, + }, + Runtime: swarm.RuntimeContainer, + }, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{ + Replicas: &replicas, + }, + }, + UpdateConfig: &swarm.UpdateConfig{ + Parallelism: 2, + Delay: 200 * time.Second, + FailureAction: swarm.UpdateFailureActionContinue, + Monitor: 2 * time.Second, + MaxFailureRatio: 0.2, + Order: swarm.UpdateOrderStopFirst, + }, + RollbackConfig: &swarm.UpdateConfig{ + Parallelism: 3, + Delay: 300 * time.Second, + FailureAction: swarm.UpdateFailureActionPause, + Monitor: 3 * time.Second, + MaxFailureRatio: 0.3, + Order: swarm.UpdateOrderStartFirst, + }, + } +} + +const defaultSwarmPort = 2477 + +func newSwarm(t *testing.T) *daemon.Swarm { + d := &daemon.Swarm{ + Daemon: daemon.New(t, "", dockerdBinary, daemon.Config{ + Experimental: testEnv.ExperimentalDaemon(), + }), + // TODO: better method of finding an unused port + Port: defaultSwarmPort, + } + // TODO: move to a NewSwarm constructor + d.ListenAddr = fmt.Sprintf("0.0.0.0:%d", d.Port) + + // avoid networking conflicts + args := []string{"--iptables=false", "--swarm-default-advertise-addr=lo"} + d.StartWithBusybox(t, args...) + + require.NoError(t, d.Init(swarm.InitRequest{})) + return d +} + +func serviceContainerCount(client client.ServiceAPIClient, id string, count uint64) func(log poll.LogT) poll.Result { + return func(log poll.LogT) poll.Result { + filter := filters.NewArgs() + filter.Add("service", id) + tasks, err := client.TaskList(context.Background(), types.TaskListOptions{ + Filters: filter, + }) + switch { + case err != nil: + return poll.Error(err) + case len(tasks) == int(count): + return poll.Success() + default: + return poll.Continue("task count at %d waiting for %d", len(tasks), count) + } + } +} diff --git a/integration/service/main_test.go b/integration/service/main_test.go new file mode 100644 index 0000000000..fac7427819 --- /dev/null +++ b/integration/service/main_test.go @@ -0,0 +1,37 @@ +package service + +import ( + "fmt" + "os" + "testing" + + "github.com/docker/docker/integration-cli/environment" +) + +var testEnv *environment.Execution + +const dockerdBinary = "dockerd" + +func TestMain(m *testing.M) { + var err error + testEnv, err = environment.New() + if err != nil { + fmt.Println(err) + 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) +} + +func setupTest(t *testing.T) func() { + environment.ProtectImages(t, testEnv) + return func() { testEnv.Clean(t, testEnv.DockerBinary()) } +} diff --git a/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go b/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go new file mode 100644 index 0000000000..3838d38e13 --- /dev/null +++ b/vendor/github.com/gotestyourself/gotestyourself/poll/poll.go @@ -0,0 +1,133 @@ +/*Package poll provides tools for testing asynchronous code. + */ +package poll + +import ( + "fmt" + "time" +) + +// TestingT is the subset of testing.T used by WaitOn +type TestingT interface { + LogT + Fatalf(format string, args ...interface{}) +} + +// LogT is a logging interface that is passed to the WaitOn check function +type LogT interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) +} + +// Settings are used to configure the behaviour of WaitOn +type Settings struct { + // Timeout is the maximum time to wait for the condition. Defaults to 10s + Timeout time.Duration + // Delay is the time to sleep between checking the condition. Detaults to + // 1ms + Delay time.Duration +} + +func defaultConfig() *Settings { + return &Settings{Timeout: 10 * time.Second, Delay: time.Millisecond} +} + +// SettingOp is a function which accepts and modifies Settings +type SettingOp func(config *Settings) + +// WithDelay sets the delay to wait between polls +func WithDelay(delay time.Duration) SettingOp { + return func(config *Settings) { + config.Delay = delay + } +} + +// WithTimeout sets the timeout +func WithTimeout(timeout time.Duration) SettingOp { + return func(config *Settings) { + config.Timeout = timeout + } +} + +// Result of a check performed by WaitOn +type Result interface { + // Error indicates that the check failed and polling should stop, and the + // the has failed + Error() error + // Done indicates that polling should stop, and the test should proceed + Done() bool + // Message provides the most recent state when polling has not completed + Message() string +} + +type result struct { + done bool + message string + err error +} + +func (r result) Done() bool { + return r.done +} + +func (r result) Message() string { + return r.message +} + +func (r result) Error() error { + return r.err +} + +// Continue returns a Result that indicates to WaitOn that it should continue +// polling. The message text will be used as the failure message if the timeout +// is reached. +func Continue(message string, args ...interface{}) Result { + return result{message: fmt.Sprintf(message, args...)} +} + +// Success returns a Result where Done() returns true, which indicates to WaitOn +// that it should stop polling and exit without an error. +func Success() Result { + return result{done: true} +} + +// Error returns a Result that indicates to WaitOn that it should fail the test +// and stop polling. +func Error(err error) Result { + return result{err: err} +} + +// WaitOn a condition or until a timeout. Poll by calling check and exit when +// check returns a done Result. To fail a test and exit polling with an error +// return a error result. +func WaitOn(t TestingT, check func(t LogT) Result, pollOps ...SettingOp) { + config := defaultConfig() + for _, pollOp := range pollOps { + pollOp(config) + } + + var lastMessage string + after := time.After(config.Timeout) + chResult := make(chan Result) + for { + go func() { + chResult <- check(t) + }() + select { + case <-after: + if lastMessage == "" { + lastMessage = "first check never completed" + } + t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage) + case result := <-chResult: + switch { + case result.Error() != nil: + t.Fatalf("polling check failed: %s", result.Error()) + case result.Done(): + return + } + time.Sleep(config.Delay) + lastMessage = result.Message() + } + } +}