Daniel García
4 years ago
committed by
GitHub
11 changed files with 181 additions and 112 deletions
@ -0,0 +1,33 @@ |
|||
# The cross-built images have the build arch (`amd64`) embedded in the image |
|||
# manifest, rather than the target arch. For example: |
|||
# |
|||
# $ docker inspect bitwardenrs/server:latest-armv7 | jq -r '.[]|.Architecture' |
|||
# amd64 |
|||
# |
|||
# Recent versions of Docker have started printing a warning when the image's |
|||
# claimed arch doesn't match the host arch. For example: |
|||
# |
|||
# WARNING: The requested image's platform (linux/amd64) does not match the |
|||
# detected host platform (linux/arm/v7) and no specific platform was requested |
|||
# |
|||
# The image still works fine, but the spurious warning creates confusion. |
|||
# |
|||
# Docker doesn't seem to provide a way to directly set the arch of an image |
|||
# at build time. To resolve the build vs. target arch discrepancy, we use |
|||
# Docker Buildx to build a new set of images with the correct target arch. |
|||
# |
|||
# Docker Buildx uses this Dockerfile to build an image for each requested |
|||
# platform. Since the Dockerfile basically consists of a single `FROM` |
|||
# instruction, we're effectively telling Buildx to build a platform-specific |
|||
# image by simply copying the existing cross-built image and setting the |
|||
# correct target arch as a side effect. |
|||
# |
|||
# References: |
|||
# |
|||
# - https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images |
|||
# - https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope |
|||
# - https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact |
|||
# |
|||
ARG LOCAL_REPO |
|||
ARG DOCKER_TAG |
|||
FROM ${LOCAL_REPO}:${DOCKER_TAG}-${TARGETARCH}${TARGETVARIANT} |
@ -1,19 +1,16 @@ |
|||
# The default Debian-based images support these arches for all database connections |
|||
# |
|||
# Other images (Alpine-based) currently |
|||
# support only a subset of these. |
|||
# The default Debian-based images support these arches for all database backends. |
|||
arches=( |
|||
amd64 |
|||
arm32v6 |
|||
arm32v7 |
|||
arm64v8 |
|||
armv6 |
|||
armv7 |
|||
arm64 |
|||
) |
|||
|
|||
if [[ "${DOCKER_TAG}" == *alpine ]]; then |
|||
# The Alpine build currently only works for amd64. |
|||
os_suffix=.alpine |
|||
# The Alpine image build currently only works for certain arches. |
|||
distro_suffix=.alpine |
|||
arches=( |
|||
amd64 |
|||
arm32v7 |
|||
armv7 |
|||
) |
|||
fi |
|||
|
@ -0,0 +1,18 @@ |
|||
#!/bin/bash |
|||
|
|||
set -ex |
|||
|
|||
# Print some environment info in case it's useful for troubleshooting. |
|||
id |
|||
pwd |
|||
df -h |
|||
env |
|||
docker info |
|||
docker version |
|||
|
|||
# Install build dependencies. |
|||
deps=( |
|||
jq |
|||
) |
|||
apt-get update |
|||
apt-get install -y "${deps[@]}" |
@ -1,117 +1,138 @@ |
|||
#!/bin/bash |
|||
|
|||
echo ">>> Pushing images..." |
|||
source ./hooks/arches.sh |
|||
|
|||
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 |
|||
# Join a list of args with a single char. |
|||
# Ref: https://stackoverflow.com/a/17841619 |
|||
join() { local IFS="$1"; shift; echo "$*"; } |
|||
|
|||
set -ex |
|||
|
|||
declare -A images |
|||
echo ">>> Starting local Docker registry..." |
|||
|
|||
# Docker Buildx's `docker-container` driver is needed for multi-platform |
|||
# builds, but it can't access existing images on the Docker host (like the |
|||
# cross-compiled ones we just built). Those images first need to be pushed to |
|||
# a registry -- Docker Hub could be used, but since it's not trivial to clean |
|||
# up those intermediate images on Docker Hub, it's easier to just run a local |
|||
# Docker registry, which gets cleaned up automatically once the build job ends. |
|||
# |
|||
# https://docs.docker.com/registry/deploying/ |
|||
# https://hub.docker.com/_/registry |
|||
# |
|||
# Use host networking so the buildx container can access the registry via |
|||
# localhost. |
|||
# |
|||
docker run -d --name registry --network host registry:2 # defaults to port 5000 |
|||
|
|||
# Docker Hub sets a `DOCKER_REPO` env var with the format `index.docker.io/user/repo`. |
|||
# Strip the registry portion to construct a local repo path for use in `Dockerfile.buildx`. |
|||
LOCAL_REGISTRY="localhost:5000" |
|||
REPO="${DOCKER_REPO#*/}" |
|||
LOCAL_REPO="${LOCAL_REGISTRY}/${REPO}" |
|||
|
|||
echo ">>> Pushing images to local registry..." |
|||
|
|||
for arch in ${arches[@]}; do |
|||
images[$arch]="${DOCKER_REPO}:${DOCKER_TAG}-${arch}" |
|||
docker_image="${DOCKER_REPO}:${DOCKER_TAG}-${arch}" |
|||
local_image="${LOCAL_REPO}:${DOCKER_TAG}-${arch}" |
|||
docker tag "${docker_image}" "${local_image}" |
|||
docker push "${local_image}" |
|||
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 |
|||
echo ">>> Setting up Docker Buildx..." |
|||
|
|||
# Same as earlier, use host networking so the buildx container can access the |
|||
# registry via localhost. |
|||
# |
|||
# Ref: https://github.com/docker/buildx/issues/94#issuecomment-534367714 |
|||
# |
|||
docker buildx create --name builder --use --driver-opt network=host |
|||
|
|||
manifest_lists=("${DOCKER_REPO}:${DOCKER_TAG}") |
|||
echo ">>> Running Docker Buildx..." |
|||
|
|||
# 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) |
|||
tags=("${DOCKER_REPO}:${DOCKER_TAG}") |
|||
|
|||
# If the Docker tag starts with a version number, assume the latest release |
|||
# is being pushed. Add an extra tag (`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) |
|||
tags+=(${DOCKER_REPO}:alpine) |
|||
else |
|||
manifest_lists+=(${DOCKER_REPO}:latest) |
|||
|
|||
# Add an extra `latest-arm32v6` tag; Docker can't seem to properly |
|||
# auto-select that image on Armv6 platforms like Raspberry Pi 1 and Zero |
|||
# (https://github.com/moby/moby/issues/41017). |
|||
# |
|||
# Add this tag only for the SQLite image, as the MySQL and PostgreSQL |
|||
# builds don't currently work on non-amd64 arches. |
|||
# |
|||
# TODO: Also add an `alpine-arm32v6` tag if multi-arch support for |
|||
# Alpine-based bitwarden_rs images is implemented before this Docker |
|||
# issue is fixed. |
|||
if [[ ${DOCKER_REPO} == *server ]]; then |
|||
docker tag "${DOCKER_REPO}:${DOCKER_TAG}-arm32v6" "${DOCKER_REPO}:latest-arm32v6" |
|||
docker push "${DOCKER_REPO}:latest-arm32v6" |
|||
fi |
|||
tags+=(${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} |
|||
tag_args=() |
|||
for tag in "${tags[@]}"; do |
|||
tag_args+=(--tag "${tag}") |
|||
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#*/}" |
|||
|
|||
# Docker Buildx takes a list of target platforms (OS/arch/variant), so map |
|||
# the arch list to a platform list (assuming the OS is always `linux`). |
|||
declare -A arch_to_platform=( |
|||
[amd64]="linux/amd64" |
|||
[armv6]="linux/arm/v6" |
|||
[armv7]="linux/arm/v7" |
|||
[arm64]="linux/arm64" |
|||
) |
|||
platforms=() |
|||
for arch in ${arches[@]}; do |
|||
# Don't delete the `arm32v6` tag; Docker can't seem to properly |
|||
# auto-select that image on Armv6 platforms like Raspberry Pi 1 and Zero |
|||
# (https://github.com/moby/moby/issues/41017). |
|||
if [[ ${arch} == 'arm32v6' ]]; then |
|||
continue |
|||
fi |
|||
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}/" |
|||
platforms+=("${arch_to_platform[$arch]}") |
|||
done |
|||
platforms="$(join "," "${platforms[@]}")" |
|||
|
|||
# Run the build, pushing the resulting images and multi-arch manifest list to |
|||
# Docker Hub. The Dockerfile is read from stdin to avoid sending any build |
|||
# context, which isn't needed here since the actual cross-compiled images |
|||
# have already been built. |
|||
docker buildx build \ |
|||
--network host \ |
|||
--build-arg LOCAL_REPO="${LOCAL_REPO}" \ |
|||
--build-arg DOCKER_TAG="${DOCKER_TAG}" \ |
|||
--platform "${platforms}" \ |
|||
"${tag_args[@]}" \ |
|||
--push \ |
|||
- < ./docker/Dockerfile.buildx |
|||
|
|||
# Add an extra arch-specific tag for `arm32v6`; Docker can't seem to properly |
|||
# auto-select that image on ARMv6 platforms like Raspberry Pi 1 and Zero |
|||
# (https://github.com/moby/moby/issues/41017). |
|||
# |
|||
# Note that we use `arm32v6` instead of `armv6` to be consistent with the |
|||
# existing bitwarden_rs tags, which adhere to the naming conventions of the |
|||
# Docker per-architecture repos (e.g., https://hub.docker.com/u/arm32v6). |
|||
# Unfortunately, these per-arch repo names aren't always consistent with the |
|||
# corresponding platform (OS/arch/variant) IDs, particularly in the case of |
|||
# 32-bit ARM arches (e.g., `linux/arm/v6` is used, not `linux/arm32/v6`). |
|||
# |
|||
# TODO: It looks like this issue should be fixed starting in Docker 20.10.0, |
|||
# so this step can be removed once fixed versions are in wider distribution. |
|||
# |
|||
# Tags: |
|||
# |
|||
# testing => testing-arm32v6 |
|||
# testing-alpine => <ignored> |
|||
# x.y.z => x.y.z-arm32v6, latest-arm32v6 |
|||
# x.y.z-alpine => <ignored> |
|||
# |
|||
if [[ "${DOCKER_TAG}" != *alpine ]]; then |
|||
image="${DOCKER_REPO}":"${DOCKER_TAG}" |
|||
|
|||
# Fetch the multi-arch manifest list and find the digest of the armv6 image. |
|||
filter='.manifests|.[]|select(.platform.architecture=="arm" and .platform.variant=="v6")|.digest' |
|||
digest="$(docker manifest inspect "${image}" | jq -r "${filter}")" |
|||
|
|||
# Pull the armv6 image by digest, retag it, and repush it. |
|||
docker pull "${DOCKER_REPO}"@"${digest}" |
|||
docker tag "${DOCKER_REPO}"@"${digest}" "${image}"-arm32v6 |
|||
docker push "${image}"-arm32v6 |
|||
|
|||
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then |
|||
docker tag "${image}"-arm32v6 "${DOCKER_REPO}:latest"-arm32v6 |
|||
docker push "${DOCKER_REPO}:latest"-arm32v6 |
|||
fi |
|||
fi |
|||
|
Loading…
Reference in new issue