Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add heroku/heroku:24 and heroku/heroku:24-build as multiarch images #245

Merged
merged 58 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
9f2885e
Add heroku-24 and heroku-24-build as multiarch
joshwlewis Jan 10, 2024
d6be7ee
Don't create /app or /workspace or set them as $HOME
joshwlewis Jan 18, 2024
97271d6
Regenerate installed-packages.txt
joshwlewis Jan 18, 2024
d5df82e
Drop mtools. It was a dependency of syslinux, which is no longer inst…
joshwlewis Jan 18, 2024
574316e
Also write the package list for build variants
joshwlewis Jan 19, 2024
6df2151
Regenerate installed packages for heroku-24
joshwlewis Jan 19, 2024
7f1e43c
Drop ghostscript, libgs10, libgs10-dev, gsfonts
joshwlewis Jan 19, 2024
458eda2
Add 24 to the CI matrix
joshwlewis Jan 19, 2024
96694c3
Enable the multiarch driver
joshwlewis Jan 19, 2024
d12b1a0
Don't try to push cnb variants for heroku-24
joshwlewis Jan 19, 2024
e2051d2
Drop language-pack-en
joshwlewis Jan 19, 2024
b586643
Use --load for buildx builds
joshwlewis Jan 19, 2024
afd1c8b
Generate locale for en_us
joshwlewis Jan 20, 2024
29c59a6
Rework scripting to handle multiarch, single arch, linux docker, and …
joshwlewis Jan 24, 2024
cfa06fe
If we're not pushing, we're loading. buildx requires us to be explicit
joshwlewis Jan 24, 2024
bb837b3
Be explicit about what platform to build for 24 single architecture b…
joshwlewis Jan 25, 2024
fd56bce
Detect docker-container driver and containerd snapshotter rather than…
joshwlewis Jan 25, 2024
e50cab0
Regenerate installed packages
joshwlewis Jan 26, 2024
5b28280
Print detailed usage context
joshwlewis Jan 26, 2024
5ce963c
Fix erroneous argument check
joshwlewis Jan 26, 2024
33bea70
Drop brz and python3-dev
joshwlewis Jan 29, 2024
a6b72c9
Update installed packages list
joshwlewis Jan 29, 2024
7edd75d
Drop socat and telnet
joshwlewis Jan 29, 2024
7cc0ae4
Drop explicit ed dependency
joshwlewis Jan 29, 2024
ce38b15
Regenerate package lists for heroku-24
joshwlewis Jan 29, 2024
72e39c9
Generate package lists for all architectures when possible
joshwlewis Feb 6, 2024
84e7125
Add CNB Spec reference
joshwlewis Feb 12, 2024
857ddb1
Add CNB Spec reference
joshwlewis Feb 12, 2024
13ae23d
Address formatting, output, and exit code feedback
joshwlewis Feb 13, 2024
070ec42
Use bash double brackets / parens
joshwlewis Feb 13, 2024
68f4eb1
Use RUN --mount to drop COPY step
joshwlewis Feb 13, 2024
0a7aa0f
Drop redundant label
joshwlewis Feb 13, 2024
86b109f
Separate build and run users for heroku-24
joshwlewis Feb 14, 2024
3ea8c4f
Add ed, socat, telnet back into the package list for heroku-24
joshwlewis Feb 14, 2024
27b4fd8
Drop unused internalTag
joshwlewis Feb 15, 2024
89b7583
Update to libmagickcore-6.q16-7-extra and regenerate package lists
joshwlewis Feb 16, 2024
a35ccec
Fix multi-arch conditional
joshwlewis Feb 16, 2024
ac2833a
Use double brackets for publishing script
joshwlewis Feb 16, 2024
680096c
Drop unnecessary $ in arithmetic
joshwlewis Feb 16, 2024
42b5274
Use noble channel for postgres, now that it is available
joshwlewis Feb 19, 2024
719aafa
Add a comment explaining locale-gen
joshwlewis Feb 19, 2024
ad73002
Update imagemagick policy to match ubuntu defaults
joshwlewis Feb 19, 2024
c9cc345
Reorder magick policy to match more closely to ubuntu
joshwlewis Feb 19, 2024
fbd6fd3
Publish to public temp tags instead of private
joshwlewis Feb 19, 2024
b9aa0b5
Drop shared cache weak secret in imagemagick config
joshwlewis Feb 19, 2024
4aed1d7
Unpublish temp tags after CI
joshwlewis Feb 19, 2024
67172a9
Fixup unpublish step
joshwlewis Feb 21, 2024
2806af8
Stop generating imagemagick policy, since ubuntu's is fine
joshwlewis Feb 21, 2024
f1b1915
Fix syntax for docker hub delete curl command
joshwlewis Feb 21, 2024
09143d1
Update tag deletion to v2 url
joshwlewis Feb 21, 2024
913cee5
Fix error handling during unpublish
joshwlewis Feb 21, 2024
7835e97
Update heroku-24 package lists
joshwlewis Feb 27, 2024
1871eb8
Regenerate heroku-24-build package lists
joshwlewis Feb 28, 2024
c6e771b
Update incorrect usage messaging
joshwlewis Feb 28, 2024
434a606
Fix some leading spaces to tabs in bin/build.sh
dzuelke Feb 29, 2024
4e33e2f
Update curl and jq usage to match languages standards
joshwlewis Feb 29, 2024
baa8a34
Update curl and jq usage to match languages standards
joshwlewis Feb 29, 2024
e582ef7
Construct targetTagName only once, and reindent bin/unpublish-tags.sh
joshwlewis Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
matrix:
stack-version: ["20", "22"]
stack-version: ["20", "22", "24"]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -50,8 +50,13 @@ jobs:
exit 1
fi
- name: Publish to image registries
run: bin/publish-to-registries.sh
run: |-
docker buildx create --use
bin/publish-to-registries.sh
if: success() && (github.ref_name == 'main' || github.ref_type == 'tag')
- name: Unpublish temp tags from this run
run: bin/unpublish-tags.sh
if: always()
- name: Convert docker image and release to Heroku staging
edmorley marked this conversation as resolved.
Show resolved Hide resolved
run: bin/convert-and-publish-to-heroku.sh
if: success() && github.ref_type == 'tag'
173 changes: 143 additions & 30 deletions bin/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,159 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
. bin/stack-helpers.sh

