Skip to content

Commit

Permalink
Enable Windows container builds with acr-builder. (#130)
Browse files Browse the repository at this point in the history
* Enable Windows container builder with acr-builder.

* Added isolation argument.
  • Loading branch information
mnltejaswini authored and ehotinger committed Jun 13, 2018
1 parent ce27e67 commit 2e2b8dd
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 17 deletions.
99 changes: 99 additions & 0 deletions Dockerfile.Windows
Original file line number Diff line number Diff line change
@@ -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 []
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
37 changes: 37 additions & 0 deletions isolation_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
26 changes: 22 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
Expand Down Expand Up @@ -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() {
Expand All @@ -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.")
Expand All @@ -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")
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
9 changes: 8 additions & 1 deletion pkg/commands/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -75,6 +75,7 @@ func NewDockerBuild(dockerfile string,
buildArgs: buildArgs,
buildSecretArgs: buildSecretArgs,
pushTo: pushTo,
isolation: isolation,
pull: pull,
noCache: noCache,
}
Expand All @@ -85,6 +86,7 @@ type dockerBuildTask struct {
buildArgs []string
buildSecretArgs []string
pushTo []string
isolation string
pull bool
noCache bool
}
Expand Down Expand Up @@ -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")
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/commands/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type dockerTestCase struct {
buildSecretArgs []string
registry string
imageNames []string
isolation string
pull bool
noCache bool
expectedCommands []test.CommandsExpectation
Expand All @@ -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)
Expand Down Expand Up @@ -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"},
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions pkg/constants/program_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 4 additions & 4 deletions pkg/driver/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions pkg/driver/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ type createBuildRequestTestCase struct {
dockerContextString string
buildArgs []string
buildSecretArgs []string
isolation string
pull bool
noCache bool
push bool
Expand All @@ -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)},
},
},
},
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -496,6 +497,7 @@ type runTestCase struct {
buildEnvs []string
buildArgs []string
buildSecretArgs []string
isolation string
pull bool
noCache bool
push bool
Expand Down Expand Up @@ -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())
Expand Down

0 comments on commit 2e2b8dd

Please sign in to comment.