diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..11bad1a4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git/ +*.pyc +.pydevproject +.settings +.project +log/django.osqa.log +tmp/* diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b728efb6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..47bf8a7a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: ci + +on: + push: + branches: + - "**" + tags: + - "v*.*.*" + pull_request: + branches: + - "master" + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/openstreetmap/osqa + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4e28eae5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# hadolint global ignore=DL3008 +FROM debian:buster + +# Set Docker User and Group IDs for matching with host +ARG UID=510 +ARG GID=510 + +# Install OSQA dependencies and cleanup apt cache +RUN apt-get update && apt-get install -y --no-install-recommends \ + python \ + python-pip \ + python-dev \ + python-setuptools \ + ca-certificates \ + gcc \ + libpq-dev \ + curl \ + gunicorn \ + gettext \ + && rm -rf /var/lib/apt/lists/* + +# Ensure terminate if pipefail +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Create OSQA user and group +RUN groupadd --system --gid ${GID} osqa && useradd --system --gid osqa --uid ${UID} osqa + +# Create OSQA working directory +WORKDIR /srv/app +# Set OSQA user as owner of working directory to allow .pyc files can be created +RUN chown osqa:osqa /srv/app + +# Install OSQA requirements +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy OSQA source code into container +COPY --chown=osqa . . + +# Set Container Default Environment Variables. Override as appropriate at runtime. +ENV OSQA_DB_ENGINE=django.db.backends.postgresql_psycopg2 +ENV OSQA_DB_NAME=osqa +ENV OSQA_DB_USER=osqa +ENV OSQA_DB_PASSWORD=osqa +ENV OSQA_DB_HOST=postgres +ENV OSQA_ALLOWED_HOSTS=localhost +ENV OSQA_CACHE_BACKEND=memcached://memcached:11211/ +ENV OSQA_APP_URL=http://localhost:8080/ +ENV OSQA_TIME_ZONE=UTC + +# Set container entrypoint script for runtime container setup +ENTRYPOINT ["./docker/entrypoint.sh"] + +# Switch to underprivileged OSQA user +USER osqa + +# Expose port 8080 for OSQA +EXPOSE 8080 + +# Run OSQA +CMD ["python", "manage.py", "runserver", "0.0.0.0:8080"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..672c3b94 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: "2.1" + +services: + + osqa: + build: + context: . + environment: + OSQA_DB_USER: osqa + OSQA_DB_PASSWORD: osqa + OSQA_DB_NAME: osqa + OSQA_DB_HOST: postgres + + ports: + - 8080:8080 + depends_on: + postgres: + condition: service_healthy + memcached: + condition: service_started + + postgres: + image: postgres:14 + environment: + POSTGRES_USER: osqa + POSTGRES_PASSWORD: osqa + POSTGRES_DB: osqa + healthcheck: + test: ["CMD-SHELL", "pg_isready -U osqa"] + interval: 10s + timeout: 5s + retries: 5 + + memcached: + image: memcached:1 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..d0db47a4 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -ex + +# Build the settings_local.py file from the template using environment variables +envsubst < docker/settings_local.py.template > settings_local.py + +python manage.py syncdb --all --noinput +python manage.py migrate --fake --noinput + +exec "$@" \ No newline at end of file diff --git a/docker/settings_local.py.template b/docker/settings_local.py.template new file mode 100644 index 00000000..b095bb05 --- /dev/null +++ b/docker/settings_local.py.template @@ -0,0 +1,79 @@ +# encoding:utf-8 +import os.path + +SITE_SRC_ROOT = os.path.dirname(__file__) +LOGGING = { + 'version': 1, + 'formatters': { + 'default': { + 'format': '[OSQA] TIME: %(asctime)s MSG: %(filename)s:%(funcName)s:%(lineno)d %(message)s', + } + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'default', + }, + }, + 'loggers' : { + # ensure that all log entries are propagated to root + 'django': { 'propagate': True }, + 'django.request': { 'propagate': True }, + 'django.security': { 'propagate': True }, + 'py.warnings': { 'propagate': True }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, +} + +#ADMINS and MANAGERS +ADMINS = () +MANAGERS = ADMINS + +DEBUG = False +DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': True +} +TEMPLATE_DEBUG = DEBUG +INTERNAL_IPS = ('127.0.0.1',) +ALLOWED_HOSTS = ('${OSQA_ALLOWED_HOSTS}',) + +DATABASES = { + 'default': { + 'ENGINE': '${OSQA_DB_ENGINE}', + 'NAME': '${OSQA_DB_NAME}', + 'USER': '${OSQA_DB_USER}', + 'PASSWORD': '${OSQA_DB_PASSWORD}', + 'HOST': '${OSQA_DB_HOST}', + 'PORT': '', + 'CONN_MAX_AGE': 600, + } +} + +CACHE_BACKEND = '${OSQA_CACHE_BACKEND}' +SESSION_ENGINE = 'django.contrib.sessions.backends.db' +# Customize the values below if OSQA is in a subfolder and especially you're planning on +# running multiple Django applications (OSQA or others) on the same domain in different +# subfolders +#SESSION_COOKIE_PATH = '/' +#CSRF_COOKIE_PATH = '/' + +# This should be equal to your domain name, plus the web application context. +# This shouldn't be followed by a trailing slash. +# I.e., http://www.yoursite.com or http://www.hostedsite.com/yourhostapp +APP_URL = '${OSQA_APP_URL}' + +#LOCALIZATIONS +TIME_ZONE = '${OSQA_TIME_ZONE}' + +#OTHER SETTINGS + +USE_I18N = True +LANGUAGE_CODE = 'en' + +OSQA_DEFAULT_SKIN = 'default' + +DISABLED_MODULES = ['books', 'recaptcha', 'project_badges', 'stats', 'localauth', 'facebookauth', 'oauthauth', 'mysqlfulltext'] diff --git a/requirements.txt b/requirements.txt index 82667873..55f26cbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,11 @@ -markdown -html5lib -python-openid -South -python-memcached -django==1.6.0 +markdown==2.4.1 +html5lib<0.99999999 +python-openid==2.2.5 +South==0.7.6 +python-memcached==1.53 +django==1.6.11 django-debug-toolbar django-endless-pagination pytz +psycopg2==2.7.7