From 9c5f767cb80fd4eab671cf4fa83880350667dbed Mon Sep 17 00:00:00 2001 From: notplants Date: Thu, 18 Dec 2025 15:09:45 -0500 Subject: [PATCH] lasuite-docs as starting point --- .drone.yml | 39 ++++++ .env.sample | 67 +++++++++ .gitignore | 1 + abra-entrypoint.sh | 17 +++ abra.sh | 15 ++ compose.yml | 322 +++++++++++++++++++++++++++++++++++++++++++ nginx.conf.tmpl | 108 +++++++++++++++ pg_backup.sh | 34 +++++ release/.git-keep-me | 0 9 files changed, 603 insertions(+) create mode 100644 .drone.yml create mode 100644 .env.sample create mode 100644 .gitignore create mode 100644 abra-entrypoint.sh create mode 100755 abra.sh create mode 100644 compose.yml create mode 100644 nginx.conf.tmpl create mode 100644 pg_backup.sh create mode 100644 release/.git-keep-me diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..8139990 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,39 @@ +--- +kind: pipeline +name: deploy to swarm-test.autonomic.zone +steps: + - name: deployment + image: git.coopcloud.tech/coop-cloud/stack-ssh-deploy:latest + settings: + host: swarm-test.autonomic.zone + stack: example_com # UPDATE ME + generate_secrets: true + purge: true + deploy_key: + from_secret: drone_ssh_swarm_test + networks: + - proxy + environment: + DOMAIN: example.swarm-test.autonomic.zone # UPDATE ME + STACK_NAME: example_com # UPDATE ME + LETS_ENCRYPT_ENV: staging + # Also set any config versions from abra.sh +trigger: + branch: + - main +--- +kind: pipeline +name: generate recipe catalogue +steps: + - name: release a new version + image: plugins/downstream + settings: + server: https://build.coopcloud.tech + token: + from_secret: drone_abra-bot_token + fork: true + repositories: + - toolshed/auto-recipes-catalogue-json + +trigger: + event: tag diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..9db8257 --- /dev/null +++ b/.env.sample @@ -0,0 +1,67 @@ +TYPE=lasuite-docs + +DOMAIN=lasuite-docs.example.com + +## Domain aliases +#EXTRA_DOMAINS=', `www.lasuite-docs.example.com`' + +LETS_ENCRYPT_ENV=production + +############################################################################## +# SECRETS +############################################################################## +# abbreviations are to fit abra 12 char secret recommendation +# DJANGO_SECRET_KEY +SECRET_DJANGO_SK_VERSION=v1 +# ODIC_RP_CLIENT_SECRET +SECRET_OIDC_RPCS_VERSION=v1 +# DJANGO_SUPERUSER_PASSWORD +SECRET_DJANGO_SP_VERSION=v1 +# MINIO_ROOT_PASSWORD +SECRET_MINIO_RP_VERSION=v1 +# MINIO_ROOT_USER +SECRET_MINIO_RU_VERSION=v1 +# COLLABORATION_SERVER_SECRET +SECRET_COLLAB_SS_VERSION=v1 +# POSTGRES_PASSWORD +SECRET_POSTGRES_P_VERSION=v1 +# Y_PROVIDER_API_KEY +SECRET_Y_API_KEY_VERSION=v1 +# DJANGO_HOST_EMAIL_PASSWORD +SECRET_EMAIL_PASS_VERSION=v1 + +############################################################################## +# EMAIL +############################################################################## +DJANGO_EMAIL_BRAND_NAME="La Suite Numérique" +DJANGO_EMAIL_HOST="mail.example.com" +DJANGO_EMAIL_LOGO_IMG="http://$DOMAIN/assets/logo-suite-numerique.png" +DJANGO_EMAIL_PORT=1025 +DJANGO_EMAIL_USE_SSL=True +DJANGO_EMAIL_USE_TLS=False +DJANGO_EMAIL_FROM=mail@example.com + +############################################################################## +# SINGLE SIGN ON +############################################################################## +# NOTE: OpenID Connect (OIDC) single sign-on is **required**, see recipe README +OIDC_REALM=yourkeycloakrealm +OIDC_OP_JWKS_ENDPOINT=https://auth.${DOMAIN}/realms/${OIDC_REALM}/protocol/openid-connect/certs +OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.${DOMAIN}/realms/${OIDC_REALM}/protocol/openid-connect/auth +OIDC_OP_TOKEN_ENDPOINT=https://auth.${DOMAIN}/realms/${OIDC_REALM}/protocol/openid-connect/token +OIDC_OP_USER_ENDPOINT=https://auth.${DOMAIN}/realms/${OIDC_REALM}/protocol/openid-connect/userinfo +OIDC_RP_CLIENT_ID=yourkeycloakclientid +OIDC_RP_SIGN_ALGO=RS256 +OIDC_RP_SCOPES="openid email" +LOGIN_REDIRECT_URL=https://${DOMAIN} +LOGIN_REDIRECT_URL_FAILURE=https://${DOMAIN} +LOGOUT_REDIRECT_URL=https://${DOMAIN} +OIDC_REDIRECT_ALLOWED_HOSTS='["https://auth.${DOMAIN}", "https://${DOMAIN}"]' +OIDC_AUTH_REQUEST_EXTRA_PARAMS='{"acr_values": "eidas1"}' + +############################################################################## +# LOGGING +############################################################################## +LOGGING_LEVEL_HANDLERS_CONSOLE=INFO +LOGGING_LEVEL_LOGGERS_ROOT=INFO +LOGGING_LEVEL_LOGGERS_APP=INFO diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a6353d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.envrc diff --git a/abra-entrypoint.sh b/abra-entrypoint.sh new file mode 100644 index 0000000..470e0dd --- /dev/null +++ b/abra-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e + +[ -f /run/secrets/postgres_p ] && export DB_PASSWORD="$(cat /run/secrets/postgres_p)" +[ -f /run/secrets/django_sk ] && export DJANGO_SECRET_KEY="$(cat /run/secrets/django_sk)" +[ -f /run/secrets/minio_rp ] && export AWS_S3_SECRET_ACCESS_KEY="$(cat /run/secrets/minio_rp)" +[ -f /run/secrets/minio_ru ] && export AWS_S3_ACCESS_KEY_ID="$(cat /run/secrets/minio_ru)" +[ -f /run/secrets/django_sp ] && export DJANGO_SUPERUSER_PASSWORD="$(cat /run/secrets/django_sp)" +[ -f /run/secrets/oidc_rpcs ] && export OIDC_RP_CLIENT_SECRET="$(cat /run/secrets/oidc_rpcs)" +[ -f /run/secrets/collab_ss ] && export COLLABORATION_SERVER_SECRET="$(cat /run/secrets/collab_ss)" +[ -f /run/secrets/y_api_key ] && export Y_PROVIDER_API_KEY="$(cat /run/secrets/y_api_key)" +[ -f /run/secrets/email_pass ] && export DJANGO_EMAIL_HOST_PASSWORD="$(cat /run/secrets/email_pass)" + +# if not in "env" mode, then execute the original entrypoint and command +if [ ! "$1" = "-e" ]; then + exec "$@" +fi \ No newline at end of file diff --git a/abra.sh b/abra.sh new file mode 100755 index 0000000..055ab38 --- /dev/null +++ b/abra.sh @@ -0,0 +1,15 @@ +# Set any config versions here +# Docs: https://docs.coopcloud.tech/maintainers/handbook/#manage-configs +export ABRA_ENTRYPOINT_VERSION=v5 +export NGINX_CONF_VERSION=v3 +export PG_BACKUP_VERSION=v3 + +environment() { + # this exports all the secrets as environment variables + source /abra-entrypoint.sh -e +} + +migrate() { + environment + python manage.py migrate --noinput +} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..8539cbe --- /dev/null +++ b/compose.yml @@ -0,0 +1,322 @@ + + + +# NOTE: based on https://github.com/suitenumerique/docs/pull/855/ and https://github.com/suitenumerique/docs/pull/583/ + +x-common-env: &common-env + DJANGO_CONFIGURATION: Production + DJANGO_ALLOWED_HOSTS: "*" + # DJANGO_SECRET_KEY supplied via secrets + DJANGO_SETTINGS_MODULE: impress.settings + # DJANGO_SUPERUSER_PASSWORD supplied via secrets + # Logging + # Set to DEBUG level for dev only + LOGGING_LEVEL_HANDLERS_CONSOLE: + LOGGING_LEVEL_LOGGERS_ROOT: + LOGGING_LEVEL_LOGGERS_APP: + # Python + PYTHONPATH: /app + # Mail + DJANGO_EMAIL_BRAND_NAME: + DJANGO_EMAIL_HOST: + DJANGO_EMAIL_LOGO_IMG: + DJANGO_EMAIL_PORT: + DJANGO_EMAIL_HOST_USER: + # DJANGO_EMAIL_HOST_PASSWORD supplied via secret + DJANGO_EMAIL_USE_SSL: + DJANGO_EMAIL_USE_TLS: + DJANGO_EMAIL_FROM: + # Backend url + IMPRESS_BASE_URL: "https://${DOMAIN}" + # Media + STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage + AWS_S3_ENDPOINT_URL: http://minio:9000 + # AWS_S3_ACCESS_KEY_ID supplied via secret (this is same MINIO_ROOT_USER) + # AWS_S3_SECRET_ACCESS_KEY supplied via secret (this is same as MINIO_ROOT_PASSWORD) + MEDIA_BASE_URL: https://${DOMAIN} + AWS_STORAGE_BUCKET_NAME: docs-media-storage + # OIDC - settings from .env, see .env.sample + OIDC_OP_JWKS_ENDPOINT: + OIDC_OP_AUTHORIZATION_ENDPOINT: + OIDC_OP_TOKEN_ENDPOINT: + OIDC_OP_USER_ENDPOINT: + OIDC_RP_CLIENT_ID: + # OIDC_RP_CLIENT_SECRET supplied via secrets + OIDC_RP_SIGN_ALGO: + OIDC_RP_SCOPES: + LOGIN_REDIRECT_URL: + LOGIN_REDIRECT_URL_FAILURE: + LOGOUT_REDIRECT_URL: + OIDC_REDIRECT_ALLOWED_HOSTS: + OIDC_AUTH_REQUEST_EXTRA_PARAMS: + # AI (Fixme: remove?) + AI_FEATURE_ENABLED: "false" + AI_BASE_URL: https://openaiendpoint.com + AI_API_KEY: password + AI_MODEL: llama + # Collaboration + COLLABORATION_API_URL: https://$DOMAIN/collaboration/api/ + +x-postgres-env: &postgres-env + # Postgresql db container configuration + POSTGRES_DB: docs + POSTGRES_USER: docs + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_p + # App database configuration + DB_HOST: db + DB_NAME: docs + DB_USER: docs + DB_PORT: 5432 + # DB_PASSWORD supplied via secrets (this is same as POSTGRES_PASSWORD) + +x-yprovider-env: &yprovider-env + COLLABORATION_LOGGING: "true" + # Y_PROVIDER_API_KEY supplied via secrets + COLLABORATION_API_URL: http://y-provider:4444/api/ + COLLABORATION_SERVER_ORIGIN: https://${DOMAIN} + # COLLABORATION_SERVER_SECRET supplied via secrets + COLLABORATION_BACKEND_BASE_URL: https://${DOMAIN} + COLLABORATION_WS_URL: wss://${DOMAIN}/collaboration/ws/ + +x-minio-env: &minio-env + MINIO_ROOT_USER_FILE: /run/secrets/minio_ru + MINIO_ROOT_PASSWORD_FILE: /run/secrets/minio_rp + +services: + app: + image: lasuite/impress-frontend:v4.0.0 + networks: + - backend + deploy: + labels: + - "traefik.enable=false" + - "coop-cloud.${STACK_NAME}.timeout=${TIMEOUT:-120}" + - "coop-cloud.${STACK_NAME}.version=0.2.4+v4.0.0" + user: "${DOCKER_USER:-1000}" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080"] + interval: 15s + timeout: 30s + retries: 20 + start_period: 10s + + backend: + image: lasuite/impress-backend:v4.0.0 + networks: + - backend + environment: + <<: [*common-env, *postgres-env, *yprovider-env] + healthcheck: + test: ["CMD", "/abra-entrypoint.sh", "python", "manage.py", "check"] + interval: 15s + timeout: 30s + retries: 20 + start_period: 10s + user: "${DOCKER_USER:-1000}" + command: ["gunicorn", "-c", "/usr/local/etc/gunicorn/impress.py", "impress.wsgi:application"] + entrypoint: ["/abra-entrypoint.sh", "/usr/local/bin/entrypoint"] + configs: + - source: abra_entrypoint + target: /abra-entrypoint.sh + mode: 0555 + secrets: + - django_sk + - django_sp + - oidc_rpcs + - collab_ss + - minio_rp + - minio_ru + - postgres_p + - y_api_key + - email_pass + + celery: + image: lasuite/impress-backend:v4.0.0 + networks: + - backend + user: "${DOCKER_USER:-1000}" + command: ["celery", "-A", "impress.celery_app", "worker", "-l", "INFO"] + environment: + <<: [*common-env, *postgres-env, *yprovider-env] + entrypoint: ["/abra-entrypoint.sh", "/usr/local/bin/entrypoint"] + configs: + - source: abra_entrypoint + target: /abra-entrypoint.sh + mode: 0555 + secrets: + - django_sk + - django_sp + - oidc_rpcs + - collab_ss + - minio_rp + - postgres_p + - y_api_key + - email_pass + + + y-provider: + image: lasuite/impress-y-provider:v4.0.0 + networks: + - backend + environment: *yprovider-env + user: "${DOCKER_USER:-1000}" + entrypoint: > + sh -c "export Y_PROVIDER_API_KEY=\"$$(cat /run/secrets/y_api_key)\" && exec /usr/local/bin/entrypoint \"$$@\"" -- + command: ["yarn", "start"] + # NOTE: healthcheck - `wget` is available in the container, but `wget http://localhost:4444` gives a 403 + secrets: + - y_api_key + + db: + image: postgres:16 + networks: + - backend + healthcheck: + test: ["CMD", "pg_isready", "-q", "-U", "docs", "-d", "docs"] + interval: 1s + timeout: 2s + retries: 300 + environment: + <<: *postgres-env + PGDATA: var/lib/postgresql/data/pgdata + volumes: + - postgres:/var/lib/postgresql/data/pgdata + deploy: + labels: + backupbot.backup: "${ENABLE_BACKUPS:-true}" + backupbot.backup.pre-hook: "/pg_backup.sh backup" + backupbot.backup.volumes.postgres.path: "backup.sql" + backupbot.restore.post-hook: '/pg_backup.sh restore' + configs: + - source: pg_backup + target: /pg_backup.sh + mode: 0555 + secrets: + - postgres_p + + redis: + image: redis:8 + networks: + - backend + + minio-bootstrap: + # NOTE: Not started by default, only run with a manual `abra app restart` / `docker service scale` + image: minio/mc:RELEASE.2025-05-21T01-59-54Z + environment: *minio-env + networks: + - backend + entrypoint: > + sh -c " + MINIO_ROOT_USER=\"\$$(cat /run/secrets/minio_ru)\" && + MINIO_ROOT_PASSWORD=\"\$$(cat /run/secrets/minio_rp)\" && + /usr/bin/mc alias set docs http://minio:9000 \$${MINIO_ROOT_USER} \"\$${MINIO_ROOT_PASSWORD}\" && + /usr/bin/mc mb --ignore-existing docs/docs-media-storage && + /usr/bin/mc version enable docs/docs-media-storage && + exit 0" + deploy: + mode: replicated + replicas: 0 + restart_policy: + condition: none + secrets: + - minio_rp + - minio_ru + + minio: + image: minio/minio:RELEASE.2025-05-24T17-08-30Z + environment: *minio-env + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1s + timeout: 20s + retries: 300 + networks: + - backend + command: minio server /data + entrypoint: ["/usr/bin/docker-entrypoint.sh"] + volumes: + - minio:/data + deploy: + labels: + backupbot.backup: "${ENABLE_BACKUPS:-true}" + entrypoint: /abra-entrypoint.sh + configs: + - source: abra_entrypoint + target: /abra-entrypoint.sh + mode: 0555 + secrets: + - minio_rp + - minio_ru + + web: + image: nginx:1.29 + configs: + - source: nginx_conf + target: /etc/nginx/conf.d/default.conf + networks: + proxy: + backend: + depends_on: + - backend + - app + environment: + - STACK_NAME + deploy: + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.${STACK_NAME}.tls=true" + - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=8083" + - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})" + - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" + - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" + +networks: + proxy: + external: true + backend: + +volumes: + postgres: + minio: + +configs: + nginx_conf: + name: ${STACK_NAME}_nginx_conf_${NGINX_CONF_VERSION} + file: nginx.conf.tmpl + template_driver: golang + pg_backup: + name: ${STACK_NAME}_pg_backup_${PG_BACKUP_VERSION} + file: pg_backup.sh + abra_entrypoint: + name: ${STACK_NAME}_entrypoint_${ABRA_ENTRYPOINT_VERSION} + file: abra-entrypoint.sh + +secrets: + django_sk: + external: true + name: ${STACK_NAME}_django_sk_${SECRET_DJANGO_SK_VERSION} + oidc_rpcs: + external: true + name: ${STACK_NAME}_oidc_rpcs_${SECRET_OIDC_RPCS_VERSION} + django_sp: + external: true + name: ${STACK_NAME}_django_sp_${SECRET_DJANGO_SP_VERSION} + postgres_p: + external: true + name: ${STACK_NAME}_postgres_p_${SECRET_POSTGRES_P_VERSION} + collab_ss: + external: true + name: ${STACK_NAME}_collab_ss_${SECRET_COLLAB_SS_VERSION} + minio_rp: + external: true + name: ${STACK_NAME}_minio_rp_${SECRET_MINIO_RP_VERSION} + minio_ru: + external: true + name: ${STACK_NAME}_minio_ru_${SECRET_MINIO_RP_VERSION} + y_api_key: + external: true + name: ${STACK_NAME}_y_api_key_${SECRET_Y_API_KEY_VERSION} + email_pass: + external: true + name: ${STACK_NAME}_email_pass_${SECRET_EMAIL_PASS_VERSION} \ No newline at end of file diff --git a/nginx.conf.tmpl b/nginx.conf.tmpl new file mode 100644 index 0000000..07d5d07 --- /dev/null +++ b/nginx.conf.tmpl @@ -0,0 +1,108 @@ +upstream docs_backend { + server {{ env "STACK_NAME" }}_backend:8000 fail_timeout=0; +} + +upstream docs_frontend { + server {{ env "STACK_NAME" }}_app:8080 fail_timeout=0; +} + +server { + listen 8083; + server_name localhost; + + # Disables server version feedback on pages and in headers + server_tokens off; + + location @proxy_to_docs_backend { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_redirect off; + proxy_pass http://docs_backend; + } + + location @proxy_to_docs_frontend { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_redirect off; + proxy_pass http://docs_frontend; + } + + location / { + try_files $uri @proxy_to_docs_frontend; + } + + location /api { + try_files $uri @proxy_to_docs_backend; + } + + location /admin { + try_files $uri @proxy_to_docs_backend; + } + + location /static { + try_files $uri @proxy_to_docs_backend; + } + + # Proxy auth for collaboration server + location /collaboration/ws/ { + # Ensure WebSocket upgrade + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + # Collaboration server + proxy_pass http://y-provider:4444; + + # Set appropriate timeout for WebSocket + proxy_read_timeout 86400; + proxy_send_timeout 86400; + + # Preserve original host and additional headers + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Origin $http_origin; + proxy_set_header Host $host; + } + + location /collaboration/api/ { + # Collaboration server + proxy_pass http://y-provider:4444; + proxy_set_header Host $host; + } + + # Proxy auth for media + location /media/ { + # Auth request configuration + auth_request /media-auth; + auth_request_set $authHeader $upstream_http_authorization; + auth_request_set $authDate $upstream_http_x_amz_date; + auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256; + + # Pass specific headers from the auth response + proxy_set_header Authorization $authHeader; + proxy_set_header X-Amz-Date $authDate; + proxy_set_header X-Amz-Content-SHA256 $authContentSha256; + + # Get resource from Minio + proxy_pass http://minio:9000/docs-media-storage/; + proxy_set_header Host minio:9000; + } + + location /media-auth { + proxy_pass http://docs_backend/api/v1.0/documents/media-auth/; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Original-URL $request_uri; + + # Prevent the body from being passed + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-Method $request_method; + } +} + diff --git a/pg_backup.sh b/pg_backup.sh new file mode 100644 index 0000000..1fb7892 --- /dev/null +++ b/pg_backup.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e + +BACKUP_FILE='/var/lib/postgresql/data/pgdata/backup.sql' + +function backup { + export PGPASSWORD=$(cat $POSTGRES_PASSWORD_FILE) + pg_dump -U ${POSTGRES_USER} ${POSTGRES_DB} > $BACKUP_FILE +} + +function restore { + cd /var/lib/postgresql/data/ + restore_config(){ + # Restore allowed connections + cat pg_hba.conf.bak > pg_hba.conf + su postgres -c 'pg_ctl reload' + } + # Don't allow any other connections than local + cp pg_hba.conf pg_hba.conf.bak + echo "local all all trust" > pg_hba.conf + su postgres -c 'pg_ctl reload' + trap restore_config EXIT INT TERM + + # Recreate Database + psql -U ${POSTGRES_USER} -d postgres -c "DROP DATABASE ${POSTGRES_DB} WITH (FORCE);" + createdb -U ${POSTGRES_USER} ${POSTGRES_DB} + psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -1 -f $BACKUP_FILE + + trap - EXIT INT TERM + restore_config +} + +$@ diff --git a/release/.git-keep-me b/release/.git-keep-me new file mode 100644 index 0000000..e69de29