diff --git a/README.md b/README.md index e9c6d47..745bac5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cloudflare compatible PowerDNS API Server -[![Discord](https://img.shields.io/discord/630271953806295051)](https://discord.gg/EPnCfz7xx5) [![Build Status](https://teamcity.mori.space/app/rest/builds/buildType:PdnsApi_Build/statusIcon)](https://teamcity.mori.space/project/PdnsApi) +[![Discord](https://img.shields.io/discord/630271953806295051)](https://discord.gg/EPnCfz7xx5) A Kotlin and Spring Boot API that exposes Cloudflare-style zone and DNS record endpoints backed by PowerDNS and MariaDB. API requests use `Authorization: Bearer `. @@ -26,6 +26,23 @@ Use JDK 25. The Gradle wrapper selects the Java 25 toolchain for compilation. | `PDNS_API_URL` | PowerDNS HTTP API base URL, for example `http://pdns:8081` | | `PDNS_NS` | Comma-separated nameservers, for example `ns1.example.com,ns2.example.com` | +## Publish AMD64 and ARM64 images + +The script builds a Linux native image for each architecture with Paketo Buildpacks, pushes the architecture-specific tags with Docker CLI credentials, then publishes a multi-platform manifest. Docker, Buildx, and a registry login with push access to the target repository are required. On a host with a different architecture, Docker must support emulation for the target platform. + +```sh +./scripts/build-multiarch-image.sh 1.0.0 +``` + +The default repository is `${DOCKER_USERNAME:-dalbodeule}/dnsapi`. Set `IMAGE_REPOSITORY` to use another repository, such as `git.mori.space/dalbodeule/dnsapi`. The script publishes `:1.0.0-amd64`, `:1.0.0-arm64`, `:1.0.0`, and `:latest`; set `PUBLISH_LATEST=0` to leave `:latest` unchanged. Review the commands without publishing by setting `DRY_RUN=1`. + +If a previous run built an architecture image locally but failed while pushing, log in to the registry and rerun with `RESUME=1`. The script checks the local image architecture and skips its costly rebuild. + +```sh +docker login --username dalbodeule +RESUME=1 ./scripts/build-multiarch-image.sh 1.0.0 +``` + ## Update API documentation ```sh diff --git a/build.gradle.kts b/build.gradle.kts index 61dbbc3..b82a6a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,18 +97,24 @@ tasks.register("updateDocs") { tasks.withType { val dockerId = System.getenv("DOCKER_USERNAME") ?: "dalbodeule" - val dockerPw = System.getenv("DOCKER_PASSWORD") ?: "" + val dockerPw = System.getenv("DOCKER_PASSWORD") docker { publish = true - publishRegistry { - username = dockerId - password = dockerPw + if (!dockerPw.isNullOrBlank()) { + publishRegistry { + username = dockerId + password = dockerPw + } } } imageName.set("$dockerId/dnsapi") - tags.set(setOf("$dockerId/dnsapi:latest", "$dockerId/dnsapi:${datetimeFormatter.format(LocalDateTime.now())}")) + if (providers.gradleProperty("multiArchImage").orNull == "true") { + tags.set(emptySet()) + } else { + tags.set(setOf("$dockerId/dnsapi:latest", "$dockerId/dnsapi:${datetimeFormatter.format(LocalDateTime.now())}")) + } buildpacks.set(setOf("docker.io/paketobuildpacks/oracle", "urn:cnb:builder:paketo-buildpacks/java-native-image")) environment = mapOf( diff --git a/scripts/build-multiarch-image.sh b/scripts/build-multiarch-image.sh new file mode 100755 index 0000000..aa1c9e1 --- /dev/null +++ b/scripts/build-multiarch-image.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 " >&2 + echo "Set IMAGE_REPOSITORY to override the default repository." >&2 + exit 2 +} + +if [[ $# -ne 1 ]]; then + usage +fi + +tag=$1 +if [[ ! $tag =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,121}$ ]]; then + echo "Invalid Docker image tag: $tag" >&2 + exit 2 +fi + +repository=${IMAGE_REPOSITORY:-${DOCKER_USERNAME:-dalbodeule}/dnsapi} +publish_latest=${PUBLISH_LATEST:-1} +dry_run=${DRY_RUN:-0} +resume=${RESUME:-0} +root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + +if [[ $publish_latest != 0 && $publish_latest != 1 ]]; then + echo "PUBLISH_LATEST must be 0 or 1" >&2 + exit 2 +fi +if [[ $dry_run != 0 && $dry_run != 1 ]]; then + echo "DRY_RUN must be 0 or 1" >&2 + exit 2 +fi +if [[ $resume != 0 && $resume != 1 ]]; then + echo "RESUME must be 0 or 1" >&2 + exit 2 +fi +if [[ $dry_run == 0 ]]; then + command -v docker >/dev/null || { echo "Docker is required" >&2; exit 1; } + docker buildx version >/dev/null +fi + +run() { + if [[ $dry_run == 1 ]]; then + printf 'DRY RUN:' + printf ' %q' "$@" + printf '\n' + else + "$@" + fi +} + +cd "$root_dir" +for arch in amd64 arm64; do + image="$repository:$tag-$arch" + if [[ $resume == 1 && $dry_run == 0 ]] && docker image inspect "$image" >/dev/null 2>&1; then + actual_platform=$(docker image inspect "$image" --format '{{.Os}}/{{.Architecture}}') + if [[ $actual_platform != "linux/$arch" ]]; then + echo "$image has platform $actual_platform, expected linux/$arch" >&2 + exit 1 + fi + echo "Reusing local image $image" + else + run ./gradlew bootBuildImage \ + -PmultiArchImage=true \ + "--imagePlatform=linux/$arch" \ + "--imageName=$image" \ + --no-publishImage + fi + run docker push "$image" +done + +manifest_args=(docker buildx imagetools create --tag "$repository:$tag") +if [[ $publish_latest == 1 ]]; then + manifest_args+=(--tag "$repository:latest") +fi +manifest_args+=("$repository:$tag-amd64" "$repository:$tag-arm64") +run "${manifest_args[@]}" +run docker buildx imagetools inspect "$repository:$tag"