diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml new file mode 100644 index 00000000..9f2206d5 --- /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}}:latest,${{vars.DOCKERHUB_USERNAME}}/${{vars.DOCKERHUB_CONTAINER}}:${{github.ref_name}} + 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..34a603a2 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM debian:bookworm-slim + +RUN apt -y update && apt -y upgrade +RUN apt install -y apache2 git libapache2-mod-wsgi-py3 mariadb-client python-celery-common python3-celery python3-debian python3-defusedxml python3-lxml python3-mysqldb python3-pip python3-progressbar python3-psycopg2 python3-redis python3-rpm + +WORKDIR /srv/patchman + +COPY . /srv/patchman/ +COPY ./etc/patchman/apache.conf.example /etc/apache2/sites-available/patchman.conf + +RUN /srv/patchman/setup.py install + +RUN a2enmod wsgi +RUN a2ensite patchman + +RUN mkdir -p /var/lib/patchman/db +RUN chown :www-data /var/lib/patchman/db && chmod 2770 /var/lib/patchman/db + +EXPOSE 80 + +COPY ./docker/docker-entrypoint.sh docker-entrypoint.sh +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..3310fccf --- /dev/null +++ b/docker/README.md @@ -0,0 +1,89 @@ +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 | +| :--- | :--- | :--- | +| `ADMIN_NAME` | `Your Name` | Your name | +| `ADMIN_EMAIL` | `you@example.com` | Your e-mail address | +| `TIMEZONE` | `America/New_York` | Your timezone | +| `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 100755 index 00000000..cf0ac72f --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +conf="/etc/patchman/local_settings.py" + +# 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 + if [ "${DB_ENGINE}" == "MySQL" ]; then + if [ -n "${DB_PORT}" ]; then + dbPort="${DB_PORT}" + else + dbPort="3306" + fi + + 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 + + elif [ "${DB_ENGINE}" == "PostgreSQL" ]; then + if [ -n "${DB_PORT}" ]; then + dbPort="${DB_PORT}" + else + dbPort="5432" + fi + + 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 + fi + fi +fi + +# Configure TIME_ZONE +if [ -n "${TIMEZONE}" ]; then + sed -i '22 {s/America\/New_York/'"${TIMEZONE/\//\\/}"'/}' "$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 "s/SECRET_KEY = ''/SECRET_KEY = '"${SECRET_KEY}"'/g" "$conf" + else + patchman-set-secret-key + fi +fi + +# Configure CACHES +if "${USE_CACHE}"; then + if [ -n "${REDIS_HOST}" ]; then + redisHost="${REDIS_HOST}" + else + redisHost="127.0.0.1" + fi + + if [ -n "${REDIS_PORT}" ]; then + redisPort="${REDIS_PORT}" + else + redisPort="6379" + fi + + # Comment DummyCache Block + sed -i '47,51 {/^#/ ! s/\(.*\)/#\1/}' "$conf" + + # Uncomment RedisCache Block + sed -i '55,61 {s/^# //}' "$conf" + + sed -i "58 {s/127.0.0.1:6379/$redisHost:$redisPort/}" "$conf" + + if [ -n "${CACHE_TIMEOUT}" ]; then + sed -i "59 {s/30/${CACHE_TIMEOUT}/}" "$conf" + fi +fi + +# Sync database on container first start +if [ ! -f /var/lib/patchman/.firstrun ]; then + patchman-manage makemigrations + patchman-manage migrate --run-syncdb --fake-initial + patchman-manage collectstatic + + # If SQLite is being used, allow httpd to write + if [ -z "${DB_ENGINE}" ]; then + chmod 660 /var/lib/patchman/db/patchman.db + fi + + touch /var/lib/patchman/.firstrun +fi + +# Starts Celery for for realtime processing of reports from clients +if "${USE_CELERY}"; then + if [ -n "${REDIS_HOST}" ]; then + redisHost="${REDIS_HOST}" + else + redisHost="127.0.0.1" + fi + + if [ -n "${REDIS_PORT}" ]; then + redisPort="${REDIS_PORT}" + else + redisPort=6379 + fi + + 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 + + C_FORCE_ROOT=1 celery -b redis://"$redisHost":"$redisPort"/0 -A patchman worker -l INFO -E & +fi + +# Starts Apache httpd process +/usr/sbin/apache2ctl -DFOREGROUND + +# Wait for any process to exit +wait -n + +# Exit with status of process that exited first +exit $?