diff --git a/Containerfile b/Containerfile new file mode 100644 index 000000000..67c87473e --- /dev/null +++ b/Containerfile @@ -0,0 +1,189 @@ +# Icinga 2 Docker image | (c) 2025 Icinga GmbH | GPLv2+ + +FROM debian:bookworm-slim AS build-base +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install all the necessary build dependencies for building Icinga 2 and the plugins. +# +# This stage includes the build dependencies for the plugins as well, so that they can share the same base +# image, since Docker builds common stages only once [^1] even if they are used in multiple build stages. +# This eliminates the need to have a separate base image for the plugins, that basically has kind of the +# same dependencies as the Icinga 2 build stage (ok, not exactly the same, but some of them are shared). +# +# [^1]: https://docs.docker.com/build/building/best-practices/#create-reusable-stages +RUN apt-get update && \ + apt-get install -y --no-install-{recommends,suggests} \ + autoconf \ + automake \ + bison \ + ccache \ + cmake \ + flex \ + g++ \ + git \ + libboost{,-{context,coroutine,date-time,filesystem,iostreams,program-options,regex,system,test,thread}}1.74-dev \ + libedit-dev \ + libmariadb-dev \ + libpq-dev \ + libssl-dev \ + libsystemd-dev \ + make && \ + rm -rf /var/lib/apt/lists/* + +# Set the default working directory for subsequent commands of the next stages. +WORKDIR /icinga2-build + +FROM build-base AS build-plugins +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install all the plugins that are not included in the monitoring-plugins package. +ADD https://github.com/lausser/check_mssql_health.git#747af4c3c261790341da164b58d84db9c7fa5480 /check_mssql_health +ADD https://github.com/lausser/check_nwc_health.git#a5295475c9bbd6df9fe7432347f7c5aba16b49df /check_nwc_health +ADD https://github.com/bucardo/check_postgres.git#58de936fdfe4073413340cbd9061aa69099f1680 /check_postgres +ADD https://github.com/matteocorti/check_ssl_cert.git#341b5813108fb2367ada81e866da989ea4fb29e7 /check_ssl_cert + +WORKDIR /check_mssql_health +RUN mkdir bin && \ + autoconf && \ + autoreconf && \ + ./configure "--build=$(uname -m)-unknown-linux-gnu" --libexecdir=/usr/lib/nagios/plugins && \ + make && \ + make install DESTDIR="$(pwd)/bin" + +WORKDIR /check_nwc_health +RUN mkdir bin && \ + autoreconf && \ + ./configure "--build=$(uname -m)-unknown-linux-gnu" --libexecdir=/usr/lib/nagios/plugins && \ + make && \ + make install DESTDIR="$(pwd)/bin" + +WORKDIR /check_postgres +RUN mkdir bin && \ + perl Makefile.PL INSTALLSITESCRIPT=/usr/lib/nagios/plugins && \ + make && \ + make install DESTDIR="$(pwd)/bin" && \ + # This is necessary because of this build error: cannot copy to non-directory: /var/lib/docker/.../merged/usr/local/man + rm -rf bin/usr/local/man + +FROM build-base AS build-icinga2 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# To access the automated build arguments in the Dockerfile originated from the Docker BuildKit [^1], +# we need to declare them here as build arguments. This is necessary because we want to use unique IDs +# for the mount cache below for each platform to avoid conflicts between multi arch builds. Otherwise, +# the build targets will invalidate the cache one another, leading to strange build errors. +# +# [^1]: https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope +ARG TARGETPLATFORM + +# Create the directory where the final Icinga 2 files will be installed. +# +# This directory will be used as the destination for the `make install` command below and will be +# copied to the final image. Other than that, this directory will not be used for anything else. +RUN mkdir /icinga2-install + +# Mount the source code as a bind mount instead of copying it, so that we can use the cache effectively. +# Additionally, add the ccache and CMake build directories as cache mounts to speed up rebuilds. +RUN --mount=type=bind,source=.,target=/icinga2,readonly \ + --mount=type=cache,id=ccache-${TARGETPLATFORM},target=/root/.ccache \ + --mount=type=cache,id=icinga2-build-${TARGETPLATFORM},target=/icinga2-build \ + PATH="/usr/lib/ccache:$PATH" \ + cmake -S /icinga2 -B /icinga2-build \ + -DCMAKE_BUILD_TYPE=ReleaseWithDebInfo \ + # The command group name below is required for the prepare-dirs script to work, as it expects + # the command group name, which by default is `icingacmd` to exist on the system. Since we + # don't create the `icingacmd` command group in this image, we need to override it with icinga. + -DICINGA2_COMMAND_GROUP=icinga \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_SYSCONFDIR=/data/etc \ + -DCMAKE_INSTALL_LOCALSTATEDIR=/data/var \ + -DICINGA2_SYSCONFIGFILE=/etc/sysconfig/icinga2 \ + -DICINGA2_RUNDIR=/run \ + # See https://github.com/Icinga/docker-icinga2/pull/103 for why we need to enable systemd support. + -DUSE_SYSTEMD=ON \ + -DICINGA2_WITH_{COMPAT,LIVESTATUS}=OFF && \ + make -j$(nproc) && \ + CTEST_OUTPUT_ON_FAILURE=1 make test && \ + make install DESTDIR=/icinga2-install + +RUN rm -rf /icinga2-install/etc/icinga2/features-enabled/mainlog.conf \ + /icinga2-install/usr/share/doc/icinga2/markdown && \ + strip -g /icinga2-install/usr/lib/*/icinga2/sbin/icinga2 && \ + strip -g /icinga2-install/usr/lib/nagios/plugins/check_nscp_api + +# Prepare the final image with the necessary configuration files and runtime dependencies. +FROM debian:bookworm-slim AS icinga2 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install the necessary runtime dependencies for the Icinga 2 binary and the monitoring-plugins. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive && \ + apt-get install -y --no-install-{recommends,suggests} \ + bc \ + ca-certificates \ + curl \ + dumb-init \ + file \ + libboost-{context,coroutine,date-time,filesystem,iostreams,program-options,regex,system,thread}1.74.0 \ + libcap2-bin \ + libedit2 \ + libldap-common \ + libmariadb3 \ + libmoosex-role-timer-perl \ + libpq5 \ + libssl3 \ + libsystemd0 \ + mailutils \ + monitoring-plugins \ + 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. +RUN adduser \ + --system \ + --group \ + --home /var/lib/icinga2 \ + --disabled-login \ + --allow-bad-names \ + --no-create-home \ + --uid 5665 icinga + +COPY --from=build-plugins /check_mssql_health/bin/ / +COPY --from=build-plugins /check_nwc_health/bin/ / +COPY --from=build-plugins /check_postgres/bin/ / +COPY --from=build-plugins /check_ssl_cert/check_ssl_cert /usr/lib/nagios/plugins/check_ssl_cert + +COPY --from=build-icinga2 /icinga2-install/ / + +# Create for all Icinga 2 directories in /data a corresponding symlink in the root directory. +# This is necessary because we want to maintain the compatibility with containers built with the +# legacy Dockerfile, which expects the Icinga 2 directories to be in the root directory. +RUN for dir in /etc/icinga2 /var/cache/icinga2 /var/lib/icinga2 /var/log/icinga2 /var/spool/icinga2; do \ + ln -vs "/data$dir" "$dir"; \ +done + +# Run the prepare-dirs script to create non-existing directories and set the correct permissions for them. +# It's invoked in the same way as in the systemd unit file in a Debian package, so this will ensure that +# all the necessary directories are created with the correct permissions and ownership. +RUN /usr/lib/icinga2/prepare-dirs /etc/sysconfig/icinga2 + +# Well, since the /data directory is intended to be used as a volume, we should also declare it as such. +# This will allow users to mount their own directories or even specific files to the /data directory +# without any issues. We've already filled the /data directory with the necessary configuration files, +# so users can simply mount their own files or directories if they want to override the default ones and +# they will be able to do so without any issues. +VOLUME ["/data"] + +COPY tools/container/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh +ENTRYPOINT ["/usr/bin/dumb-init", "-c", "--", "/usr/local/bin/docker-entrypoint.sh"] + +EXPOSE 5665 +USER icinga + +CMD ["icinga2", "daemon"] diff --git a/tools/container/docker-entrypoint.sh b/tools/container/docker-entrypoint.sh new file mode 100755 index 000000000..c3af044a7 --- /dev/null +++ b/tools/container/docker-entrypoint.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +set -eo pipefail + +# Function to display messages with different severity levels +# Usage: icinga2_log +icinga2_log() { + local severity="$1" + local message="$2" + + local color="" + local reset="" + + # Check if we are running in a terminal that supports colors, + # otherwise fallback to plain text output. + if [ -t 2 ]; then + reset="\033[0m" + # Set color codes based on severity + case "$severity" in + "information") + color="\033[1;32m" # Green + ;; + "warning") + color="\033[1;33m" # Yellow + ;; + "error") + color="\033[1;31m" # Red + ;; + esac + fi + + # Print the message with the appropriate color and reset code to stderr + echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] ${color}${severity}${reset}/DockerEntrypoint: ${message}" >&2 +} + +# The entrypoint script expects at least one command to run. +if [ $# -eq 0 ]; then + icinga2_log "warning" "Icinga 2 Docker entrypoint script requires at least one command to run." + exit 1 +fi + +icinga2_log "information" "Icinga 2 Docker entrypoint script started." + +ca="/var/lib/icinga2/certs/ca.crt" +if [ ! -e "$ca" ]; then + nodeSetup=("node" "setup") + runNodeSetup=false + + # The following loop looks for environment variables that start with ICINGA_ and applies some transformations + # to the keys before processing them in one way or another. Their values are never modified or printed in + # unintended ways. The key transformations have the following rules and are applied in the order they are listed: + # + # - Since it only processes environment variables that start with ICINGA_, it'll first strip that prefix. + # It then passes the key through awk to convert it to lowercase e.g. ICINGA_CN becomes cn. + # - For each key, that hits one of the cases below, it will be processed a bit differently. In the first match, + # the environment variable is expected to be a boolean (1 or 0) and it only becomes part of the node setup + # command if and only if its value is 1. In that case, underscores in the key are replaced with dashes and + # passed as-is to the node setup command (e.g., ICINGA_ACCEPT_COMMANDS=1 becomes --accept-commands). + # - For the second match, the key is expected to be a key-value pair that should be passed to the node setup + # command. In this case, the key is transformed in the same way as above, i.e., underscores are replaced with + # dashes and the value is passed as-is (e.g., ICINGA_CN=example.com becomes --cn example.com). + # - For the third match, the trusted certificate is expected to be a PEM-encoded certificate that should be + # written to a temporary file and passed to the node setup command. + # - Lastly, the CA certificate is likewise expected to be a PEM-encoded certificate that should be written to + # the expected location at /var/lib/icinga2/certs/ca.crt. + # + # When encountering an environment variable prefixed with ICINGA_ that we don't know how to handle, we log it + # as an informational message and continue processing the next environment variable but it doesn't cause the + # script to fail. + while IFS='=' read -r k value; do + # Strip the ICINGA_ prefix and convert the key to lowercase. + key=$(echo "${k#ICINGA_}" | awk '{print tolower($0)}') + + case "$key" in + "accept_commands" | "accept_config" | "disable_confd" | "master") + runNodeSetup=true + if [ "$value" = "1" ]; then + nodeSetup+=("--${key//_/-}") + fi + ;; + "cn" | "endpoint" | "global_zones" | "listen" | "parent_host" | "parent_zone" | "zone" | "ticket") + runNodeSetup=true + nodeSetup+=("--${key//_/-}" "$value") + ;; + "trustedcert") + icinga2_log "information" "Writing trusted certificate to temporary file." + runNodeSetup=true + trustedCertFile=$(mktemp /tmp/trusted.cert) + echo "$value" > "$trustedCertFile" + nodeSetup+=("--$key" "$trustedCertFile") + chmod 0644 "$trustedCertFile" + ;; + "cacert") + icinga2_log "information" "Writing CA certificate to $ca." + runNodeSetup=true + echo "$value" > "$ca" + chmod 0644 "$ca" + ;; + *) + # We don't know how to handle this environment variable, so log it and move on. + icinga2_log "warning" "Ignoring unknown environment variable $k" + ;; + esac + done < <(env | grep -E '^ICINGA_') + + if [ "$runNodeSetup" = true ]; then + icinga2_log "information" "Running Icinga 2 node setup command..." + + icinga2 "${nodeSetup[@]}" + # If the node setup command wasn't successful, we shouldn't reach this point due to set -e. + icinga2_log "information" "Node setup completed successfully." + else + icinga2_log "information" "No node setup required based on environment variables." + fi +fi + +mSmtpRc="/var/lib/icinga2/.msmtprc" +# This script should initialize the container's msmtp configuration but never overwrite an existing configuration file. +# If the file already exists, it should not be modified, even if the MSMTPRC environment variable is set. +if [ ! -e "$mSmtpRc" ] && [ -n "${MSMTPRC}" ]; then + icinga2_log "information" "Configuring msmtp with the provided MSMTPRC environment variable." + echo "$MSMTPRC" > "$mSmtpRc" + chmod 0644 "$mSmtpRc" +fi + +icinga2_log "information" "Starting Icinga 2 daemon." + +exec "$@"