From d5d09a5c7e391d7dde457de9c9d37f4573ca298f Mon Sep 17 00:00:00 2001 From: lihe Date: Mon, 14 Sep 2026 11:43:52 +0800 Subject: [PATCH] build: add runtime shells and publish consensus images --- .github/scripts/test-runtime-image.sh | 47 ++++++++++++++++++++++++ .github/workflows/build.yml | 52 +++++++++++++++++++-------- .github/workflows/release.yml | 52 +++++++++++++++++++-------- README.md | 15 +++++--- deployments/Dockerfile.consensus | 17 +++++++-- deployments/Dockerfile.execution | 15 ++++++-- 6 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 .github/scripts/test-runtime-image.sh diff --git a/.github/scripts/test-runtime-image.sh b/.github/scripts/test-runtime-image.sh new file mode 100644 index 0000000..a6bbcab --- /dev/null +++ b/.github/scripts/test-runtime-image.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +image_ref="${1:?usage: test-runtime-image.sh IMAGE BINARY}" +binary="${2:?usage: test-runtime-image.sh IMAGE BINARY}" + +# Use the image's default user and isolated temporary files, never a node volume. +docker run --rm --network none --entrypoint /bin/bash "$image_ref" -euxc ' + [[ "$(id -u)" == 999 && "$(id -g)" == 999 ]] + for command in bash sh cat touch tail rm mkdir mktemp; do + command -v "$command" + done + test -s /etc/ssl/certs/ca-certificates.crt + test_dir=$(mktemp -d) + + # The init script must delete only the discovery key, including on a rerun. + mkdir "$test_dir/execution" + touch "$test_dir/execution/discovery-secret" "$test_dir/execution/keep" + for attempt in 1 2; do + bash -euc '\''rm -rf -- "$1/execution/discovery-secret"'\'' bash "$test_dir" + test ! -e "$test_dir/execution/discovery-secret" + test -f "$test_dir/execution/keep" + done + + printf "normal\n" > "$test_dir/runmode" + runmode=$(cat "$test_dir/runmode") + [[ X${runmode} == Xnormal ]] + printf "prune\n" > "$test_dir/runmode" + [[ "$(tail -n 1 "$test_dir/runmode")" == prune ]] + runmode=$(cat "$test_dir/runmode") + if [[ X${runmode} != Xnormal ]]; then + if [[ X${runmode} == Xprune ]]; then + touch "$test_dir/prune.done" + fi + touch "$test_dir/done.done" + tail -f /dev/null & + child=$! + kill -TERM "$child" + status=0 + wait "$child" || status=$? + [[ "$status" == 143 ]] + fi + test -f "$test_dir/prune.done" + test -f "$test_dir/done.done" + /bin/sh -eu -c '\''test "$(cat "$1/runmode")" = prune'\'' sh "$test_dir" + exec "/usr/local/bin/$1" --version +' bash "$binary" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e18a95..e5e93de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,24 +18,31 @@ permissions: env: AWS_REGION: us-east-1 REGISTRY: public.ecr.aws/b2h7a5c4/chaintable - IMAGE_NAME: arc-node CHAINS: "arc" - DOCKERFILE: deployments/Dockerfile.execution - BAKE_TARGET: arc-execution jobs: build-artifacts: - name: Build ${{ matrix.arch }} + name: Build ${{ matrix.component }} ${{ matrix.arch }} # Same-repository PRs publish review images. External forks cannot receive publishing credentials. if: github.repository == 'Chaintable/arc-node' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) strategy: fail-fast: false matrix: + arch: [amd64, arm64] + component: [execution, consensus] include: - arch: amd64 runner: ubuntu-latest - arch: arm64 runner: ubuntu-24.04-arm + - component: execution + image_name: arc-node + - component: consensus + image_name: arc-consensus + env: + IMAGE_NAME: ${{ matrix.image_name }} + DOCKERFILE: deployments/Dockerfile.${{ matrix.component }} + BAKE_TARGET: arc-${{ matrix.component }} runs-on: ${{ matrix.runner }} timeout-minutes: 180 permissions: @@ -117,8 +124,8 @@ jobs: ${{ env.BAKE_TARGET }}.platform=linux/${{ matrix.arch }} ${{ env.BAKE_TARGET }}.tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.revision }}-${{ matrix.arch }} ${{ env.BAKE_TARGET }}.output=type=registry - ${{ env.BAKE_TARGET }}.cache-from=type=gha,scope=arc-execution-${{ matrix.arch }} - ${{ env.BAKE_TARGET }}.cache-to=type=gha,mode=max,scope=arc-execution-${{ matrix.arch }} + ${{ env.BAKE_TARGET }}.cache-from=type=gha,scope=arc-${{ matrix.component }}-${{ matrix.arch }} + ${{ env.BAKE_TARGET }}.cache-to=type=gha,mode=max,scope=arc-${{ matrix.component }}-${{ matrix.arch }} env: BUILDX_NO_DEFAULT_ATTESTATIONS: 1 CI: "true" @@ -140,10 +147,25 @@ jobs: exit 1 fi + - name: Verify runtime shell and utilities + env: + REVISION: ${{ steps.vars.outputs.revision }} + COMPONENT: ${{ matrix.component }} + ARCH: ${{ matrix.arch }} + run: | + bash .github/scripts/test-runtime-image.sh \ + "${REGISTRY}/${IMAGE_NAME}:${REVISION}-${ARCH}" "arc-node-${COMPONENT}" + merge-docker-manifest: - name: Merge multi-arch manifest + name: Merge ${{ matrix.image_name }} multi-arch manifest runs-on: ubuntu-latest needs: build-artifacts + strategy: + fail-fast: false + matrix: + image_name: [arc-node, arc-consensus] + env: + IMAGE_NAME: ${{ matrix.image_name }} permissions: contents: read id-token: write @@ -204,10 +226,12 @@ jobs: "${REGISTRY}/${IMAGE_NAME}:${REVISION}-amd64" \ "${REGISTRY}/${IMAGE_NAME}:${REVISION}-arm64" docker buildx imagetools inspect "${REGISTRY}/${IMAGE_NAME}:${REVISION}" - # Writer aliases reference the same multi-architecture manifest, without rebuilding. - for chain in ${CHAINS}; do - docker buildx imagetools create \ - --tag "${REGISTRY}/${chain}-writer:${REVISION}" \ - "${REGISTRY}/${IMAGE_NAME}:${REVISION}" - docker buildx imagetools inspect "${REGISTRY}/${chain}-writer:${REVISION}" - done + # Only the execution image has Writer aliases; never alias consensus as Writer. + if [[ "$IMAGE_NAME" == "arc-node" ]]; then + for chain in ${CHAINS}; do + docker buildx imagetools create \ + --tag "${REGISTRY}/${chain}-writer:${REVISION}" \ + "${REGISTRY}/${IMAGE_NAME}:${REVISION}" + docker buildx imagetools inspect "${REGISTRY}/${chain}-writer:${REVISION}" + done + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5a99bc..ab97898 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,24 +15,31 @@ permissions: env: AWS_REGION: us-east-1 REGISTRY: public.ecr.aws/b2h7a5c4/chaintable - IMAGE_NAME: arc-node CHAINS: "arc" - DOCKERFILE: deployments/Dockerfile.execution - BAKE_TARGET: arc-execution jobs: build-artifacts: - name: Build ${{ matrix.arch }} + name: Build ${{ matrix.component }} ${{ matrix.arch }} # Same-repository PRs publish review images. External forks cannot receive publishing credentials. if: github.repository == 'Chaintable/arc-node' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) strategy: fail-fast: false matrix: + arch: [amd64, arm64] + component: [execution, consensus] include: - arch: amd64 runner: ubuntu-latest - arch: arm64 runner: ubuntu-24.04-arm + - component: execution + image_name: arc-node + - component: consensus + image_name: arc-consensus + env: + IMAGE_NAME: ${{ matrix.image_name }} + DOCKERFILE: deployments/Dockerfile.${{ matrix.component }} + BAKE_TARGET: arc-${{ matrix.component }} runs-on: ${{ matrix.runner }} timeout-minutes: 180 permissions: @@ -114,8 +121,8 @@ jobs: ${{ env.BAKE_TARGET }}.platform=linux/${{ matrix.arch }} ${{ env.BAKE_TARGET }}.tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.revision }}-${{ matrix.arch }} ${{ env.BAKE_TARGET }}.output=type=registry - ${{ env.BAKE_TARGET }}.cache-from=type=gha,scope=arc-execution-${{ matrix.arch }} - ${{ env.BAKE_TARGET }}.cache-to=type=gha,mode=max,scope=arc-execution-${{ matrix.arch }} + ${{ env.BAKE_TARGET }}.cache-from=type=gha,scope=arc-${{ matrix.component }}-${{ matrix.arch }} + ${{ env.BAKE_TARGET }}.cache-to=type=gha,mode=max,scope=arc-${{ matrix.component }}-${{ matrix.arch }} env: BUILDX_NO_DEFAULT_ATTESTATIONS: 1 CI: "true" @@ -137,10 +144,25 @@ jobs: exit 1 fi + - name: Verify runtime shell and utilities + env: + REVISION: ${{ steps.vars.outputs.revision }} + COMPONENT: ${{ matrix.component }} + ARCH: ${{ matrix.arch }} + run: | + bash .github/scripts/test-runtime-image.sh \ + "${REGISTRY}/${IMAGE_NAME}:${REVISION}-${ARCH}" "arc-node-${COMPONENT}" + merge-docker-manifest: - name: Merge multi-arch manifest + name: Merge ${{ matrix.image_name }} multi-arch manifest runs-on: ubuntu-latest needs: build-artifacts + strategy: + fail-fast: false + matrix: + image_name: [arc-node, arc-consensus] + env: + IMAGE_NAME: ${{ matrix.image_name }} permissions: contents: read id-token: write @@ -201,10 +223,12 @@ jobs: "${REGISTRY}/${IMAGE_NAME}:${REVISION}-amd64" \ "${REGISTRY}/${IMAGE_NAME}:${REVISION}-arm64" docker buildx imagetools inspect "${REGISTRY}/${IMAGE_NAME}:${REVISION}" - # Writer aliases reference the same multi-architecture manifest, without rebuilding. - for chain in ${CHAINS}; do - docker buildx imagetools create \ - --tag "${REGISTRY}/${chain}-writer:${REVISION}" \ - "${REGISTRY}/${IMAGE_NAME}:${REVISION}" - docker buildx imagetools inspect "${REGISTRY}/${chain}-writer:${REVISION}" - done + # Only the execution image has Writer aliases; never alias consensus as Writer. + if [[ "$IMAGE_NAME" == "arc-node" ]]; then + for chain in ${CHAINS}; do + docker buildx imagetools create \ + --tag "${REGISTRY}/${chain}-writer:${REVISION}" \ + "${REGISTRY}/${IMAGE_NAME}:${REVISION}" + docker buildx imagetools inspect "${REGISTRY}/${chain}-writer:${REVISION}" + done + fi diff --git a/README.md b/README.md index 494cbf4..0fe9ee9 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,23 @@ background-tracer / ETL ## Chaintable images -The public image workflows build the execution node from this repository for +The public image workflows build the execution and consensus nodes from this repository for `linux/amd64` and `linux/arm64`: - Base image: `public.ecr.aws/b2h7a5c4/chaintable/arc-node` - Writer alias: `public.ecr.aws/b2h7a5c4/chaintable/arc-writer` (the same manifest) +- Consensus image: `public.ecr.aws/b2h7a5c4/chaintable/arc-consensus` Same-repository PRs, default-branch pushes, and manual builds use an eight-character commit tag; per-architecture base tags append `-amd64` or `-arm64`. GitHub Releases use `v-ct.N` tags. Use a fixed tag after its build and manifest jobs -succeed; provisioning both public ECR repositories is a maintainer prerequisite. +succeed; provisioning all three public ECR repositories is a maintainer prerequisite. -These images contain `arc-node-execution` and `arc-snapshots`, not the consensus -node. The consensus client and external ETL are separate services. The Arc +The execution images contain `arc-node-execution` and `arc-snapshots`; the consensus +image contains `arc-node-consensus` and `arc-snapshots`. Both use Debian 12 with Bash, +`sh`, and coreutils for Kubernetes init/runmode scripts, retaining user `arc` +(UID/GID 999) and their direct binary entrypoints. The consensus client and external +ETL remain separate services. The Arc documentation below describes the underlying client, not the Chaintable image publication process. @@ -47,6 +51,9 @@ GIT_VERSION="$(git describe --tags --always --dirty)" \ docker buildx bake arc-execution --set arc-execution.tags=arc-writer:local ``` +For consensus, use target `arc-consensus` and tag override +`--set arc-consensus.tags=arc-consensus:local` with the same build metadata. + See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md) for the Chaintable contribution and vulnerability-reporting processes. diff --git a/deployments/Dockerfile.consensus b/deployments/Dockerfile.consensus index 6ce8df0..d5e917b 100644 --- a/deployments/Dockerfile.consensus +++ b/deployments/Dockerfile.consensus @@ -29,7 +29,7 @@ RUN cargo chef prepare --recipe-path arc-node-consensus-recipe.json ############### FROM chef AS builder -# Create a non-root user 'arc' with UID/GID 999. This method is used because distroless images lack user management tools like 'useradd'. +# Create the runtime user 'arc' with UID/GID 999 without adding user-management tools to the final image. # The user's home directory is set to /data and shell to /bin/false for security. RUN printf 'arc:x:999:999::/data:/bin/false\n' > /tmp/passwd && \ printf 'arc:x:999:\n' > /tmp/group @@ -88,11 +88,22 @@ RUN --mount=type=cache,id=malachite-cargo-registry,sharing=locked,target=/usr/lo ####################### # release runtime image ####################### -FROM gcr.io/distroless/cc-debian12:nonroot AS release-runtime +FROM public.ecr.aws/docker/library/debian:bookworm-slim AS runtime-base -# Copy CA certificates from the chef stage into the runtime image. +# Bootstrap the HTTPS package sources with the existing certificate bundle. COPY --from=chef /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +# Kubernetes init/runmode scripts require Bash and coreutils; keep Debian 12 +# compatibility with the builder and the previous distroless runtime. +COPY assets/apt/apt-retry.conf /etc/apt/apt.conf.d/apt-retry.conf +COPY assets/apt/debian.sources /etc/apt/sources.list.d/debian.sources +RUN rm -f /etc/apt/sources.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends bash coreutils libstdc++6 && \ + rm -rf /var/lib/apt/lists/* + +FROM runtime-base AS release-runtime + # Copy user information from the builder stage into the runtime image. COPY --from=builder /tmp/passwd /etc/passwd COPY --from=builder /tmp/group /etc/group diff --git a/deployments/Dockerfile.execution b/deployments/Dockerfile.execution index 69dd42c..4c8f3ab 100644 --- a/deployments/Dockerfile.execution +++ b/deployments/Dockerfile.execution @@ -29,7 +29,7 @@ RUN cargo chef prepare --recipe-path arc-node-execution-recipe.json ############### FROM chef AS builder -# Create a non-root user 'arc' with UID/GID 999. This method is used because distroless images lack user management tools like 'useradd'. +# Create the runtime user 'arc' with UID/GID 999 without adding user-management tools to the final image. # The user's home directory is set to /data and shell to /bin/false for security. RUN printf 'arc:x:999:999::/data:/bin/false\n' > /tmp/passwd && \ printf 'arc:x:999:\n' > /tmp/group @@ -88,10 +88,21 @@ RUN --mount=type=cache,id=reth-cargo-registry,sharing=locked,target=/usr/local/c ####################### # release runtime image ####################### -FROM gcr.io/distroless/cc-debian12:nonroot AS release-runtime +FROM public.ecr.aws/docker/library/debian:bookworm-slim AS runtime-base COPY --from=chef /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +# Kubernetes init/runmode scripts require Bash and coreutils; keep Debian 12 +# compatibility with the builder and the previous distroless runtime. +COPY assets/apt/apt-retry.conf /etc/apt/apt.conf.d/apt-retry.conf +COPY assets/apt/debian.sources /etc/apt/sources.list.d/debian.sources +RUN rm -f /etc/apt/sources.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends bash coreutils libstdc++6 && \ + rm -rf /var/lib/apt/lists/* + +FROM runtime-base AS release-runtime + # Copy user information from the builder stage into the runtime image. COPY --from=builder /tmp/passwd /etc/passwd COPY --from=builder /tmp/group /etc/group