diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..9c91b799 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +core.autocrlf false +*.golden text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10697135..889000a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Run golangci-lint run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b . v1.27.0 - ./golangci-lint run + ./golangci-lint run --timeout 10m0s build: name: Build @@ -62,3 +62,34 @@ jobs: - name: E2E Test run: make e2e-local + + windows-build: + name: Windows Build + runs-on: windows-latest + env: + GO111MODULE: "on" + steps: + - name: Set up Go 1.14 + uses: actions/setup-go@v1 + with: + go-version: 1.14 + id: go + + - name: Checkout code into the Go module directory + uses: actions/checkout@v2 + + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Test + run: make -f builder.Makefile test + + - name: Build + run: make -f builder.Makefile cli + + - name: E2E Test + run: make e2e-win-ci diff --git a/Makefile b/Makefile index aec71963..78bf00fb 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,10 @@ cli: ## Compile the cli --output ./bin e2e-local: ## Run End to end local tests - go test -v ./tests/e2e ./moby/e2e + go test -v ./tests/e2e ./tests/skip-win-ci-e2e ./moby/e2e + +e2e-win-ci: ## Run End to end local tests on windows CI, no docker for linux containers available ATM + go test -v ./tests/e2e e2e-aci: ## Run End to end ACI tests (requires azure login) go test -v ./tests/aci-e2e diff --git a/azure/convert/volume.go b/azure/convert/volume.go index 0acafdf6..2f05dfd6 100644 --- a/azure/convert/volume.go +++ b/azure/convert/volume.go @@ -3,7 +3,6 @@ package convert import ( "fmt" "net/url" - "path/filepath" "strings" "github.com/pkg/errors" @@ -113,7 +112,8 @@ func (v *volumeInput) parse(name string, s string) error { v.name = name v.target = volumeURL.Path if v.target == "" { - v.target = filepath.Join("/run/volumes/", v.share) + // Do not use filepath.Join, on Windows it will replace / by \ + v.target = "/run/volumes/" + v.share } return nil } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 2d250c58..bd39c83e 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -28,13 +28,8 @@ package main import ( - "fmt" - "io/ioutil" - "log" "os" - "os/exec" "path/filepath" - "strings" "testing" "time" @@ -65,7 +60,7 @@ func (s *E2eSuite) TestContextDefault() { output := s.NewDockerCommand("context", "show").ExecOrDie() Expect(output).To(ContainSubstring("default")) output = s.NewCommand("docker", "context", "ls").ExecOrDie() - golden.Assert(s.T(), output, "ls-out-default.golden") + golden.Assert(s.T(), output, GoldenFile("ls-out-default")) }) } @@ -106,7 +101,7 @@ func (s *E2eSuite) TestSetupError() { It("should display an error if cannot shell out to docker-classic", func() { err := os.Setenv("PATH", s.BinDir) Expect(err).To(BeNil()) - err = os.Remove(filepath.Join(s.BinDir, "docker-classic")) + err = os.Remove(filepath.Join(s.BinDir, DockerClassicExecutable())) Expect(err).To(BeNil()) output, err := s.NewDockerCommand("ps").Exec() Expect(output).To(ContainSubstring("docker-classic")) @@ -115,37 +110,6 @@ func (s *E2eSuite) TestSetupError() { }) } -func (s *E2eSuite) TestKillChildOnCancel() { - It("should kill docker-classic if parent command is cancelled", func() { - out := s.ListProcessesCommand().ExecOrDie() - Expect(out).NotTo(ContainSubstring("docker-classic")) - - dir := s.ConfigDir - Expect(ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(`FROM alpine:3.10 -RUN sleep 100`), 0644)).To(Succeed()) - shutdown := make(chan time.Time) - errs := make(chan error) - ctx := s.NewDockerCommand("build", "--no-cache", "-t", "test-sleep-image", ".").WithinDirectory(dir).WithTimeout(shutdown) - go func() { - _, err := ctx.Exec() - errs <- err - }() - err := WaitFor(time.Second, 10*time.Second, errs, func() bool { - out := s.ListProcessesCommand().ExecOrDie() - return strings.Contains(out, "docker-classic") - }) - Expect(err).NotTo(HaveOccurred()) - log.Println("Killing docker process") - - close(shutdown) - err = WaitFor(time.Second, 12*time.Second, nil, func() bool { - out := s.ListProcessesCommand().ExecOrDie() - return !strings.Contains(out, "docker-classic") - }) - Expect(err).NotTo(HaveOccurred()) - }) -} - func (s *E2eSuite) TestLegacy() { It("should list all legacy commands", func() { output := s.NewDockerCommand("--help").ExecOrDie() @@ -159,7 +123,7 @@ func (s *E2eSuite) TestLegacy() { It("should run local container in less than 10 secs", func() { s.NewDockerCommand("pull", "hello-world").ExecOrDie() - output := s.NewDockerCommand("run", "--rm", "hello-world").WithTimeout(time.NewTimer(10 * time.Second).C).ExecOrDie() + output := s.NewDockerCommand("run", "--rm", "hello-world").WithTimeout(time.NewTimer(20 * time.Second).C).ExecOrDie() Expect(output).To(ContainSubstring("Hello from Docker!")) }) } @@ -187,7 +151,7 @@ func (s *E2eSuite) TestMockBackend() { currentContext := s.NewDockerCommand("context", "use", "test-example").ExecOrDie() Expect(currentContext).To(ContainSubstring("test-example")) output := s.NewDockerCommand("context", "ls").ExecOrDie() - golden.Assert(s.T(), output, "ls-out-test-example.golden") + golden.Assert(s.T(), output, GoldenFile("ls-out-test-example")) output = s.NewDockerCommand("context", "show").ExecOrDie() Expect(output).To(ContainSubstring("test-example")) }) @@ -222,40 +186,6 @@ func (s *E2eSuite) TestMockBackend() { }) } -func (s *E2eSuite) TestAPIServer() { - _, err := exec.LookPath("yarn") - if err != nil || os.Getenv("SKIP_NODE") != "" { - s.T().Skip("skipping, yarn not installed") - } - It("can run 'serve' command", func() { - cName := "test-example" - s.NewDockerCommand("context", "create", cName, "example").ExecOrDie() - - sPath := fmt.Sprintf("unix:///%s/docker.sock", s.ConfigDir) - server, err := serveAPI(s.ConfigDir, sPath) - Expect(err).To(BeNil()) - defer killProcess(server) - - s.NewCommand("yarn", "install").WithinDirectory("../node-client").ExecOrDie() - output := s.NewCommand("yarn", "run", "start", cName, sPath).WithinDirectory("../node-client").ExecOrDie() - Expect(output).To(ContainSubstring("nginx")) - }) -} - func TestE2e(t *testing.T) { suite.Run(t, new(E2eSuite)) } - -func killProcess(process *os.Process) { - err := process.Kill() - Expect(err).To(BeNil()) -} - -func serveAPI(configDir string, address string) (*os.Process, error) { - cmd := exec.Command("../../bin/docker", "--config", configDir, "serve", "--address", address) - err := cmd.Start() - if err != nil { - return nil, err - } - return cmd.Process, nil -} diff --git a/tests/e2e/testdata/ls-out-default-windows.golden b/tests/e2e/testdata/ls-out-default-windows.golden new file mode 100644 index 00000000..d1fad0ef --- /dev/null +++ b/tests/e2e/testdata/ls-out-default-windows.golden @@ -0,0 +1,2 @@ +NAME TYPE DESCRIPTION DOCKER ENPOINT KUBERNETES ENDPOINT ORCHESTRATOR +default * docker Current DOCKER_HOST based configuration npipe:////./pipe/docker_engine swarm diff --git a/tests/e2e/testdata/ls-out-test-example-windows.golden b/tests/e2e/testdata/ls-out-test-example-windows.golden new file mode 100644 index 00000000..b83ef4c1 --- /dev/null +++ b/tests/e2e/testdata/ls-out-test-example-windows.golden @@ -0,0 +1,3 @@ +NAME TYPE DESCRIPTION DOCKER ENPOINT KUBERNETES ENDPOINT ORCHESTRATOR +default docker Current DOCKER_HOST based configuration npipe:////./pipe/docker_engine swarm +test-example * example diff --git a/tests/framework/helper.go b/tests/framework/helper.go index 2becf271..d3353f05 100644 --- a/tests/framework/helper.go +++ b/tests/framework/helper.go @@ -28,6 +28,7 @@ package framework import ( + "runtime" "strings" "github.com/robpike/filter" @@ -48,6 +49,19 @@ func Columns(line string) []string { return filter.Choose(strings.Split(line, " "), nonEmptyString).([]string) } +// GoldenFile golden file specific to platform +func GoldenFile(name string) string { + if IsWindows() { + return name + "-windows.golden" + } + return name + ".golden" +} + +// IsWindows windows or other GOOS +func IsWindows() bool { + return runtime.GOOS == "windows" +} + // It runs func func It(description string, test func()) { test() diff --git a/tests/framework/suite.go b/tests/framework/suite.go index b27e9a55..91b7e3d2 100644 --- a/tests/framework/suite.go +++ b/tests/framework/suite.go @@ -33,7 +33,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "time" "github.com/onsi/gomega" @@ -56,13 +55,14 @@ func (s *Suite) SetupSuite() { log.Error(message) cp := filepath.Join(s.ConfigDir, "config.json") d, _ := ioutil.ReadFile(cp) + fmt.Printf("Bin dir:%s\n", s.BinDir) fmt.Printf("Contents of %s:\n%s\n\nContents of config dir:\n", cp, string(d)) for _, p := range dirContents(s.ConfigDir) { fmt.Println(p) } s.T().Fail() }) - s.linkClassicDocker() + s.copyExecutablesInBinDir() } // TearDownSuite is run after all tests @@ -79,22 +79,42 @@ func dirContents(dir string) []string { return res } -func (s *Suite) linkClassicDocker() { - p, err := exec.LookPath("docker-classic") +func (s *Suite) copyExecutablesInBinDir() { + p, err := exec.LookPath(DockerClassicExecutable()) if err != nil { - p, err = exec.LookPath("docker") + p, err = exec.LookPath(dockerExecutable()) } gomega.Expect(err).To(gomega.BeNil()) - err = os.Symlink(p, filepath.Join(s.BinDir, "docker-classic")) + err = copyFile(p, filepath.Join(s.BinDir, DockerClassicExecutable())) gomega.Expect(err).To(gomega.BeNil()) - dockerPath, err := filepath.Abs("../../bin/docker") + dockerPath, err := filepath.Abs("../../bin/" + dockerExecutable()) gomega.Expect(err).To(gomega.BeNil()) - err = os.Symlink(dockerPath, filepath.Join(s.BinDir, "docker")) + err = copyFile(dockerPath, filepath.Join(s.BinDir, dockerExecutable())) gomega.Expect(err).To(gomega.BeNil()) - err = os.Setenv("PATH", fmt.Sprintf("%s:%s", s.BinDir, os.Getenv("PATH"))) + err = os.Setenv("PATH", concatenatePath(s.BinDir)) gomega.Expect(err).To(gomega.BeNil()) } +func concatenatePath(path string) string { + if IsWindows() { + return fmt.Sprintf("%s;%s", path, os.Getenv("PATH")) + } + return fmt.Sprintf("%s:%s", path, os.Getenv("PATH")) +} + +func copyFile(sourceFile string, destinationFile string) error { + input, err := ioutil.ReadFile(sourceFile) + if err != nil { + return err + } + + err = ioutil.WriteFile(destinationFile, input, 0777) + if err != nil { + return err + } + return nil +} + // BeforeTest is run before each test func (s *Suite) BeforeTest(suite, test string) { d, _ := ioutil.TempDir("", "") @@ -109,7 +129,7 @@ func (s *Suite) AfterTest(suite, test string) { // ListProcessesCommand creates a command to list processes, "tasklist" on windows, "ps" otherwise. func (s *Suite) ListProcessesCommand() *CmdContext { - if runtime.GOOS == "windows" { + if IsWindows() { return s.NewCommand("tasklist") } return s.NewCommand("ps") @@ -125,12 +145,20 @@ func (s *Suite) NewCommand(command string, args ...string) *CmdContext { } func dockerExecutable() string { - if runtime.GOOS == "windows" { + if IsWindows() { return "docker.exe" } return "docker" } +// DockerClassicExecutable binary name based on platform +func DockerClassicExecutable() string { + if IsWindows() { + return "docker-classic.exe" + } + return "docker-classic" +} + // NewDockerCommand creates a docker builder. func (s *Suite) NewDockerCommand(args ...string) *CmdContext { return s.NewCommand(dockerExecutable(), args...) diff --git a/tests/skip-win-ci-e2e/skip_win_ci_test.go b/tests/skip-win-ci-e2e/skip_win_ci_test.go new file mode 100644 index 00000000..cf055464 --- /dev/null +++ b/tests/skip-win-ci-e2e/skip_win_ci_test.go @@ -0,0 +1,127 @@ +/* + Copyright (c) 2020 Docker Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH + THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" + + . "github.com/docker/api/tests/framework" +) + +type NonWinCIE2eSuite struct { + Suite +} + +func (s *NonWinCIE2eSuite) TestKillChildOnCancel() { + It("should kill docker-classic if parent command is cancelled", func() { + out := s.ListProcessesCommand().ExecOrDie() + Expect(out).NotTo(ContainSubstring("docker-classic")) + + dir := s.ConfigDir + Expect(ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(`FROM alpine:3.10 +RUN sleep 100`), 0644)).To(Succeed()) + shutdown := make(chan time.Time) + errs := make(chan error) + ctx := s.NewDockerCommand("build", "--no-cache", "-t", "test-sleep-image", ".").WithinDirectory(dir).WithTimeout(shutdown) + go func() { + _, err := ctx.Exec() + errs <- err + }() + err := WaitFor(time.Second, 10*time.Second, errs, func() bool { + out := s.ListProcessesCommand().ExecOrDie() + return strings.Contains(out, "docker-classic") + }) + Expect(err).NotTo(HaveOccurred()) + log.Println("Killing docker process") + + close(shutdown) + err = WaitFor(time.Second, 12*time.Second, nil, func() bool { + out := s.ListProcessesCommand().ExecOrDie() + return !strings.Contains(out, "docker-classic") + }) + Expect(err).NotTo(HaveOccurred()) + }) +} + +func (s *NonWinCIE2eSuite) TestAPIServer() { + _, err := exec.LookPath("yarn") + if err != nil || os.Getenv("SKIP_NODE") != "" { + s.T().Skip("skipping, yarn not installed") + } + It("can run 'serve' command", func() { + cName := "test-example" + s.NewDockerCommand("context", "create", cName, "example").ExecOrDie() + + //sPath := fmt.Sprintf("unix:///%s/docker.sock", s.ConfigDir) + sPath, cliAddress := s.getGrpcServerAndCLientAddress() + server, err := serveAPI(s.ConfigDir, sPath) + Expect(err).To(BeNil()) + defer killProcess(server) + + s.NewCommand("yarn", "install").WithinDirectory("../node-client").ExecOrDie() + output := s.NewCommand("yarn", "run", "start", cName, cliAddress).WithinDirectory("../node-client").ExecOrDie() + Expect(output).To(ContainSubstring("nginx")) + }) +} + +func (s *NonWinCIE2eSuite) getGrpcServerAndCLientAddress() (string, string) { + if IsWindows() { + return "npipe:////./pipe/clibackend", "unix:////./pipe/clibackend" + } + socketName := fmt.Sprintf("unix:///%s/docker.sock", s.ConfigDir) + return socketName, socketName +} + +func TestE2e(t *testing.T) { + suite.Run(t, new(NonWinCIE2eSuite)) +} + +func killProcess(process *os.Process) { + err := process.Kill() + Expect(err).To(BeNil()) +} + +func serveAPI(configDir string, address string) (*os.Process, error) { + cmd := exec.Command("../../bin/docker", "--config", configDir, "serve", "--address", address) + err := cmd.Start() + if err != nil { + return nil, err + } + return cmd.Process, nil +}