Skip to content

Commit

Permalink
Merge pull request #1 from jericop/initial-commit
Browse files Browse the repository at this point in the history
Initial commit
  • Loading branch information
jericop authored Mar 21, 2024
2 parents 6f14e25 + 67e5e01 commit efa0350
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 1 deletion.
16 changes: 16 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Build and push to local registry
on: pull_request
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- uses: buildpacks/github-actions/[email protected]
- uses: buildpacks/github-actions/[email protected]
- id: local-registry-push
run: |
./pack-with-buildkit.sh
27 changes: 27 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Build and push to ghcr.io
on:
push:
branches:
- "main"
env:
PUBLISH_TO_GHCR_IO_IMAGE_URI: ghcr.io/jericop/inline-app:latest
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- uses: buildpacks/github-actions/[email protected]
- uses: buildpacks/github-actions/[email protected]
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- id: ghcr-push
run: |
./pack-with-buildkit.sh
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
cnb-build-files
Dockerfile
lifecycle-commands.log
pack-build.log
78 changes: 78 additions & 0 deletions Dockerfile-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Pin the lifecycle version so we can be sure of the behavior if we run this script in the future
FROM chainguard/crane:latest as crane
FROM buildpacksio/lifecycle:0.16.3 as lifecycle
FROM ghcr.io/jericop/builder-jammy:latest
USER root

COPY --from=crane /usr/bin/crane /usr/local/bin/crane
COPY --from=lifecycle /cnb/lifecycle /cnb/lifecycle
COPY ./cnb-build-files/cnb/buildpacks /cnb/buildpacks
COPY ./cnb-build-files/layers /layers

RUN mkdir -p /cache
RUN chown -R cnb:cnb /workspace /cache
RUN chown -R cnb:cnb /layers /platform
RUN find /layers

USER cnb

ENV CNB_PLATFORM_API=0.9

COPY ./ /workspace

WORKDIR /workspace

# Add RUN command(s) below here to run the lifecycle commands and build and export the app to a registry
# It also fixes the architecture because it always gets set to amd64 when running in buildkit.

RUN <<RUN_EOF

export image_arch=amd64
if [ $(arch) = "aarch64" ]; then
export image_arch=arm64
fi

export image_uri="localhost:55000/inline-app:$image_arch"

echo "#!/bin/bash" > /workspace/lifecycle-build.sh
cat <<IN_BUILDKIT_LIFECYCLE_SCRIPT_EOF >> /workspace/lifecycle-build.sh

set -euo pipefail
set -x

/cnb/lifecycle/analyzer -log-level debug -stack /layers/stack.toml -run-image ghcr.io/jericop/run-jammy:latest localhost:55000/inline-app:$image_arch
/cnb/lifecycle/detector -app /workspace -log-level debug
/cnb/lifecycle/restorer -cache-dir /cache -log-level debug
/cnb/lifecycle/builder -app /workspace -log-level debug
/cnb/lifecycle/exporter -log-level debug -app /workspace -cache-dir /cache -stack /layers/stack.toml localhost:55000/inline-app:$image_arch

crane pull $image_uri image.tar

mkdir -p contents
tar -xvf image.tar -C contents
cd contents
ls
cat manifest.json | jq -r '.[0].Config'
current_config=\$(cat manifest.json | jq -r '.[0].Config')
echo \$current_config
cat \$current_config | jq -c ".architecture = \"\${image_arch}\" | .os = \"linux\"" | tr -d '\n' > newconfig.json
new_config="sha256:$(sha256sum newconfig.json | cut -d' ' -f1)"
rm -f "\${current_config}"
mv newconfig.json "\${new_config}"
cat manifest.json| jq -c ".[0].Config = \"\${new_config}\"" | tr -d '\n' > newmanifest.json
mv -f newmanifest.json manifest.json
tar -cvf ../fixed-arch-image.tar *
cd ..

crane push fixed-arch-image.tar $image_uri

rm -rf image.tar fixed-arch-image.tar contents

IN_BUILDKIT_LIFECYCLE_SCRIPT_EOF

# Run the lifecycle script and ensure correct architecture is set
chmod +x /workspace/lifecycle-build.sh
/workspace/lifecycle-build.sh

RUN_EOF

27 changes: 27 additions & 0 deletions Dockerfile-initial
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Pin the lifecycle version so we can be sure of the behavior if we run this script in the future
FROM chainguard/crane:latest as crane
FROM buildpacksio/lifecycle:0.16.3 as lifecycle
FROM ghcr.io/jericop/builder-jammy:latest
USER root

