diff --git a/Dockerfile.Windows b/Dockerfile.Windows new file mode 100644 index 00000000..b54b3c5b --- /dev/null +++ b/Dockerfile.Windows @@ -0,0 +1,99 @@ +ARG WINDOWS_IMAGE=microsoft/windowsservercore:1803 +FROM $WINDOWS_IMAGE as environment + +# set the default shell as powershell. +# $ProgressPreference: https://github.com/PowerShell/PowerShell/issues/2138#issuecomment-251261324 +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# install MinGit (especially for "go get" and docker build by git repos) +ENV GIT_VERSION 2.17.1 +ENV GIT_TAG v${GIT_VERSION}.windows.1 +ENV GIT_DOWNLOAD_URL https://github.com/git-for-windows/git/releases/download/${GIT_TAG}/MinGit-${GIT_VERSION}-64-bit.zip +ENV GIT_DOWNLOAD_SHA256 668d16a799dd721ed126cc91bed49eb2c072ba1b25b50048280a4e2c5ed56e59 +RUN Write-Host ('Downloading {0} ...' -f $env:GIT_DOWNLOAD_URL); \ + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \ + Invoke-WebRequest -Uri $env:GIT_DOWNLOAD_URL -OutFile 'git.zip'; \ + \ + Write-Host 'Expanding ...'; \ + Expand-Archive -Path git.zip -DestinationPath C:\git\.; \ + \ + Write-Host 'Removing ...'; \ + Remove-Item git.zip -Force; \ + \ + Write-Host 'Updating PATH ...'; \ + $env:PATH = 'C:\git\cmd;C:\git\mingw64\bin;C:\git\usr\bin;' + $env:PATH; \ + [Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine); \ + \ + Write-Host 'Verifying install ...'; \ + Write-Host ' git --version'; git --version; \ + \ + Write-Host 'Complete.'; + +# ideally, this would be C:\go to match Linux a bit closer, but C:\go is the recommended install path for Go itself on Windows +ENV GOPATH C:\\gopath + +# PATH isn't actually set in the Docker image, so we have to set it from within the container +RUN $newPath = ('{0}\bin;C:\go\bin;{1}' -f $env:GOPATH, $env:PATH); \ + Write-Host ('Updating PATH: {0}' -f $newPath); \ + [Environment]::SetEnvironmentVariable('PATH', $newPath, [EnvironmentVariableTarget]::Machine); + +# install go lang +# ideally we should be able to use FROM golang:windowsservercore-1803. This is not done due to two reasons +# 1. The go lang for 1803 tag is not available. +# 2. The image pulls 2.11.1 version of MinGit which has an issue with git submodules command. https://github.com/git-for-windows/git/issues/1007#issuecomment-384281260 + +ENV GOLANG_VERSION 1.10.3 + +RUN $url = ('https://golang.org/dl/go{0}.windows-amd64.zip' -f $env:GOLANG_VERSION); \ + Write-Host ('Downloading {0} ...' -f $url); \ + Invoke-WebRequest -Uri $url -OutFile 'go.zip'; \ + \ + $sha256 = 'a3f19d4fc0f4b45836b349503e347e64e31ab830dedac2fc9c390836d4418edb'; \ + Write-Host ('Verifying sha256 ({0}) ...' -f $sha256); \ + if ((Get-FileHash go.zip -Algorithm sha256).Hash -ne $sha256) { \ + Write-Host 'FAILED!'; \ + exit 1; \ + }; \ + \ + Write-Host 'Expanding ...'; \ + Expand-Archive go.zip -DestinationPath C:\; \ + \ + Write-Host 'Verifying install ("go version") ...'; \ + go version; \ + \ + Write-Host 'Removing ...'; \ + Remove-Item go.zip -Force; \ + \ + Write-Host 'Complete.'; + +# Build the docker executable +FROM environment as dockercli +ARG DOCKER_CLI_LKG_COMMIT=4cb3c70f36baeade76879694a587358be2a74854 +WORKDIR \\gopath\\src\\github.com\\docker\\cli +RUN git clone https://github.com/docker/cli.git \gopath\src\github.com\docker\cli; \ + git checkout $DOCKER_CLI_LKG_COMMIT; \ + go get github.com/LK4D4/vndr; \ + # apply the patch for named pipes to work. + vndr github.com/Microsoft/go-winio 3f914f36b87e3f60c9a4c6404ab0fb9c42f08fc3 https://github.com/AzureCR/go-winio.git; \ + go generate github.com\docker\cli\vendor\github.com\Microsoft\go-winio; \ + scripts\\make.ps1 -Binary -ForceBuildAll + +# Build the acr-builder +FROM environment as builder +COPY --from=dockercli /gopath/src/github.com/docker/cli/build/docker.exe c:/docker/docker.exe +WORKDIR \\gopath\\src\\github.com\\Azure\\acr-builder +COPY ./ /gopath/src/github.com/Azure/acr-builder +RUN Write-Host ('Running build' ); \ + go build; \ + Write-Host ('Running unit tests'); \ + $packageList=$packageList | Select-String -NotMatch "/vendor/" | Select-String -NotMatch "/tests/"; \ + go test -cover $packageList + +# setup the runtime environment +FROM environment as runtime +COPY --from=dockercli /gopath/src/github.com/docker/cli/build/docker.exe c:/docker/docker.exe +COPY --from=builder /gopath/src/github.com/Azure/acr-builder/acr-builder.exe c:/acr-builder/acr-builder.exe +RUN setx /M PATH $('c:\acr-builder;c:\docker;{0}' -f $env:PATH) + +ENTRYPOINT ["acr-builder.exe"] +CMD [] diff --git a/README.md b/README.md index 2c77b997..e57efb7a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Run the following on your project directory to build the project and push to a d * `--build-env` Custom environment variables defined for the build process. This parameter can be specified multiple times. (For more details, see `Build Environment`). * `--push` Specify if push is required if build is successful. * `--pull` Attempt to pull a newer version of the base images if it's already cached locally. +* `--hyperv-isolation` Build using Hyper-V hypervisor partition based isolation. This is used for Windows container builds. * `--no-cache` Not using any cached layer when building the image. * `--verbose` Enable verbose output for debugging. diff --git a/isolation_test.go b/isolation_test.go new file mode 100644 index 00000000..e21ceb38 --- /dev/null +++ b/isolation_test.go @@ -0,0 +1,37 @@ +package main + +import "testing" + +func TestIsolationValidValues(t *testing.T) { + validValues := []string{ + "", + "hyperv", + "process", + "default", + } + + for _, value := range validValues { + err := validateIsolation(value) + + if err != nil { + t.Errorf("Expected to be success. But returned error for value %s", value) + } + } +} + +func TestIsolationInValidValues(t *testing.T) { + inValidValues := []string{ + "hyperv_isolation", + "h12", + "process ", + "isolation", + } + + for _, value := range inValidValues { + err := validateIsolation(value) + + if err == nil { + t.Errorf("Expected to be failed. But returned success for value %s", value) + } + } +} diff --git a/main.go b/main.go index 337e5aa4..51eefb12 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "flag" "fmt" "os" @@ -31,8 +32,9 @@ func (i *stringSlice) Set(value string) error { } var ( - help = flag.Bool("help", false, "Prints the help message") - versionFlag = flag.Bool("version", false, "Prints the version of the builder") + help = flag.Bool("help", false, "Prints the help message") + versionFlag = flag.Bool("version", false, "Prints the version of the builder") + validIsolations = map[string]bool{"": true, "default": true, "process": true, "hyperv": true} ) func main() { @@ -41,7 +43,7 @@ func main() { // Untested code paths: // required unless the host is properly logged in // if the program is launched in docker container, use option -v /var/run/docker.sock:/var/run/docker.sock -v ~/.docker:/root/.docker - var dockerUser, dockerPW, dockerRegistry string + var isolation, dockerUser, dockerPW, dockerRegistry string var buildArgs, buildSecretArgs, buildEnvs stringSlice var pull, noCache, push, debug bool flag.StringVar(&dockerContextString, constants.ArgNameDockerContextString, "", "Working directory for the builder.") @@ -54,6 +56,7 @@ func main() { flag.StringVar(&dockerPW, constants.ArgNameDockerPW, "", "Docker password or OAuth identity token.") flag.Var(&buildEnvs, constants.ArgNameBuildEnv, "Custom environment variables defined for the build process") flag.BoolVar(&pull, constants.ArgNamePull, false, "Attempt to pull a newer version of the base images") + flag.StringVar(&isolation, constants.ArgNameIsolation, "", "Specify isolation technology for container. Supported values are default,process and hyperv") flag.BoolVar(&noCache, constants.ArgNameNoCache, false, "Not using any cached layer when building the image") flag.BoolVar(&push, constants.ArgNamePush, false, "Push on success") flag.BoolVar(&debug, constants.ArgNameDebug, false, "Enable verbose output for debugging") @@ -73,6 +76,14 @@ func main() { return } + err := validateIsolation(isolation) + + if err != nil { + logrus.Errorf("%s", err) + flag.PrintDefaults() + os.Exit(constants.GeneralErrorExitCode) + } + if debug { logrus.SetLevel(logrus.DebugLevel) } @@ -87,7 +98,7 @@ func main() { dockerfile, normalizedDockerImages, dockerUser, dockerPW, dockerRegistry, dockerContextString, - buildEnvs, buildArgs, buildSecretArgs, pull, noCache, push) + buildEnvs, buildArgs, buildSecretArgs, isolation, pull, noCache, push) if err != nil { logrus.Error(err) @@ -124,3 +135,10 @@ func getNormalizedDockerImageNames(dockerImages []string) []string { return normalizedDockerImages } + +func validateIsolation(isolation string) error { + if !validIsolations[isolation] { + return errors.New("Invalid value for isolation argument") + } + return nil +} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 6c2474bb..07e36bdd 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -54,7 +54,7 @@ func (u *dockerUsernamePassword) Authenticate(runner build.Runner) error { // NewDockerBuild creates a build target with specified docker file and build parameters func NewDockerBuild(dockerfile string, - buildArgs, buildSecretArgs []string, registry string, imageNames []string, pull, noCache bool) build.Target { + buildArgs, buildSecretArgs []string, registry string, imageNames []string, isolation string, pull, noCache bool) build.Target { var pushTo []string // If imageName is empty, skip push. // If registry is empty, it means push to DockerHub. @@ -75,6 +75,7 @@ func NewDockerBuild(dockerfile string, buildArgs: buildArgs, buildSecretArgs: buildSecretArgs, pushTo: pushTo, + isolation: isolation, pull: pull, noCache: noCache, } @@ -85,6 +86,7 @@ type dockerBuildTask struct { buildArgs []string buildSecretArgs []string pushTo []string + isolation string pull bool noCache bool } @@ -136,6 +138,11 @@ func (t *dockerBuildTask) ScanForDependencies(runner build.Runner) ([]build.Imag func (t *dockerBuildTask) Build(runner build.Runner) error { args := []string{"build"} + if t.isolation != "" { + isolationString := fmt.Sprintf("--isolation=%s", t.isolation) + args = append(args, isolationString) + } + if t.pull { args = append(args, "--pull") } diff --git a/pkg/commands/docker_test.go b/pkg/commands/docker_test.go index e6187daf..dc9587aa 100644 --- a/pkg/commands/docker_test.go +++ b/pkg/commands/docker_test.go @@ -47,6 +47,7 @@ type dockerTestCase struct { buildSecretArgs []string registry string imageNames []string + isolation string pull bool noCache bool expectedCommands []test.CommandsExpectation @@ -57,7 +58,7 @@ func testDockerBuild(t *testing.T, tc dockerTestCase) { runner := new(test.MockRunner) runner.PrepareCommandExpectation(tc.expectedCommands) defer runner.AssertExpectations(t) - target := NewDockerBuild(tc.dockerfile, tc.buildArgs, tc.buildSecretArgs, tc.registry, tc.imageNames, tc.pull, tc.noCache) + target := NewDockerBuild(tc.dockerfile, tc.buildArgs, tc.buildSecretArgs, tc.registry, tc.imageNames, tc.isolation, tc.pull, tc.noCache) err := target.Build(runner) if tc.expectedExecutionErr != "" { assert.NotNil(t, err) @@ -106,7 +107,7 @@ func TestDockerBuildHappy(t *testing.T) { func TestExport(t *testing.T) { imageNames := []string{"myImage"} - task := NewDockerBuild("myDockerfile", []string{}, []string{}, "myRegistry/", imageNames, false, false) + task := NewDockerBuild("myDockerfile", []string{}, []string{}, "myRegistry/", imageNames, "", false, false) exports := task.Export() testCommon.AssertSameEnv(t, []build.EnvVar{ {Name: constants.ExportsDockerfilePath, Value: "myDockerfile"}, @@ -117,7 +118,7 @@ func testDockerPush(t *testing.T, tc dockerTestCase) { runner := new(test.MockRunner) runner.PrepareCommandExpectation(tc.expectedCommands) defer runner.AssertExpectations(t) - target := NewDockerBuild(tc.dockerfile, tc.buildArgs, tc.buildSecretArgs, tc.registry, tc.imageNames, tc.pull, tc.noCache) + target := NewDockerBuild(tc.dockerfile, tc.buildArgs, tc.buildSecretArgs, tc.registry, tc.imageNames, tc.isolation, tc.pull, tc.noCache) err := target.Push(runner) if tc.expectedExecutionErr != "" { assert.NotNil(t, err) @@ -181,7 +182,7 @@ func testDockerScanDependencies(t *testing.T, tc dockerDependenciesTestCase) { {Name: "project_root", Value: filepath.Join("..", "..", "tests", "resources", "docker-dotnet")}, }, []build.EnvVar{})) target := NewDockerBuild(tc.path, testCommon.DotnetExampleMinimalBuildArg, - []string{}, testCommon.DotnetExampleTargetRegistryName+"/", []string{testCommon.DotnetExampleTargetImageName}, false, false) + []string{}, testCommon.DotnetExampleTargetRegistryName+"/", []string{testCommon.DotnetExampleTargetImageName}, "", false, false) dependencies, err := target.ScanForDependencies(runner) if tc.expectedErr == "" { assert.Nil(t, err) diff --git a/pkg/constants/program_args.go b/pkg/constants/program_args.go index 67f95e4e..4e63d0f3 100644 --- a/pkg/constants/program_args.go +++ b/pkg/constants/program_args.go @@ -32,6 +32,9 @@ const ( // ArgNamePull is the parameter determining if attempting to pull a newer version of the base images. Default: false ArgNamePull = "pull" + // ArgNameIsolation is the parameter name for specifying isolation technology for container. This option is useful for running docker containers in Windows.Supported values are default, process and hyperv + ArgNameIsolation = "isolation" + // ArgNameNoCache is the parameter determining if not using any cached layer when building the image. Default: false ArgNameNoCache = "no-cache" diff --git a/pkg/driver/build.go b/pkg/driver/build.go index e10ec43b..ccad4b58 100644 --- a/pkg/driver/build.go +++ b/pkg/driver/build.go @@ -30,7 +30,7 @@ func (b *Builder) Run( dockerfile string, dockerImages []string, dockerUser, dockerPW, dockerRegistry, dockerContextString string, - buildEnvs, buildArgs, buildSecretArgs []string, pull, noCache, push bool, + buildEnvs, buildArgs, buildSecretArgs []string, isolation string, pull, noCache, push bool, ) (dependencies []build.ImageDependencies, err error) { if dockerRegistry == "" { @@ -48,7 +48,7 @@ func (b *Builder) Run( dockerfile, dockerImages, dockerUser, dockerPW, dockerRegistry, dockerContextString, - buildArgs, buildSecretArgs, pull, noCache, push) + buildArgs, buildSecretArgs, isolation, pull, noCache, push) if err != nil { return @@ -66,7 +66,7 @@ func (b *Builder) createBuildRequest( dockerfile string, dockerImages []string, dockerUser, dockerPW, dockerRegistry, dockerContextString string, - buildArgs, buildSecretArgs []string, pull, noCache, push bool) (*build.Request, error) { + buildArgs, buildSecretArgs []string, isolation string, pull, noCache, push bool) (*build.Request, error) { if push && dockerRegistry == "" { return nil, fmt.Errorf("Docker registry is needed for push, use --%s or environment variable %s to provide its value", constants.ArgNameDockerRegistry, constants.ExportsDockerRegistry) @@ -98,7 +98,7 @@ func (b *Builder) createBuildRequest( } source := commands.NewDockerSource(dockerContextString, dockerfile) - target := commands.NewDockerBuild(dockerfile, buildArgs, buildSecretArgs, registrySuffixed, dockerImages, pull, noCache) + target := commands.NewDockerBuild(dockerfile, buildArgs, buildSecretArgs, registrySuffixed, dockerImages, isolation, pull, noCache) return &build.Request{ DockerRegistry: registrySuffixed, diff --git a/pkg/driver/build_test.go b/pkg/driver/build_test.go index a1032e27..f55fdd65 100644 --- a/pkg/driver/build_test.go +++ b/pkg/driver/build_test.go @@ -388,6 +388,7 @@ type createBuildRequestTestCase struct { dockerContextString string buildArgs []string buildSecretArgs []string + isolation string pull bool noCache bool push bool @@ -404,7 +405,7 @@ func TestCreateBuildRequestNoParams(t *testing.T) { Targets: []build.SourceTarget{ { Source: localSource, - Builds: []build.Target{commands.NewDockerBuild("", nil, nil, "", nil, false, false)}, + Builds: []build.Target{commands.NewDockerBuild("", nil, nil, "", nil, "", false, false)}, }, }, }, @@ -421,7 +422,7 @@ func TestCreateGitBuildRequest(t *testing.T) { pull := true noCache := false dockerBuildTarget := commands.NewDockerBuild(dockerfile, - buildArgs, buildSecretArgs, registry+"/", imageNames, pull, noCache) + buildArgs, buildSecretArgs, registry+"/", imageNames, "", pull, noCache) gitSource := commands.NewDockerSource(giturl, dockerfile) testCreateBuildRequest(t, createBuildRequestTestCase{ dockerfile: dockerfile, @@ -475,7 +476,7 @@ func testCreateBuildRequest(t *testing.T, tc createBuildRequestTestCase) { req, err := builder.createBuildRequest( tc.dockerfile, tc.dockerImages, tc.dockerUser, tc.dockerPW, tc.dockerRegistry, tc.dockerContextString, - tc.buildArgs, tc.buildSecretArgs, tc.pull, tc.noCache, tc.push) + tc.buildArgs, tc.buildSecretArgs, tc.isolation, tc.pull, tc.noCache, tc.push) if tc.expectedError != "" { assert.NotNil(t, err) @@ -496,6 +497,7 @@ type runTestCase struct { buildEnvs []string buildArgs []string buildSecretArgs []string + isolation string pull bool noCache bool push bool @@ -667,7 +669,7 @@ func testRun(t *testing.T, tc runTestCase) { tc.dockerfile, tc.dockerImages, tc.dockerUser, tc.dockerPW, tc.dockerRegistry, tc.dockerContextString, - tc.buildEnvs, tc.buildArgs, tc.buildSecretArgs, tc.pull, tc.noCache, tc.push) + tc.buildEnvs, tc.buildArgs, tc.buildSecretArgs, tc.isolation, tc.pull, tc.noCache, tc.push) if tc.expectedErr != "" { assert.NotNil(t, err) assert.Regexp(t, regexp.MustCompile(tc.expectedErr), err.Error())