diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..ee1d3c8e45 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,70 @@ +name: Docker Official Image + +on: + push: + branches: [ master ] + paths: + - 'docker/**' + - '.github/workflows/docker.yml' + pull_request: + paths: + - 'docker/**' + - '.github/workflows/docker.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + bashbrew: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c + with: + go-version: stable + cache: false + + - name: Install Bashbrew + run: | + set -eux + GOBIN="$PWD/docker/tmp/bin" go install github.com/docker-library/bashbrew/cmd/bashbrew@v0.1.14 + echo "$PWD/docker/tmp/bin" >> "$GITHUB_PATH" + + - name: Validate Bashbrew manifest + run: | + set -eux + mkdir -p docker/tmp/bashbrew-library + (cd docker && GIT_COMMIT="$(git rev-parse HEAD)" ./generate-stackbrew-library.sh) > docker/tmp/bashbrew-library/thrift + BASHBREW_LIBRARY="$PWD/docker/tmp/bashbrew-library" bashbrew cat thrift >/dev/null + BASHBREW_LIBRARY="$PWD/docker/tmp/bashbrew-library" bashbrew list --build-order --uniq thrift >/dev/null + + docker: + name: Docker (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-22.04 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Verify Docker Buildx + run: docker buildx version + + - name: Build and test Docker images + timeout-minutes: 90 + run: docker/test.sh diff --git a/doc/ReleaseManagement.md b/doc/ReleaseManagement.md index 0943b5aee6..b1c4de529d 100644 --- a/doc/ReleaseManagement.md +++ b/doc/ReleaseManagement.md @@ -370,6 +370,20 @@ Voting on the development mailing list provides additional benefits (wisdom from 1. Update the web site content to include the new release. The repository is located at https://github.com/apache/thrift-website and there are plenty of instructions how to update both staging and live web site. With regard to the release, its actually quite simple: check out the main branch and edit two lines in _config.yml, then commit. The build bot will update staging. After checking everything is right, simply fast-forward "asf-site" to "asf-staging" and push, then production site will automatically get updated as well +1. Update the Docker Official Image packaging. Docker images are convenience artifacts built from the voted ASF source release; they are not ASF release artifacts and must not block the source release announcement if Docker Library review is delayed. Submit Docker updates only after the vote has passed, release artifacts have been promoted, and the web site/download page has been updated. + + The Docker Official Image library tracks the two latest full Apache Thrift releases. Initial restored Docker image maintenance starts with the current release only; older releases are not backfilled. `latest` and unqualified OS aliases move according to the newest retained release and the explicit base metadata in `docker/versions.json`. + + ```bash + thrift$ cd docker + thrift/docker$ ./update.sh 1.0.0 + thrift/docker$ ./test.sh --all-platforms + ``` + + `update.sh` records archive.apache.org source URLs so retained Docker tags remain rebuildable; if the archive has not synced yet, wait and rerun it. Commit and push the Docker packaging update to Apache Thrift before generating the Docker Library manifest. Then run `./generate-official-images-library.sh /path/to/official-images/library/thrift` from `thrift/docker`; Docker Library manifests are generated artifacts and are not checked into this repository. + + See [`docker/README.md`](../docker/README.md) for the detailed Docker Official Images workflow. Include Docker image availability in the announcement only if the Docker tags have already landed; otherwise follow up after Docker Official Images publishes them. + 1. Make an announcement on the dev@ and user@ mailing lists of the release. There's no template to follow, but you can point folks to the official web site at https://thrift.apache.org, and to the GitHub site at https://github.org/apache.thrift. ### Post-Release diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000000..3fec32c842 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1 @@ +tmp/ diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000000..82520ca123 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1 @@ +/tmp/ diff --git a/docker/0.22.0/Dockerfile b/docker/0.22.0/Dockerfile new file mode 100644 index 0000000000..1a34969371 --- /dev/null +++ b/docker/0.22.0/Dockerfile @@ -0,0 +1,78 @@ +FROM debian:trixie AS builder + +ARG THRIFT_VERSION=0.22.0 +ARG THRIFT_SOURCE_URL=https://archive.apache.org/dist/thrift/0.22.0/thrift-0.22.0.tar.gz +ARG THRIFT_SOURCE_SHA256=794a0e455787960d9f27ab92c38e34da27e8deeda7a5db0e59dc64a00df8a1e5 +ARG THRIFT_SOURCE_SIGNER=8CD87F186F06E958EFCA963D76BD340FC4B75865 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + bison \ + ca-certificates \ + cmake \ + curl \ + flex \ + g++ \ + gnupg \ + make \ + ; \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src + +RUN set -eux; \ + curl -fsSL -o thrift.tar.gz "$THRIFT_SOURCE_URL"; \ + curl -fsSL -o thrift.tar.gz.asc "$THRIFT_SOURCE_URL.asc"; \ + curl -fsSL -o thrift-KEYS https://downloads.apache.org/thrift/KEYS; \ + echo "$THRIFT_SOURCE_SHA256 *thrift.tar.gz" | sha256sum -c -; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --import thrift-KEYS; \ + gpg --batch --status-fd 1 --verify thrift.tar.gz.asc thrift.tar.gz > gpg.status; \ + grep -F "[GNUPG:] VALIDSIG $THRIFT_SOURCE_SIGNER " gpg.status; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + mkdir thrift; \ + tar -xzf thrift.tar.gz -C thrift --strip-components=1; \ + rm thrift.tar.gz thrift.tar.gz.asc thrift-KEYS gpg.status + +RUN set -eux; \ + cmake -S thrift -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_COMPILER=ON \ + -DBUILD_LIBRARIES=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_TUTORIALS=OFF; \ + cmake --build build --target thrift-compiler -j "$(nproc)"; \ + cmake --install build --prefix /opt/thrift + +FROM debian:trixie-slim + +ARG THRIFT_VERSION=0.22.0 + +LABEL org.opencontainers.image.title="Apache Thrift Compiler" \ + org.opencontainers.image.version="$THRIFT_VERSION" \ + org.opencontainers.image.source="https://github.com/apache/thrift" \ + org.opencontainers.image.url="https://thrift.apache.org/" \ + org.opencontainers.image.licenses="Apache-2.0" + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends libstdc++6; \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/thrift/bin/thrift /usr/local/bin/thrift +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY tests/smoke.thrift /tmp/smoke.thrift + +RUN set -eux; \ + thrift --version | grep -F "$THRIFT_VERSION"; \ + mkdir -p /tmp/smoke-out; \ + thrift --gen json -o /tmp/smoke-out /tmp/smoke.thrift; \ + find /tmp/smoke-out -type f | grep -q .; \ + rm -rf /tmp/smoke.thrift /tmp/smoke-out + +WORKDIR /data + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["thrift"] diff --git a/docker/0.22.0/Dockerfile.alpine b/docker/0.22.0/Dockerfile.alpine new file mode 100644 index 0000000000..4364074f43 --- /dev/null +++ b/docker/0.22.0/Dockerfile.alpine @@ -0,0 +1,74 @@ +FROM alpine:3.23 AS builder + +ARG THRIFT_VERSION=0.22.0 +ARG THRIFT_SOURCE_URL=https://archive.apache.org/dist/thrift/0.22.0/thrift-0.22.0.tar.gz +ARG THRIFT_SOURCE_SHA256=794a0e455787960d9f27ab92c38e34da27e8deeda7a5db0e59dc64a00df8a1e5 +ARG THRIFT_SOURCE_SIGNER=8CD87F186F06E958EFCA963D76BD340FC4B75865 + +RUN set -eux; \ + apk add --no-cache \ + bison \ + build-base \ + ca-certificates \ + cmake \ + curl \ + flex \ + gnupg \ + ; \ + update-ca-certificates + +WORKDIR /usr/src + +RUN set -eux; \ + curl -fsSL -o thrift.tar.gz "$THRIFT_SOURCE_URL"; \ + curl -fsSL -o thrift.tar.gz.asc "$THRIFT_SOURCE_URL.asc"; \ + curl -fsSL -o thrift-KEYS https://downloads.apache.org/thrift/KEYS; \ + echo "$THRIFT_SOURCE_SHA256 *thrift.tar.gz" | sha256sum -c -; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --import thrift-KEYS; \ + gpg --batch --status-fd 1 --verify thrift.tar.gz.asc thrift.tar.gz > gpg.status; \ + grep -F "[GNUPG:] VALIDSIG $THRIFT_SOURCE_SIGNER " gpg.status; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + mkdir thrift; \ + tar -xzf thrift.tar.gz -C thrift --strip-components=1; \ + rm thrift.tar.gz thrift.tar.gz.asc thrift-KEYS gpg.status + +RUN set -eux; \ + cmake -S thrift -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_COMPILER=ON \ + -DBUILD_LIBRARIES=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_TUTORIALS=OFF; \ + cmake --build build --target thrift-compiler -j "$(nproc)"; \ + cmake --install build --prefix /opt/thrift + +FROM alpine:3.23 + +ARG THRIFT_VERSION=0.22.0 + +LABEL org.opencontainers.image.title="Apache Thrift Compiler" \ + org.opencontainers.image.version="$THRIFT_VERSION" \ + org.opencontainers.image.source="https://github.com/apache/thrift" \ + org.opencontainers.image.url="https://thrift.apache.org/" \ + org.opencontainers.image.licenses="Apache-2.0" + +RUN set -eux; \ + apk add --no-cache libstdc++ + +COPY --from=builder /opt/thrift/bin/thrift /usr/local/bin/thrift +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY tests/smoke.thrift /tmp/smoke.thrift + +RUN set -eux; \ + thrift --version | grep -F "$THRIFT_VERSION"; \ + mkdir -p /tmp/smoke-out; \ + thrift --gen json -o /tmp/smoke-out /tmp/smoke.thrift; \ + find /tmp/smoke-out -type f | grep -q .; \ + rm -rf /tmp/smoke.thrift /tmp/smoke-out + +WORKDIR /data + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["thrift"] diff --git a/docker/0.23.0/Dockerfile b/docker/0.23.0/Dockerfile new file mode 100644 index 0000000000..9240de14e8 --- /dev/null +++ b/docker/0.23.0/Dockerfile @@ -0,0 +1,78 @@ +FROM debian:trixie AS builder + +ARG THRIFT_VERSION=0.23.0 +ARG THRIFT_SOURCE_URL=https://archive.apache.org/dist/thrift/0.23.0/thrift-0.23.0.tar.gz +ARG THRIFT_SOURCE_SHA256=1859d932d2ae1f13d16c5a196931208c116310a5ff50f2bfd11d3db03be8f46f +ARG THRIFT_SOURCE_SIGNER=8CD87F186F06E958EFCA963D76BD340FC4B75865 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + bison \ + ca-certificates \ + cmake \ + curl \ + flex \ + g++ \ + gnupg \ + make \ + ; \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src + +RUN set -eux; \ + curl -fsSL -o thrift.tar.gz "$THRIFT_SOURCE_URL"; \ + curl -fsSL -o thrift.tar.gz.asc "$THRIFT_SOURCE_URL.asc"; \ + curl -fsSL -o thrift-KEYS https://downloads.apache.org/thrift/KEYS; \ + echo "$THRIFT_SOURCE_SHA256 *thrift.tar.gz" | sha256sum -c -; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --import thrift-KEYS; \ + gpg --batch --status-fd 1 --verify thrift.tar.gz.asc thrift.tar.gz > gpg.status; \ + grep -F "[GNUPG:] VALIDSIG $THRIFT_SOURCE_SIGNER " gpg.status; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + mkdir thrift; \ + tar -xzf thrift.tar.gz -C thrift --strip-components=1; \ + rm thrift.tar.gz thrift.tar.gz.asc thrift-KEYS gpg.status + +RUN set -eux; \ + cmake -S thrift -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_COMPILER=ON \ + -DBUILD_LIBRARIES=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_TUTORIALS=OFF; \ + cmake --build build --target thrift-compiler -j "$(nproc)"; \ + cmake --install build --prefix /opt/thrift + +FROM debian:trixie-slim + +ARG THRIFT_VERSION=0.23.0 + +LABEL org.opencontainers.image.title="Apache Thrift Compiler" \ + org.opencontainers.image.version="$THRIFT_VERSION" \ + org.opencontainers.image.source="https://github.com/apache/thrift" \ + org.opencontainers.image.url="https://thrift.apache.org/" \ + org.opencontainers.image.licenses="Apache-2.0" + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends libstdc++6; \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/thrift/bin/thrift /usr/local/bin/thrift +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY tests/smoke.thrift /tmp/smoke.thrift + +RUN set -eux; \ + thrift --version | grep -F "$THRIFT_VERSION"; \ + mkdir -p /tmp/smoke-out; \ + thrift --gen json -o /tmp/smoke-out /tmp/smoke.thrift; \ + find /tmp/smoke-out -type f | grep -q .; \ + rm -rf /tmp/smoke.thrift /tmp/smoke-out + +WORKDIR /data + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["thrift"] diff --git a/docker/0.23.0/Dockerfile.alpine b/docker/0.23.0/Dockerfile.alpine new file mode 100644 index 0000000000..7f9bad1864 --- /dev/null +++ b/docker/0.23.0/Dockerfile.alpine @@ -0,0 +1,74 @@ +FROM alpine:3.23 AS builder + +ARG THRIFT_VERSION=0.23.0 +ARG THRIFT_SOURCE_URL=https://archive.apache.org/dist/thrift/0.23.0/thrift-0.23.0.tar.gz +ARG THRIFT_SOURCE_SHA256=1859d932d2ae1f13d16c5a196931208c116310a5ff50f2bfd11d3db03be8f46f +ARG THRIFT_SOURCE_SIGNER=8CD87F186F06E958EFCA963D76BD340FC4B75865 + +RUN set -eux; \ + apk add --no-cache \ + bison \ + build-base \ + ca-certificates \ + cmake \ + curl \ + flex \ + gnupg \ + ; \ + update-ca-certificates + +WORKDIR /usr/src + +RUN set -eux; \ + curl -fsSL -o thrift.tar.gz "$THRIFT_SOURCE_URL"; \ + curl -fsSL -o thrift.tar.gz.asc "$THRIFT_SOURCE_URL.asc"; \ + curl -fsSL -o thrift-KEYS https://downloads.apache.org/thrift/KEYS; \ + echo "$THRIFT_SOURCE_SHA256 *thrift.tar.gz" | sha256sum -c -; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --import thrift-KEYS; \ + gpg --batch --status-fd 1 --verify thrift.tar.gz.asc thrift.tar.gz > gpg.status; \ + grep -F "[GNUPG:] VALIDSIG $THRIFT_SOURCE_SIGNER " gpg.status; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + mkdir thrift; \ + tar -xzf thrift.tar.gz -C thrift --strip-components=1; \ + rm thrift.tar.gz thrift.tar.gz.asc thrift-KEYS gpg.status + +RUN set -eux; \ + cmake -S thrift -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_COMPILER=ON \ + -DBUILD_LIBRARIES=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_TUTORIALS=OFF; \ + cmake --build build --target thrift-compiler -j "$(nproc)"; \ + cmake --install build --prefix /opt/thrift + +FROM alpine:3.23 + +ARG THRIFT_VERSION=0.23.0 + +LABEL org.opencontainers.image.title="Apache Thrift Compiler" \ + org.opencontainers.image.version="$THRIFT_VERSION" \ + org.opencontainers.image.source="https://github.com/apache/thrift" \ + org.opencontainers.image.url="https://thrift.apache.org/" \ + org.opencontainers.image.licenses="Apache-2.0" + +RUN set -eux; \ + apk add --no-cache libstdc++ + +COPY --from=builder /opt/thrift/bin/thrift /usr/local/bin/thrift +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY tests/smoke.thrift /tmp/smoke.thrift + +RUN set -eux; \ + thrift --version | grep -F "$THRIFT_VERSION"; \ + mkdir -p /tmp/smoke-out; \ + thrift --gen json -o /tmp/smoke-out /tmp/smoke.thrift; \ + find /tmp/smoke-out -type f | grep -q .; \ + rm -rf /tmp/smoke.thrift /tmp/smoke-out + +WORKDIR /data + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["thrift"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000000..1a19071b50 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,107 @@ +# Apache Thrift Docker Official Image Packaging + +This directory contains the packaging for the Apache Thrift compiler Docker Official Image. + +The image is compiler-only. It contains the `thrift` compiler/code generator and the runtime libraries needed by that binary. It does not include language-specific Thrift runtime libraries or the contributor build images from `build/docker/*`. + +Do not push images from this repository. Public images are built and published by Docker Official Images after review in `docker-library/official-images`. + +## Usage + +Generate code from the current project directory: + +```console +docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" thrift --gen py -o /data /data/service.thrift +``` + +The `-u` flag prevents generated files in the bind mount from being owned by root. + +Run the compiler version check: + +```console +docker run --rm thrift --version +``` + +Use Alpine explicitly when you need the Alpine/musl variant: + +```console +docker pull thrift:0.22-alpine +``` + +## Files + +- `versions.json` records release artifact URLs, checksums, signer fingerprints, release dates, base versions, and maintenance state. +- `0.22.0/Dockerfile` builds the Debian-based image for Thrift 0.22.0. +- `0.22.0/Dockerfile.alpine` builds the Alpine-based image for Thrift 0.22.0. +- `docker-entrypoint.sh` provides the CLI-friendly container entrypoint. +- `tests/smoke.thrift` is the shared smoke-test fixture. +- `generate-stackbrew-library.sh` prints Docker Library metadata for Bashbrew. +- `generate-official-images-library.sh` writes that metadata to `tmp/` or to a local `docker-library/official-images` checkout. + +## Updating for a Release + +After the ASF release vote passes, release artifacts are promoted, and the website/download page is updated: + +```console +cd docker +./update.sh 0.23.0 +./test.sh --all-platforms +``` + +`update.sh` verifies the source tarball checksum and PGP signature, records the signer fingerprint, updates Dockerfile `ARG` values, and applies the two-latest-full-releases retention policy. It requires the release tarball to be available from `archive.apache.org` because committed Docker metadata must remain rebuildable after the release leaves the active download mirrors. + +Set base versions explicitly if needed: + +```console +DEBIAN_VERSION=trixie ALPINE_VERSION=3.23 RELEASED_DATE=2025-05-14 ./update.sh 0.22.0 +``` + +The script does not scrape the Thrift website. `RELEASED_DATE` is informational metadata. + +## Testing + +Run native-platform tests: + +```console +cd docker +./test.sh +``` + +Run the supported platform matrix: + +```console +cd docker +./test.sh --all-platforms +``` + +The full local matrix uses Buildx for `linux/amd64` and `linux/arm64`; arm64 tests require QEMU/binfmt support on non-arm64 hosts. Apache CI runs separate native `amd64` and `arm64` jobs. + +The tests build Debian and Alpine variants, verify the compiler version, exercise entrypoint behavior, generate JSON and Python output from `tests/smoke.thrift`, and check bind-mount output ownership on the native platform. + +## Docker Official Images + +Docker Official Images does not run the maintenance scripts in this directory. Its tooling reads `docker-library/official-images/library/thrift`, fetches each `GitCommit`, and builds the referenced `Directory` and `File` entries. + +After the Docker packaging commit is on Apache Thrift `master`, generate the submit-ready manifest into `tmp/`: + +```console +./generate-official-images-library.sh +``` + +Or write directly into a local `docker-library/official-images` checkout: + +```console +./generate-official-images-library.sh /path/to/official-images/library/thrift +``` + +Docker Library manifests are generated artifacts; they are not checked into this repository. + +Docker Hub documentation is maintained through `docker-library/docs`. + +## Release Rules + +- Only voted ASF releases may be used. +- Do not publish release candidates, pre-releases, branch builds, nightlies, or continuous builds. +- The Docker Official Image is a convenience artifact built from voted ASF source releases; it is not itself the ASF release artifact. +- The Docker Official Image library tracks the two latest full Apache Thrift releases after Docker image maintenance is restored. Initial restoration starts with 0.22.0 only; 0.21.0 is not backfilled. +- Older tags can remain pullable on Docker Hub after they are removed from the library file, but they are no longer rebuilt or maintained. diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000000..ac3ccae395 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +if [ "$#" -eq 0 ]; then + set -- thrift +elif [ "${1#-}" != "$1" ]; then + set -- thrift "$@" +fi + +exec "$@" diff --git a/docker/generate-official-images-library.sh b/docker/generate-official-images-library.sh new file mode 100755 index 0000000000..90a953fac6 --- /dev/null +++ b/docker/generate-official-images-library.sh @@ -0,0 +1,44 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" + +usage() { + echo "usage: $0 [output-file]" >&2 + echo "default output-file: tmp/thrift-official-images-library" >&2 +} + +if [ "$#" -gt 1 ]; then + usage + exit 64 +fi + +output="${1:-tmp/thrift-official-images-library}" +repo_root="$(git rev-parse --show-toplevel)" +git_commit="${GIT_COMMIT:-$(git -C "$repo_root" rev-parse HEAD)}" + +case "$git_commit" in + 0000000000000000000000000000000000000000) + echo "refusing to generate an Official Images manifest with the placeholder GitCommit" >&2 + exit 1 + ;; +esac + +if [ "${ALLOW_DIRTY_OFFICIAL_IMAGES_MANIFEST:-0}" != "1" ] && + { ! git -C "$repo_root" diff --quiet -- docker || + ! git -C "$repo_root" diff --cached --quiet -- docker; }; then + echo "commit Docker packaging changes before generating the Official Images manifest" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$output")" +target="$(cd "$(dirname "$output")" && pwd -P)/$(basename "$output")" + +GIT_COMMIT="$git_commit" ./generate-stackbrew-library.sh > "$target" + +if grep -q 'GitCommit: 0000000000000000000000000000000000000000' "$target"; then + echo "generated manifest still contains the placeholder GitCommit: $target" >&2 + exit 1 +fi + +echo "Wrote $target with GitCommit: $git_commit" diff --git a/docker/generate-stackbrew-library.sh b/docker/generate-stackbrew-library.sh new file mode 100755 index 0000000000..b18b2b3d02 --- /dev/null +++ b/docker/generate-stackbrew-library.sh @@ -0,0 +1,93 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" + +repo_root="$(git rev-parse --show-toplevel)" +export GIT_COMMIT="${GIT_COMMIT:-$(git -C "$repo_root" rev-parse HEAD)}" + +python3 - "$@" <<'PY' +import json +import os +from pathlib import Path + + +def version_key(version): + return tuple(int(part) for part in version.split(".")) + + +def minor(version): + major, minor_, _patch = version.split(".") + return f"{major}.{minor_}" + + +def newest_by(items, key_func): + result = {} + for version, meta in items: + key = key_func(version, meta) + if key not in result or version_key(version) > version_key(result[key][0]): + result[key] = (version, meta) + return result + + +root = Path(".") +data = json.loads((root / "versions.json").read_text()) +versions = sorted(data["versions"].items(), key=lambda item: version_key(item[0]), reverse=True) +maintained = versions[: int(data.get("maintained_releases", 2))] + +git_commit = os.environ["GIT_COMMIT"] +latest_version = maintained[0][0] +latest_minor = newest_by(maintained, lambda version, _meta: minor(version)) +latest_debian_base = newest_by(maintained, lambda _version, meta: meta["debian"]) +latest_alpine_base = newest_by(maintained, lambda _version, meta: meta["alpine"]) + +print("# generated via ./generate-stackbrew-library.sh; do not edit directly") +print("Maintainers: Apache Thrift Developers (@asfbot),") +print(" Jens Geyer (@Jens-G),") +print(" Randy Abernethy (@RandyAbernethy),") +print(" Duru Can Celasun (@dcelasun)") +print("GitRepo: https://github.com/apache/thrift.git") +print() + +for version, meta in maintained: + version_minor = minor(version) + debian = meta["debian"] + alpine = meta["alpine"] + + debian_tags = [version] + if latest_minor[version_minor][0] == version: + debian_tags.append(version_minor) + if version == latest_version: + debian_tags.append("latest") + debian_tags.extend([f"{version}-{debian}"]) + if latest_minor[version_minor][0] == version: + debian_tags.append(f"{version_minor}-{debian}") + if latest_debian_base[debian][0] == version: + debian_tags.append(debian) + + print(f"Tags: {', '.join(debian_tags)}") + print("Architectures: amd64, arm64v8") + print(f"GitCommit: {git_commit}") + print("Directory: docker") + print(f"File: {version}/Dockerfile") + print() + + alpine_tags = [f"{version}-alpine{alpine}"] + if latest_minor[version_minor][0] == version: + alpine_tags.append(f"{version_minor}-alpine{alpine}") + if latest_alpine_base[alpine][0] == version: + alpine_tags.append(f"alpine{alpine}") + alpine_tags.append(f"{version}-alpine") + if latest_minor[version_minor][0] == version: + alpine_tags.append(f"{version_minor}-alpine") + if version == latest_version: + alpine_tags.append("alpine") + + print(f"Tags: {', '.join(alpine_tags)}") + print("Architectures: amd64, arm64v8") + print(f"GitCommit: {git_commit}") + print("Directory: docker") + print(f"File: {version}/Dockerfile.alpine") + if version != maintained[-1][0]: + print() +PY diff --git a/docker/test.sh b/docker/test.sh new file mode 100755 index 0000000000..d72f128367 --- /dev/null +++ b/docker/test.sh @@ -0,0 +1,135 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" + +all_platforms=false +if [ "${1:-}" = "--all-platforms" ]; then + all_platforms=true + shift +fi + +if [ "$#" -ne 0 ]; then + echo "usage: $0 [--all-platforms]" >&2 + exit 64 +fi + +server_platform="$(docker version --format '{{.Server.Os}}/{{.Server.Arch}}')" +case "$server_platform" in + linux/x86_64) server_platform=linux/amd64 ;; + linux/aarch64) server_platform=linux/arm64 ;; +esac + +if [ "$all_platforms" = true ]; then + platforms="linux/amd64 linux/arm64" +else + platforms="$server_platform" +fi + +mkdir -p tmp + +image_tag() { + platform_slug="$(echo "$3" | tr '/' '-')" + echo "thrift-docker-test:$1-$2-$platform_slug" +} + +version_field() { + python3 - "$1" "$2" <<'PY' +import json +import sys +from pathlib import Path + +data = json.loads(Path("versions.json").read_text()) +print(data["versions"][sys.argv[1]][sys.argv[2]]) +PY +} + +maintained_versions() { + python3 - <<'PY' +import json +from pathlib import Path + +data = json.loads(Path("versions.json").read_text()) +versions = sorted(data["versions"], key=lambda version: tuple(int(part) for part in version.split(".")), reverse=True) +for version in versions[: int(data.get("maintained_releases", 2))]: + print(version) +PY +} + +file_owner() { + if stat -c '%u:%g' "$1" >/dev/null 2>&1; then + stat -c '%u:%g' "$1" + else + stat -f '%u:%g' "$1" + fi +} + +smoke_image() { + image="$1" + platform="$2" + version="$3" + variant="$4" + + if docker run --rm --platform "$platform" "$image" > tmp/no-args.out 2>&1; then + : + fi + grep -F "Usage: thrift" tmp/no-args.out >/dev/null + docker run --rm --platform "$platform" "$image" --version | grep -F "$version" >/dev/null + docker run --rm --platform "$platform" "$image" thrift --version | grep -F "$version" >/dev/null + docker run --rm --platform "$platform" "$image" sh -c 'command -v thrift' >/dev/null + + out_dir="tmp/out-$version-$variant-$(echo "$platform" | tr '/' '-')" + rm -rf "$out_dir" + mkdir -p "$out_dir" + + docker run --rm --platform "$platform" \ + -u "$(id -u):$(id -g)" \ + -v "$PWD:/data" \ + "$image" --gen json -o /data/"$out_dir" /data/tests/smoke.thrift + find "$out_dir" -type f | grep -q . + + docker run --rm --platform "$platform" \ + -u "$(id -u):$(id -g)" \ + -v "$PWD:/data" \ + "$image" --gen py -o /data/"$out_dir" /data/tests/smoke.thrift + find "$out_dir" -type f | grep -q . + + if [ "$platform" = "$server_platform" ]; then + first_file="$(find "$out_dir" -type f | head -n 1)" + expected_owner="$(id -u):$(id -g)" + actual_owner="$(file_owner "$first_file")" + if [ "$actual_owner" != "$expected_owner" ]; then + echo "generated file has owner $actual_owner, expected $expected_owner: $first_file" >&2 + exit 1 + fi + fi +} + +for version in $(maintained_versions); do + for platform in $platforms; do + for variant in debian alpine; do + case "$variant" in + debian) + dockerfile="$version/Dockerfile" + ;; + alpine) + dockerfile="$version/Dockerfile.alpine" + ;; + esac + + image="$(image_tag "$version" "$variant" "$platform")" + echo "Building $image for $platform" + docker buildx build --platform "$platform" --load -t "$image" -f "$dockerfile" . + echo "Testing $image for $platform" + smoke_image "$image" "$platform" "$version" "$variant" + done + done +done + +./generate-stackbrew-library.sh > tmp/stackbrew-library +if grep -q 'GitCommit: 0000000000000000000000000000000000000000' tmp/stackbrew-library; then + echo "generated stackbrew metadata contains the placeholder GitCommit" >&2 + exit 1 +fi + +ALLOW_DIRTY_OFFICIAL_IMAGES_MANIFEST=1 ./generate-official-images-library.sh tmp/thrift-official-images-library diff --git a/docker/tests/smoke.thrift b/docker/tests/smoke.thrift new file mode 100644 index 0000000000..62fd7bf3f1 --- /dev/null +++ b/docker/tests/smoke.thrift @@ -0,0 +1,6 @@ +namespace * smoke +namespace py smoke + +struct Ping { + 1: string message +} diff --git a/docker/update.sh b/docker/update.sh new file mode 100755 index 0000000000..eab46bc786 --- /dev/null +++ b/docker/update.sh @@ -0,0 +1,142 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 64 +fi + +version="$1" +debian="${DEBIAN_VERSION:-trixie}" +alpine="${ALPINE_VERSION:-3.23}" +released="${RELEASED_DATE:-$(date -u +%F)}" +source_url="https://archive.apache.org/dist/thrift/$version/thrift-$version.tar.gz" +keys_url="https://downloads.apache.org/thrift/KEYS" +tmp_dir="tmp/update-$version" + +mkdir -p "$tmp_dir" + +check_url() { + label="$1" + url="$2" + + echo "Checking $label: $url" >&2 + curl -fsSL --range 0-0 -o /dev/null "$url" +} + +download() { + label="$1" + url="$2" + output="$3" + + echo "Downloading $label: $url" >&2 + curl -fsSL -o "$output" "$url" +} + +if ! check_url "archive.apache.org source tarball" "$source_url"; then + echo "The Docker metadata uses archive.apache.org source URLs so retained tags remain rebuildable." >&2 + echo "Wait until the ASF archive has synced this release, then rerun update.sh." >&2 + exit 1 +fi + +download "source tarball" "$source_url" "$tmp_dir/thrift.tar.gz" +download "source signature" "$source_url.asc" "$tmp_dir/thrift.tar.gz.asc" +download "source checksum" "$source_url.sha256" "$tmp_dir/thrift.tar.gz.sha256" +download "Thrift KEYS" "$keys_url" "$tmp_dir/KEYS" + +published_sha="$(awk '{ print $1; exit }' "$tmp_dir/thrift.tar.gz.sha256")" +computed_sha="$( + python3 - "$tmp_dir/thrift.tar.gz" <<'PY' +import hashlib +import sys + +h = hashlib.sha256() +with open(sys.argv[1], "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) +print(h.hexdigest()) +PY +)" + +if [ "$published_sha" != "$computed_sha" ]; then + echo "published checksum does not match downloaded tarball" >&2 + echo "published: $published_sha" >&2 + echo "computed: $computed_sha" >&2 + exit 1 +fi + +GNUPGHOME="$tmp_dir/gnupg" +export GNUPGHOME +mkdir -p "$GNUPGHOME" +chmod 700 "$GNUPGHOME" +gpg --batch --import "$tmp_dir/KEYS" >/dev/null +gpg --batch --status-fd 1 --verify "$tmp_dir/thrift.tar.gz.asc" "$tmp_dir/thrift.tar.gz" > "$tmp_dir/gpg.status" +signer="$(awk '/^\[GNUPG:\] VALIDSIG / { print $3; exit }' "$tmp_dir/gpg.status")" +gpgconf --kill all || true + +if [ -z "$signer" ]; then + echo "could not determine signer fingerprint from gpg status" >&2 + exit 1 +fi + +source_version="$( + python3 - <<'PY' +import json +from pathlib import Path + +data = json.loads(Path("versions.json").read_text()) +versions = sorted(data["versions"], key=lambda version: tuple(int(part) for part in version.split(".")), reverse=True) +print(versions[0]) +PY +)" + +if [ ! -d "$version" ]; then + cp -R "$source_version" "$version" +fi + +python3 - "$version" "$source_url" "$computed_sha" "$signer" "$released" "$debian" "$alpine" <<'PY' +import json +import re +import sys +from pathlib import Path + +version, source_url, sha256, signer, released, debian, alpine = sys.argv[1:] +root = Path(".") + + +data = json.loads((root / "versions.json").read_text()) +data["versions"][version] = { + "source": source_url, + "sha256": sha256, + "signer": signer, + "released": released, + "debian": debian, + "alpine": alpine, +} + +(root / "versions.json").write_text(json.dumps(data, indent=2, sort_keys=False) + "\n") + + +def replace(pattern, replacement, text): + text, count = re.subn(pattern, replacement, text, flags=re.MULTILINE) + if count == 0: + raise SystemExit(f"pattern not found: {pattern}") + return text + + +def update_dockerfile(path, base_builder, base_runtime): + text = path.read_text() + text = replace(r"^FROM .+ AS builder$", f"FROM {base_builder} AS builder", text) + text = replace(r"^FROM (?!.* AS builder).+$", f"FROM {base_runtime}", text) + text = replace(r"^ARG THRIFT_VERSION=.*$", f"ARG THRIFT_VERSION={version}", text) + text = replace(r"^ARG THRIFT_SOURCE_URL=.*$", f"ARG THRIFT_SOURCE_URL={source_url}", text) + text = replace(r"^ARG THRIFT_SOURCE_SHA256=.*$", f"ARG THRIFT_SOURCE_SHA256={sha256}", text) + text = replace(r"^ARG THRIFT_SOURCE_SIGNER=.*$", f"ARG THRIFT_SOURCE_SIGNER={signer}", text) + path.write_text(text) + + +update_dockerfile(root / version / "Dockerfile", f"debian:{debian}", f"debian:{debian}-slim") +update_dockerfile(root / version / "Dockerfile.alpine", f"alpine:{alpine}", f"alpine:{alpine}") +PY diff --git a/docker/versions.json b/docker/versions.json new file mode 100644 index 0000000000..56aad916bb --- /dev/null +++ b/docker/versions.json @@ -0,0 +1,21 @@ +{ + "maintained_releases": 2, + "versions": { + "0.22.0": { + "source": "https://archive.apache.org/dist/thrift/0.22.0/thrift-0.22.0.tar.gz", + "sha256": "794a0e455787960d9f27ab92c38e34da27e8deeda7a5db0e59dc64a00df8a1e5", + "signer": "8CD87F186F06E958EFCA963D76BD340FC4B75865", + "released": "2025-05-14", + "debian": "trixie", + "alpine": "3.23" + }, + "0.23.0": { + "source": "https://archive.apache.org/dist/thrift/0.23.0/thrift-0.23.0.tar.gz", + "sha256": "1859d932d2ae1f13d16c5a196931208c116310a5ff50f2bfd11d3db03be8f46f", + "signer": "8CD87F186F06E958EFCA963D76BD340FC4B75865", + "released": "2026-04-27", + "debian": "trixie", + "alpine": "3.23" + } + } +}