COPY --from=crane /usr/bin/crane /usr/local/bin/crane
COPY --from=lifecycle /cnb/lifecycle /cnb/lifecycle
COPY ./cnb-build-files/cnb/buildpacks /cnb/buildpacks
COPY ./cnb-build-files/layers /layers

RUN mkdir -p /cache
RUN chown -R cnb:cnb /workspace /cache
RUN chown -R cnb:cnb /layers /platform
RUN find /layers

USER cnb

ENV CNB_PLATFORM_API=0.9

COPY ./ /workspace

WORKDIR /workspace

# Add RUN command(s) below here to run the lifecycle commands and build and export the app to a registry
# It also fixes the architecture because it always gets set to amd64 when running in buildkit.

36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
# cnb-lfx-buildkit-poc
# cnb-lfx-buildkit-poc

This repo is an initial proof-of-concept for the following LFX mentorship project.

[CNCF - Cloud Native Buildpacks: Proof of concept making multiarch images with buildkit (2024 Term 1)](https://mentorship.lfx.linuxfoundation.org/project/2c5ced86-d23b-41f5-aec3-59730e29f092)

It is heavliy inspired by the following article. While the article is invaluable for understanding how the lifecycle works, it requires a fair amount of manual steps, which must be repeated if any changes are made to buildpacks. For the sake of simplicity, I am using an inline buildpack and running `pack build` once in order to copy out the necessary files.

* https://medium.com/buildpacks/unpacking-cloud-native-buildpacks-ff51b5a767bf

## Primary objective

The primary objective is to create a Dockerfile that has all of the lifecycle commands needed to build and push architecture-specific images to a registry. `Dockerfile-example` is an example of how it works. You can then combine the architecture-specific images into a manifest list (multi-arch) image.

## `pack-with-buildkit.sh`

This script will do the following:

* Sets up a buildkit builder with `host` network access
* Starts a local registry on a random port
* Run `pack build` using the `project.toml` which has an inline buildpack that copies the files we need to /workspace
* Copies the files we need out of the container built with pack so we can use them in a new build (with buildkit)
* Generates a `Dockerfile` from `Dockerfile-initial` that will look like `Dockerfile-example`
* Runs `docker buildx build --platform linux/amd64,linux/arm64` to execute the lifecycle commands through buildkit
* This builds and publishes images to the local registry with `amd64` and `arm64` tags
* There is some hackery that happens to fix the architecture (for now)
* Finally it creates a manifest list with the `amd64` and `arm64` tagged images

## Requirements

* Multi-arch builders are required. I'm using ones that I build and publish.

## Published image(s)

* ghcr.io/jericop/inline-app:latest
135 changes: 135 additions & 0 deletions pack-with-buildkit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env bash

set -euo pipefail

repo_name="mybuildpack"
registry_name="local-registry"
builder_name="${registry_name}"

cleanup() {
docker buildx stop "${builder_name}" > /dev/null 2>&1
docker buildx rm "${builder_name}" > /dev/null 2>&1

log_message "Stopping local registry"
docker stop "${registry_name}" > /dev/null 2>&1
}

log_message() {
echo "$1"
}

# https://github.com/docker/buildx/issues/166#issuecomment-1804970076
# First, we need to create our own buildx builder that uses the host nework and docker-container driver so that we can get multiarch support
log_message "Setting up docker buildx builder"
docker buildx use "${builder_name}" > /dev/null 2>&1 || docker buildx create --name "${builder_name}" --driver docker-container --driver-opt network=host --bootstrap --use > /dev/null 2>&1

# Next, we need to check if a registry is already running
log_message "Setting up a local registry on random port"
docker container inspect "${registry_name}" > /dev/null 2>&1 || docker run -d -e REGISTRY_STORAGE_DELETE_ENABLED=true -p 0:5000 --rm --name "${registry_name}" registry:2 > /dev/null 2>&1

# Get local registry port since we asked docker to assign a random port by choosing port 0
registry_port=$(docker inspect "${registry_name}" | jq -r '.[0].NetworkSettings.Ports["5000/tcp"][0].HostPort')
log_message "Local registry is listening on port ${registry_port}"

# Rather than creating the necessary files manually, we let pack generate and the inline buildpack
# copy them to /layers/build-files we publish to a local registry so we can capture
pack build localhost:$registry_port/inline-app \
--publish --verbose --network host 2>&1 | tee pack-build.log \
| grep -B3 -A4 "Args: '/cnb/lifecycle" > lifecycle-commands.log

docker pull localhost:$registry_port/inline-app

if [ -d cnb-build-files ]; then
rm -rf cnb-build-files
fi
mkdir -p cnb-build-files

# Copy the files created by pack during the build from the volume and into the local directory
docker run --rm --entrypoint bash --user root \
--volume $(pwd)/cnb-build-files:/hostmnt \
localhost:$registry_port/inline-app \
-c 'cp -R /workspace/build-files/* /hostmnt/'

# We copy the initial Dockerfile (without lifecycle commands) to Dockerfile
cp Dockerfile-initial Dockerfile

# Then we add a run command (heredoc) with the lifecycle commands to build and push the architecture-specific images
cat <<ADD_RUN_COMMAND_TO_DOCKERFILE_EOF >> Dockerfile
RUN <<RUN_EOF
export image_arch=amd64
if [ \$(arch) = "aarch64" ]; then
export image_arch=arm64
fi
export image_uri="localhost:$registry_port/inline-app:\$image_arch"
echo "#!/bin/bash" > /workspace/lifecycle-build.sh
cat <<IN_BUILDKIT_LIFECYCLE_SCRIPT_EOF >> /workspace/lifecycle-build.sh
set -euo pipefail
set -x
ADD_RUN_COMMAND_TO_DOCKERFILE_EOF

# Add the lifecycle commands from the saved log output of `pack build` we ran above
while read cmd; do
if echo $cmd | grep -q "localhost:$registry_port/inline-app\$"; then
echo "$cmd:\$image_arch" >> Dockerfile
else
echo "$cmd" >> Dockerfile
fi
done < <(grep Args: lifecycle-commands.log | cut -d"'" -f2)

cat <<'CONTINUE_ADD_RUN_COMMAND_TO_DOCKERFILE_EOF' >> Dockerfile
crane pull $image_uri image.tar
mkdir -p contents
tar -xvf image.tar -C contents
cd contents
ls
cat manifest.json | jq -r '.[0].Config'
current_config=\$(cat manifest.json | jq -r '.[0].Config')
echo \$current_config
cat \$current_config | jq -c ".architecture = \"\${image_arch}\" | .os = \"linux\"" | tr -d '\n' > newconfig.json
new_config="sha256:$(sha256sum newconfig.json | cut -d' ' -f1)"
rm -f "\${current_config}"
mv newconfig.json "\${new_config}"
cat manifest.json| jq -c ".[0].Config = \"\${new_config}\"" | tr -d '\n' > newmanifest.json
mv -f newmanifest.json manifest.json
tar -cvf ../fixed-arch-image.tar *
cd ..
crane push fixed-arch-image.tar $image_uri
rm -rf image.tar fixed-arch-image.tar contents
IN_BUILDKIT_LIFECYCLE_SCRIPT_EOF
# Run the lifecycle script and ensure correct architecture is set
chmod +x /workspace/lifecycle-build.sh
/workspace/lifecycle-build.sh
RUN_EOF
CONTINUE_ADD_RUN_COMMAND_TO_DOCKERFILE_EOF

# After all of that, we are finally ready to build the multi-arch images using the lifecycle with buildkit.
# We are not publishing with this command because the lifecyle will publish the images to the local registry for us.
docker buildx build --tag ignored --platform linux/amd64,linux/arm64 .

# Now we can combine the linux/amd64 and linux/arm64 images into a manifest list and push it to the local registry
docker buildx imagetools create \
--tag localhost:$registry_port/inline-app:latest \
localhost:$registry_port/inline-app:amd64 \
localhost:$registry_port/inline-app:arm64

crane manifest localhost:$registry_port/inline-app:latest | jq

if [[ ! -z "${PUBLISH_TO_GHCR_IO_IMAGE_URI:-}" ]]; then
crane copy localhost:$registry_port/inline-app:latest ghcr.io/jericop/inline-app:latest
log_message "Published image to: $PUBLISH_TO_GHCR_IO_IMAGE_URI"
fi

# This image can then be copied to another registry
38 changes: 38 additions & 0 deletions project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[project]
name = "inline-app"

# stack images need to be multi-arch for this to work
[build]
builder = "ghcr.io/jericop/builder-jammy:latest"

[[build.buildpacks]]
id = "inline/shell-buildpack"

[build.buildpacks.script]
api = "0.8"
inline = """
set -x
# sleep 300
build_files=/workspace/build-files
mkdir -p $build_files/cnb
mkdir -p $build_files/layers
ls /cnb/*.toml
cp -r /cnb/*.toml $build_files/cnb/
cp -r /cnb/buildpacks $build_files/cnb/
cp /cnb/order.toml $build_files/layers/
cp /layers/*.toml $build_files/layers/
find $build_files
cat <<EOF > ${1}/launch.toml
[[processes]]
type = 'web'
command = 'bin/bash'
default = true
EOF
"""

0 comments on commit efa0350

Please sign in to comment.