Merge pull request #1029 from jjlin/multi-arch

Multi-arch image support
This commit is contained in:
Daniel García 2020-07-16 22:59:12 +02:00 committed by GitHub
commit 0b04caab78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 31 deletions

View file

@ -9,13 +9,13 @@
{% elif "amd64" in target_file %} {% elif "amd64" in target_file %}
{% set runtime_stage_base_image = "debian:buster-slim" %} {% set runtime_stage_base_image = "debian:buster-slim" %}
{% set package_arch_name = "" %} {% set package_arch_name = "" %}
{% elif "aarch64" in target_file %} {% elif "arm64v8" in target_file %}
{% set runtime_stage_base_image = "balenalib/aarch64-debian:buster" %} {% set runtime_stage_base_image = "balenalib/aarch64-debian:buster" %}
{% set package_arch_name = "arm64" %} {% set package_arch_name = "arm64" %}
{% elif "armv6" in target_file %} {% elif "arm32v6" in target_file %}
{% set runtime_stage_base_image = "balenalib/rpi-debian:buster" %} {% set runtime_stage_base_image = "balenalib/rpi-debian:buster" %}
{% set package_arch_name = "armel" %} {% set package_arch_name = "armel" %}
{% elif "armv7" in target_file %} {% elif "arm32v7" in target_file %}
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:buster" %} {% set runtime_stage_base_image = "balenalib/armv7hf-debian:buster" %}
{% set package_arch_name = "armhf" %} {% set package_arch_name = "armhf" %}
{% endif %} {% endif %}
@ -73,7 +73,7 @@ RUN rustup set profile minimal
ENV USER "root" ENV USER "root"
ENV RUSTFLAGS='-C link-arg=-s' ENV RUSTFLAGS='-C link-arg=-s'
{% elif "aarch64" in target_file or "armv" in target_file %} {% elif "arm32" in target_file or "arm64" in target_file %}
# Install required build libs for {{ package_arch_name }} architecture. # Install required build libs for {{ package_arch_name }} architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \ /etc/apt/sources.list.d/deb-src.list \
@ -85,7 +85,7 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
libc6-dev{{ package_arch_prefix }} libc6-dev{{ package_arch_prefix }}
{% endif -%} {% endif -%}
{% if "aarch64" in target_file %} {% if "arm64v8" in target_file %}
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@ -97,7 +97,7 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
{% elif "armv6" in target_file %} {% elif "arm32v6" in target_file %}
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@ -109,19 +109,7 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
{% elif "armv6" in target_file %} {% elif "arm32v7" in target_file %}
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabihf \
&& mkdir -p ~/.cargo \
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
{% elif "armv7" in target_file %}
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@ -162,17 +150,17 @@ COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs COPY ./build.rs ./build.rs
{% if "aarch64" in target_file %} {% if "arm64v8" in target_file %}
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu" ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
{% elif "armv6" in target_file %} {% elif "arm32v6" in target_file %}
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc" ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi" ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
{% elif "armv7" in target_file %} {% elif "arm32v7" in target_file %}
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc" ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
@ -182,13 +170,13 @@ ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
{% if "alpine" in target_file %} {% if "alpine" in target_file %}
RUN rustup target add x86_64-unknown-linux-musl RUN rustup target add x86_64-unknown-linux-musl
{% elif "aarch64" in target_file %} {% elif "arm64v8" in target_file %}
RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add aarch64-unknown-linux-gnu
{% elif "armv6" in target_file %} {% elif "arm32v6" in target_file %}
RUN rustup target add arm-unknown-linux-gnueabi RUN rustup target add arm-unknown-linux-gnueabi
{% elif "armv7" in target_file %} {% elif "arm32v7" in target_file %}
RUN rustup target add armv7-unknown-linux-gnueabihf RUN rustup target add armv7-unknown-linux-gnueabihf
{% endif %} {% endif %}
# Builds your dependencies and removes the # Builds your dependencies and removes the
@ -208,11 +196,11 @@ RUN touch src/main.rs
# your actual source files being built # your actual source files being built
{% if "amd64" in target_file %} {% if "amd64" in target_file %}
RUN cargo build --features ${DB} --release RUN cargo build --features ${DB} --release
{% elif "aarch64" in target_file %} {% elif "arm64v8" in target_file %}
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
{% elif "armv6" in target_file %} {% elif "arm32v6" in target_file %}
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
{% elif "armv7" in target_file %} {% elif "arm32v7" in target_file %}
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
{% endif %} {% endif %}
@ -277,11 +265,11 @@ COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault COPY --from=vault /web-vault ./web-vault
{% if "alpine" in target_file %} {% if "alpine" in target_file %}
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs . COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
{% elif "aarch64" in target_file %} {% elif "arm64v8" in target_file %}
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs . COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
{% elif "armv6" in target_file %} {% elif "arm32v6" in target_file %}
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs . COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
{% elif "armv7" in target_file %} {% elif "arm32v7" in target_file %}
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs . COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
{% else %} {% else %}
COPY --from=build app/target/release/bitwarden_rs . COPY --from=build app/target/release/bitwarden_rs .

3
docker/README.md Normal file
View file

@ -0,0 +1,3 @@
The arch-specific directory names follow the arch identifiers used by the Docker official images:
https://github.com/docker-library/official-images/blob/master/README.md#architectures-other-than-amd64

20
hooks/README.md Normal file
View file