joshwlewis marked this conversation as resolved.
Show resolved Hide resolved
[ $# -eq 1 ] || abort "usage: $(basename "${BASH_SOURCE[0]}") STACK_VERSION"
REPO="heroku/heroku"
STACK_VERSION=${1:-"NAN"}
PUBLISH_SUFFIX=${2:-}
BASE_NAME=$(basename "${BASH_SOURCE[0]}")

STACK_VERSION=$1
print_usage(){
>&2 echo "usage: ${BASE_NAME} STACK_VERSION [PUBLISH_SUFFIX]"
>&2 cat <<-EOF

This script builds heroku base images and writes package lists. It builds
multi-arch images for heroku-24 and newer, and amd64 images for heroku-22 and
older. It works in the following scenarios:

Local builds with Docker Desktop and the 'containerd' snapshotter. In this
mode, all resulting images will be loaded into the local container store.
The 'default' and 'docker-container' drivers both work in this mode.
Note that the 'containerd' snapshotter is not compatible with 'pack'.

For CI tests and package list generation with Docker and the 'default'
Docker driver. In this mode, resulting images will be loaded into the
local container store, the package lists generated, but only for amd64.
The 'default' Docker driver is not able to store/retreive multi-arch images
locally with the default snapshotter, and the 'containerd' snapshotter is
only available with Docker Desktop. The 'docker-container' driver will not
work in this mode (it can't load any images from the default local store).

Publishing images in CI with Docker and the 'docker-container'
driver. Pass in a REPO and PUBLISH_SUFFIX argument to publish images
directly during the build. Since Docker is unable to store/reference
multi-arch images locally, the publish process involves building+pushing
an image to a disposable tag, then retagging it. The 'default' Docker
driver will not work in this mode (it can't build cross-architecture).
EOF
}

[[ $STACK_VERSION =~ ^[0-9]+$ ]] || (>&2 print_usage && abort "fatal: invalid STACK_VERSION")

have_docker_container_driver=
if (docker buildx inspect; true) | grep -q 'Driver:\s*docker-container$'; then
have_docker_container_driver=1
fi

have_containerd_snapshotter=
if docker info -f "{{ .DriverStatus }}" | grep -qF "io.containerd.snapshotter."; then have_containerd_snapshotter=1; fi


if (( STACK_VERSION <= 22 )); then
# heroku/heroku:22 and prior images do not support multiple chip
# architectures or multi-arch images. Instead, they are amd64 only.
DOCKER_ARGS=("build" "--platform=linux/amd64")
# heroku/heroku:22 and prior images need separate *cnb* variants that
# add compatibility for Cloud Native Buildpacks.
VARIANTS=("-build:" "-cnb:" "-cnb-build:-build")
else
# heroku/heroku:24 images and beyond are multi-arch (amd64+arm64) images.
# Due to weak feature support parity between Docker on Linux and Docker
# Desktop building and publishing across platforms has caveats (see the
# top of this file).
if [[ $have_containerd_snapshotter ]] || { [[ $PUBLISH_SUFFIX ]] && [[ $have_docker_container_driver ]]; }; then
DOCKER_ARGS=("buildx" "build" "--platform=linux/amd64,linux/arm64")
elif [[ ! $PUBLISH_SUFFIX ]] && [[ ! $have_docker_container_driver ]]; then
DOCKER_ARGS=("buildx" "build" "--platform=linux/amd64")
>&2 echo "WARNING: heroku-24 and newer images are multi-arch images," \
"but this script is building single architecture images" \
"due to limitations of the current platform." \
"To build a multi-arch image, enable the 'containerd'" \
"snapshotter in Docker Desktop and/or use a 'docker-container'" \
"Docker BuildKit driver."
else
>&2 echo "ERROR: Can't build images with this configuration. Enable" \
"the 'containerd' snapshotter in Docker Desktop, enable" \
"the 'docker-container' driver in Docker, or use this script" \
"in build-only mode (don't provide PUBLISH_SUFFIX argument)."
abort 1
fi
# heroku/heroku:24 and beyond images include CNB specific
# modifications, so separate *cnb* variants are not created.
VARIANTS=("-build:")
fi

if [[ $PUBLISH_SUFFIX ]]; then
# If there is a tag suffix, this script is pushing to a remote registry.
DOCKER_ARGS+=("--push")
else
# Otherwise, load the image into the local image store.
DOCKER_ARGS+=("--load")
fi

write_package_list() {
local image_tag="$1"
local output_file="${2}/installed-packages.txt"
echo '# List of packages present in the final image. Regenerate using bin/build.sh' > "$output_file"
docker run --rm "$image_tag" dpkg-query --show --showformat='${Package}\n' >> "$output_file"
local image_tag="$1"
local dockerfile_dir="$2"

# Extract the stack version from the dockerfile_dir variable (e.g., heroku-24)
local stack_version
stack_version=$(echo "$dockerfile_dir" | sed -n 's/^heroku-\([0-9]*\).*$/\1/p')

local archs=("amd64")
# heroku-24 and newer are multiarch. If containerd is available,
# the package list for each architecture can be generated.
if (( stack_version >= 24 )); then
if [[ $have_containerd_snapshotter ]]; then
archs+=(arm64)
else
>&2 echo "WARNING: Generating package list for single architecture." \
"Use the 'containerd' snapshotter to generate package lists" \
"for all architectures."
fi
fi
local output_file=""
for arch in "${archs[@]}"; do
if (( stack_version >= 24 )); then
output_file="${dockerfile_dir}/installed-packages-${arch}.txt"
else
output_file="${dockerfile_dir}/installed-packages.txt"
fi
display "Generating package list: ${output_file}"
echo "# List of packages present in the final image. Regenerate using bin/build.sh" > "$output_file"
docker run --rm --platform="linux/${arch}" "$image_tag" dpkg-query --show --showformat='${Package}\n' >> "$output_file"
done
}

RUN_IMAGE_TAG="heroku/heroku:${STACK_VERSION}"
RUN_IMAGE_TAG="${REPO}:${STACK_VERSION}${PUBLISH_SUFFIX}"
RUN_DOCKERFILE_DIR="heroku-${STACK_VERSION}"
[[ -d "${RUN_DOCKERFILE_DIR}" ]] || abort "fatal: directory ${RUN_DOCKERFILE_DIR} not found"
display "Building ${RUN_DOCKERFILE_DIR} / ${RUN_IMAGE_TAG} Heroku run image"
docker build --pull --tag "${RUN_IMAGE_TAG}" "${RUN_DOCKERFILE_DIR}" | indent
write_package_list "${RUN_IMAGE_TAG}" "${RUN_DOCKERFILE_DIR}"

# The --pull option is not used for variants to ensure they are based on the
# run image built above, rather than the one last published to Docker Hub.

BUILD_IMAGE_TAG="${RUN_IMAGE_TAG}-build"
BUILD_DOCKERFILE_DIR="${RUN_DOCKERFILE_DIR}-build"
display "Building ${BUILD_DOCKERFILE_DIR} / ${BUILD_IMAGE_TAG} Heroku build image"
docker build --tag "$BUILD_IMAGE_TAG" "$BUILD_DOCKERFILE_DIR" | indent
write_package_list "$BUILD_IMAGE_TAG" "$BUILD_DOCKERFILE_DIR"
[[ -d "${RUN_DOCKERFILE_DIR}" ]] || abort "fatal: directory ${RUN_DOCKERFILE_DIR} not found"
display "Building ${RUN_DOCKERFILE_DIR} / ${RUN_IMAGE_TAG} image"
# The --pull option is used for the run image, so that the latest updates
# from upstream ubuntu images are included.
docker "${DOCKER_ARGS[@]}" --pull \
--tag "${RUN_IMAGE_TAG}" "${RUN_DOCKERFILE_DIR}" | indent
write_package_list "${RUN_IMAGE_TAG}" "${RUN_DOCKERFILE_DIR}"

# write_package_list is not needed for *cnb* variants, as they don't install
# any additional packages over their non-*cnb* counterparts.
for VARIANT in "${VARIANTS[@]}"; do
VARIANT_NAME=$(echo "$VARIANT" | cut -d ":" -f 1)
DEPENDENCY_NAME=$(echo "$VARIANT" | cut -d ":" -f 2)
VARIANT_IMAGE_TAG="${REPO}:${STACK_VERSION}${VARIANT_NAME}${PUBLISH_SUFFIX}"
VARIANT_DOCKERFILE_DIR="heroku-${STACK_VERSION}${VARIANT_NAME}"
DEPENDENCY_IMAGE_TAG="${REPO}:${STACK_VERSION}${DEPENDENCY_NAME}${PUBLISH_SUFFIX}"

CNB_RUN_IMAGE_TAG="${RUN_IMAGE_TAG}-cnb"
CNB_RUN_DOCKERFILE_DIR="${RUN_DOCKERFILE_DIR}-cnb"
display "Building ${CNB_RUN_DOCKERFILE_DIR} / ${CNB_RUN_IMAGE_TAG} CNB run image"
docker build --tag "$CNB_RUN_IMAGE_TAG" "$CNB_RUN_DOCKERFILE_DIR" | indent
[[ -d "${VARIANT_DOCKERFILE_DIR}" ]] || abort "fatal: directory ${VARIANT_DOCKERFILE_DIR} not found"
display "Building ${VARIANT_DOCKERFILE_DIR} / ${VARIANT_IMAGE_TAG} image"
# The --pull option is not used for variants since they depend on images
# built earlier in this script.
docker "${DOCKER_ARGS[@]}" --build-arg "BASE_IMAGE=${DEPENDENCY_IMAGE_TAG}" \
--tag "${VARIANT_IMAGE_TAG}" "${VARIANT_DOCKERFILE_DIR}" | indent

CNB_BUILD_IMAGE_TAG="${RUN_IMAGE_TAG}-cnb-build"
CNB_BUILD_DOCKERFILE_DIR="${RUN_DOCKERFILE_DIR}-cnb-build"
display "Building ${CNB_BUILD_DOCKERFILE_DIR} / ${CNB_BUILD_IMAGE_TAG} CNB build image"
docker build --tag "$CNB_BUILD_IMAGE_TAG" "$CNB_BUILD_DOCKERFILE_DIR" | indent
# generate the package list for non-cnb variants. cnb variants don't
# influence the list of installed packages.
if [[ ! "$VARIANT_NAME" = -cnb* ]]; then
write_package_list "$VARIANT_IMAGE_TAG" "$VARIANT_DOCKERFILE_DIR"
fi
done

display "Size breakdown..."
docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}" \
| grep -E "(ubuntu|heroku)" | sed '1!G;h;$!d' | indent
| grep -E "(ubuntu|heroku)" | sed '1!G;h;$!d' | indent
29 changes: 18 additions & 11 deletions bin/publish-to-registries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
set -euo pipefail
set -x

bin/build.sh "${STACK_VERSION}"

(
# Disable tracing (until the end of this subshell) to prevent logging registry tokens.
set +x
Expand All @@ -14,25 +12,34 @@ bin/build.sh "${STACK_VERSION}"
)

push_group() {
local targetTagBase="$1"
local targetTagSuffix="$2"
for variant in "" "-build" "-cnb" "-cnb-build"; do
source="${publicTag}${variant}"
target="${targetTagBase}${variant}${targetTagSuffix}"
local tagBase="$1"
local sourceTagSuffix="$2"
local targetTagSuffix="$3"
variants=("" "-build")
if (( STACK_VERSION <= 22 )); then
variants+=("-cnb" "-cnb-build")
fi
for variant in "${variants[@]}"; do
source="${tagBase}${variant}${sourceTagSuffix}"
target="${tagBase}${variant}${targetTagSuffix}"
docker tag "${source}" "${target}"
docker push "${target}"
done
}

tempTagSuffix=".temp-${GITHUB_RUN_ID}"
# build+push to a temporary tag (e.g. heroku/heroku:22.temp_12345678)
bin/build.sh "${STACK_VERSION}" "${tempTagSuffix}"

publicTag="heroku/heroku:${STACK_VERSION}"

# Push nightly tags to Docker Hub (e.g. heroku/heroku:22.nightly)
push_group "${publicTag}" ".nightly"
push_group "${publicTag}" "${tempTagSuffix}" ".nightly"

if [ "$GITHUB_REF_TYPE" == 'tag' ]; then
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then
# Push release tags to Docker Hub (e.g. heroku/heroku:22.v99)
push_group "${publicTag}" ".${GITHUB_REF_NAME}"
push_group "${publicTag}" "${tempTagSuffix}" ".${GITHUB_REF_NAME}"

# Push latest/no-suffix tags to Docker Hub (e.g. heroku/heroku:22)
push_group "${publicTag}" ""
push_group "${publicTag}" "${tempTagSuffix}" ""
fi
38 changes: 38 additions & 0 deletions bin/unpublish-tags.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash

set -euo pipefail

dockerhub_token=$(curl -sS -f --retry 3 --retry-connrefused --connect-timeout 5 --max-time 30 -H "Content-Type: application/json" -X POST -d "{\"username\": \"${DOCKER_HUB_USERNAME}\", \"password\": \"${DOCKER_HUB_TOKEN}\"}" https://hub.docker.com/v2/users/login/ | jq --exit-status -r .token)

unpublish_group() {
local stackVersion="$1"
local targetTagSuffix="$2"
local status=0
variants=("" "-build")
if (( stackVersion <= 22 )); then
variants+=("-cnb" "-cnb-build")
fi
for variant in "${variants[@]}"; do
targetTagName="${stackVersion}${variant}${targetTagSuffix}"
echo "Deleting heroku/heroku:${targetTagName}"
response=$(curl -sS --retry 3 --retry-connrefused --connect-timeout 5 --max-time 30 -X DELETE \
-H "Authorization: JWT ${dockerhub_token}" \
"https://hub.docker.com/v2/namespaces/heroku/repositories/heroku/tags/${targetTagName}"
)

if [[ -z $response ]]; then
>&2 echo "Deleted."
elif [[ $response =~ "tag not found" ]]; then
>&2 echo "Tag does not exist."
else
>&2 echo "Couldn't delete. Response: ${response}"
status=22
fi
done
return $status
}

stackVersion="${1:-$STACK_VERSION}"
tempTagSuffix="${2:-".temp-$GITHUB_RUN_ID"}"
# delete each tag in a group on Docker Hub.
unpublish_group "${stackVersion}" "${tempTagSuffix}"
3 changes: 2 additions & 1 deletion heroku-20-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
FROM heroku/heroku:20
ARG BASE_IMAGE=heroku/heroku:20
FROM $BASE_IMAGE
COPY setup.sh /tmp/setup.sh
RUN /tmp/setup.sh
3 changes: 2 additions & 1 deletion heroku-20-cnb-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM heroku/heroku:20-build
ARG BASE_IMAGE=heroku/heroku:20-build
FROM $BASE_IMAGE

RUN groupadd heroku --gid 1000 && \
useradd heroku -u 1000 -g 1000 -s /bin/bash -m
Expand Down
3 changes: 2 additions & 1 deletion heroku-20-cnb/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM heroku/heroku:20
ARG BASE_IMAGE=heroku/heroku:20
FROM $BASE_IMAGE

RUN ln -s /workspace /app

Expand Down
3 changes: 2 additions & 1 deletion heroku-22-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
FROM heroku/heroku:22
ARG BASE_IMAGE=heroku/heroku:22
FROM $BASE_IMAGE
COPY setup.sh /tmp/setup.sh
RUN /tmp/setup.sh
3 changes: 2 additions & 1 deletion heroku-22-cnb-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM heroku/heroku:22-build
ARG BASE_IMAGE=heroku/heroku:22-build
FROM $BASE_IMAGE

RUN groupadd heroku --gid 1000 && \
useradd heroku -u 1000 -g 1000 -s /bin/bash -m
Expand Down
3 changes: 2 additions & 1 deletion heroku-22-cnb/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM heroku/heroku:22
ARG BASE_IMAGE=heroku/heroku:22
FROM $BASE_IMAGE

RUN ln -s /workspace /app

Expand Down
1 change: 1 addition & 0 deletions heroku-24-build/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
installed-packages*.txt
10 changes: 10 additions & 0 deletions heroku-24-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ARG BASE_IMAGE=heroku/heroku:24
FROM $BASE_IMAGE
USER root
RUN --mount=target=/build /build/setup.sh

joshwlewis marked this conversation as resolved.
Show resolved Hide resolved
# https://github.com/buildpacks/spec/blob/main/platform.md#build-image
USER 1002
ENV CNB_USER_ID=1002
ENV CNB_GROUP_ID=1000
ENV CNB_STACK_ID "heroku-24"
Loading