From b2e9cfcd625c3e3f2b4efd1a5e7313d52fc7f38d Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Thu, 23 Apr 2026 13:17:11 +0200 Subject: [PATCH] Build containerimage on armhf and debian:experimental --- .github/workflows/container-image-base.yml | 228 +++++++++++++++++++++ .github/workflows/container-image.yml | 4 +- Containerfile | 39 +--- 3 files changed, 232 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/container-image-base.yml diff --git a/.github/workflows/container-image-base.yml b/.github/workflows/container-image-base.yml new file mode 100644 index 000000000..98efce775 --- /dev/null +++ b/.github/workflows/container-image-base.yml @@ -0,0 +1,228 @@ +# The Build and Publish Container Image workflow builds container images and pushes them to both +# GitHub Container Registry (GHCR) and Docker Hub. It sets up QEMU and Docker Buildx for cross-platform +# builds, and builds the container images using the Containerfile. For all non-pull request events that +# trigger this workflow, it logs into GHCR and Docker Hub using credentials from the workflow call inputs, +# tags and pushes the images to both registries, and generates and pushes signed build provenance attestations +# to each registry. Additionally, when a building and publishing the latest tag, it syncs the README file +# determined by the container_readme_filepath input (or the For-Container.md file found in the ./doc/ directory +# if not provided) with Docker Hub. For pull request events, it just builds the images but does not push them +# to the registries. + +name: Container Image + +on: + workflow_call: + inputs: + image_name: + required: false + type: string + description: 'Name of the container image to build and publish, e.g., "icinga/icinga2".' + documentation_url: + required: true + type: string + description: 'URL to the Icinga documentation for this project.' + container_readme_filepath: + required: false + type: string + description: 'Path to the README file to sync with Docker Hub. Defaults to the For-Container.md file in the ./doc/ directory.' + # We do not need to require the secrets.GITHUB_TOKEN here because it is automatically + # inherited from the workflow call [^1]. + # + # [^1]: https://docs.github.com/en/actions/reference/workflows-and-actions/reusable-workflows#github-context + secrets: + dockerhub_username: + required: true + description: 'Username for Docker Hub.' + dockerhub_token: + required: true + description: 'Personal access token for Docker Hub.' + +env: + # If we did not receive a custom image name from the workflow call inputs, we use the repository name prefixed + # with "icinga/" as the default image name. Actually, there's also the ${{ github.repository }} context variable, + # which contains the repository name in the format "owner/repo", but it is not suitable for container image names + # in our case because they must be lowercase, and our organization name is Icinga. Our repository names on the other + # hand, are all lowercase, so no additional modifications are necessary. + IMAGE_NAME: ${{ inputs.image_name || format('icinga/{0}', github.event.repository.name) }} + + # The LATEST variable determines if the current release tag is the greatest tag overall. + # If true, the container image will be tagged as 'latest' when pushed to the container registries. + LATEST: false + + # The LATEST_MAJOR variable determines if the current release tag is the greatest within its major version. + # If true, the container image will be tagged with the major version (e.g., '1') when pushed to the registries. + LATEST_MAJOR: false + + # The path to the README file to sync with Docker Hub. If not provided, it defaults to + # the For-Container.md file found in the ./doc/ directory. + README_FILEPATH: ${{ inputs.container_readme_filepath }} + +jobs: + build-and-publish: + name: Build and Publish + runs-on: ubuntu-latest + + permissions: + contents: read # Read github repository contents (actually required only for private repositories). + packages: write # Push container images to GitHub Container Registry. + attestations: write # Push signed build provenance attestations to the GHCR. + id-token: write # Required for the actions/attest-build-provenance@v3 action to generate attestations. + + steps: + # Explicitly using the checkout action (instead of relying on docker/build-push-action to do it implicitly) + # because we need to fetch all tags. + - name: Checkout + uses: actions/checkout@v6 + with: + # Switch to fetch-tags: true once https://github.com/actions/checkout/issues/1467 is fixed. + fetch-depth: 0 + + # Updates env.LATEST and env.LATEST_MAJOR based on + # whether the current release tag is the greatest overall and/or + # within its major version. + - name: Prepare metadata (release tags) + if: github.event_name == 'release' && github.event.action == 'published' + run: | + # Retrieve the greatest existing tag in the repository by sorting tags in descending order. + # Options used: + # * --sort=-v:refname sorts tags as versions, placing the highest version at the top. + # * -c 'versionsort.suffix=-' ensures that pre-release tags (e.g., 1.0.0-rc1) are sorted correctly, + # so they are not considered greater than their corresponding final release (e.g., 1.0.0). + # Intentionally not using head -1 to prevent potential broken pipe errors. + greatest_tag=$(git -c 'versionsort.suffix=-' tag --list --sort=-v:refname | awk 'NR==1') + + if [ "${{ github.ref_name }}" = "$greatest_tag" ]; then + echo "The current tag ${{ github.ref_name }} is the greatest overall. Tagging as 'latest'." + + # Update environment variable to enable tagging as 'latest'. + echo "LATEST=true" >> "$GITHUB_ENV" + else + echo "The current tag ${{ github.ref_name }} is not the greatest overall compared to $greatest_tag. Not tagging as 'latest'." + fi + + major_version=$(echo ${{ github.ref_name }} | cut -d. -f1) + greatest_major=$(git -c 'versionsort.suffix=-' tag --list "${major_version}.*" --sort=-v:refname | awk 'NR==1') + if [ "${{ github.ref_name }}" = "$greatest_major" ]; then + echo "The current tag ${{ github.ref_name }} is the greatest within its major version. Tagging with major version ${major_version#v}." + + # Update environment variable to enable tagging with major version. + echo "LATEST_MAJOR=true" >> "$GITHUB_ENV" + else + echo "The current tag ${{ github.ref_name }} is not the greatest within its major version compared to $greatest_major. Not tagging with major version ${major_version#v}." + fi + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + # This will generate tags and labels for both the GHCR image and Docker Hub image. + images: | + # GitHub Container Registry + ghcr.io/${{ env.IMAGE_NAME }} + # Docker Hub + docker.io/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.documentation=${{ inputs.documentation_url }} + org.opencontainers.image.vendor=Icinga GmbH + flavor: | + # Disable automatic 'latest' tagging as our custom logic is used to + # determine when to apply the 'latest' tag. + latest=false + tags: | + type=edge + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}},enable=${{ env.LATEST_MAJOR }} + type=raw,value=latest,enable=${{ env.LATEST }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.dockerhub_username }} + password: ${{ secrets.dockerhub_token }} + + - name: Build and push Container image + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Containerfile + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/armhf + push: false + # The tags generated in the metadata step include tags for both Docker Hub and GHCR image names, + # allowing the build and push action to build and push images to both registries. + tags: ${{ steps.meta.outputs.tags }} + + - name: Generate artifact attestation for GitHub Container Registry + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v3 + with: + subject-name: ghcr.io/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build-and-push.outputs.digest }} + push-to-registry: false + + - name: Generate artifact attestation for Docker Hub + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v3 + with: + # According to the documentation [^1], + # "index.docker.io" should be used as the registry portion of the image name when pushing to Docker Hub. + # + # [^1]: https://github.com/actions/attest-build-provenance?tab=readme-ov-file#container-image + subject-name: index.docker.io/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build-and-push.outputs.digest }} + push-to-registry: false + + - name: Prepare For-Container.md file + if: ${{ env.LATEST == 'true' }} + run: | + if [ -z "${{ env.README_FILEPATH }}" ]; then + files=$(find ./doc -type f -name 'For-Container.md') + if [ -z "$files" ]; then + echo "No For-Container.md file found in the ./doc/ directory." + exit 1 + fi + # Must be a single file, otherwise exit with error. + if [ $(echo "$files" | wc -l) -ne 1 ]; then + echo "Multiple For-Container.md files found in the ./doc/ directory. Please specify a single file using the container_readme_filepath input." + echo "$files" + exit 1 + fi + + file_path=$(echo "$files" | head -n 1) + echo "No custom container README file path provided. Using default path: $file_path" + echo "README_FILEPATH=$file_path" >> "$GITHUB_ENV" + else + # Check if the provided file exists. + if [ -f "${{ env.README_FILEPATH }}" ]; then + echo "Using provided container README file path: ${{ env.README_FILEPATH }}" + else + echo "Provided container README file path does not exist: ${{ env.README_FILEPATH }}" + exit 1 + fi + fi + + - name: Sync For-Container.md + uses: ms-jpq/sync-dockerhub-readme@e2991ea1ba48832e73555cdbd5b82f5a2e91ee9b # v1 + if: ${{ env.LATEST == 'true' }} + with: + username: ${{ secrets.dockerhub_username }} + password: ${{ secrets.dockerhub_token }} + repository: ${{ env.IMAGE_NAME }} + readme: ${{ env.README_FILEPATH }} diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index 18985d65b..6f6c37076 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -3,7 +3,7 @@ name: Container Image on: push: branches: - - master + - enable-parallel-testing-armhf pull_request: {} release: types: @@ -24,7 +24,7 @@ jobs: id-token: write name: Container Image - uses: icinga/github-actions/.github/workflows/container-image.yml@main + uses: ./.github/workflows/container-image-base.yml with: documentation_url: https://icinga.com/docs/icinga2 secrets: diff --git a/Containerfile b/Containerfile index 1da77e4ba..c0a0f3317 100644 --- a/Containerfile +++ b/Containerfile @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025 Icinga GmbH # SPDX-License-Identifier: GPL-2.0-or-later -FROM debian:trixie-slim AS build-base +FROM debian:experimental AS build-base # Install all the necessary build dependencies for building Icinga 2 and the plugins. # @@ -113,6 +113,7 @@ RUN --mount=type=bind,source=.,target=/icinga2,readonly \ PATH="/usr/lib/ccache:$PATH" \ cmake -S /icinga2 -B /icinga2-build \ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ + -DCMAKE_CXX_FLAGS="-fstack-protector-strong" \ # Podman supports forwarding notifications from containers to systemd, so build Icinga 2 with systemd support. -DUSE_SYSTEMD=ON \ -DBUILD_TESTING=${ICINGA2_BUILD_TESTING} \ @@ -146,42 +147,6 @@ FROM debian:trixie-slim AS icinga2 # The real UID of the Icinga user to be used in the final image. ARG ICINGA_USER_ID=5665 -# Install the necessary runtime dependencies for the Icinga 2 binary and the monitoring-plugins. -RUN apt-get update && \ - apt-get upgrade -y && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests \ - bc \ - ca-certificates \ - curl \ - dumb-init \ - file \ - libboost-context1.83.0 \ - libboost-coroutine1.83.0 \ - libboost-date-time1.83.0 \ - libboost-filesystem1.83.0 \ - libboost-iostreams1.83.0 \ - libboost-program-options1.83.0 \ - libboost-regex1.83.0 \ - libboost-system1.83.0 \ - libboost-thread1.83.0 \ - libcap2-bin \ - libedit2 \ - libldap-common \ - libmariadb3 \ - libmoosex-role-timer-perl \ - libpq5 \ - libprotobuf-lite32t64 \ - libssl3 \ - libsystemd0 \ - mailutils \ - monitoring-plugins \ - msmtp \ - msmtp-mta \ - openssh-client \ - openssl && \ - # Official Debian images automatically run `apt-get clean` after every install, so we don't need to do it here. - rm -rf /var/lib/apt/lists/* - # Create the icinga user and group with a specific UID as recommended by Docker best practices. # The user has a home directory at /var/lib/icinga2, and if configured, that directory will also # be used to store the ".msmtprc" file created by the entrypoint script.