diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f72bbff2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.gitignore +__pycache__ +*.pyc +*.pyo +*.egg-info +.env +.venv +docs/ +tests/ +*.md +!README.md diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml new file mode 100644 index 00000000..4825c77b --- /dev/null +++ b/.github/workflows/docker-build.yaml @@ -0,0 +1,42 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{vars.DOCKERHUB_USERNAME}}/${{vars.DOCKERHUB_CONTAINER}}:unstable + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Checkout for next job + uses: actions/checkout@v4 + + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{vars.DOCKERHUB_USERNAME}}/${{vars.DOCKERHUB_CONTAINER}} + readme-filepath: docker/README.md diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..7684c6b3 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,78 @@ +FROM debian:trixie-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATCHMAN_HOME=/srv/patchman + +RUN apt-get -y update && apt-get -y upgrade && \ + apt-get install -y --no-install-recommends \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR ${PATCHMAN_HOME} + +COPY requirements.txt . +RUN pip install --no-cache-dir --break-system-packages setuptools -r requirements.txt + +COPY . ${PATCHMAN_HOME}/ + +RUN ${PATCHMAN_HOME}/setup.py install --no-compile && \ + rm -rf build/ dist/ *.egg-info/ .eggs/ /root/.cache/ + + +FROM debian:trixie-slim AS runtime + +LABEL maintainer="4950815+RicardoJeronimo@users.noreply.github.com" \ + org.opencontainers.image.title="Patchman" \ + org.opencontainers.image.base.name="debian:trixie-slim" \ + org.opencontainers.image.source="https://github.com/RicardoJeronimo/patchman" + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATCHMAN_HOME=/srv/patchman + +RUN apt-get -y update && apt-get -y upgrade && \ + apt-get install -y --no-install-recommends \ + apache2 \ + curl \ + git \ + gosu \ + libapache2-mod-wsgi-py3 \ + libmagic1 \ + mariadb-client \ + python3-debian \ + python3-defusedxml \ + python3-lxml \ + python3-mysqldb \ + python3-packaging \ + python3-psycopg2 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR ${PATCHMAN_HOME} + +COPY --from=builder /usr/local/lib/ /usr/local/lib/ +COPY --from=builder /usr/local/bin/patchman* /usr/local/bin/ +COPY --from=builder /usr/local/bin/celery /usr/local/bin/ +COPY --from=builder /etc/patchman/ /etc/patchman/ +COPY --from=builder ${PATCHMAN_HOME}/ ${PATCHMAN_HOME}/ + +RUN cp ${PATCHMAN_HOME}/etc/patchman/apache.conf.example /etc/apache2/sites-available/patchman.conf && \ + echo "ServerName localhost" >> /etc/apache2/apache2.conf && \ + a2enmod wsgi && \ + a2ensite patchman && \ + a2dissite 000-default && \ + groupadd --system patchman && \ + useradd --system --gid patchman --no-create-home patchman && \ + mkdir -p /var/lib/patchman/db && \ + chown patchman:www-data /var/lib/patchman/db && \ + chmod 2770 /var/lib/patchman/db + +COPY ./docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -f http://localhost/patchman/ || exit 1 + +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..b8c7afc5 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,99 @@ +Source: https://github.com/ricardojeronimo/patchman + +Upstream: https://github.com/furlongm/patchman + + +## Getting Started + +To get started, pull the latest image from Docker Hub and run it. +``` +docker pull ricardojeronim0/patchman:latest +docker run -d -p 80:80 --name patchman ricardojeronim0/patchman +``` + +This container will run migrations on first startup, but you still need to create a superuser before being able to log in on the web interface. + +``` +docker exec -it patchman patchman-manage createsuperuser +``` + +## Configuration + +This container is configured using environment variables. The following variables are available to customize the container's behavior. + +| Variable | Default Value | Description | +| :--- | :--- | :--- | +| `DEBUG` | `False` | Debug mode | +| `ADMIN_NAME` | `Your Name` | Your name | +| `ADMIN_EMAIL` | `you@example.com` | Your e-mail address | +| `TIMEZONE` | `America/New_York` | Your timezone | +| `LANGUAGE_CODE` | `en-us` | Your language | +| `SECRET_KEY` | | Unique string not to be shared with anyone. Leave empty to generate one randomly | +| `MAX_MIRRORS` | `2` | Maximum number of mirrors to add or refresh per repo | +| `MAX_MIRROR_FAILURES` | `14` | Maximum number of failures before disabling a mirror, set to `-1` to never disable mirrors | +| `DAYS_WITHOUT_REPORT` | `14` | Number of days to wait before raising that a host has not reported | +| `ERRATA_OS_UPDATES` | `yum, rocky, alma, arch, ubuntu, debian` | List of errata sources to update, remove unwanted ones to improve performance | +| `ALMA_RELEASES` | `8, 9, 10` | List of Alma Linux releases to update | +| `DEBIAN_CODENAMES` | `bookworm, trixie` | List of Debian Linux releases to update | +| `UBUNTU_CODENAMES` | `jammy, noble` | List of Ubuntu Linux releases to update | +| `DB_ENGINE` | `SQLite` | Database engine to be used. Choose between `MySQL` or `PostgreSQL`, leave empty to use default `SQLite` | +| `DB_HOST` | | Database hostname, IP or container name | +| `DB_PORT` | | Database port | +| `DB_DATABASE` | | Database name | +| `DB_USER` | | Database user | +| `DB_PASSWORD` | | Database password | +| `REDIS_HOST` | `127.0.0.1` | Redis hostname, IP or container name | +| `REDIS_PORT` | `6379` | Redis port | +| `USE_CELERY` | `False` | Change to `True` for realtime processing of reports from clients | +| `USE_CACHE` | `False` | Change to `True` cache contents and reduce the load on the server | +| `CACHE_TIMEOUT` | `30` | Cache time in seconds. Be aware that the UI results may be out of date for this amount of time | + + +## Docker Compose Example + +For more complex deployments, `docker-compose` is the recommended approach. Below is an example `docker-compose.yaml` file that demonstrates how to configure the container and connect it to a separate MySQL service, and Redis for async processing and/or caching. + +```yaml +--- +services: + patchman: + container_name: patchman + image: ricardojeronim0/patchman:latest + restart: unless-stopped + environment: + ADMIN_NAME: admin_name + ADMIN_EMAIL: admin_mail@domain.tld + TIMEZONE: America/New_York + DB_ENGINE: MySQL + DB_HOST: patchman-db + DB_PORT: 3306 + DB_DATABASE: patchman + DB_USER: user + DB_PASSWORD: changeme + REDIS_HOST: redis + REDIS_PORT: 6379 + USE_CELERY: True + USE_CACHE: True + CACHE_TIMEOUT: 20 + ports: + - 80:80/tcp + depends_on: + - patchman-db + - redis + + patchman-db: + container_name: patchman-db + image: mysql:latest + restart: unless-stopped + command: ["mysqld", "--character-set-server=utf8", "--collation-server=utf8_general_ci"] + environment: + MYSQL_ROOT_PASSWORD: changeme + MYSQL_DATABASE: patchman + MYSQL_USER: user + MYSQL_PASSWORD: changeme + + redis: + container_name: redis + image: redis:latest + restart: unless-stopped +``` diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..77f80400 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,42 @@ +--- +services: + patchman: + container_name: patchman + image: furlongm/patchman:latest + restart: unless-stopped + environment: + ADMIN_NAME: admin_name + ADMIN_EMAIL: admin_mail@domain.tld + TIMEZONE: America/New_York + DB_ENGINE: MySQL + DB_HOST: patchman-db + DB_PORT: 3306 + DB_DATABASE: patchman + DB_USER: user + DB_PASSWORD: changeme + REDIS_HOST: redis + REDIS_PORT: 6379 + USE_CELERY: True + USE_CACHE: True + CACHE_TIMEOUT: 20 + ports: + - 80:80/tcp + depends_on: + - patchman-db + - redis + + patchman-db: + container_name: patchman-db + image: mysql:latest + restart: unless-stopped + command: ["mysqld", "--character-set-server=utf8", "--collation-server=utf8_general_ci"] + environment: + MYSQL_ROOT_PASSWORD: changeme + MYSQL_DATABASE: patchman + MYSQL_USER: user + MYSQL_PASSWORD: changeme + + redis: + container_name: redis + image: redis:latest + restart: unless-stopped diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 00000000..2f08d634 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,201 @@ +#!/bin/bash +set -euo pipefail + +log() { echo "[INFO] $(date -u '+%Y-%m-%dT%H:%M:%SZ') $*"; } +warn() { echo "[WARN] $(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" >&2; } +die() { echo "[ERROR] $(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" >&2; exit 1; } + +conf="/etc/patchman/local_settings.py" +[ -f "$conf" ] || die "Configuration file not found: $conf." + +# Configure DEBUG +if [ "${DEBUG:-false}" = true ]; then + log "DEBUG mode enabled." + sed -i '3 {s/False/True/}' "$conf" +fi + +# Configure ADMINS +if [ -n "${ADMIN_NAME:-}" ]; then + sed -i '6 {s/Your Name/'"${ADMIN_NAME}"'/}' "$conf" +fi + +if [ -n "${ADMIN_EMAIL:-}" ]; then + sed -i '6 {s/you@example.com/'"${ADMIN_EMAIL}"'/}' "$conf" +fi + +# Configure DATABASES +if [ -n "${DB_ENGINE:-}" ]; then + sed -i '9,18 {/^#/ ! s/\(.*\)/#\1/}' "$conf" + + if [[ $(grep -v "#" "$conf" | grep -c "ENGINE") -lt 2 ]]; then + case "${DB_ENGINE}" in + SQLite) + log "Using SQLite database." + ;; + MySQL) + dbPort="${DB_PORT:-3306}" + [ -n "${DB_DATABASE:-}" ] || die "DB_DATABASE is required for MySQL." + [ -n "${DB_USER:-}" ] || die "DB_USER is required for MySQL." + [ -n "${DB_HOST:-}" ] || die "DB_HOST is required for MySQL." + log "Configuring MySQL database at ${DB_HOST}:${dbPort}." + + cat <<-EOF >> "$conf" + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': '${DB_DATABASE}', + 'USER': '${DB_USER}', + 'PASSWORD': '${DB_PASSWORD}', + 'HOST': '${DB_HOST}', + 'PORT': '$dbPort', + 'STORAGE_ENGINE': 'INNODB', + 'CHARSET' : 'utf8' + } + } + EOF + ;; + + PostgreSQL) + dbPort="${DB_PORT:-5432}" + [ -n "${DB_DATABASE:-}" ] || die "DB_DATABASE is required for PostgreSQL." + [ -n "${DB_USER:-}" ] || die "DB_USER is required for PostgreSQL." + [ -n "${DB_HOST:-}" ] || die "DB_HOST is required for PostgreSQL." + log "Configuring PostgreSQL database at ${DB_HOST}:${dbPort}." + + cat <<-EOF >> "$conf" + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': '${DB_DATABASE}', + 'USER': '${DB_USER}', + 'PASSWORD': '${DB_PASSWORD}', + 'HOST': '${DB_HOST}', + 'PORT': '$dbPort', + 'CHARSET' : 'utf8' + } + } + EOF + ;; + + *) + die "Invalid DB_ENGINE: '${DB_ENGINE}'." + ;; + esac + fi +fi + +# Configure TIME_ZONE +if [ -n "${TIMEZONE:-}" ]; then + sed -i '22 {s/America\/New_York/'"${TIMEZONE/\//\\/}"'/}' "$conf" +fi + +# Configure LANGUAGE_CODE +if [ -n "${LANGUAGE_CODE:-}" ]; then + sed -i '26 {s/en-us/'"${LANGUAGE_CODE}"'/}' "$conf" +fi + +# Configure SECRET_KEY +if [ -z "$(grep "SECRET_KEY" "$conf" | cut -d " " -f 3 | tr -d "'")" ]; then + if [ -n "${SECRET_KEY:-}" ]; then + sed -i "29 {s/SECRET_KEY = ''/SECRET_KEY = '${SECRET_KEY}'/}" "$conf" + else + patchman-set-secret-key + fi +fi + +# Configure MAX_MIRRORS +if [ -n "${MAX_MIRRORS:-}" ]; then + sed -i '36 {s/2/'"${MAX_MIRRORS}"'/}' "$conf" +fi + +# Configure MAX_MIRROR_FAILURES +if [ -n "${MAX_MIRROR_FAILURES:-}" ]; then + sed -i '39 {s/14/'"${MAX_MIRROR_FAILURES}"'/}' "$conf" +fi + +# Configure DAYS_WITHOUT_REPORT +if [ -n "${DAYS_WITHOUT_REPORT:-}" ]; then + sed -i '42 {s/14/'"${DAYS_WITHOUT_REPORT}"'/}' "$conf" +fi + +# Configure ERRATA_OS_UPDATES +if [ -n "${ERRATA_OS_UPDATES:-}" ]; then + errataOSUpdates="${ERRATA_OS_UPDATES// /}" + sed -i '45 {s/\[.*\]/['"'${errataOSUpdates//,/\', \'}'"']/}' "$conf" +fi + +# Configure ALMA_RELEASES +if [ -n "${ALMA_RELEASES:-}" ]; then + sed -i '48 {s/\[.*\]/['"${ALMA_RELEASES}"']/}' "$conf" +fi + +# Configure DEBIAN_CODENAMES +if [ -n "${DEBIAN_CODENAMES:-}" ]; then + debianCodenames="${DEBIAN_CODENAMES// /}" + sed -i '51 {s/\[.*\]/['"'${debianCodenames//,/\', \'}'"']/}' "$conf" +fi + +# Configure UBUNTU_CODENAMES +if [ -n "${UBUNTU_CODENAMES:-}" ]; then + ubuntuCodenames="${UBUNTU_CODENAMES// /}" + sed -i '54 {s/\[.*\]/['"'${ubuntuCodenames//,/\', \'}'"']/}' "$conf" +fi + +# Configure CACHES +redisHost="${REDIS_HOST:-127.0.0.1}" +redisPort="${REDIS_PORT:-6379}" + +if [ "${USE_CACHE:-false}" = true ]; then + log "Configuring Redis cache at ${redisHost}:${redisPort}." + sed -i "62 {s/127.0.0.1:6379/$redisHost:$redisPort/}" "$conf" + + if [ -n "${CACHE_TIMEOUT:-}" ]; then + sed -i "67 {s/0/${CACHE_TIMEOUT}/}" "$conf" + fi +else + log "Cache disabled, using DummyCache." + sed -i '61 {s/redis.RedisCache/dummy.DummyCache/}' "$conf" + sed -i '62 {/^#/ ! s/\(.*\)/#\1/}' "$conf" +fi + +# Sync database on container first start +if [ ! -f /var/lib/patchman/.firstrun ]; then + log "First run detected, initialising database..." + log "Running makemigrations..." + patchman-manage makemigrations + log "Running migrate..." + patchman-manage migrate --run-syncdb --fake-initial + log "Running collectstatic..." + patchman-manage collectstatic --noinput + + if [ -z "${DB_ENGINE:-}" ]; then + chmod 660 /var/lib/patchman/db/patchman.db + fi + + touch /var/lib/patchman/.firstrun + log "Initialisation complete." +fi + +if [ "${USE_CELERY:-false}" = true ]; then + log "Starting Celery worker..." + + if [ -z "$(grep "USE_ASYNC_PROCESSING" "$conf" | cut -d " " -f 3 | tr -d "'")" ]; then + echo "" >> "$conf" + echo "USE_ASYNC_PROCESSING = True" >> "$conf" + fi + + if [ -z "$(grep "CELERY_BROKER_URL" "$conf" | cut -d " " -f 3 | tr -d "'")" ]; then + echo "CELERY_BROKER_URL = 'redis://$redisHost:$redisPort/0'" >> "$conf" + fi + + gosu www-data celery \ + -b redis://"$redisHost":"$redisPort"/0 \ + -A patchman worker \ + -l INFO -E & +fi + +# Starts Apache httpd process +log "Starting Apache..." +exec /usr/sbin/apache2ctl -DFOREGROUND