@ -0,0 +1,20 @@
The hooks in this directory are used to create multi-arch images using Docker Hub automated builds.
Docker Hub hooks provide these predefined [environment variables](https://docs.docker.com/docker-hub/builds/advanced/#environment-variables-for-building-and-testing):
* `SOURCE_BRANCH`: the name of the branch or the tag that is currently being tested.
* `SOURCE_COMMIT`: the SHA1 hash of the commit being tested.
* `COMMIT_MSG`: the message from the commit being tested and built.
* `DOCKER_REPO`: the name of the Docker repository being built.
* `DOCKERFILE_PATH`: the dockerfile currently being built.
* `DOCKER_TAG`: the Docker repository tag being built.
* `IMAGE_NAME`: the name and tag of the Docker repository being built. (This variable is a combination of `DOCKER_REPO:DOCKER_TAG`.)
The current multi-arch image build relies on the original bitwarden_rs Dockerfiles, which use cross-compilation for architectures other than `amd64`, and don't yet support all arch/database/OS combinations. However, cross-compilation is much faster than QEMU-based builds (e.g., using `docker buildx`). This situation may need to be revisited at some point.
## References
* https://docs.docker.com/docker-hub/builds/advanced/
* https://docs.docker.com/engine/reference/commandline/manifest/
* https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/
* https://success.docker.com/article/how-do-i-authenticate-with-the-v2-api

30
hooks/arches.sh Normal file
View file

@ -0,0 +1,30 @@
# The default Debian-based SQLite images support these arches.
#
# Other images (Alpine-based, or with other database backends) currently
# support only a subset of these.
arches=(
amd64
arm32v6
arm32v7
arm64v8
)
case "${DOCKER_REPO}" in
*-mysql)
db=mysql
arches=(amd64)
;;
*-postgresql)
db=postgresql
arches=(amd64)
;;
*)
db=sqlite
;;
esac
if [[ "${DOCKER_TAG}" == *alpine ]]; then
# The Alpine build currently only works for amd64.
os_suffix=.alpine
arches=(amd64)
fi

14
hooks/build Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
echo ">>> Building images..."
source ./hooks/arches.sh
set -ex
for arch in "${arches[@]}"; do
docker build \
-t "${DOCKER_REPO}:${DOCKER_TAG}-${arch}" \
-f docker/${arch}/${db}/Dockerfile${os_suffix} \
.
done

96
hooks/push Executable file
View file

@ -0,0 +1,96 @@
#!/bin/bash
echo ">>> Pushing images..."
export DOCKER_CLI_EXPERIMENTAL=enabled
declare -A annotations=(
[amd64]="--os linux --arch amd64"
[arm32v6]="--os linux --arch arm --variant v6"
[arm32v7]="--os linux --arch arm --variant v7"
[arm64v8]="--os linux --arch arm64 --variant v8"
)
source ./hooks/arches.sh
set -ex
declare -A images
for arch in ${arches[@]}; do
images[$arch]="${DOCKER_REPO}:${DOCKER_TAG}-${arch}"
done
# Push the images that were just built; manifest list creation fails if the
# images (manifests) referenced don't already exist in the Docker registry.
for image in "${images[@]}"; do
docker push "${image}"
done
manifest_lists=("${DOCKER_REPO}:${DOCKER_TAG}")
# If the Docker tag starts with a version number, assume the latest release is
# being pushed. Add an extra manifest (`latest` or `alpine`, as appropriate)
# to make it easier for users to track the latest release.
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ "${DOCKER_TAG}" == *alpine ]]; then
manifest_lists+=(${DOCKER_REPO}:alpine)
else
manifest_lists+=(${DOCKER_REPO}:latest)
fi
fi
for manifest_list in "${manifest_lists[@]}"; do
# Create the (multi-arch) manifest list of arch-specific images.
docker manifest create ${manifest_list} ${images[@]}
# Make sure each image manifest is annotated with the correct arch info.
# Docker does not auto-detect the arch of each cross-compiled image, so
# everything would appear as `linux/amd64` otherwise.
for arch in "${arches[@]}"; do
docker manifest annotate ${annotations[$arch]} ${manifest_list} ${images[$arch]}
done
# Push the manifest list.
docker manifest push --purge ${manifest_list}
done
# Avoid logging credentials and tokens.
set +ex
# Delete the arch-specific tags, if credentials for doing so are available.
# Note that `DOCKER_PASSWORD` must be the actual user password. Passing a JWT
# obtained using a personal access token results in a 403 error with
# {"detail": "access to the resource is forbidden with personal access token"}
if [[ -z "${DOCKER_USERNAME}" || -z "${DOCKER_PASSWORD}" ]]; then
exit 0
fi
# Given a JSON input on stdin, extract the string value associated with the
# specified key. This avoids an extra dependency on a tool like `jq`.
extract() {
local key="$1"
# Extract "<key>":"<val>" (assumes key/val won't contain double quotes).
# The colon may have whitespace on either side.
grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]\+\"" |
# Extract just <val> by deleting the last '"', and then greedily deleting
# everything up to '"'.
sed -e 's/"$//' -e 's/.*"//'
}
echo ">>> Getting API token..."
jwt=$(curl -sS -X POST \
-H "Content-Type: application/json" \
-d "{\"username\":\"${DOCKER_USERNAME}\",\"password\": \"${DOCKER_PASSWORD}\"}" \
"https://hub.docker.com/v2/users/login" |
extract 'token')
# Strip the registry portion from `index.docker.io/user/repo`.
repo="${DOCKER_REPO#*/}"
for arch in ${arches[@]}; do
tag="${DOCKER_TAG}-${arch}"
echo ">>> Deleting '${repo}:${tag}'..."
curl -sS -X DELETE \
-H "Authorization: Bearer ${jwt}" \
"https://hub.docker.com/v2/repositories/${repo}/tags/${tag}/"
done