diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index aa941894d..53eac1938 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -616,8 +616,6 @@ func testAcceptance( var untrustedBuilderName string it.Before(func() { - h.SkipIf(t, dockerHostOS() == "windows", "untrusted builders are not yet supported for windows builds") - var err error untrustedBuilderName, err = createBuilder( t, @@ -630,9 +628,6 @@ func testAcceptance( }) it.After(func() { - if dockerHostOS() == "windows" { - return - } h.DockerRmi(dockerCli, untrustedBuilderName) }) @@ -2360,6 +2355,7 @@ func assertMockAppRunsWithOutput(t *testing.T, assert h.AssertionManager, repoNa ctrID := runDockerImageExposePort(t, assert, containerName, repoName) defer dockerCli.ContainerKill(context.TODO(), containerName, "SIGKILL") defer dockerCli.ContainerRemove(context.TODO(), containerName, dockertypes.ContainerRemoveOptions{Force: true}) + logs, err := dockerCli.ContainerLogs(context.TODO(), ctrID, dockertypes.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, diff --git a/acceptance/testdata/mock_app/run b/acceptance/testdata/mock_app/run index a1653205f..93630c7ef 100755 --- a/acceptance/testdata/mock_app/run +++ b/acceptance/testdata/mock_app/run @@ -6,7 +6,7 @@ port="${1-8080}" echo "listening on port $port" -resp=$(echo "HTTP/1.1 200 OK\n" && cat "$PWD"/*-dep /contents*.txt) +resp=$(echo "HTTP/1.1 200 OK\n" && cat "$PWD"/*-deps/*-dep /contents*.txt) while true; do nc -l -p "$port" -c "echo \"$resp\"" done diff --git a/acceptance/testdata/mock_app/run.bat b/acceptance/testdata/mock_app/run.bat index 1183af17e..4fb4646cc 100644 --- a/acceptance/testdata/mock_app/run.bat +++ b/acceptance/testdata/mock_app/run.bat @@ -3,6 +3,6 @@ set port=8080 if [%1] neq [] set port=%1 -C:\util\server.exe -p %port% -g "%cd%\*-dep, c:\contents*.txt" +C:\util\server.exe -p %port% -g "%cd%\*-deps\*-dep, c:\contents*.txt" diff --git a/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build b/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build index 24cb49967..fc20479ce 100755 --- a/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build +++ b/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build @@ -15,7 +15,7 @@ if [[ -f "$platform_dir/env/ENV1_CONTENTS" ]]; then mkdir "$launch_dir/env1-launch-layer" contents=$(cat "$platform_dir/env/ENV1_CONTENTS") echo "$contents" > "$launch_dir/env1-launch-layer/env1-launch-dep" - ln -snf "$launch_dir/env1-launch-layer/env1-launch-dep" env1-launch-dep + ln -snf "$launch_dir/env1-launch-layer" env1-launch-deps echo "launch = true" > "$launch_dir/env1-launch-layer.toml" fi @@ -25,7 +25,7 @@ if [[ -f "$platform_dir/env/ENV2_CONTENTS" ]]; then mkdir "$launch_dir/env2-launch-layer" contents=$(cat "$platform_dir/env/ENV2_CONTENTS") echo "$contents" > "$launch_dir/env2-launch-layer/env2-launch-dep" - ln -snf "$launch_dir/env2-launch-layer/env2-launch-dep" env2-launch-dep + ln -snf "$launch_dir/env2-launch-layer" env2-launch-deps echo "launch = true" > "$launch_dir/env2-launch-layer.toml" fi diff --git a/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build.bat b/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build.bat index bc0b4f071..dd5e96d5c 100644 --- a/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build.bat +++ b/acceptance/testdata/mock_buildpacks/0.2/read-env-buildpack/bin/build.bat @@ -10,7 +10,7 @@ if exist %platform_dir%\env\ENV1_CONTENTS ( mkdir %launch_dir%\env1-launch-layer set /p contents=<%platform_dir%\env\ENV1_CONTENTS echo !contents!> %launch_dir%\env1-launch-layer\env1-launch-dep - mklink env1-launch-dep %launch_dir%\env1-launch-layer\env1-launch-dep + mklink /j env1-launch-deps %launch_dir%\env1-launch-layer echo launch = true> %launch_dir%\env1-launch-layer.toml ) @@ -20,7 +20,7 @@ if exist %platform_dir%\env\ENV2_CONTENTS ( mkdir %launch_dir%\env2-launch-layer set /p contents=<%platform_dir%\env\ENV2_CONTENTS echo !contents!> %launch_dir%\env2-launch-layer\env2-launch-dep - mklink env2-launch-dep %launch_dir%\env2-launch-layer\env2-launch-dep + mklink /j env2-launch-deps %launch_dir%\env2-launch-layer echo launch = true> %launch_dir%\env2-launch-layer.toml ) diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build index e2a7c1bb2..01568be0f 100755 --- a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build @@ -16,7 +16,7 @@ echo "Color: Styled" mkdir "$launch_dir/launch-layer" echo "Launch Dep Contents" > "$launch_dir/launch-layer/launch-dep" -ln -snf "$launch_dir/launch-layer/launch-dep" launch-dep +ln -snf "$launch_dir/launch-layer" launch-deps echo "launch = true" > "$launch_dir/launch-layer.toml" ## makes a cached launch layer @@ -24,12 +24,12 @@ if [[ ! -f "$launch_dir/cached-launch-layer.toml" ]]; then echo "making cached launch layer" mkdir "$launch_dir/cached-launch-layer" echo "Cached Dep Contents" > "$launch_dir/cached-launch-layer/cached-dep" - ln -snf "$launch_dir/cached-launch-layer/cached-dep" cached-dep + ln -snf "$launch_dir/cached-launch-layer" cached-deps echo "launch = true" > "$launch_dir/cached-launch-layer.toml" echo "cache = true" >> "$launch_dir/cached-launch-layer.toml" else echo "reusing cached launch layer" - ln -snf "$launch_dir/cached-launch-layer/cached-dep" cached-dep + ln -snf "$launch_dir/cached-launch-layer" cached-deps fi ## adds a process diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build.bat b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build.bat index 8da9e3ec6..2047dc714 100644 --- a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build.bat +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack/bin/build.bat @@ -4,23 +4,23 @@ echo --- Build: Simple Layers Buildpack set launch_dir=%1 :: makes a launch layer -echo making launch layer +echo making launch layer %launch_dir%\launch-layer mkdir %launch_dir%\launch-layer echo Launch Dep Contents > "%launch_dir%\launch-layer\launch-dep -mklink launch-dep %launch_dir%\launch-layer\launch-dep +mklink /j launch-deps %launch_dir%\launch-layer echo launch = true > %launch_dir%\launch-layer.toml :: makes a cached launch layer if not exist %launch_dir%\cached-launch-layer.toml ( - echo making cached launch layer + echo making cached launch layer %launch_dir%\cached-launch-layer mkdir %launch_dir%\cached-launch-layer echo Cached Dep Contents > %launch_dir%\cached-launch-layer\cached-dep - mklink cached-dep %launch_dir%\cached-launch-layer\cached-dep + mklink /j cached-deps %launch_dir%\cached-launch-layer echo launch = true > %launch_dir%\cached-launch-layer.toml echo cache = true >> %launch_dir%\cached-launch-layer.toml ) else ( - echo reusing cached launch layer - mklink cached-dep %launch_dir%\cached-launch-layer\cached-dep + echo reusing cached launch layer %launch_dir%\cached-launch-layer + mklink /j cached-deps %launch_dir%\cached-launch-layer ) :: adds a process diff --git a/acceptance/testdata/mock_stack/windows/build/Dockerfile b/acceptance/testdata/mock_stack/windows/build/Dockerfile index 0c0e063e2..37029d129 100644 --- a/acceptance/testdata/mock_stack/windows/build/Dockerfile +++ b/acceptance/testdata/mock_stack/windows/build/Dockerfile @@ -1,8 +1,8 @@ FROM mcr.microsoft.com/windows/nanoserver:1809 -# placeholder values until correct values are deteremined -ENV CNB_USER_ID=0 -ENV CNB_GROUP_ID=0 +# non-zero sets all user-owned directories to BUILTIN\Users +ENV CNB_USER_ID=1 +ENV CNB_GROUP_ID=1 USER ContainerAdministrator diff --git a/acceptance/testdata/mock_stack/windows/run/Dockerfile b/acceptance/testdata/mock_stack/windows/run/Dockerfile index 1e8bd9316..76bcfbe6b 100644 --- a/acceptance/testdata/mock_stack/windows/run/Dockerfile +++ b/acceptance/testdata/mock_stack/windows/run/Dockerfile @@ -9,9 +9,9 @@ FROM mcr.microsoft.com/windows/nanoserver:1809 COPY --from=gobuild /util/server.exe /util/server.exe -# placeholder values until correct values are deteremined -ENV CNB_USER_ID=0 -ENV CNB_GROUP_ID=0 +# non-zero sets all user-owned directories to BUILTIN\Users +ENV CNB_USER_ID=1 +ENV CNB_GROUP_ID=1 USER ContainerAdministrator diff --git a/build.go b/build.go index 81e3c4c88..cc84c76a2 100644 --- a/build.go +++ b/build.go @@ -311,10 +311,6 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { } func lifecycleImageSupported(builderOS string, lifecycleVersion *builder.Version) bool { - if builderOS == "windows" { - return false - } - return lifecycleVersion.Equal(builder.VersionMustParse(prevLifecycleVersionSupportingImage)) || !lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingImage)) } diff --git a/build_test.go b/build_test.go index 991b3565b..02c176bc6 100644 --- a/build_test.go +++ b/build_test.go @@ -1447,18 +1447,6 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { }) when("builder is untrusted", func() { - when("building Windows containers", func() { - it("errors and mentions that builder must be trusted", func() { - defaultBuilderImage.SetPlatform("windows", "", "") - h.AssertError(t, subject.Build(context.TODO(), BuildOptions{ - Image: "some/app", - Builder: defaultBuilderName, - Publish: true, - TrustBuilder: false, - }), "does not have an associated lifecycle image. Builder must be trusted.") - }) - }) - when("lifecycle image is available", func() { it("uses the 5 phases with the lifecycle image", func() { h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ diff --git a/internal/build/container_ops.go b/internal/build/container_ops.go index aa659f499..8d1b43002 100644 --- a/internal/build/container_ops.go +++ b/internal/build/container_ops.go @@ -8,7 +8,6 @@ import ( "io/ioutil" "os" "runtime" - "strings" "github.com/BurntSushi/toml" "github.com/docker/docker/api/types" @@ -16,35 +15,32 @@ import ( "github.com/docker/docker/client" "github.com/pkg/errors" + "github.com/buildpacks/pack/internal/paths" + "github.com/buildpacks/pack/internal/archive" "github.com/buildpacks/pack/internal/builder" "github.com/buildpacks/pack/internal/container" - "github.com/buildpacks/pack/internal/style" ) type ContainerOperation func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error // CopyDir copies a local directory (src) to the destination on the container while filtering files and changing it's UID/GID. -func CopyDir(src, dst string, uid, gid int, fileFilter func(string) bool) ContainerOperation { +func CopyDir(src, dst string, uid, gid int, os string, fileFilter func(string) bool) ContainerOperation { return func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error { - info, err := ctrClient.Info(ctx) - if err != nil { - return err + tarPath := dst + if os == "windows" { + tarPath = paths.WindowsToSlash(dst) } - if info.OSType == "windows" { - readerDst := strings.ReplaceAll(dst, `\`, "/")[2:] // Strip volume, convert slashes to conform to TAR format - reader, err := createReader(src, readerDst, uid, gid, fileFilter) - if err != nil { - return errors.Wrapf(err, "create tar archive from '%s'", src) - } - defer reader.Close() - return copyDirWindows(ctx, ctrClient, containerID, reader, dst, stdout, stderr) - } - reader, err := createReader(src, dst, uid, gid, fileFilter) + + reader, err := createReader(src, tarPath, uid, gid, fileFilter) if err != nil { return errors.Wrapf(err, "create tar archive from '%s'", src) } defer reader.Close() + + if os == "windows" { + return copyDirWindows(ctx, ctrClient, containerID, reader, dst, stdout, stderr) + } return copyDir(ctx, ctrClient, containerID, reader) } } @@ -76,17 +72,13 @@ func copyDir(ctx context.Context, ctrClient client.CommonAPIClient, containerID // for Windows containers and does not work. Instead, we perform the copy from inside a container // using xcopy. // See: https://github.com/moby/moby/issues/40771 -func copyDirWindows(ctx context.Context, ctrClient client.CommonAPIClient, containerID string, appReader io.Reader, dst string, stdout, stderr io.Writer) error { +func copyDirWindows(ctx context.Context, ctrClient client.CommonAPIClient, containerID string, reader io.Reader, dst string, stdout, stderr io.Writer) error { info, err := ctrClient.ContainerInspect(ctx, containerID) if err != nil { return err } - pathElements := strings.Split(dst, `\`) - if len(pathElements) < 1 { - return fmt.Errorf("cannot determine base name for destination path: %s", style.Symbol(dst)) - } - baseName := pathElements[len(pathElements)-1] + baseName := paths.WindowsBasename(dst) mnt, err := findMount(info, dst) if err != nil { @@ -97,10 +89,16 @@ func copyDirWindows(ctx context.Context, ctrClient client.CommonAPIClient, conta &dcontainer.Config{ Image: info.Image, Cmd: []string{ - "xcopy", - fmt.Sprintf(`c:\windows\%s`, baseName), - dst, - "/e", "/h", "/y", "/c", "/b", + "cmd", + "/c", + + //xcopy args + // e - recursively create subdirectories + // h - copy hidden and system files + // b - copy symlinks, do not dereference + // x - copy attributes + // y - suppress prompting + fmt.Sprintf(`xcopy c:\windows\%s %s /e /h /b /x /y`, baseName, dst), }, WorkingDir: "/", User: windowsContainerAdmin, @@ -116,7 +114,7 @@ func copyDirWindows(ctx context.Context, ctrClient client.CommonAPIClient, conta } defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true}) - err = ctrClient.CopyToContainer(ctx, ctr.ID, "/windows", appReader, types.CopyToContainerOptions{}) + err = ctrClient.CopyToContainer(ctx, ctr.ID, "/windows", reader, types.CopyToContainerOptions{}) if err != nil { return errors.Wrap(err, "copy app to container") } @@ -136,11 +134,11 @@ func findMount(info types.ContainerJSON, dst string) (types.MountPoint, error) { return m, nil } } - return types.MountPoint{}, errors.New("no matching mount found") + return types.MountPoint{}, fmt.Errorf("no matching mount found for %s", dst) } // WriteStackToml writes a `stack.toml` based on the StackMetadata provided to the destination path. -func WriteStackToml(dstPath string, stack builder.StackMetadata) ContainerOperation { +func WriteStackToml(dstPath string, stack builder.StackMetadata, os string) ContainerOperation { return func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error { buf := &bytes.Buffer{} err := toml.NewEncoder(buf).Encode(stack) @@ -149,10 +147,21 @@ func WriteStackToml(dstPath string, stack builder.StackMetadata) ContainerOperat } tarBuilder := archive.TarBuilder{} - tarBuilder.AddFile(dstPath, 0755, archive.NormalizedDateTime, buf.Bytes()) + + tarPath := dstPath + if os == "windows" { + tarPath = paths.WindowsToSlash(dstPath) + } + + tarBuilder.AddFile(tarPath, 0755, archive.NormalizedDateTime, buf.Bytes()) reader := tarBuilder.Reader(archive.DefaultTarWriterFactory()) defer reader.Close() + if os == "windows" { + dirName := paths.WindowsDir(dstPath) + return copyDirWindows(ctx, ctrClient, containerID, reader, dirName, stdout, stderr) + } + return ctrClient.CopyToContainer(ctx, containerID, "/", reader, types.CopyToContainerOptions{}) } } @@ -174,3 +183,67 @@ func createReader(src, dst string, uid, gid int, fileFilter func(string) bool) ( return archive.ReadZipAsTar(src, dst, uid, gid, -1, false, fileFilter), nil } + +//EnsureVolumeAccess grants full access permissions to volumes for UID/GID-based user +//When UID/GID are 0 it grants explicit full access to BUILTIN\Administrators and any other UID/GID grants full access to BUILTIN\Users +//Changing permissions on volumes through stopped containers does not work on Docker for Windows so we start the container and make change using icacls +//See: https://github.com/moby/moby/issues/40771 +func EnsureVolumeAccess(uid, gid int, os string, volumeNames ...string) ContainerOperation { + return func(ctrClient client.CommonAPIClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error { + if os != "windows" { + return nil + } + + containerInfo, err := ctrClient.ContainerInspect(ctx, containerID) + if err != nil { + return err + } + + cmd := "" + binds := []string{} + for i, volumeName := range volumeNames { + containerPath := fmt.Sprintf("c:/volume-mnt-%d", i) + binds = append(binds, fmt.Sprintf("%s:%s", volumeName, containerPath)) + + if cmd != "" { + cmd += "&&" + } + + //icacls args + // /grant - add new permissions instead of replacing + // (OI) - object inherit + // (CI) - container inherit + // F - full access + // /t - recursively apply + // /l - perform on a symbolic link itself versus its target + // /q - suppress success messages + cmd += fmt.Sprintf(`icacls %s /grant *%s:(OI)(CI)F /t /l /q`, containerPath, paths.WindowsPathSID(uid, gid)) + } + + ctr, err := ctrClient.ContainerCreate(ctx, + &dcontainer.Config{ + Image: containerInfo.Image, + Cmd: []string{"cmd", "/c", cmd}, + WorkingDir: "/", + User: windowsContainerAdmin, + }, + &dcontainer.HostConfig{ + Binds: binds, + Isolation: dcontainer.IsolationProcess, + }, + nil, "", + ) + if err != nil { + return err + } + defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true}) + + return container.Run( + ctx, + ctrClient, + ctr.ID, + ioutil.Discard, // Suppress icacls output + stderr, + ) + } +} diff --git a/internal/build/container_ops_test.go b/internal/build/container_ops_test.go index 61abcfe0e..b0fb38e28 100644 --- a/internal/build/container_ops_test.go +++ b/internal/build/container_ops_test.go @@ -7,9 +7,13 @@ import ( "math/rand" "path/filepath" "runtime" + "strings" "testing" "time" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/mount" + dcontainer "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/heroku/color" @@ -35,22 +39,30 @@ func TestContainerOperations(t *testing.T) { ctrClient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) - info, err := ctrClient.Info(context.TODO()) - h.AssertNil(t, err) - h.SkipIf(t, info.OSType == "windows", "These tests are not yet compatible with Windows-based containers") - spec.Run(t, "container-ops", testContainerOps, spec.Report(report.Terminal{}), spec.Sequential()) } func testContainerOps(t *testing.T, when spec.G, it spec.S) { var ( - imageName string - outBuf, errBuf bytes.Buffer + imageName string + osType string ) it.Before(func() { imageName = "container-ops.test-" + h.RandString(10) - h.CreateImage(t, ctrClient, imageName, `FROM busybox`) + + info, err := ctrClient.Info(context.TODO()) + h.AssertNil(t, err) + osType = info.OSType + + dockerfileContent := `FROM busybox` + if osType == "windows" { + dockerfileContent = `FROM mcr.microsoft.com/windows/nanoserver:1809` + } + + h.CreateImage(t, ctrClient, imageName, dockerfileContent) + + h.AssertNil(t, err) }) it.After(func() { @@ -59,72 +71,148 @@ func testContainerOps(t *testing.T, when spec.G, it spec.S) { when("#CopyDir", func() { it("writes contents with proper owner/permissions", func() { - copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), "/some-location", 123, 456, nil) - ctx := context.Background() + containerDir := "/some-vol" + if osType == "windows" { + containerDir = `c:\some-vol` + } + + ctrCmd := []string{"ls", "-al", "/some-vol"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `dir /q /s c:\some-vol`} + } - ctr, err := createContainer(ctx, imageName, "ls", "-al", "/some-location") + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + + copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), containerDir, 123, 456, osType, nil) + var outBuf, errBuf bytes.Buffer err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - perms := "-rw-r--r--" - if runtime.GOOS == "windows" { - perms = "-rwxrwxrwx" + h.AssertEq(t, errBuf.String(), "") + if osType == "windows" { + h.AssertContainsMatch(t, strings.ReplaceAll(outBuf.String(), "\r", ""), ` +(.*) ... . +(.*) ... .. +(.*) 17 ... fake-app-file +(.*) ... fake-app-symlink \[fake-app-file\] +(.*) 0 ... file-to-ignore +`) + } else { + if runtime.GOOS == "windows" { + // LCOW does not currently support symlinks + h.AssertContainsMatch(t, outBuf.String(), ` +-rwxrwxrwx 1 123 456 (.*) fake-app-file +-rwxrwxrwx 1 123 456 (.*) fake-app-symlink +-rwxrwxrwx 1 123 456 (.*) file-to-ignore +`) + } else { + h.AssertContainsMatch(t, outBuf.String(), ` +-rw-r--r-- 1 123 456 (.*) fake-app-file +lrwxrwxrwx 1 123 456 (.*) fake-app-symlink -> fake-app-file +-rw-r--r-- 1 123 456 (.*) file-to-ignore +`) + } } - - output := outBuf.String() - h.AssertContainsMatch(t, output, fmt.Sprintf(` -%s 1 123 456 (.*) fake-app-file -%s 1 123 456 (.*) file-to-ignore -`, perms, perms)) }) it("writes contents ignoring from file filter", func() { - copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), "/some-location", 123, 456, func(filename string) bool { - return filepath.Base(filename) != "file-to-ignore" - }) - ctx := context.Background() + containerDir := "/some-vol" + if osType == "windows" { + containerDir = `c:\some-vol` + } - ctr, err := createContainer(ctx, imageName, "ls", "-al", "/some-location") + ctrCmd := []string{"ls", "-al", "/some-vol"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `dir /q /s /n c:\some-vol`} + } + + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), containerDir, 123, 456, osType, func(filename string) bool { + return filepath.Base(filename) != "file-to-ignore" + }) + + var outBuf, errBuf bytes.Buffer err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - output := outBuf.String() - h.AssertNotContains(t, output, "file-to-ignore") + h.AssertEq(t, errBuf.String(), "") + h.AssertContains(t, outBuf.String(), "fake-app-file") + h.AssertNotContains(t, outBuf.String(), "file-to-ignore") }) it("writes contents from zip file", func() { - copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app.zip"), "/some-location", 123, 456, nil) - ctx := context.Background() + containerDir := "/some-vol" + if osType == "windows" { + containerDir = `c:\some-vol` + } - ctr, err := createContainer(ctx, imageName, "ls", "-al", "/some-location") + ctrCmd := []string{"ls", "-al", "/some-vol"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `dir /q /s /n c:\some-vol`} + } + + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app.zip"), containerDir, 123, 456, osType, nil) + + var outBuf, errBuf bytes.Buffer err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - output := outBuf.String() - h.AssertContainsMatch(t, output, ` + h.AssertEq(t, errBuf.String(), "") + if osType == "windows" { + h.AssertContainsMatch(t, strings.ReplaceAll(outBuf.String(), "\r", ""), ` +(.*) ... . +(.*) ... .. +(.*) 17 ... fake-app-file +`) + } else { + h.AssertContainsMatch(t, outBuf.String(), ` -rw-r--r-- 1 123 456 (.*) fake-app-file `) + } }) }) when("#WriteStackToml", func() { it("writes file", func() { - writeOp := build.WriteStackToml("/some/stack.toml", builder.StackMetadata{ + containerDir := "/layers-vol" + containerPath := "/layers-vol/stack.toml" + if osType == "windows" { + containerDir = `c:\layers-vol` + containerPath = `c:\layers-vol\stack.toml` + } + + ctrCmd := []string{"ls", "-al", "/layers-vol/stack.toml"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `dir /q /n c:\layers-vol\stack.toml`} + } + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) + h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + + writeOp := build.WriteStackToml(containerPath, builder.StackMetadata{ RunImage: builder.RunImageMetadata{ Image: "image-1", Mirrors: []string{ @@ -132,23 +220,42 @@ func testContainerOps(t *testing.T, when spec.G, it spec.S) { "mirror-2", }, }, - }) - ctx := context.Background() - ctr, err := createContainer(ctx, imageName, "ls", "-al", "/some/stack.toml") - h.AssertNil(t, err) + }, osType) + var outBuf, errBuf bytes.Buffer err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - output := outBuf.String() - h.AssertContains(t, output, `-rwxr-xr-x 1 root root 69 Jan 1 1980 /some/stack.toml`) + h.AssertEq(t, errBuf.String(), "") + if osType == "windows" { + h.AssertContains(t, outBuf.String(), `01/01/1980 12:00 AM 69 ... stack.toml`) + } else { + h.AssertContains(t, outBuf.String(), `-rwxr-xr-x 1 root root 69 Jan 1 1980 /layers-vol/stack.toml`) + } }) it("has expected contents", func() { - writeOp := build.WriteStackToml("/some/stack.toml", builder.StackMetadata{ + containerDir := "/layers-vol" + containerPath := "/layers-vol/stack.toml" + if osType == "windows" { + containerDir = `c:\layers-vol` + containerPath = `c:\layers-vol\stack.toml` + } + + ctrCmd := []string{"cat", "/layers-vol/stack.toml"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `type c:\layers-vol\stack.toml`} + } + + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) + h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + + writeOp := build.WriteStackToml(containerPath, builder.StackMetadata{ RunImage: builder.RunImageMetadata{ Image: "image-1", Mirrors: []string{ @@ -156,29 +263,99 @@ func testContainerOps(t *testing.T, when spec.G, it spec.S) { "mirror-2", }, }, - }) - ctx := context.Background() - ctr, err := createContainer(ctx, imageName, "cat", "/some/stack.toml") - h.AssertNil(t, err) + }, osType) + var outBuf, errBuf bytes.Buffer err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - output := outBuf.String() - h.AssertContains(t, output, `[run-image] + h.AssertEq(t, errBuf.String(), "") + h.AssertContains(t, outBuf.String(), `[run-image] image = "image-1" mirrors = ["mirror-1", "mirror-2"] `) }) }) + + when("#EnsureVolumeAccess", func() { + it("changes owner of volume", func() { + h.SkipIf(t, osType != "windows", "no-op for linux") + + ctx := context.Background() + + ctrCmd := []string{"ls", "-al", "/my-volume"} + containerDir := "/my-volume" + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `icacls c:\my-volume`} + containerDir = `c:\my-volume` + } + + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) + h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + + inspect, err := ctrClient.ContainerInspect(ctx, ctr.ID) + if err != nil { + return + } + + // use container's current volumes + var ctrVolumes []string + for _, m := range inspect.Mounts { + if m.Type == mount.TypeVolume { + ctrVolumes = append(ctrVolumes, m.Name) + } + } + + var outBuf, errBuf bytes.Buffer + + // reuse same volume twice to demonstrate multiple ops + initVolumeOp := build.EnsureVolumeAccess(123, 456, osType, ctrVolumes[0], ctrVolumes[0]) + err = initVolumeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) + h.AssertNil(t, err) + err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + h.AssertNil(t, err) + + h.AssertEq(t, errBuf.String(), "") + h.AssertContains(t, outBuf.String(), `BUILTIN\Users:(OI)(CI)(F)`) + }) + }) } -func createContainer(ctx context.Context, imageName string, cmd ...string) (dcontainer.ContainerCreateCreatedBody, error) { +func createContainer(ctx context.Context, imageName, containerDir, osType string, cmd ...string) (dcontainer.ContainerCreateCreatedBody, error) { + isolationType := dcontainer.IsolationDefault + if osType == "windows" { + isolationType = dcontainer.IsolationProcess + } + return ctrClient.ContainerCreate(ctx, - &dcontainer.Config{Image: imageName, Cmd: cmd}, - &dcontainer.HostConfig{}, nil, "", + &dcontainer.Config{ + Image: imageName, + Cmd: cmd, + }, + &dcontainer.HostConfig{ + Binds: []string{fmt.Sprintf("%s:%s", fmt.Sprintf("tests-volume-%s", h.RandString(5)), filepath.ToSlash(containerDir))}, + Isolation: isolationType, + }, nil, "", ) } + +func cleanupContainer(ctx context.Context, ctrID string) { + inspect, err := ctrClient.ContainerInspect(ctx, ctrID) + if err != nil { + return + } + + // remove container + ctrClient.ContainerRemove(ctx, ctrID, types.ContainerRemoveOptions{}) + + // remove volumes + for _, m := range inspect.Mounts { + if m.Type == mount.TypeVolume { + ctrClient.VolumeRemove(ctx, m.Name, true) + } + } +} diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 0814d2d09..03fd87c9b 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -42,6 +42,11 @@ func NewLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, return nil, err } + osType, err := opts.Builder.Image().OS() + if err != nil { + return nil, err + } + exec := &LifecycleExecution{ logger: logger, docker: docker, @@ -49,16 +54,10 @@ func NewLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, appVolume: paths.FilterReservedNames("pack-app-" + randString(10)), platformAPI: latestSupportedPlatformAPI, opts: opts, + os: osType, + mountPaths: mountPathsForOS(osType), } - os, err := opts.Builder.Image().OS() - if err != nil { - return nil, err - } - - exec.os = os - exec.mountPaths = mountPathsForOS(os) - return exec, nil } @@ -196,7 +195,7 @@ func (l *LifecycleExecution) Create( WithArgs(repoName), WithNetwork(networkMode), WithBinds(append(volumes, fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir()))...), - WithContainerOperations(CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.opts.FileFilter)), + WithContainerOperations(CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.opts.FileFilter)), } if publish { @@ -232,7 +231,10 @@ func (l *LifecycleExecution) Detect(ctx context.Context, networkMode string, vol ), WithNetwork(networkMode), WithBinds(volumes...), - WithContainerOperations(CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.opts.FileFilter)), + WithContainerOperations( + EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume), + CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.opts.FileFilter), + ), ) detect := phaseFactory.New(configProvider) @@ -389,7 +391,7 @@ func (l *LifecycleExecution) newExport(repoName, runImage string, publish bool, WithRoot(), WithNetwork(networkMode), WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), - WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack())), + WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack(), l.os)), } if publish { diff --git a/internal/build/lifecycle_execution_test.go b/internal/build/lifecycle_execution_test.go index e8849c3b2..6a233cf83 100644 --- a/internal/build/lifecycle_execution_test.go +++ b/internal/build/lifecycle_execution_test.go @@ -626,8 +626,9 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBind) - h.AssertEq(t, len(configProvider.ContainerOps()), 1) - h.AssertFunctionName(t, configProvider.ContainerOps()[0], "CopyDir") + h.AssertEq(t, len(configProvider.ContainerOps()), 2) + h.AssertFunctionName(t, configProvider.ContainerOps()[0], "EnsureVolumeAccess") + h.AssertFunctionName(t, configProvider.ContainerOps()[1], "CopyDir") }) }) diff --git a/internal/build/phase_test.go b/internal/build/phase_test.go index 03f613b40..fbf5d2411 100644 --- a/internal/build/phase_test.go +++ b/internal/build/phase_test.go @@ -71,6 +71,7 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { outBuf, errBuf bytes.Buffer docker client.CommonAPIClient logger logging.Logger + osType string ) it.Before(func() { @@ -79,6 +80,11 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { var err error docker, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) + + info, err := ctrClient.Info(context.Background()) + h.AssertNil(t, err) + osType = info.OSType + lifecycleExec, err = CreateFakeLifecycleExecution(logger, docker, filepath.Join("testdata", "fake-app"), repoName) h.AssertNil(t, err) phaseFactory = build.NewDefaultPhaseFactory(lifecycleExec) @@ -140,6 +146,7 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { "/workspace", lifecycleExec.Builder().UID(), lifecycleExec.Builder().GID(), + osType, nil, ), ), @@ -164,7 +171,7 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { when("app is a dir", func() { it("preserves original mod times", func() { - assertAppModTimePreserved(t, lifecycleExec, phaseFactory, &outBuf, &errBuf) + assertAppModTimePreserved(t, lifecycleExec, phaseFactory, &outBuf, &errBuf, osType) }) }) @@ -175,7 +182,7 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) phaseFactory = build.NewDefaultPhaseFactory(lifecycleExec) - assertAppModTimePreserved(t, lifecycleExec, phaseFactory, &outBuf, &errBuf) + assertAppModTimePreserved(t, lifecycleExec, phaseFactory, &outBuf, &errBuf, osType) }) }) @@ -215,7 +222,7 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { lifecycleExec, build.WithArgs("read", "/workspace/fake-app-file"), build.WithContainerOperations( - build.CopyDir(lifecycleExec.AppPath(), "/workspace", 0, 0, nil), + build.CopyDir(lifecycleExec.AppPath(), "/workspace", 0, 0, osType, nil), ), )) h.AssertNil(t, err) @@ -367,14 +374,14 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { }) } -func assertAppModTimePreserved(t *testing.T, lifecycle *build.LifecycleExecution, phaseFactory build.PhaseFactory, outBuf *bytes.Buffer, errBuf *bytes.Buffer) { +func assertAppModTimePreserved(t *testing.T, lifecycle *build.LifecycleExecution, phaseFactory build.PhaseFactory, outBuf *bytes.Buffer, errBuf *bytes.Buffer, osType string) { t.Helper() readPhase := phaseFactory.New(build.NewPhaseConfigProvider( phaseName, lifecycle, build.WithArgs("read", "/workspace/fake-app-file"), build.WithContainerOperations( - build.CopyDir(lifecycle.AppPath(), "/workspace", 0, 0, nil), + build.CopyDir(lifecycle.AppPath(), "/workspace", 0, 0, osType, nil), ), )) assertRunSucceeds(t, readPhase, outBuf, errBuf) diff --git a/internal/build/testdata/fake-app/fake-app-symlink b/internal/build/testdata/fake-app/fake-app-symlink new file mode 120000 index 000000000..89264cea2 --- /dev/null +++ b/internal/build/testdata/fake-app/fake-app-symlink @@ -0,0 +1 @@ +fake-app-file \ No newline at end of file diff --git a/internal/container/run.go b/internal/container/run.go index 218776a4c..04bedc065 100644 --- a/internal/container/run.go +++ b/internal/container/run.go @@ -15,21 +15,24 @@ import ( func Run(ctx context.Context, docker client.CommonAPIClient, ctrID string, out, errOut io.Writer) error { bodyChan, errChan := docker.ContainerWait(ctx, ctrID, dcontainer.WaitConditionNextExit) - if err := docker.ContainerStart(ctx, ctrID, types.ContainerStartOptions{}); err != nil { - return errors.Wrap(err, "container start") - } - logs, err := docker.ContainerLogs(ctx, ctrID, types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, + resp, err := docker.ContainerAttach(ctx, ctrID, types.ContainerAttachOptions{ + Stream: true, + Stdout: true, + Stderr: true, }) if err != nil { - return errors.Wrap(err, "container logs stdout") + return err + } + defer resp.Close() + + if err := docker.ContainerStart(ctx, ctrID, types.ContainerStartOptions{}); err != nil { + return errors.Wrap(err, "container start") } copyErr := make(chan error) go func() { - _, err := stdcopy.StdCopy(out, errOut, logs) + _, err := stdcopy.StdCopy(out, errOut, resp.Reader) + copyErr <- err }() @@ -41,5 +44,6 @@ func Run(ctx context.Context, docker client.CommonAPIClient, ctrID string, out, case err := <-errChan: return err } + return <-copyErr } diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 813e22b21..083de50a8 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -15,8 +15,8 @@ func IsURI(ref string) bool { return schemeRegexp.MatchString(ref) } -func IsDir(path string) (bool, error) { - fileInfo, err := os.Stat(path) +func IsDir(p string) (bool, error) { + fileInfo, err := os.Stat(p) if err != nil { return false, err } @@ -24,22 +24,22 @@ func IsDir(path string) (bool, error) { return fileInfo.IsDir(), nil } -func FilePathToURI(path string) (string, error) { +func FilePathToURI(p string) (string, error) { var err error - if !filepath.IsAbs(path) { - path, err = filepath.Abs(path) + if !filepath.IsAbs(p) { + p, err = filepath.Abs(p) if err != nil { return "", err } } if runtime.GOOS == "windows" { - if strings.HasPrefix(path, `\\`) { - return "file://" + filepath.ToSlash(strings.TrimPrefix(path, `\\`)), nil + if strings.HasPrefix(p, `\\`) { + return "file://" + filepath.ToSlash(strings.TrimPrefix(p, `\\`)), nil } - return "file:///" + filepath.ToSlash(path), nil + return "file:///" + filepath.ToSlash(p), nil } - return "file://" + path, nil + return "file://" + p, nil } // examples: @@ -87,7 +87,7 @@ func ToAbsolute(uri, relativeTo string) (string, error) { return uri, nil } -func FilterReservedNames(path string) string { +func FilterReservedNames(p string) string { // The following keys are reserved on Windows // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#win32-file-namespaces reservedNameConversions := map[string]string{ @@ -99,8 +99,46 @@ func FilterReservedNames(path string) string { "prn": "p_r_n", } for k, v := range reservedNameConversions { - path = strings.Replace(path, k, v, -1) + p = strings.Replace(p, k, v, -1) } - return path + return p +} + +//WindowsDir is equivalent to path.Dir or filepath.Dir but always for Windows paths +//reproduced because Windows implementation is not exported +func WindowsDir(p string) string { + pathElements := strings.Split(p, `\`) + + dirName := strings.Join(pathElements[:len(pathElements)-1], `\`) + + return dirName +} + +//WindowsBasename is equivalent to path.Basename or filepath.Basename but always for Windows paths +//reproduced because Windows implementation is not exported +func WindowsBasename(p string) string { + pathElements := strings.Split(p, `\`) + + return pathElements[len(pathElements)-1] +} + +//WindowsToSlash is equivalent to path.ToSlash or filepath.ToSlash but always for Windows paths +//reproduced because Windows implementation is not exported +func WindowsToSlash(p string) string { + slashPath := strings.ReplaceAll(p, `\`, "/") // convert slashes + if len(slashPath) < 2 { + return "" + } + + return slashPath[2:] // strip volume +} + +//WindowsPathSID returns the appropriate SID for a given UID and GID +//This the basic logic for path permissions in Pack and Lifecycle +func WindowsPathSID(uid, gid int) string { + if uid == 0 && gid == 0 { + return "S-1-5-32-544" // BUILTIN\Administrators + } + return "S-1-5-32-545" // BUILTIN\Users } diff --git a/internal/paths/paths_test.go b/internal/paths/paths_test.go index e9e240f59..a96c97057 100644 --- a/internal/paths/paths_test.go +++ b/internal/paths/paths_test.go @@ -154,4 +154,61 @@ func testPaths(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("#WindowsDir", func() { + it("returns the path directory", func() { + path := WindowsDir(`C:\layers\file.txt`) + h.AssertEq(t, path, `C:\layers`) + }) + + it("returns empty for empty", func() { + path := WindowsBasename("") + h.AssertEq(t, path, "") + }) + }) + + when("#WindowsBasename", func() { + it("returns the path basename", func() { + path := WindowsBasename(`C:\layers\file.txt`) + h.AssertEq(t, path, `file.txt`) + }) + + it("returns empty for empty", func() { + path := WindowsBasename("") + h.AssertEq(t, path, "") + }) + }) + + when("#WindowsToSlash", func() { + it("returns the path; backward slashes converted to forward with volume stripped ", func() { + path := WindowsToSlash(`C:\layers\file.txt`) + h.AssertEq(t, path, `/layers/file.txt`) + }) + + it("returns / for volume", func() { + path := WindowsToSlash(`c:\`) + h.AssertEq(t, path, `/`) + }) + + it("returns empty for empty", func() { + path := WindowsToSlash("") + h.AssertEq(t, path, "") + }) + }) + + when("#WindowsPathSID", func() { + when("UID and GID are both 0", func() { + it(`returns the built-in BUILTIN\Administrators SID`, func() { + sid := WindowsPathSID(0, 0) + h.AssertEq(t, sid, "S-1-5-32-544") + }) + }) + + when("UID and GID are both non-zero", func() { + it(`returns the built-in BUILTIN\Users SID`, func() { + sid := WindowsPathSID(99, 99) + h.AssertEq(t, sid, "S-1-5-32-545") + }) + }) + }) } diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 67c35ddde..1ae8847f9 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -580,31 +580,22 @@ func SkipUnless(t *testing.T, expression bool, reason string) { func RunContainer(ctx context.Context, dockerCli client.CommonAPIClient, id string, stdout io.Writer, stderr io.Writer) error { bodyChan, errChan := dockerCli.ContainerWait(ctx, id, container.WaitConditionNextExit) - if err := dockerCli.ContainerStart(ctx, id, dockertypes.ContainerStartOptions{}); err != nil { - return errors.Wrap(err, "container start") - } - - info, err := dockerCli.Info(ctx) + logs, err := dockerCli.ContainerAttach(ctx, id, dockertypes.ContainerAttachOptions{ + Stream: true, + Stdout: true, + Stderr: true, + }) if err != nil { - return errors.Wrap(err, "getting docker info") - } - if info.OSType == "windows" { - // wait for logs to show - time.Sleep(time.Second) + return err } - logs, err := dockerCli.ContainerLogs(ctx, id, dockertypes.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, - }) - if err != nil { - return errors.Wrap(err, "container logs stdout") + if err := dockerCli.ContainerStart(ctx, id, dockertypes.ContainerStartOptions{}); err != nil { + return errors.Wrap(err, "container start") } copyErr := make(chan error) go func() { - _, err := stdcopy.StdCopy(stdout, stderr, logs) + _, err := stdcopy.StdCopy(stdout, stderr, logs.Reader) copyErr <- err }()