From 70110dcc77957b6375ab4c46e42bad67b4aa4392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 7 Jul 2023 18:17:06 +0200 Subject: [PATCH] ci: Docker images for various PHP versions Also adds semver support. --- .dockerignore | 20 +- .github/workflows/docker.yml | 176 ++++++++++++++++-- .github/workflows/tests.yml | 5 + CONTRIBUTING.md | 2 +- Dockerfile | 49 ++++- ...der-alpine.Dockerfile => alpine.Dockerfile | 25 ++- builder-debian.Dockerfile | 47 ----- Dockerfile.dev => dev.Dockerfile | 0 docker-bake.hcl | 130 +++++++------ frankenphp.c | 3 +- 10 files changed, 326 insertions(+), 131 deletions(-) rename builder-alpine.Dockerfile => alpine.Dockerfile (59%) delete mode 100644 builder-debian.Dockerfile rename Dockerfile.dev => dev.Dockerfile (100%) diff --git a/.dockerignore b/.dockerignore index d8c526a..fcf47e5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,10 @@ -/.git/ -/.github/ -/.gitmodules/ -/.idea/ -/.vscode/ -/docs/ -/*Dockerfile* -/.*ignore -/*.hcl -/*.md +# ignored +**/* + +# authorized +!**/Caddyfile +!**/*.go +!**/go.* +!**/*.c +!**/*.h +!testdata/*.php diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df74f9a..08f714f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,35 +11,179 @@ on: workflow_dispatch: inputs: {} jobs: - build: + prepare: runs-on: ubuntu-latest + outputs: + # Push only if we're committing in the main branch + push: ${{toJson(github.ref == 'refs/heads/main' && github.event_name != 'pull_request')}} + variants: ${{ steps.matrix.outputs.variants }} + platforms: ${{ steps.matrix.outputs.platforms }} + metadata: ${{ steps.matrix.outputs.metadata }} steps: - uses: actions/checkout@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + with: + version: latest + + - name: Create variants matrix + id: matrix + run: | + METADATA=$(docker buildx bake --print) + echo "variants=$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< $METADATA)" >> "$GITHUB_OUTPUT" + echo "platforms=$(jq -c 'first(.target[]) | .platforms' <<< $METADATA)" >> "$GITHUB_OUTPUT" + echo "metadata=$(jq -c <<< $METADATA)" >> "$GITHUB_OUTPUT" + env: + LATEST: '1' # TODO: unset this variable when releasing the first tagged version + SHA: ${{github.sha}} + VERSION: ${{github.ref_name}} + + build: + runs-on: ubuntu-latest + needs: + - prepare + strategy: + fail-fast: false + matrix: + variant: ${{ fromJson(needs.prepare.outputs.variants) }} + platform: ${{ fromJson(needs.prepare.outputs.platforms) }} + include: + - race: "" + qemu: true + - platform: linux/amd64 + qemu: false + race: "-race" # The Go race detector is only supported on amd64 + - platform: linux/386 + qemu: false + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + if: matrix.qemu + uses: docker/setup-qemu-action@v2 + with: + platforms: ${{matrix.platform}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + platforms: ${{matrix.platform}} + version: latest + - name: Login to DockerHub - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + if: fromJson(needs.prepare.outputs.push) uses: docker/login-action@v2 with: - #registry: ${{secrets.REGISTRY_LOGIN_SERVER}} username: ${{secrets.REGISTRY_USERNAME}} password: ${{secrets.REGISTRY_PASSWORD}} - - name: Build for amd64 + + - name: Build + id: build uses: docker/bake-action@v3 with: pull: true - load: true - set: "*.platform=linux/amd64" - - name: Build for all platforms - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: docker/bake-action@v3 + targets: | + builder-${{matrix.variant}} + runner-${{matrix.variant}} + # Remove tags to prevent "can't push tagged ref [...] by digest" error + set: | + *.tags= + *.platform=${{matrix.platform}} + *.cache-from=type=gha,scope=${{github.ref}}-${{matrix.platform}} + *.cache-from=type=gha,scope=refs/heads/main-${{matrix.platform}} + *.cache-to=type=gha,scope=${{github.ref}}-${{matrix.platform}} + *.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=${{ needs.prepare.outputs.push }} + env: + LATEST: '1' # TODO: unset this variable when releasing the first tagged version + SHA: ${{github.sha}} + VERSION: ${{github.ref_name}} + + # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600 + - name: Export metadata + if: fromJson(needs.prepare.outputs.push) + run: | + mkdir -p /tmp/metadata/builder /tmp/metadata/runner + + builderDigest=$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) + touch "/tmp/metadata/builder/${builderDigest#sha256:}" + + runnerDigest=$(jq -r '."runner-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) + touch "/tmp/metadata/runner/${runnerDigest#sha256:}" + env: + METADATA: ${{steps.build.outputs.metadata}} + + - name: Upload runner metadata + if: fromJson(needs.prepare.outputs.push) + uses: actions/upload-artifact@v3 with: - pull: true - push: true - - run: docker image ls - - name: Run tests on Debian Bookworm + name: metadata-builder-${{matrix.variant}} + path: /tmp/metadata/builder/* + if-no-files-found: error + retention-days: 1 + + - name: Upload runner metadata + if: fromJson(needs.prepare.outputs.push) + uses: actions/upload-artifact@v3 + with: + name: metadata-runner-${{matrix.variant}} + path: /tmp/metadata/runner/* + if-no-files-found: error + retention-days: 1 + + - name: Run tests + if: '!matrix.qemu' + continue-on-error: true run: | - docker run --rm dunglas/frankenphp:builder-bookworm "sh -c 'go test -race -v ./... && cd caddy && go test -race -v ./...'" - - name: Run tests on Alpine + docker run --platform=${{matrix.platform}} --rm \ + dunglas/frankenphp@$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) \ + "sh -c 'go test ${{matrix.race}} -v ./... && cd caddy && go test ${{matrix.race}} -v ./...'" + env: + METADATA: ${{steps.build.outputs.metadata}} + + # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ + push: + runs-on: ubuntu-latest + needs: + - prepare + - build + if: fromJson(needs.prepare.outputs.push) + strategy: + fail-fast: false + matrix: + variant: ${{ fromJson(needs.prepare.outputs.variants) }} + target: ['builder', 'runner'] + steps: + - name: Download metadata + uses: actions/download-artifact@v3 + with: + name: metadata-${{matrix.target}}-${{matrix.variant}} + path: /tmp/metadata + + - run: ls -R /tmp/metadata + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{secrets.REGISTRY_USERNAME}} + password: ${{secrets.REGISTRY_PASSWORD}} + + - name: Create manifest list and push + working-directory: /tmp/metadata run: | - docker run --rm dunglas/frankenphp:builder-alpine "sh -c 'go test -race -v ./... && cd caddy && go test -race -v ./...'" + docker buildx imagetools create $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | map("-t " + .) | join(" ")' <<< $METADATA) \ + $(printf 'dunglas/frankenphp@sha256:%s ' *) + env: + METADATA: ${{needs.prepare.outputs.metadata}} + + - name: Inspect image + run: | + docker buildx imagetools inspect $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | first' <<< $METADATA) + env: + METADATA: ${{needs.prepare.outputs.metadata}} + \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea05085..a753d03 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,11 @@ jobs: php-versions: ['8.2', '8.3'] steps: - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: '1.20' + - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} @@ -19,10 +21,13 @@ jobs: coverage: none env: phpts: ts + - name: Set include flags run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV" + - name: Run library tests run: go test -race -v ./... + - name: Run Caddy module tests working-directory: caddy/ run: go test -race -v ./... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 430e1e2..12baa12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Build the dev Docker image: - docker build -t frankenphp-dev -f Dockerfile.dev . + docker build -t frankenphp-dev -f dev.Dockerfile . docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp-dev The image contains the usual development tools (Go, GDB, Valgrind, Neovim...). diff --git a/Dockerfile b/Dockerfile index 3456130..a0ec445 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,52 @@ # syntax=docker/dockerfile:1 -FROM php-base +FROM php-base AS builder + +COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go +COPY --from=golang-base /usr/local/go /usr/local/go + +# This is required to link the FrankenPHP binary to the PHP binary +RUN apt-get update && \ + apt-get -y --no-install-recommends install \ + libargon2-dev \ + libcurl4-openssl-dev \ + libonig-dev \ + libreadline-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libxml2-dev \ + zlib1g-dev \ + && \ + apt-get clean + +WORKDIR /go/src/app + +COPY go.mod go.sum ./ +RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get + +RUN mkdir caddy && cd caddy +COPY caddy/go.mod caddy/go.sum ./caddy/ + +RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get + +COPY *.* ./ +COPY caddy caddy +COPY C-Thread-Pool C-Thread-Pool +COPY internal internal +COPY testdata testdata + +# todo: automate this? +# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for PHP values +ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS=$PHP_CFLAGS CGO_CPPFLAGS=$PHP_CPPFLAGS + +RUN cd caddy/frankenphp && \ + go build && \ + cp frankenphp /usr/local/bin && \ + cp /go/src/app/caddy/frankenphp/Caddyfile /etc/Caddyfile + +ENTRYPOINT ["/bin/bash","-c"] + +FROM php-base AS runner COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ diff --git a/builder-alpine.Dockerfile b/alpine.Dockerfile similarity index 59% rename from builder-alpine.Dockerfile rename to alpine.Dockerfile index 9592963..67e3309 100644 --- a/builder-alpine.Dockerfile +++ b/alpine.Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM php-base +FROM php-base AS builder COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go COPY --from=golang-base /usr/local/go /usr/local/go @@ -44,3 +44,26 @@ RUN cd caddy/frankenphp && \ cp /go/src/app/caddy/frankenphp/Caddyfile /etc/Caddyfile ENTRYPOINT ["/bin/sh","-c"] + +FROM php-base AS runner + +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + +WORKDIR /app + +RUN mkdir -p /app/public +RUN echo ' /app/public/index.php + +COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp +COPY --from=builder /etc/Caddyfile /etc/Caddyfile + +COPY --from=php-base /usr/local/include/php/ /usr/local/include/php +COPY --from=php-base /usr/local/lib/libphp.* /usr/local/lib +COPY --from=php-base /usr/local/lib/php/ /usr/local/lib/php +COPY --from=php-base /usr/local/php/ /usr/local/php +COPY --from=php-base /usr/local/bin/ /usr/local/bin +COPY --from=php-base /usr/src /usr/src + +RUN sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint + +CMD [ "--config", "/etc/Caddyfile" ] diff --git a/builder-debian.Dockerfile b/builder-debian.Dockerfile deleted file mode 100644 index bf7919f..0000000 --- a/builder-debian.Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM php-base - -COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go -COPY --from=golang-base /usr/local/go /usr/local/go - -# This is required to link the FrankenPHP binary to the PHP binary -RUN apt-get update && \ - apt-get -y --no-install-recommends install \ - libargon2-dev \ - libcurl4-openssl-dev \ - libonig-dev \ - libreadline-dev \ - libsodium-dev \ - libsqlite3-dev \ - libssl-dev \ - libxml2-dev \ - zlib1g-dev \ - && \ - apt-get clean - -WORKDIR /go/src/app - -COPY go.mod go.sum ./ -RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get - -RUN mkdir caddy && cd caddy -COPY caddy/go.mod caddy/go.sum ./caddy/ - -RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get - -COPY *.* ./ -COPY caddy caddy -COPY C-Thread-Pool C-Thread-Pool -COPY internal internal -COPY testdata testdata - -# todo: automate this? -# see https://github.com/docker-library/php/blob/master/8.2-rc/bullseye/zts/Dockerfile#L57-L59 for php values -ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS=$PHP_CFLAGS CGO_CPPFLAGS=$PHP_CPPFLAGS - -RUN cd caddy/frankenphp && \ - go build && \ - cp frankenphp /usr/local/bin && \ - cp /go/src/app/caddy/frankenphp/Caddyfile /etc/Caddyfile - -ENTRYPOINT ["/bin/bash","-c"] diff --git a/Dockerfile.dev b/dev.Dockerfile similarity index 100% rename from Dockerfile.dev rename to dev.Dockerfile diff --git a/docker-bake.hcl b/docker-bake.hcl index f5e956f..b5930d3 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,68 +1,90 @@ -variable "REPO_NAME" { +variable "IMAGE_NAME" { default = "dunglas/frankenphp" } -group "default" { - targets = ["bookworm-variants", "alpine-variants"] +variable "VERSION" { + default = "dev" } -group "bookworm-variants" { - targets = ["bookworm-php-82", "builder-bookworm-php-82"] +variable "SHA" {} + +variable "LATEST" { + default = false } -group "alpine-variants" { - targets = ["alpine-php-82", "builder-alpine-php-82"] +variable "CACHE" { + default = "" } -target "common" { - context = "." - platforms = ["linux/amd64", "linux/arm64"] +function "tag" { + params = [version, os, php-version, tgt] + result = [ + version != "" ? format("%s:%s%s-php%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version, os) : "", + os == "bookworm" && php-version == "8.2" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "", + php-version == "8.2" && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "", + os == "bookworm" && version != "" ? format("%s:%s%s-php%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version) : "" + ] } -target "common-bookworm" { +# cleanTag ensures that the tag is a valid Docker tag +# see https://github.com/distribution/distribution/blob/v2.8.2/reference/regexp.go#L37 +function "clean_tag" { + params = [tag] + result = substr(regex_replace(regex_replace(tag, "[^\\w.-]", "-"), "^([^\\w])", "r$0"), 0, 127) +} + +# semver adds semver-compliant tag if a semver version number is passed, or returns the revision itself +# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +function "semver" { + params = [rev] + result = __semver(_semver(regexall("^v?(?P0|[1-9]\\d*)\\.(?P0|[1-9]\\d*)\\.(?P0|[1-9]\\d*)(?:-(?P(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", rev))) +} + +function "_semver" { + params = [matches] + result = length(matches) == 0 ? {} : matches[0] +} + +function "__semver" { + params = [v] + result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? ["latest", v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"] +} + +target "default" { + name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}" + matrix = { + os = ["bookworm", "alpine"] + php-version = ["8.2", "8.3.0alpha3"] + tgt = ["builder", "runner"] + } contexts = { - php-base = "docker-image://php:8.2-zts-bookworm" - golang-base = "docker-image://golang:1.20-bookworm" + php-base = "docker-image://php:${php-version}-zts-${os}" + golang-base = "docker-image://golang:1.20-${os}" + } + dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile" + context = "./" + target = tgt + platforms = [ + "linux/amd64", + "linux/386", + "linux/arm/v6", + "linux/arm/v7", + "linux/arm64", + ] + tags = distinct(flatten([ + LATEST ? tag("latest", os, php-version, tgt) : [], + tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, php-version, tgt), + [for v in semver(VERSION) : tag(v, os, php-version, tgt)] + ])) + labels = { + "org.opencontainers.image.title" = "FrankenPHP" + "org.opencontainers.image.description" = "The modern PHP app server" + "org.opencontainers.image.url" = "https://frankenphp.dev" + "org.opencontainers.image.source" = "https://github.com/dunglas/frankenphp" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.vendor" = "Kévin Dunglas" + "org.opencontainers.image.created" = "${timestamp()}" + "org.opencontainers.image.version" = VERSION + "org.opencontainers.image.revision" = SHA } } - -target "common-alpine" { - contexts = { - php-base = "docker-image://php:8.2-zts-alpine3.18" - golang-base = "docker-image://golang:1.20-alpine3.18" - } -} - -# Builders - -target "builder-bookworm-php-82" { - inherits = ["common-bookworm"] - dockerfile = "builder-debian.Dockerfile" - tags = ["${REPO_NAME}:builder", "${REPO_NAME}:builder-bookworm"] -} - -target "builder-alpine-php-82" { - inherits = ["common-alpine"] - dockerfile = "builder-alpine.Dockerfile" - tags = ["${REPO_NAME}:builder-alpine"] -} - -# -# FrankenPHP -# - -target "bookworm-php-82" { - inherits = ["common", "common-bookworm"] - contexts = { - builder = "target:builder-bookworm-php-82" - } - tags = ["${REPO_NAME}:bookworm", "${REPO_NAME}:latest"] -} - -target "alpine-php-82" { - inherits = ["common", "common-alpine"] - contexts = { - builder = "target:builder-alpine-php-82" - } - tags = ["${REPO_NAME}:alpine"] -} diff --git a/frankenphp.c b/frankenphp.c index 48b0c2f..8b0369d 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -583,8 +583,9 @@ static void *manager_thread(void *arg) { free(arg); uintptr_t rh; - while ((rh = go_fetch_request())) + while ((rh = go_fetch_request())) { thpool_add_work(thpool, go_execute_script, (void *) rh); + } /* channel closed, shutdown gracefully */ thpool_wait(thpool);