From 054b921a2c140f9b8250f6c7e12dec9fba23ce0f Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Wed, 23 Jul 2014 14:12:42 -0400 Subject: [PATCH] Add Daemon test utils Docker-DCO-1.1-Signed-off-by: Tibor Vass (github: tiborvass) --- hack/make.sh | 1 + hack/make/test-integration-cli | 8 +- integration-cli/docker_utils.go | 209 ++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 3 deletions(-) diff --git a/hack/make.sh b/hack/make.sh index 05e3310889..13307c4f4d 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -156,6 +156,7 @@ go_test_dir() { testcover=( -cover -coverprofile "$coverprofile" $coverpkg ) fi ( + export DEST echo '+ go test' $TESTFLAGS "${DOCKER_PKG}${dir#.}" cd "$dir" go test ${testcover[@]} -ldflags "$LDFLAGS" "${BUILDFLAGS[@]}" $TESTFLAGS diff --git a/hack/make/test-integration-cli b/hack/make/test-integration-cli index 837bd8737a..e371fac071 100644 --- a/hack/make/test-integration-cli +++ b/hack/make/test-integration-cli @@ -38,7 +38,9 @@ exec > >(tee -a $DEST/test.log) 2>&1 bundle_test_integration_cli - DOCKERD_PID=$(set -x; cat $DEST/docker.pid) - ( set -x; kill $DOCKERD_PID ) - wait $DOCKERD_PID || true + for pid in $(find "$DEST" -name docker.pid); do + DOCKER_PID=$(set -x; cat "$pid") + ( set -x; kill $DOCKER_PID ) + wait $DOCKERD_PID || true + done ) diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index cf1dd203f8..6133f3727b 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -1,11 +1,14 @@ package main import ( + "errors" "fmt" "io" "io/ioutil" + "net" "net/http" "net/http/httptest" + "net/http/httputil" "os" "os/exec" "path" @@ -13,8 +16,214 @@ import ( "strconv" "strings" "testing" + "time" ) +// Daemon represents a Docker daemon for the testing framework. +type Daemon struct { + t *testing.T + logFile *os.File + folder string + stdin io.WriteCloser + stdout, stderr io.ReadCloser + cmd *exec.Cmd + storageDriver string + execDriver string + wait chan error +} + +// NewDaemon returns a Daemon instance to be used for testing. +// This will create a directory such as daemon123456789 in the folder specified by $DEST. +// The daemon will not automatically start. +func NewDaemon(t *testing.T) *Daemon { + dest := os.Getenv("DEST") + if dest == "" { + t.Fatal("Please set the DEST environment variable") + } + + dir := filepath.Join(dest, fmt.Sprintf("daemon%d", time.Now().Unix())) + daemonFolder, err := filepath.Abs(dir) + if err != nil { + t.Fatal("Could not make '%s' an absolute path: %v", dir, err) + } + + if err := os.MkdirAll(filepath.Join(daemonFolder, "graph"), 0600); err != nil { + t.Fatal("Could not create %s/graph directory", daemonFolder) + } + + return &Daemon{ + t: t, + folder: daemonFolder, + storageDriver: os.Getenv("DOCKER_GRAPHDRIVER"), + execDriver: os.Getenv("DOCKER_EXECDRIVER"), + } +} + +// Start will start the daemon and return once it is ready to receive requests. +// You can specify additional daemon flags (e.g. "--restart=false"). +func (d *Daemon) Start(arg ...string) error { + dockerBinary, err := exec.LookPath(dockerBinary) + if err != nil { + d.t.Fatalf("could not find docker binary in $PATH: %v", err) + } + + args := []string{ + "--host", d.sock(), + "--daemon", "--debug", + "--graph", fmt.Sprintf("%s/graph", d.folder), + "--storage-driver", d.storageDriver, + "--exec-driver", d.execDriver, + "--pidfile", fmt.Sprintf("%s/docker.pid", d.folder), + } + args = append(args, arg...) + d.cmd = exec.Command(dockerBinary, args...) + + d.logFile, err = os.OpenFile(filepath.Join(d.folder, "docker.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + d.t.Fatalf("Could not create %s/docker.log: %v", d.folder, err) + } + + d.cmd.Stdout = d.logFile + d.cmd.Stderr = d.logFile + + if err := d.cmd.Start(); err != nil { + return fmt.Errorf("Could not start daemon container: %v", err) + } + + d.wait = make(chan error) + + go func() { + d.wait <- d.cmd.Wait() + d.t.Log("exiting daemon") + close(d.wait) + }() + + tick := time.Tick(500 * time.Millisecond) + // make sure daemon is ready to receive requests + for { + d.t.Log("waiting for daemon to start") + select { + case <-time.After(2 * time.Second): + return errors.New("timeout: daemon does not respond") + case <-tick: + c, err := net.Dial("unix", filepath.Join(d.folder, "docker.sock")) + if err != nil { + continue + } + + client := httputil.NewClientConn(c, nil) + defer client.Close() + + req, err := http.NewRequest("GET", "/_ping", nil) + if err != nil { + d.t.Fatalf("could not create new request: %v", err) + } + + resp, err := client.Do(req) + if err != nil { + continue + } + if resp.StatusCode != http.StatusOK { + d.t.Logf("received status != 200 OK: %s", resp.Status) + } + + d.t.Log("daemon started") + return nil + } + } +} + +// StartWithBusybox will first start the daemon with Daemon.Start() +// then save the busybox image from the main daemon and load it into this Daemon instance. +func (d *Daemon) StartWithBusybox(arg ...string) error { + if err := d.Start(arg...); err != nil { + return err + } + bb := filepath.Join(d.folder, "busybox.tar") + if _, err := os.Stat(bb); err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("unexpected error on busybox.tar stat: %v", err) + } + // saving busybox image from main daemon + if err := exec.Command(dockerBinary, "save", "--output", bb, "busybox:latest").Run(); err != nil { + return fmt.Errorf("could not save busybox image: %v", err) + } + } + // loading busybox image to this daemon + if _, err := d.Cmd("load", "--input", bb); err != nil { + return fmt.Errorf("could not load busybox image: %v", err) + } + if err := os.Remove(bb); err != nil { + d.t.Logf("Could not remove %s: %v", bb, err) + } + return nil +} + +// Stop will send a SIGINT every second and wait for the daemon to stop. +// If it timeouts, a SIGKILL is sent. +// Stop will not delete the daemon directory. If a purged daemon is needed, +// instantiate a new one with NewDaemon. +func (d *Daemon) Stop() error { + if d.cmd == nil || d.wait == nil { + return errors.New("Daemon not started") + } + + defer func() { + d.logFile.Close() + d.cmd = nil + }() + + i := 1 + tick := time.Tick(time.Second) + + if err := d.cmd.Process.Signal(os.Interrupt); err != nil { + return fmt.Errorf("Could not send signal: %v", err) + } +out: + for { + select { + case err := <-d.wait: + return err + case <-time.After(20 * time.Second): + d.t.Log("timeout") + break out + case <-tick: + d.t.Logf("Attempt #%d: daemon is still running with pid %d", i+1, d.cmd.Process.Pid) + if err := d.cmd.Process.Signal(os.Interrupt); err != nil { + return fmt.Errorf("Could not send signal: %v", err) + } + i++ + } + } + + if err := d.cmd.Process.Kill(); err != nil { + d.t.Logf("Could not kill daemon: %v", err) + return err + } + + return nil +} + +// Restart will restart the daemon by first stopping it and then starting it. +func (d *Daemon) Restart(arg ...string) error { + d.Stop() + return d.Start(arg...) +} + +func (d *Daemon) sock() string { + return fmt.Sprintf("unix://%s/docker.sock", d.folder) +} + +// Cmd will execute a docker CLI command against this Daemon. +// Example: d.Cmd("version") will run docker -H unix://path/to/unix.sock version +func (d *Daemon) Cmd(name string, arg ...string) (string, error) { + args := []string{"--host", d.sock(), name} + args = append(args, arg...) + c := exec.Command(dockerBinary, args...) + b, err := c.CombinedOutput() + return string(b), err +} + func deleteContainer(container string) error { container = strings.Replace(container, "\n", " ", -1) container = strings.Trim(container, " ")