From 6380bd32d7c062a5da82587c5bf7a3307e0fdb55 Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Thu, 7 Nov 2019 11:59:24 +1100 Subject: [PATCH] Enable Multiarch docker builds --- .gitignore | 2 + .travis.yml | 70 ++++++++++----- Dockerfile | 2 +- Dockerfile.arm32v7 | 48 ++++++++++ Dockerfile.arm64v8 | 48 ++++++++++ cmd/authelia-scripts/cmd_bootstrap.go | 6 +- cmd/authelia-scripts/cmd_ci.go | 8 -- cmd/authelia-scripts/cmd_docker.go | 125 +++++++++++++++++++++++--- cmd/authelia-scripts/docker.go | 27 +++++- cmd/authelia-scripts/main.go | 2 +- 10 files changed, 291 insertions(+), 47 deletions(-) create mode 100644 Dockerfile.arm32v7 create mode 100644 Dockerfile.arm64v8 diff --git a/.gitignore b/.gitignore index bce5b61f1..f2cb9b4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ users_database.test.yml .idea .authelia-interrupt + +qemu-*-static \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5dd33a9af..ff24e4f36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,34 +1,62 @@ language: go + required: sudo + go: -- '1.13' + - '1.13' + services: -- docker -- ntp -- xvfb -addons: - chrome: stable - apt: - sources: - - google-chrome - packages: - - libgif-dev - - google-chrome-stable + - docker + - ntp + - xvfb before_script: -- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash -- nvm install v11 && nvm use v11 && npm i -script: -- "source bootstrap.sh" -- "authelia-scripts ci" -after_success: -- "authelia-scripts docker publish" + - export PATH=./cmd/authelia-scripts/:/tmp:$PATH -# TODO(c.michaud): publish built artifact on Github. +jobs: + include: + - stage: test + addons: + chrome: stable + apt: + sources: + - google-chrome + packages: + - libgif-dev + - google-chrome-stable + script: + - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash + - nvm install v11 && nvm use v11 && npm i + - source bootstrap.sh + - authelia-scripts ci + # TODO(c.michaud): publish built artifact on Github. + - &build-images + stage: build images + env: + - ARCH=amd64 + install: skip + script: + - while sleep 9m; do echo '===== Prevent build from terminating ====='; done & + - authelia-scripts docker build --arch=$ARCH + - kill %1 + after_success: + - authelia-scripts docker push-image --arch=$ARCH + - <<: *build-images + env: + - ARCH=arm32v7 + - <<: *build-images + env: + - ARCH=arm64v8 + - stage: deploy manifests + env: + - DOCKER_CLI_EXPERIMENTAL=enabled + install: skip + script: + - authelia-scripts docker push-manifest notifications: email: recipients: - - clement.michaud34@gmail.com + - clement.michaud34@gmail.com on_success: change on_failure: always diff --git a/Dockerfile b/Dockerfile index e6b3bee3a..4f81f0d71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /go/src/app COPY . . # CGO_ENABLED=1 is mandatory for building go-sqlite3 -RUN cd cmd/authelia && GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o authelia +RUN cd cmd/authelia && GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags netgo -ldflags '-w' -o authelia # ======================================== diff --git a/Dockerfile.arm32v7 b/Dockerfile.arm32v7 new file mode 100644 index 000000000..89660420b --- /dev/null +++ b/Dockerfile.arm32v7 @@ -0,0 +1,48 @@ +# ======================================= +# ===== Build image for the backend ===== +# ======================================= +FROM arm32v7/golang:1.13-alpine AS builder-backend + +# qemu binary, gcc and musl-dev are required for building go-sqlite3 +COPY ./qemu-arm-static /usr/bin/qemu-arm-static +RUN apk --no-cache add gcc musl-dev + +WORKDIR /go/src/app +COPY . . + +# CGO_ENABLED=1 is mandatory for building go-sqlite3 +RUN cd cmd/authelia && GOOS=linux GOARCH=arm CGO_ENABLED=1 go build -tags netgo -ldflags '-w' -o authelia + + +# ======================================== +# ===== Build image for the frontend ===== +# ======================================== +FROM node:11-alpine AS builder-frontend + +WORKDIR /node/src/app +COPY client . + +# Install the dependencies and build +RUN npm ci && npm run build + +# =================================== +# ===== Authelia official image ===== +# =================================== +FROM arm32v7/alpine:3.10.3 + +COPY ./qemu-arm-static /usr/bin/qemu-arm-static + +RUN apk --no-cache add ca-certificates tzdata && \ + rm /usr/bin/qemu-arm-static + +WORKDIR /usr/app + +COPY --from=builder-backend /go/src/app/cmd/authelia/authelia authelia +COPY --from=builder-frontend /node/src/app/build public_html + +EXPOSE 9091 + +VOLUME /etc/authelia +VOLUME /var/lib/authelia + +CMD ["./authelia", "-config", "/etc/authelia/config.yml"] diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 new file mode 100644 index 000000000..ca52b457b --- /dev/null +++ b/Dockerfile.arm64v8 @@ -0,0 +1,48 @@ +# ======================================= +# ===== Build image for the backend ===== +# ======================================= +FROM arm64v8/golang:1.13-alpine AS builder-backend + +# qemu binary, gcc and musl-dev are required for building go-sqlite3 +COPY ./qemu-aarch64-static /usr/bin/qemu-aarch64-static +RUN apk --no-cache add gcc musl-dev + +WORKDIR /go/src/app +COPY . . + +# CGO_ENABLED=1 is mandatory for building go-sqlite3 +RUN cd cmd/authelia && GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build -tags netgo -ldflags '-w' -o authelia + + +# ======================================== +# ===== Build image for the frontend ===== +# ======================================== +FROM node:11-alpine AS builder-frontend + +WORKDIR /node/src/app +COPY client . + +# Install the dependencies and build +RUN npm ci && npm run build + +# =================================== +# ===== Authelia official image ===== +# =================================== +FROM arm64v8/alpine:3.10.3 + +COPY ./qemu-aarch64-static /usr/bin/qemu-aarch64-static + +RUN apk --no-cache add ca-certificates tzdata && \ + rm /usr/bin/qemu-aarch64-static + +WORKDIR /usr/app + +COPY --from=builder-backend /go/src/app/cmd/authelia/authelia authelia +COPY --from=builder-frontend /node/src/app/build public_html + +EXPOSE 9091 + +VOLUME /etc/authelia +VOLUME /var/lib/authelia + +CMD ["./authelia", "-config", "/etc/authelia/config.yml"] diff --git a/cmd/authelia-scripts/cmd_bootstrap.go b/cmd/authelia-scripts/cmd_bootstrap.go index 61f38ae77..d556721df 100644 --- a/cmd/authelia-scripts/cmd_bootstrap.go +++ b/cmd/authelia-scripts/cmd_bootstrap.go @@ -94,7 +94,7 @@ func shell(cmd string) { runCommand("bash", "-c", cmd) } -func buildDockerImages() { +func buildHelperDockerImages() { shell("docker build -t authelia-example-backend example/compose/nginx/backend") shell("docker build -t authelia-duo-api example/compose/duo-api") } @@ -210,8 +210,8 @@ func Bootstrap(cobraCmd *cobra.Command, args []string) { installClientNpmPackages() bootstrapPrintln("Building development Docker images...") - buildDockerImages() - DockerBuildOfficialImage() + buildHelperDockerImages() + dockerBuildOfficialImage(defaultArch) bootstrapPrintln("Installing Kubernetes dependencies for testing in /tmp... (no junk installed on host)") installKubernetesDependencies() diff --git a/cmd/authelia-scripts/cmd_ci.go b/cmd/authelia-scripts/cmd_ci.go index a5bbbc9ce..b28003d94 100644 --- a/cmd/authelia-scripts/cmd_ci.go +++ b/cmd/authelia-scripts/cmd_ci.go @@ -41,14 +41,6 @@ func RunCI(cmd *cobra.Command, args []string) { panic(err) } - fmt.Println("===== Docker image build stage =====") - command = CommandWithStdout("authelia-scripts", "docker", "build") - err = command.Run() - - if err != nil { - panic(err) - } - fmt.Println("===== End-to-end testing stage =====") command = CommandWithStdout("authelia-scripts", "suites", "test", "--headless", "--only-forbidden") err = command.Run() diff --git a/cmd/authelia-scripts/cmd_docker.go b/cmd/authelia-scripts/cmd_docker.go index dce9efcbd..d79b86ed0 100644 --- a/cmd/authelia-scripts/cmd_docker.go +++ b/cmd/authelia-scripts/cmd_docker.go @@ -5,13 +5,67 @@ import ( "fmt" "log" "os" + "strings" "github.com/spf13/cobra" ) -func DockerBuildOfficialImage() error { +var arch string +var supportedArch = []string{"amd64", "arm32v7", "arm64v8"} +var defaultArch = "amd64" + +func init() { + DockerBuildCmd.PersistentFlags().StringVar(&arch, "arch", defaultArch, "target architecture among: "+strings.Join(supportedArch, ", ")) + DockerPushCmd.PersistentFlags().StringVar(&arch, "arch", defaultArch, "target architecture among: "+strings.Join(supportedArch, ", ")) + +} + +func checkArchIsSupported(arch string) { + for _, a := range supportedArch { + if arch == a { + return + } + } + log.Fatal("Architecture is not supported. Please select one of " + strings.Join(supportedArch, ", ") + ".") +} + +func dockerBuildOfficialImage(arch string) error { docker := &Docker{} - return docker.Build(IntermediateDockerImageName, ".") + // Set default Architecture Dockerfile to amd64 + dockerfile := "Dockerfile" + + // If not the default value + if arch != defaultArch { + dockerfile = fmt.Sprintf("%s.%s", dockerfile, arch) + } + + if arch == "arm32v7" { + err := CommandWithStdout("docker", "run", "--rm", "--privileged", "multiarch/qemu-user-static", "--reset", "-p", "yes").Run() + + if err != nil { + panic(err) + } + + err = CommandWithStdout("bash", "-c", "wget https://github.com/multiarch/qemu-user-static/releases/download/v4.1.0-1/qemu-arm-static -O ./qemu-arm-static && chmod +x ./qemu-arm-static").Run() + + if err != nil { + panic(err) + } + } else if arch == "arm64v8" { + err := CommandWithStdout("docker", "run", "--rm", "--privileged", "multiarch/qemu-user-static", "--reset", "-p", "yes").Run() + + if err != nil { + panic(err) + } + + err = CommandWithStdout("bash", "-c", "wget https://github.com/multiarch/qemu-user-static/releases/download/v4.1.0-1/qemu-aarch64-static -O ./qemu-aarch64-static && chmod +x ./qemu-aarch64-static").Run() + + if err != nil { + panic(err) + } + } + + return docker.Build(IntermediateDockerImageName, dockerfile, ".") } // DockerBuildCmd Command for building docker image of Authelia. @@ -19,7 +73,8 @@ var DockerBuildCmd = &cobra.Command{ Use: "build", Short: "Build the docker image of Authelia", Run: func(cmd *cobra.Command, args []string) { - err := DockerBuildOfficialImage() + checkArchIsSupported(arch) + err := dockerBuildOfficialImage(arch) if err != nil { log.Fatal(err) @@ -36,10 +91,20 @@ var DockerBuildCmd = &cobra.Command{ // DockerPushCmd Command for pushing Authelia docker image to Dockerhub var DockerPushCmd = &cobra.Command{ - Use: "publish", + Use: "push-image", Short: "Publish Authelia docker image to Dockerhub", Run: func(cmd *cobra.Command, args []string) { - publishDockerImage() + checkArchIsSupported(arch) + publishDockerImage(arch) + }, +} + +// DockerManifestCmd Command for pushing Authelia docker manifest to Dockerhub +var DockerManifestCmd = &cobra.Command{ + Use: "push-manifest", + Short: "Publish Authelia docker manifest to Dockerhub", + Run: func(cmd *cobra.Command, args []string) { + publishDockerManifest() }, } @@ -76,14 +141,33 @@ func deploy(docker *Docker, tag string) { panic(err) } - docker.Push(imageWithTag) + err = docker.Push(imageWithTag) if err != nil { panic(err) } } -func publishDockerImage() { +func deployManifest(docker *Docker, tag string, amd64tag string, arm32v7tag string, arm64v8tag string) { + imageWithTag := DockerImageName + ":" + tag + fmt.Println("===================================================") + fmt.Println("Docker manifest " + imageWithTag + " will be deployed on Dockerhub.") + fmt.Println("===================================================") + + err := docker.Tag(DockerImageName, imageWithTag) + + if err != nil { + panic(err) + } + + err = docker.Manifest(imageWithTag, amd64tag, arm32v7tag, arm64v8tag) + + if err != nil { + panic(err) + } +} + +func publishDockerImage(arch string) { docker := &Docker{} travisBranch := os.Getenv("TRAVIS_BRANCH") @@ -92,12 +176,31 @@ func publishDockerImage() { if travisBranch == "master" && travisPullRequest == "false" { login(docker) - deploy(docker, "master") + deploy(docker, "master-"+arch) } else if travisTag != "" { login(docker) - deploy(docker, travisTag) - deploy(docker, "latest") + deploy(docker, travisTag+"-"+arch) + deploy(docker, "latest-"+arch) } else { - fmt.Println("Docker image will not be built") + fmt.Println("Docker image will not be published") + } +} + +func publishDockerManifest() { + docker := &Docker{} + + travisBranch := os.Getenv("TRAVIS_BRANCH") + travisPullRequest := os.Getenv("TRAVIS_PULL_REQUEST") + travisTag := os.Getenv("TRAVIS_TAG") + + if travisBranch == "master" && travisPullRequest == "false" { + login(docker) + deployManifest(docker, "master", "master-amd64", "master-arm32v7", "master-arm64v8") + } else if travisTag != "" { + login(docker) + deployManifest(docker, travisTag, travisTag+"-amd64", travisTag+"-arm32v7", travisTag+"-arm64v8") + deployManifest(docker, "latest", "latest-amd64", "latest-arm32v7", "latest-arm64v8") + } else { + fmt.Println("Docker manifest will not be published") } } diff --git a/cmd/authelia-scripts/docker.go b/cmd/authelia-scripts/docker.go index b78616a96..0a4628fae 100644 --- a/cmd/authelia-scripts/docker.go +++ b/cmd/authelia-scripts/docker.go @@ -4,8 +4,8 @@ package main type Docker struct{} // Build build a docker image -func (d *Docker) Build(tag string, target string) error { - return CommandWithStdout("docker", "build", "-t", tag, target).Run() +func (d *Docker) Build(tag string, dockerfile string, target string) error { + return CommandWithStdout("docker", "build", "-t", tag, "-f", dockerfile, target).Run() } // Tag tag a docker image. @@ -22,3 +22,26 @@ func (d *Docker) Login(username, password string) error { func (d *Docker) Push(tag string) error { return CommandWithStdout("docker", "push", tag).Run() } + +// Manifest push a docker manifest to dockerhub. +func (d *Docker) Manifest(tag string, amd64tag string, arm32v7tag string, arm64v8tag string) error { + err := CommandWithStdout("docker", "manifest", "create", tag, amd64tag, arm32v7tag, arm64v8tag).Run() + + if err != nil { + panic(err) + } + + err = CommandWithStdout("docker", "manifest", "annotate", tag, arm32v7tag, "--os", "linux", "--arch", "arm").Run() + + if err != nil { + panic(err) + } + + err = CommandWithStdout("docker", "manifest", "annotate", tag, arm64v8tag, "--os", "linux", "--arch", "arm64", "--variant", "v8").Run() + + if err != nil { + panic(err) + } + + return CommandWithStdout("docker", "manifest", "push", "--purge", tag).Run() +} \ No newline at end of file diff --git a/cmd/authelia-scripts/main.go b/cmd/authelia-scripts/main.go index be50af62e..cc475ce94 100755 --- a/cmd/authelia-scripts/main.go +++ b/cmd/authelia-scripts/main.go @@ -42,7 +42,7 @@ var Commands = []AutheliaCommandDefinition{ AutheliaCommandDefinition{ Name: "docker", Short: "Commands related to building and publishing docker image", - SubCommands: CobraCommands{DockerBuildCmd, DockerPushCmd}, + SubCommands: CobraCommands{DockerBuildCmd, DockerPushCmd, DockerManifestCmd}, }, AutheliaCommandDefinition{ Name: "hash-password [password]",