mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-07 18:57:12 +02:00
Compare commits
12 Commits
ce3fe1cd51
...
8059f272d5
Author | SHA1 | Date | |
---|---|---|---|
8059f272d5 | |||
7c814e6e83 | |||
d760c042c2 | |||
6cac8085a8 | |||
3a83f3d14e | |||
61d852c508 | |||
188b098503 | |||
bc56940e55 | |||
5dfc2efb5a | |||
7f9dc65b37 | |||
163a925096 | |||
a8c88634b5 |
@@ -10,8 +10,8 @@ defaults_networks:
|
||||
# /28 Networks, 14 Usable Ip Addresses
|
||||
web-app-akaunting:
|
||||
subnet: 192.168.101.0/28
|
||||
# Free:
|
||||
# subnet: 192.168.101.16/28
|
||||
web-app-confluence:
|
||||
subnet: 192.168.101.16/28
|
||||
web-app-baserow:
|
||||
subnet: 192.168.101.32/28
|
||||
web-app-mobilizon:
|
||||
@@ -34,8 +34,8 @@ defaults_networks:
|
||||
subnet: 192.168.101.176/28
|
||||
web-app-listmonk:
|
||||
subnet: 192.168.101.192/28
|
||||
# Free:
|
||||
# subnet: 192.168.101.208/28
|
||||
web-app-jira:
|
||||
subnet: 192.168.101.208/28
|
||||
web-app-matomo:
|
||||
subnet: 192.168.101.224/28
|
||||
web-app-mastodon:
|
||||
@@ -48,8 +48,8 @@ defaults_networks:
|
||||
subnet: 192.168.102.16/28
|
||||
web-app-moodle:
|
||||
subnet: 192.168.102.32/28
|
||||
# Free:
|
||||
# subnet: 192.168.102.48/28
|
||||
web-app-bookwyrm:
|
||||
subnet: 192.168.102.48/28
|
||||
web-app-nextcloud:
|
||||
subnet: 192.168.102.64/28
|
||||
web-app-openproject:
|
||||
@@ -96,6 +96,8 @@ defaults_networks:
|
||||
subnet: 192.168.103.160/28
|
||||
web-svc-logout:
|
||||
subnet: 192.168.103.176/28
|
||||
web-app-chess:
|
||||
subnet: 192.168.103.192/28
|
||||
|
||||
# /24 Networks / 254 Usable Clients
|
||||
web-app-bigbluebutton:
|
||||
|
@@ -26,7 +26,7 @@ ports:
|
||||
web-app-gitea: 8002
|
||||
web-app-wordpress: 8003
|
||||
web-app-mediawiki: 8004
|
||||
# Free : 8005
|
||||
web-app-confluence: 8005
|
||||
web-app-yourls: 8006
|
||||
web-app-mailu: 8007
|
||||
web-app-elk: 8008
|
||||
@@ -36,7 +36,7 @@ ports:
|
||||
web-app-funkwhale: 8012
|
||||
web-app-roulette-wheel: 8013
|
||||
web-app-joomla: 8014
|
||||
# Free: 8015
|
||||
web-app-jira: 8015
|
||||
web-app-pgadmin: 8016
|
||||
web-app-baserow: 8017
|
||||
web-app-matomo: 8018
|
||||
@@ -70,6 +70,8 @@ ports:
|
||||
web-app-pretix: 8046
|
||||
web-app-mig: 8047
|
||||
web-svc-logout: 8048
|
||||
web-app-bookwyrm: 8049
|
||||
web-app-chess: 8050
|
||||
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
|
||||
public:
|
||||
# The following ports should be changed to 22 on the subdomain via stream mapping
|
||||
|
@@ -1,4 +0,0 @@
|
||||
---
|
||||
- name: reload virtualbox kernel modules
|
||||
become: true
|
||||
command: vboxreload
|
@@ -15,10 +15,17 @@
|
||||
- name: docker compose pull
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
lock="{{ [ PATH_DOCKER_COMPOSE_PULL_LOCK_DIR, docker_compose.directories.instance ] | path_join | hash('sha1') }}"
|
||||
lock="{{ [ PATH_DOCKER_COMPOSE_PULL_LOCK_DIR, (docker_compose.directories.instance | hash('sha1')) ~ '.lock' ] | path_join }}"
|
||||
if [ ! -e "$lock" ]; then
|
||||
mkdir -p "$(dirname "$lock")"
|
||||
docker compose pull
|
||||
if docker compose config | grep -qE '^[[:space:]]+build:'; then
|
||||
docker compose build --pull
|
||||
fi
|
||||
if docker compose pull --help 2>/dev/null | grep -q -- '--ignore-buildable'; then
|
||||
docker compose pull --ignore-buildable
|
||||
else
|
||||
docker compose pull || true
|
||||
fi
|
||||
: > "$lock"
|
||||
echo "pulled"
|
||||
fi
|
||||
|
@@ -1,6 +0,0 @@
|
||||
|
||||
- name: "reload svc-bkp-loc-2-usb service"
|
||||
systemd:
|
||||
name: "{{ 'svc-bkp-loc-2-usb' | get_service_name(SOFTWARE_NAME) }}"
|
||||
state: reloaded
|
||||
daemon_reload: yes
|
@@ -1,55 +0,0 @@
|
||||
- name: Load memberof module from file in OpenLDAP container
|
||||
shell: >
|
||||
docker exec -i {{ openldap_name }} ldapmodify -Y EXTERNAL -H ldapi:/// -f {{ openldap_ldif_docker_path }}configuration/01_member_of_configuration.ldif
|
||||
listen:
|
||||
- "Import configuration LDIF files"
|
||||
- "Import all LDIF files"
|
||||
# @todo Remove the following ignore errors when setting up a new server
|
||||
# Just here because debugging would take to much time
|
||||
ignore_errors: true
|
||||
|
||||
- name: Refint Module Activation for OpenLDAP
|
||||
shell: >
|
||||
docker exec -i {{ openldap_name }} ldapadd -Y EXTERNAL -H ldapi:/// -f {{ openldap_ldif_docker_path }}configuration/02_member_of_configuration.ldif
|
||||
listen:
|
||||
- "Import configuration LDIF files"
|
||||
- "Import all LDIF files"
|
||||
register: ldapadd_result
|
||||
failed_when: ldapadd_result.rc not in [0, 68]
|
||||
# @todo Remove the following ignore errors when setting up a new server
|
||||
# Just here because debugging would take to much time
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Import schemas"
|
||||
shell: >
|
||||
docker exec -i {{ openldap_name }} ldapadd -Y EXTERNAL -H ldapi:/// -f "{{ openldap_ldif_docker_path }}schema/{{ item | basename | regex_replace('\.j2$', '') }}"
|
||||
register: ldapadd_result
|
||||
changed_when: "'adding new entry' in ldapadd_result.stdout"
|
||||
failed_when: ldapadd_result.rc not in [0, 80]
|
||||
listen:
|
||||
- "Import schema LDIF files"
|
||||
- "Import all LDIF files"
|
||||
loop: "{{ lookup('fileglob', role_path ~ '/templates/ldif/schema/*.j2', wantlist=True) }}"
|
||||
|
||||
- name: Refint Overlay Configuration for OpenLDAP
|
||||
shell: >
|
||||
docker exec -i {{ openldap_name }} ldapmodify -Y EXTERNAL -H ldapi:/// -f {{ openldap_ldif_docker_path }}configuration/03_member_of_configuration.ldif
|
||||
listen:
|
||||
- "Import configuration LDIF files"
|
||||
- "Import all LDIF files"
|
||||
register: ldapadd_result
|
||||
failed_when: ldapadd_result.rc not in [0, 68]
|
||||
# @todo Remove the following ignore errors when setting up a new server
|
||||
# Just here because debugging would take to much time
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Import users, groups, etc. to LDAP"
|
||||
shell: >
|
||||
docker exec -i {{ openldap_name }} ldapadd -x -D "{{LDAP.DN.ADMINISTRATOR.DATA}}" -w "{{ LDAP.BIND_CREDENTIAL }}" -c -f "{{ openldap_ldif_docker_path }}groups/{{ item | basename | regex_replace('\.j2$', '') }}"
|
||||
register: ldapadd_result
|
||||
changed_when: "'adding new entry' in ldapadd_result.stdout"
|
||||
failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
|
||||
listen:
|
||||
- "Import groups LDIF files"
|
||||
- "Import all LDIF files"
|
||||
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/groups/*.j2') | sort }}"
|
26
roles/web-app-bookwyrm/README.md
Normal file
26
roles/web-app-bookwyrm/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# BookWyrm
|
||||
|
||||
## Description
|
||||
|
||||
**BookWyrm** is a self-hosted social reading platform where users can share books, post reviews, follow each other, and join federated conversations across the Fediverse. It is a community-driven alternative to proprietary platforms like Goodreads. Readers can catalog their library, track reading progress, and discover new books through friends and federated timelines.
|
||||
|
||||
## Overview
|
||||
|
||||
BookWyrm provides a federated social network for books built on ActivityPub. Each instance can be private, invitation-only, or open for public registration. Users can import/export book lists, interact with others across the Fediverse, and maintain their own curated reading environment. As an admin, you can configure moderation tools, content rules, and federation policies to suit your community.
|
||||
|
||||
## Features
|
||||
|
||||
- **Federated Social Network:** Connects with other BookWyrm instances and ActivityPub platforms.
|
||||
- **Book Cataloging:** Add, search, and organize books; import/export libraries.
|
||||
- **Reading Status & Reviews:** Mark books as “to read,” “reading,” or “finished,” and publish reviews or quotes.
|
||||
- **Timelines & Interaction:** Follow other readers, comment on reviews, and engage in federated discussions.
|
||||
- **Privacy & Moderation:** Fine-grained controls for content visibility, moderation, and federation settings.
|
||||
- **Community Building:** Host a private club, classroom library, or large public community for readers.
|
||||
- **Optional SSO Integration:** Can work with OIDC for unified login across platforms.
|
||||
|
||||
## Further Resources
|
||||
|
||||
- [BookWyrm GitHub](https://github.com/bookwyrm-social/bookwyrm)
|
||||
- [BookWyrm Documentation](https://docs.joinbookwyrm.com/)
|
||||
- [ActivityPub (Wikipedia)](https://en.wikipedia.org/wiki/ActivityPub)
|
||||
- [Fediverse (Wikipedia)](https://en.wikipedia.org/wiki/Fediverse)
|
@@ -1,2 +0,0 @@
|
||||
# Todo
|
||||
- Implement https://joinbookwyrm.com/de/
|
36
roles/web-app-bookwyrm/config/main.yml
Normal file
36
roles/web-app-bookwyrm/config/main.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
credentials: {}
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true
|
||||
redis:
|
||||
enabled: true
|
||||
application:
|
||||
version: 'v0.7.5'
|
||||
name: bookwyrm
|
||||
worker:
|
||||
enabled: true
|
||||
volumes:
|
||||
data: "bookwyrm_data"
|
||||
media: "bookwyrm_media"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
desktop: true
|
||||
central_database: true
|
||||
logout: true
|
||||
oidc: false
|
||||
ldap: false
|
||||
server:
|
||||
csp:
|
||||
whitelist: {}
|
||||
flags: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "book.{{ PRIMARY_DOMAIN }}"
|
||||
aliases:
|
||||
- "bookwyrm.{{ PRIMARY_DOMAIN }}"
|
||||
rbac:
|
||||
roles: {}
|
||||
registration_open: false
|
||||
allow_invite_request: false
|
@@ -1,22 +1,26 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Deploys BookWyrm social reading server via Docker Compose, with basic domain and port wiring."
|
||||
author: "Kevin Veen-Birchenbach"
|
||||
description: "BookWyrm is a self-hosted federated social reading platform where users share reviews, track reading, and connect with others across the Fediverse."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Kevin Veen-Birchenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- bookwyrm
|
||||
- social
|
||||
- docker
|
||||
- books
|
||||
- social-network
|
||||
- fediverse
|
||||
- activitypub
|
||||
- reading
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://s.infinito.nexus/code/tree/main/roles/web-app-bookwyrm"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Any
|
||||
versions:
|
||||
- all
|
||||
logo:
|
||||
class: "fas fa-book"
|
||||
run_after:
|
||||
- web-app-matomo
|
||||
- web-app-keycloak
|
||||
- web-app-mailu
|
||||
dependencies: []
|
||||
|
6
roles/web-app-bookwyrm/schema/main.yml
Normal file
6
roles/web-app-bookwyrm/schema/main.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
credentials:
|
||||
secret_key:
|
||||
description: "Django SECRET_KEY for BookWyrm"
|
||||
algorithm: "alphanumeric" # uses generate_value('alphanumeric') → 64 random a-zA-Z0-9
|
||||
validation:
|
||||
min_length: 50 # Django recommends ≥50 characters
|
7
roles/web-app-bookwyrm/tasks/main.yml
Normal file
7
roles/web-app-bookwyrm/tasks/main.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- block:
|
||||
- name: "load docker, db/redis and proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-full-stateful
|
||||
- include_tasks: utils/run_once.yml
|
||||
when: run_once_web_app_bookwyrm is not defined
|
38
roles/web-app-bookwyrm/templates/Dockerfile.j2
Normal file
38
roles/web-app-bookwyrm/templates/Dockerfile.j2
Normal file
@@ -0,0 +1,38 @@
|
||||
# Build BookWyrm from source (no upstream image available)
|
||||
FROM python:3.11-bookworm AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git build-essential libpq-dev \
|
||||
libjpeg-dev zlib1g-dev libxml2-dev libxslt1-dev libffi-dev libmagic-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
# Shallow clone the chosen tag/branch
|
||||
RUN git clone --depth=1 --branch "{{ BOOKWYRM_VERSION }}" https://github.com/bookwyrm-social/bookwyrm.git .
|
||||
|
||||
# Pre-install Python deps to a wheelhouse for faster final image
|
||||
RUN pip install --upgrade pip \
|
||||
&& pip wheel --wheel-dir /wheels -r requirements.txt
|
||||
|
||||
FROM python:3.11-bookworm
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
WORKDIR /app
|
||||
|
||||
# Copy app source and wheels
|
||||
COPY --from=builder /src /app
|
||||
COPY --from=builder /wheels /wheels
|
||||
|
||||
# System deps for runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 curl \
|
||||
libjpeg62-turbo zlib1g libxml2 libxslt1.1 libffi8 libmagic1 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir --no-index --find-links=/wheels -r /app/requirements.txt \
|
||||
&& adduser --disabled-password --gecos '' bookwyrm \
|
||||
&& mkdir -p /app/data /app/media \
|
||||
&& chown -R bookwyrm:bookwyrm /app
|
||||
|
||||
USER bookwyrm
|
||||
|
||||
# Gunicorn/Celery are configured by upstream files in repo
|
||||
# Ports/healthcheck handled by compose template
|
43
roles/web-app-bookwyrm/templates/docker-compose.yml.j2
Normal file
43
roles/web-app-bookwyrm/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,43 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
|
||||
application:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
command: >-
|
||||
bash -lc '
|
||||
python manage.py migrate --noinput &&
|
||||
python manage.py collectstatic --noinput &&
|
||||
gunicorn bookwyrm.wsgi:application --bind 0.0.0.0:{{ container_port }}
|
||||
'
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: "{{ BOOKWYRM_CUSTOM_IMAGE }}"
|
||||
container_name: "{{ BOOKWYRM_CONTAINER }}"
|
||||
hostname: "{{ BOOKWYRM_HOSTNAME }}"
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}"
|
||||
volumes:
|
||||
- 'data:/app/data'
|
||||
- 'media:/app/media'
|
||||
{% include 'roles/docker-container/templates/healthcheck/curl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
worker:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
image: "{{ BOOKWYRM_CUSTOM_IMAGE }}"
|
||||
container_name: "{{ BOOKWYRM_WORKER_CONTAINER }}"
|
||||
command: "bash -lc 'celery -A celerywyrm worker -l INFO'"
|
||||
volumes:
|
||||
- 'data:/app/data'
|
||||
- 'media:/app/media'
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: {{ BOOKWYRM_DATA_VOLUME }}
|
||||
media:
|
||||
name: {{ BOOKWYRM_MEDIA_VOLUME }}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
50
roles/web-app-bookwyrm/templates/env.j2
Normal file
50
roles/web-app-bookwyrm/templates/env.j2
Normal file
@@ -0,0 +1,50 @@
|
||||
# Core
|
||||
BOOKWYRM_URL="{{ BOOKWYRM_URL }}"
|
||||
DOMAIN="{{ BOOKWYRM_HOSTNAME }}"
|
||||
ALLOWED_HOSTS="{{ BOOKWYRM_HOSTNAME }},127.0.0.1,localhost"
|
||||
PORT="{{ WEB_PORT }}"
|
||||
WEB_PROTOCOL="{{ WEB_PROTOCOL }}"
|
||||
MEDIA_ROOT="/app/media"
|
||||
DATA_ROOT="/app/data"
|
||||
REGISTRATION_OPEN={{ BOOKWYRM_REGISTRATION_OPEN }}
|
||||
ALLOW_INVITE_REQUESTS={{ BOOKWYRM_ALLOW_INVITE_REQUESTS }}
|
||||
|
||||
# Django/Secrets (provide via vault/env in production)
|
||||
SECRET_KEY="{{ BOOKWYRM_SECRET_KEY }}"
|
||||
|
||||
# Email / SMTP (BookWyrm expects these names)
|
||||
EMAIL_HOST="{{ EMAIL_HOST }}"
|
||||
EMAIL_PORT="{{ EMAIL_PORT }}"
|
||||
EMAIL_USE_TLS={{ EMAIL_USE_TLS }}
|
||||
EMAIL_USE_SSL={{ EMAIL_USE_SSL }}
|
||||
EMAIL_HOST_USER="{{ EMAIL_HOST_USER }}"
|
||||
EMAIL_HOST_PASSWORD="{{ EMAIL_HOST_PASSWORD }}"
|
||||
DEFAULT_FROM_EMAIL="{{ EMAIL_DEFAULT_FROM }}"
|
||||
|
||||
# Database
|
||||
DATABASE_URL="postgres://{{ database_username }}:{{ database_password }}@{{ database_host }}:{{ database_port }}/{{ database_name }}"
|
||||
|
||||
# Redis / Celery
|
||||
REDIS_BROKER_URL="{{ BOOKWYRM_REDIS_BROKER_URL }}"
|
||||
REDIS_CACHE_URL="{{ BOOKWYRM_REDIS_BASE_URL }}/1"
|
||||
CELERY_BROKER_URL="{{ BOOKWYRM_REDIS_BROKER_URL }}"
|
||||
|
||||
# Proxy (if BookWyrm sits behind reverse proxy)
|
||||
FORWARDED_ALLOW_IPS="*"
|
||||
USE_X_FORWARDED_HOST="true"
|
||||
SECURE_PROXY_SSL_HEADER="HTTP_X_FORWARDED_PROTO,{{ WEB_PROTOCOL }}"
|
||||
|
||||
# OIDC (optional – only if BOOKWYRM_OIDC_ENABLED)
|
||||
{% if BOOKWYRM_OIDC_ENABLED %}
|
||||
OIDC_TITLE="{{ BOOKWYRM_OIDC_LABEL | replace('\"','\\\"') }}"
|
||||
OIDC_ISSUER="{{ BOOKWYRM_OIDC_ISSUER }}"
|
||||
OIDC_AUTHORIZATION_ENDPOINT="{{ BOOKWYRM_OIDC_AUTH_URL }}"
|
||||
OIDC_TOKEN_ENDPOINT="{{ BOOKWYRM_OIDC_TOKEN_URL }}"
|
||||
OIDC_USERINFO_ENDPOINT="{{ BOOKWYRM_OIDC_USERINFO_URL }}"
|
||||
OIDC_END_SESSION_ENDPOINT="{{ BOOKWYRM_OIDC_LOGOUT_URL }}"
|
||||
OIDC_JWKS_URI="{{ BOOKWYRM_OIDC_JWKS_URL }}"
|
||||
OIDC_CLIENT_ID="{{ BOOKWYRM_OIDC_CLIENT_ID }}"
|
||||
OIDC_CLIENT_SECRET="{{ BOOKWYRM_OIDC_CLIENT_SECRET }}"
|
||||
OIDC_SCOPES="{{ BOOKWYRM_OIDC_SCOPES }}"
|
||||
OIDC_UNIQUE_ATTRIBUTE="{{ BOOKWYRM_OIDC_UNIQUE_ATTRIBUTE }}"
|
||||
{% endif %}
|
@@ -1 +1,57 @@
|
||||
application_id: web-app-bookwyrm
|
||||
# General
|
||||
application_id: "web-app-bookwyrm"
|
||||
database_type: "postgres"
|
||||
|
||||
# Container
|
||||
container_port: 8000
|
||||
container_hostname: "{{ domains | get_domain(application_id) }}"
|
||||
|
||||
# BookWyrm
|
||||
|
||||
BOOKWYRM_REGISTRATION_OPEN: "{{ applications | get_app_conf(application_id, 'registration_open') | string | lower }}"
|
||||
BOOKWYRM_ALLOW_INVITE_REQUESTS: "{{ applications | get_app_conf(application_id, 'allow_invite_request') | string | lower }}"
|
||||
|
||||
## Credentrials
|
||||
BOOKWYRM_SECRET_KEY: "{{ applications | get_app_conf(application_id, 'credentials.secret_key') }}"
|
||||
|
||||
## URLs
|
||||
BOOKWYRM_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
BOOKWYRM_HOSTNAME: "{{ container_hostname }}"
|
||||
|
||||
## OIDC (optional; can be fronted by oauth2-proxy or native if you wire it)
|
||||
BOOKWYRM_OIDC_ENABLED: "{{ applications | get_app_conf(application_id, 'features.oidc') }}"
|
||||
BOOKWYRM_OIDC_LABEL: "{{ OIDC.BUTTON_TEXT }}"
|
||||
BOOKWYRM_OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
|
||||
BOOKWYRM_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
|
||||
BOOKWYRM_OIDC_ISSUER: "{{ OIDC.CLIENT.ISSUER_URL }}"
|
||||
BOOKWYRM_OIDC_AUTH_URL: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
BOOKWYRM_OIDC_TOKEN_URL: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
BOOKWYRM_OIDC_USERINFO_URL: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
BOOKWYRM_OIDC_LOGOUT_URL: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
BOOKWYRM_OIDC_JWKS_URL: "{{ OIDC.CLIENT.CERTS }}"
|
||||
BOOKWYRM_OIDC_SCOPES: "openid,email,profile"
|
||||
BOOKWYRM_OIDC_UNIQUE_ATTRIBUTE: "{{ OIDC.ATTRIBUTES.USERNAME }}"
|
||||
|
||||
## Docker
|
||||
BOOKWYRM_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.application.name') }}"
|
||||
BOOKWYRM_DATA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
BOOKWYRM_MEDIA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.media') }}"
|
||||
BOOKWYRM_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.application.version') }}"
|
||||
BOOKWYRM_CUSTOM_IMAGE: "bookwyrm_custom"
|
||||
BOOKWYRM_WORKER_CONTAINER: "{{ BOOKWYRM_CONTAINER }}-worker"
|
||||
|
||||
## Redis
|
||||
BOOKWYRM_REDIS_HOST: "redis"
|
||||
BOOKWYRM_REDIS_PORT: 6379
|
||||
BOOKWYRM_REDIS_BASE_URL: "redis://{{ BOOKWYRM_REDIS_HOST }}:{{ BOOKWYRM_REDIS_PORT }}"
|
||||
BOOKWYRM_REDIS_BROKER_URL: "{{ BOOKWYRM_REDIS_BASE_URL }}/0"
|
||||
|
||||
# Email
|
||||
EMAIL_HOST: "{{ SYSTEM_EMAIL.HOST }}"
|
||||
EMAIL_PORT: "{{ SYSTEM_EMAIL.PORT }}"
|
||||
EMAIL_HOST_USER: "{{ users['no-reply'].email }}"
|
||||
EMAIL_HOST_PASSWORD: "{{ users['no-reply'].mailu_token }}"
|
||||
# TLS/SSL: If TLS is true → TLS; else → SSL
|
||||
EMAIL_USE_TLS: "{{ SYSTEM_EMAIL.TLS | ternary('true','false') }}"
|
||||
EMAIL_USE_SSL: "{{ not SYSTEM_EMAIL.TLS | ternary('true','false') }}"
|
||||
EMAIL_DEFAULT_FROM: "BookWyrm <{{ users['no-reply'].email }}>"
|
||||
|
@@ -1,2 +1,25 @@
|
||||
# Todo
|
||||
- Implement https://joinbookwyrm.com/de/
|
||||
# web-app-chess
|
||||
|
||||
## Description
|
||||
|
||||
**castling.club** is a federated chess server built on the ActivityPub protocol.
|
||||
It provides an open and decentralized way to play chess online, where games and moves are visible across the Fediverse.
|
||||
|
||||
## Overview
|
||||
|
||||
Instead of relying on closed platforms, castling.club uses an arbiter actor (“the King”) to validate moves and mediate matches.
|
||||
This ensures fair play, federation with platforms like Mastodon or Friendica, and community visibility of ongoing games.
|
||||
The service runs as a lightweight Node.js app backed by PostgreSQL.
|
||||
|
||||
## Features
|
||||
|
||||
- **Federated Chess Matches:** Challenge and play with others across the Fediverse.
|
||||
- **Rule Enforcement:** The arbiter validates each move for correctness.
|
||||
- **Open Identities:** Use your existing Fediverse account; no new silo account needed.
|
||||
- **Game Visibility:** Matches and moves can appear in social timelines.
|
||||
- **Lightweight Service:** Built with Node.js and PostgreSQL for efficiency.
|
||||
|
||||
## Further Resources
|
||||
|
||||
- [castling.club GitHub Repository](https://github.com/stephank/castling.club)
|
||||
- [ActivityPub Specification (W3C)](https://www.w3.org/TR/activitypub/)
|
||||
|
34
roles/web-app-chess/config/main.yml
Normal file
34
roles/web-app-chess/config/main.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
# roles/web-app-chess/config/main.yml
|
||||
credentials: {}
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true # Use central DB role (recommended)
|
||||
application:
|
||||
image: "node" # Base image family; final image is custom
|
||||
version: "20-bullseye" # >=16 as required upstream
|
||||
name: "web-app-chess"
|
||||
backup:
|
||||
no_stop_required: true
|
||||
volumes:
|
||||
data: "chess_data"
|
||||
features:
|
||||
matomo: false
|
||||
css: false
|
||||
desktop: false
|
||||
central_database: true
|
||||
logout: false
|
||||
oidc: false
|
||||
server:
|
||||
csp:
|
||||
whitelist: {}
|
||||
flags: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "chess.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
rbac:
|
||||
roles: {}
|
||||
source:
|
||||
repo: "https://github.com/stephank/castling.club.git"
|
||||
ref: "main"
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birchenbach"
|
||||
description: "Stub role for deploying a Chess web application via Docker Compose (implementation pending)."
|
||||
description: "Federated chess server based on ActivityPub. Play and follow games across the Fediverse with verified rules and open identities."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
@@ -10,13 +10,16 @@ galaxy_info:
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- chess
|
||||
- docker
|
||||
- federation
|
||||
- activitypub
|
||||
- social
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://s.infinito.nexus/code/tree/main/roles/web-app-chess"
|
||||
documentation: "https://github.com/stephank/castling.club"
|
||||
logo:
|
||||
class: "fas fa-chess-king"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Any
|
||||
versions: [ all ]
|
||||
dependencies: []
|
||||
|
||||
|
10
roles/web-app-chess/tasks/02_assets.yml
Normal file
10
roles/web-app-chess/tasks/02_assets.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
- block:
|
||||
- name: "load docker, db and proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-full-stateful
|
||||
|
||||
- name: "Place entrypoint and other assets"
|
||||
include_tasks: 02_assets.yml
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
when: run_once_web_app_chess is not defined
|
8
roles/web-app-chess/tasks/main.yml
Normal file
8
roles/web-app-chess/tasks/main.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- block:
|
||||
- name: "load docker, db and proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-full-stateful
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
when: run_once_web_app_chess is not defined
|
47
roles/web-app-chess/templates/Dockerfile.j2
Normal file
47
roles/web-app-chess/templates/Dockerfile.j2
Normal file
@@ -0,0 +1,47 @@
|
||||
# Multi-stage build for castling.club
|
||||
# Stage 1: build
|
||||
FROM node:{{ CHESS_VERSION }} AS build
|
||||
|
||||
ARG CHESS_REPO_URL={{ CHESS_REPO_URL }}
|
||||
ARG CHESS_REPO_REF={{ CHESS_REPO_REF }}
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git ca-certificates openssl dumb-init python3 build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
RUN git clone --depth 1 --branch "${CHESS_REPO_REF}" "${CHESS_REPO_URL}" ./
|
||||
|
||||
# Yarn is preinstalled in Node images via corepack; enable it.
|
||||
RUN corepack enable
|
||||
|
||||
# Install deps and build TS
|
||||
RUN yarn install --frozen-lockfile && yarn build
|
||||
|
||||
# Stage 2: runtime
|
||||
FROM node:{{ CHESS_VERSION }}
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT={{ container_port }}
|
||||
WORKDIR /app
|
||||
|
||||
# Minimal runtime packages + dumb-init
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssl dumb-init postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy built app
|
||||
COPY --from=build /src /app
|
||||
|
||||
# Create data dir for signing keys & cache
|
||||
RUN mkdir -p /app/data && chown -R node:node /app
|
||||
VOLUME ["/app/data"]
|
||||
|
||||
# Entrypoint script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
USER node
|
||||
EXPOSE {{ container_port }}
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
CMD ["docker-entrypoint.sh"]
|
29
roles/web-app-chess/templates/docker-compose.yml.j2
Normal file
29
roles/web-app-chess/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,29 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
application:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
CHESS_REPO_URL: "{{ CHESS_REPO_URL }}"
|
||||
CHESS_REPO_REF: "{{ CHESS_REPO_REF }}"
|
||||
image: "castling_custom"
|
||||
container_name: "{{ CHESS_CONTAINER }}"
|
||||
hostname: "{{ CHESS_HOSTNAME }}"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}"
|
||||
volumes:
|
||||
- 'data:/app/data'
|
||||
env_file:
|
||||
- .env
|
||||
{% include 'roles/docker-container/templates/healthcheck/curl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: {{ CHESS_DATA_VOLUME }}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
27
roles/web-app-chess/templates/docker-entrypoint.sh
Normal file
27
roles/web-app-chess/templates/docker-entrypoint.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_KEY_FILE="${APP_KEY_FILE:-/app/data/{{ CHESS_KEY_FILENAME }}}"
|
||||
APP_KEY_PUB="${APP_KEY_FILE}.pub"
|
||||
|
||||
# 1) Generate signing key pair if missing
|
||||
if [[ ! -f "${APP_KEY_FILE}" || ! -f "${APP_KEY_PUB}" ]]; then
|
||||
echo "[chess] generating RSA signing key pair at ${APP_KEY_FILE}"
|
||||
/app/tools/gen-signing-key.sh "${APP_KEY_FILE}"
|
||||
fi
|
||||
|
||||
# 2) Wait for PostgreSQL if env is provided
|
||||
if [[ -n "${PGHOST:-}" ]]; then
|
||||
echo "[chess] waiting for PostgreSQL at ${PGHOST}:${PGPORT:-5432}..."
|
||||
until pg_isready -h "${PGHOST}" -p "${PGPORT:-5432}" -U "${PGUSER:-postgres}" >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
# 3) Run migrations (idempotent)
|
||||
echo "[chess] running migrations"
|
||||
yarn migrate up
|
||||
|
||||
# 4) Start app
|
||||
echo "[chess] starting server on port ${PORT:-5080}"
|
||||
exec yarn start
|
16
roles/web-app-chess/templates/env.j2
Normal file
16
roles/web-app-chess/templates/env.j2
Normal file
@@ -0,0 +1,16 @@
|
||||
# App basics
|
||||
APP_SCHEME="{{ 'https' if WEB_PROTOCOL == 'https' else 'http' }}"
|
||||
APP_DOMAIN="{{ CHESS_HOSTNAME }}"
|
||||
APP_ADMIN_URL="{{ CHESS_ADMIN_URL }}"
|
||||
APP_ADMIN_EMAIL="{{ CHESS_ADMIN_EMAIL }}"
|
||||
APP_KEY_FILE="/app/data/{{ CHESS_KEY_FILENAME }}"
|
||||
APP_HMAC_SECRET="{{ CHESS_HMAC_SECRET }}"
|
||||
NODE_ENV="production"
|
||||
PORT="{{ container_port }}"
|
||||
|
||||
# PostgreSQL (libpq envs)
|
||||
PGHOST="{{ database_host }}"
|
||||
PGPORT="{{ database_port }}"
|
||||
PGDATABASE="{{ database_name }}"
|
||||
PGUSER="{{ database_username }}"
|
||||
PGPASSWORD="{{ database_password }}"
|
@@ -1 +1,25 @@
|
||||
application_id: web-app-chess
|
||||
# General
|
||||
application_id: "web-app-chess"
|
||||
database_type: "postgres"
|
||||
container_port: 5080
|
||||
container_hostname: "{{ domains | get_domain(application_id) }}"
|
||||
|
||||
# App URLs & meta
|
||||
#CHESS_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
CHESS_HOSTNAME: "{{ container_hostname }}"
|
||||
CHESS_ADMIN_URL: ""
|
||||
CHESS_ADMIN_EMAIL: ""
|
||||
|
||||
# Docker image
|
||||
#CHESS_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.application.image') }}"
|
||||
CHESS_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.application.version') }}"
|
||||
CHESS_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.application.name') }}"
|
||||
CHESS_DATA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
|
||||
# Build source
|
||||
CHESS_REPO_URL: "{{ applications | get_app_conf(application_id, 'source.repo') }}"
|
||||
CHESS_REPO_REF: "{{ applications | get_app_conf(application_id, 'source.ref') }}"
|
||||
|
||||
# Security
|
||||
CHESS_HMAC_SECRET: "{{ lookup('password', '/dev/null length=63 chars=ascii_letters,digits') }}"
|
||||
CHESS_KEY_FILENAME: "signing-key"
|
||||
|
25
roles/web-app-confluence/README.md
Normal file
25
roles/web-app-confluence/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Confluence
|
||||
|
||||
## Description
|
||||
|
||||
Confluence is Atlassian’s enterprise wiki and collaboration platform. This role deploys Confluence via Docker Compose, wires it to PostgreSQL, and integrates proxy awareness, optional OIDC SSO, health checks, and production-friendly defaults for Infinito.Nexus.
|
||||
|
||||
## Overview
|
||||
|
||||
The role builds a minimal custom image on top of the official Confluence image, prepares persistent volumes, and exposes the app behind your reverse proxy. Configuration is driven by variables (image, version, volumes, domains, OIDC). JVM heap sizing is auto-derived from host RAM with safe caps to avoid `Xms > Xmx`.
|
||||
|
||||
## Features
|
||||
|
||||
* **Fully Dockerized:** Compose stack with a dedicated data volume (`confluence_data`) and a slim overlay image for future add-ons.
|
||||
* **Reverse-Proxy Ready:** Sets `ATL_PROXY_NAME/PORT/SCHEME/SECURE` so Confluence generates correct external URLs behind HTTPS.
|
||||
* **OIDC SSO (Optional):** Pre-templated vars for issuer, client, scopes, JWKS; compatible with Atlassian DC SSO/OIDC marketplace apps.
|
||||
* **Central Database:** PostgreSQL integration (local or central DB) with bootstrap credentials from role vars.
|
||||
* **JVM Auto-Tuning:** `JVM_MINIMUM_MEMORY` / `JVM_MAXIMUM_MEMORY` computed from host memory with upper bounds.
|
||||
* **Health Checks:** Curl-based container healthcheck for early failure detection.
|
||||
* **CSP & Canonical Domains:** Hooks into platform CSP/SSL/domain management to keep policies strict and URLs stable.
|
||||
* **Backup Friendly:** Data isolated under `/var/atlassian/application-data/confluence`.
|
||||
|
||||
## Further Resources
|
||||
|
||||
* Product page: [Atlassian Confluence](https://www.atlassian.com/software/confluence)
|
||||
* Docker Hub (official image): [atlassian/confluence](https://hub.docker.com/r/atlassian/confluence)
|
@@ -15,7 +15,8 @@ features:
|
||||
desktop: true
|
||||
central_database: true
|
||||
logout: true
|
||||
oidc: true
|
||||
oidc: false # Not enabled for demo version
|
||||
ldap: false # Not enabled for demo version
|
||||
server:
|
||||
csp:
|
||||
whitelist: {}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
FROM "{{ CONFLUENCE_IMAGE }}:{{ CONFLUENCE_VERSION }}"
|
||||
|
||||
# Optional: install OIDC SSO app (example path/name)
|
||||
# COPY ./plugins/atlassian-sso-dc-latest.obr /opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/
|
||||
|
||||
# Ensure proper permissions for app data
|
||||
RUN mkdir -p /var/atlassian/application-data/confluence && \
|
||||
chown -R 2001:2001 /var/atlassian/application-data/confluence
|
@@ -3,9 +3,7 @@
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
CONFLUENCE_BASE_IMAGE: "{{ CONFLUENCE_IMAGE }}:{{ CONFLUENCE_VERSION }}"
|
||||
image: "{{ CONFLUENCE_IMAGE }}:{{ CONFLUENCE_VERSION }}-oidc"
|
||||
image: "{{ CONFLUENCE_CUSTOM_IMAGE }}"
|
||||
container_name: "{{ CONFLUENCE_CONTAINER }}"
|
||||
hostname: '{{ CONFLUENCE_HOSTNAME}}'
|
||||
ports:
|
||||
@@ -14,8 +12,7 @@
|
||||
- 'data:/var/atlassian/application-data/confluence'
|
||||
{% include 'roles/docker-container/templates/healthcheck/curl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
depends_on:
|
||||
- database
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
|
@@ -1,12 +1,20 @@
|
||||
## Confluence core
|
||||
CONFLUENCE_URL="{{ CONFLUENCE_URL }}"
|
||||
|
||||
|
||||
ATL_PROXY_NAME={{ CONFLUENCE_HOSTNAME }}
|
||||
ATL_PROXY_PORT={{ WEB_PORT }}
|
||||
ATL_TOMCAT_SCHEME={{ WEB_PROTOCOL }}
|
||||
ATL_TOMCAT_SECURE={{ (WEB_PORT == 443) | lower }}
|
||||
JVM_MINIMUM_MEMORY={{ CONFLUENCE_JVM_MIN }}
|
||||
JVM_MAXIMUM_MEMORY={{ CONFLUENCE_JVM_MAX }}
|
||||
|
||||
## Database
|
||||
CONFLUENCE_DATABASE_NAME="{{ database_name }}"
|
||||
CONFLUENCE_DATABASE_USER="{{ database_username }}"
|
||||
CONFLUENCE_DATABASE_PASSWORD="{{ database_password }}"
|
||||
CONFLUENCE_DATABASE_HOST="{{ database_host }}"
|
||||
CONFLUENCE_DATABASE_PORT="{{ database_port }}"
|
||||
ATL_DB_TYPE=postgres72
|
||||
ATL_DB_DRIVER=org.postgresql.Driver
|
||||
ATL_JDBC_URL=jdbc:postgresql://{{ database_host }}:{{ database_port }}/{{ database_name }}
|
||||
ATL_JDBC_USER={{ database_username }}
|
||||
ATL_JDBC_PASSWORD={{ database_password }}
|
||||
|
||||
## OIDC
|
||||
{% if CONFLUENCE_OIDC_ENABLED %}
|
||||
|
@@ -1,27 +1,41 @@
|
||||
application_id: "web-app-confluence"
|
||||
database_type: "postgres"
|
||||
container_port: 8090 # Standardport Confluence
|
||||
# General
|
||||
application_id: "web-app-confluence"
|
||||
database_type: "postgres"
|
||||
|
||||
# URLs
|
||||
CONFLUENCE_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
CONFLUENCE_HOSTNAME: "{{ domains | get_domain(application_id) }}"
|
||||
# Container
|
||||
container_port: 8090
|
||||
container_hostname: "{{ domains | get_domain(application_id) }}"
|
||||
|
||||
# OIDC
|
||||
CONFLUENCE_OIDC_ENABLED: "{{ applications | get_app_conf(application_id, 'features.oidc') }}"
|
||||
CONFLUENCE_OIDC_LABEL: "{{ OIDC.BUTTON_TEXT }}"
|
||||
CONFLUENCE_OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
|
||||
CONFLUENCE_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
|
||||
CONFLUENCE_OIDC_ISSUER: "{{ OIDC.CLIENT.ISSUER_URL }}"
|
||||
CONFLUENCE_OIDC_AUTH_URL: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
CONFLUENCE_OIDC_TOKEN_URL: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
CONFLUENCE_OIDC_USERINFO_URL: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
CONFLUENCE_OIDC_LOGOUT_URL: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
CONFLUENCE_OIDC_JWKS_URL: "{{ OIDC.CLIENT.CERTS }}"
|
||||
CONFLUENCE_OIDC_SCOPES: "openid,email,profile"
|
||||
# Confluence
|
||||
|
||||
## URLs
|
||||
CONFLUENCE_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
CONFLUENCE_HOSTNAME: "{{ container_hostname }}"
|
||||
|
||||
## OIDC
|
||||
CONFLUENCE_OIDC_ENABLED: "{{ applications | get_app_conf(application_id, 'features.oidc') }}"
|
||||
CONFLUENCE_OIDC_LABEL: "{{ OIDC.BUTTON_TEXT }}"
|
||||
CONFLUENCE_OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
|
||||
CONFLUENCE_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
|
||||
CONFLUENCE_OIDC_ISSUER: "{{ OIDC.CLIENT.ISSUER_URL }}"
|
||||
CONFLUENCE_OIDC_AUTH_URL: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
CONFLUENCE_OIDC_TOKEN_URL: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
CONFLUENCE_OIDC_USERINFO_URL: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
CONFLUENCE_OIDC_LOGOUT_URL: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
CONFLUENCE_OIDC_JWKS_URL: "{{ OIDC.CLIENT.CERTS }}"
|
||||
CONFLUENCE_OIDC_SCOPES: "openid,email,profile"
|
||||
CONFLUENCE_OIDC_UNIQUE_ATTRIBUTE: "{{ OIDC.ATTRIBUTES.USERNAME }}"
|
||||
|
||||
# Docker
|
||||
CONFLUENCE_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.application.version') }}"
|
||||
CONFLUENCE_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.application.image') }}"
|
||||
CONFLUENCE_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.application.name') }}"
|
||||
CONFLUENCE_DATA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
## Docker
|
||||
CONFLUENCE_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.application.version') }}"
|
||||
CONFLUENCE_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.application.image') }}"
|
||||
CONFLUENCE_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.application.name') }}"
|
||||
CONFLUENCE_DATA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
CONFLUENCE_CUSTOM_IMAGE: "{{ CONFLUENCE_IMAGE }}_custom"
|
||||
|
||||
## Performance
|
||||
CONFLUENCE_TOTAL_MB: "{{ ansible_memtotal_mb | int }}"
|
||||
CONFLUENCE_JVM_MAX_MB: "{{ [ (CONFLUENCE_TOTAL_MB | int // 2), 12288 ] | min }}"
|
||||
CONFLUENCE_JVM_MIN_MB: "{{ [ (CONFLUENCE_TOTAL_MB | int // 4), (CONFLUENCE_JVM_MAX_MB | int) ] | min }}"
|
||||
CONFLUENCE_JVM_MIN: "{{ CONFLUENCE_JVM_MIN_MB }}m"
|
||||
CONFLUENCE_JVM_MAX: "{{ CONFLUENCE_JVM_MAX_MB }}m"
|
25
roles/web-app-jira/README.md
Normal file
25
roles/web-app-jira/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Jira
|
||||
|
||||
## Description
|
||||
|
||||
Jira Software is Atlassian’s issue and project-tracking platform. This role deploys Jira via Docker Compose, connects it to PostgreSQL, and adds proxy awareness, optional OIDC SSO, health checks, and production-oriented defaults for Infinito.Nexus.
|
||||
|
||||
## Overview
|
||||
|
||||
The role builds a lean custom image on top of the official Jira Software image, provisions persistent volumes, and exposes the app behind your reverse proxy. Variables control image/version/volumes/domains/SSO. JVM heap sizing is auto-derived from host RAM with safe caps to prevent `Xms > Xmx`.
|
||||
|
||||
## Features
|
||||
|
||||
* **Fully Dockerized:** Compose stack with a dedicated data volume (`jira_data`) and a minimal overlay image to enable future plugins/config.
|
||||
* **Reverse-Proxy/HTTPS Ready:** Preconfigured Atlassian Tomcat proxy envs so Jira respects external scheme/host/port.
|
||||
* **OIDC SSO (Optional):** Pre-templated vars for issuer, client, endpoints, scopes; compatible with Atlassian DC SSO/OIDC marketplace apps.
|
||||
* **Central Database:** PostgreSQL integration (local or central) with credentials sourced from role configuration.
|
||||
* **JVM Auto-Tuning:** Safe calculation of `JVM_MINIMUM_MEMORY` / `JVM_MAXIMUM_MEMORY` with caps to avoid VM init errors.
|
||||
* **Health Checks:** Container healthcheck for quicker failure detection and stable automation.
|
||||
* **CSP & Canonical Domains:** Integrates with platform CSP and domain management.
|
||||
* **Backup Ready:** Persistent data under `/var/atlassian/application-data/jira`.
|
||||
|
||||
## Further Resources
|
||||
|
||||
* Product page: [Atlassian Jira Software](https://www.atlassian.com/software/jira)
|
||||
* Docker Hub (official image): [atlassian/jira-software](https://hub.docker.com/r/atlassian/jira-software)
|
29
roles/web-app-jira/config/main.yml
Normal file
29
roles/web-app-jira/config/main.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
credentials: {}
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true
|
||||
application:
|
||||
image: atlassian/jira-software
|
||||
version: latest
|
||||
name: jira
|
||||
volumes:
|
||||
data: "jira_data"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
desktop: true
|
||||
central_database: true
|
||||
logout: true
|
||||
oidc: false # Not enabled for demo version
|
||||
ldap: false # Not enabled for demo version
|
||||
server:
|
||||
csp:
|
||||
whitelist: {}
|
||||
flags: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "jira.{{ PRIMARY_DOMAIN }}"
|
||||
rbac:
|
||||
roles: {}
|
20
roles/web-app-jira/meta/main.yml
Normal file
20
roles/web-app-jira/meta/main.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Jira Software is Atlassian’s issue & project tracking platform. This role deploys Jira in Docker, adds optional OIDC support, and integrates with the Infinito.Nexus ecosystem."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags: []
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://s.infinito.nexus/code/"
|
||||
logo:
|
||||
class: "fas fa-diagram-project"
|
||||
run_after:
|
||||
- web-app-matomo
|
||||
- web-app-keycloak
|
||||
- web-app-mailu
|
||||
dependencies: []
|
0
roles/web-app-jira/schema/main.yml
Normal file
0
roles/web-app-jira/schema/main.yml
Normal file
7
roles/web-app-jira/tasks/main.yml
Normal file
7
roles/web-app-jira/tasks/main.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- block:
|
||||
- name: "load docker, db and proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-full-stateful
|
||||
- include_tasks: utils/run_once.yml
|
||||
when: run_once_web_app_jira is not defined
|
8
roles/web-app-jira/templates/Dockerfile.j2
Normal file
8
roles/web-app-jira/templates/Dockerfile.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM "{{ JIRA_IMAGE }}:{{ JIRA_VERSION }}"
|
||||
|
||||
# Optional: install OIDC SSO app (example path/name)
|
||||
# COPY ./plugins/atlassian-sso-dc-latest.obr /opt/atlassian/jira/atlassian-bundled-plugins/
|
||||
|
||||
# Ensure proper permissions for app data
|
||||
RUN mkdir -p /var/atlassian/application-data/jira && \
|
||||
chown -R 2001:2001 /var/atlassian/application-data/jira
|
23
roles/web-app-jira/templates/docker-compose.yml.j2
Normal file
23
roles/web-app-jira/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
application:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: "{{ JIRA_CUSTOM_IMAGE }}"
|
||||
container_name: "{{ JIRA_CONTAINER }}"
|
||||
hostname: '{{ JIRA_HOSTNAME }}'
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:8080"
|
||||
volumes:
|
||||
- 'data:/var/atlassian/application-data/jira'
|
||||
{% include 'roles/docker-container/templates/healthcheck/curl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: {{ JIRA_DATA_VOLUME }}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
31
roles/web-app-jira/templates/env.j2
Normal file
31
roles/web-app-jira/templates/env.j2
Normal file
@@ -0,0 +1,31 @@
|
||||
## Jira core
|
||||
JIRA_URL="{{ JIRA_URL }}"
|
||||
|
||||
## Database
|
||||
ATL_DB_TYPE=postgres72
|
||||
ATL_DB_DRIVER=org.postgresql.Driver
|
||||
ATL_JDBC_URL=jdbc:postgresql://{{ database_host }}:{{ database_port }}/{{ database_name }}
|
||||
ATL_JDBC_USER={{ database_username }}
|
||||
ATL_JDBC_PASSWORD={{ database_password }}
|
||||
|
||||
ATL_PROXY_NAME={{ JIRA_HOSTNAME }}
|
||||
ATL_PROXY_PORT={{ WEB_PORT }}
|
||||
ATL_TOMCAT_SCHEME={{ WEB_PROTOCOL }}
|
||||
ATL_TOMCAT_SECURE={{ (WEB_PORT == 443) | lower }}
|
||||
JVM_MINIMUM_MEMORY={{ JIRA_JVM_MIN }}
|
||||
JVM_MAXIMUM_MEMORY={{ JIRA_JVM_MAX }}
|
||||
|
||||
## OIDC
|
||||
{% if JIRA_OIDC_ENABLED %}
|
||||
JIRA_OIDC_TITLE="{{ JIRA_OIDC_LABEL | replace('\"','\\\"') }}"
|
||||
JIRA_OIDC_ISSUER="{{ JIRA_OIDC_ISSUER }}"
|
||||
JIRA_OIDC_AUTHORIZATION_ENDPOINT="{{ JIRA_OIDC_AUTH_URL }}"
|
||||
JIRA_OIDC_TOKEN_ENDPOINT="{{ JIRA_OIDC_TOKEN_URL }}"
|
||||
JIRA_OIDC_USERINFO_ENDPOINT="{{ JIRA_OIDC_USERINFO_URL }}"
|
||||
JIRA_OIDC_END_SESSION_ENDPOINT="{{ JIRA_OIDC_LOGOUT_URL }}"
|
||||
JIRA_OIDC_JWKS_URI="{{ JIRA_OIDC_JWKS_URL }}"
|
||||
JIRA_OIDC_CLIENT_ID="{{ JIRA_OIDC_CLIENT_ID }}"
|
||||
JIRA_OIDC_CLIENT_SECRET="{{ JIRA_OIDC_CLIENT_SECRET }}"
|
||||
JIRA_OIDC_SCOPES="{{ JIRA_OIDC_SCOPES }}"
|
||||
JIRA_OIDC_UNIQUE_ATTRIBUTE="{{ JIRA_OIDC_UNIQUE_ATTRIBUTE }}"
|
||||
{% endif %}
|
41
roles/web-app-jira/vars/main.yml
Normal file
41
roles/web-app-jira/vars/main.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
# General
|
||||
application_id: "web-app-jira"
|
||||
database_type: "postgres"
|
||||
|
||||
# Container
|
||||
container_port: 8080 # Standardport Jira
|
||||
container_hostname: "{{ domains | get_domain(application_id) }}"
|
||||
|
||||
# Jira
|
||||
|
||||
## URLs
|
||||
JIRA_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
JIRA_HOSTNAME: "{{ container_hostname }}"
|
||||
|
||||
## OIDC
|
||||
JIRA_OIDC_ENABLED: "{{ applications | get_app_conf(application_id, 'features.oidc') }}"
|
||||
JIRA_OIDC_LABEL: "{{ OIDC.BUTTON_TEXT }}"
|
||||
JIRA_OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
|
||||
JIRA_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
|
||||
JIRA_OIDC_ISSUER: "{{ OIDC.CLIENT.ISSUER_URL }}"
|
||||
JIRA_OIDC_AUTH_URL: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
JIRA_OIDC_TOKEN_URL: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
JIRA_OIDC_USERINFO_URL: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
JIRA_OIDC_LOGOUT_URL: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
JIRA_OIDC_JWKS_URL: "{{ OIDC.CLIENT.CERTS }}"
|
||||
JIRA_OIDC_SCOPES: "openid,email,profile"
|
||||
JIRA_OIDC_UNIQUE_ATTRIBUTE: "{{ OIDC.ATTRIBUTES.USERNAME }}"
|
||||
|
||||
## Docker
|
||||
JIRA_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.application.version') }}"
|
||||
JIRA_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.application.image') }}"
|
||||
JIRA_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.application.name') }}"
|
||||
JIRA_DATA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
JIRA_CUSTOM_IMAGE: "{{ JIRA_IMAGE }}_custom"
|
||||
|
||||
## Performance (auto-derive from host memory)
|
||||
JIRA_TOTAL_MB: "{{ ansible_memtotal_mb | int }}"
|
||||
JIRA_JVM_MAX_MB: "{{ [ (JIRA_TOTAL_MB | int // 2), 12288 ] | min }}"
|
||||
JIRA_JVM_MIN_MB: "{{ [ (JIRA_TOTAL_MB | int // 4), (JIRA_JVM_MAX_MB | int) ] | min }}"
|
||||
JIRA_JVM_MIN: "{{ JIRA_JVM_MIN_MB }}m"
|
||||
JIRA_JVM_MAX: "{{ JIRA_JVM_MAX_MB }}m"
|
@@ -48,7 +48,7 @@ TURN_SECRET={{ applications | get_app_conf(application_id, 'credentials.talk_tur
|
||||
SIGNALING_SECRET={{ applications | get_app_conf(application_id, 'credentials.talk_signaling_secret') }}
|
||||
INTERNAL_SECRET={{ applications | get_app_conf(application_id, 'credentials.talk_internal_secret') }}
|
||||
TZ={{ HOST_TIMEZONE }}
|
||||
TALK_PORT=3478
|
||||
TALK_PORT={{ NEXTCLOUD_TALK_INT_TURN_PORT }}
|
||||
{% endif %}
|
||||
|
||||
{% if NEXTCLOUD_WHITEBOARD_ENABLED %}
|
||||
|
21
roles/web-app-postmarks/README.md
Normal file
21
roles/web-app-postmarks/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Postmarks
|
||||
|
||||
## Description
|
||||
|
||||
Run **Postmarks**, a small mail-service client, via Docker Compose—ideal as a utility component for apps that need SMTP interactions in your stack.
|
||||
|
||||
## Overview
|
||||
|
||||
This role installs and configures the Postmarks client container with basic domain wiring. It is designed to run behind your standard reverse proxy and to interoperate with other applications that rely on SMTP functionality.
|
||||
|
||||
## Features
|
||||
|
||||
- **Containerized Client:** Simple Docker Compose deployment for the Postmarks tool.
|
||||
- **SMTP-Oriented Usage:** Suited for scenarios where applications need to interact with a mail service.
|
||||
- **Minimal Footprint:** Small, focused utility component that fits neatly into larger stacks.
|
||||
- **Desktop Integration Hooks:** This README ensures the role is discoverable in your Web App Desktop.
|
||||
|
||||
## Further Resources
|
||||
|
||||
- [Postmarks (GitHub)](https://github.com/ckolderup/postmarks)
|
||||
- [Simple Mail Transfer Protocol (RFC 5321)](https://www.rfc-editor.org/rfc/rfc5321)
|
22
roles/web-app-socialhome/README.md
Normal file
22
roles/web-app-socialhome/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# SocialHome
|
||||
|
||||
## Description
|
||||
|
||||
Deploy **SocialHome**, a federated social network focused on content hubs and federation. This role provides a Docker-based scaffold and domain wiring so you can bring SocialHome into your Infinito.Nexus stack.
|
||||
|
||||
## Overview
|
||||
|
||||
This role sets up a SocialHome application using Docker Compose with basic domain and port wiring. It follows your standard role layout and prepares the service to run behind your existing reverse proxy. The current version is a scaffold intended to be expanded with database/cache services and app-specific settings.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dockerized Scaffold:** Baseline Docker Compose integration and role structure to get you started quickly.
|
||||
- **Domain & Port Wiring:** Integrates cleanly with your central domain/ports configuration.
|
||||
- **Ready for Federation:** Intended to support ActivityPub-based federation once the application is fully wired.
|
||||
- **Extensible Configuration:** Room for adding database, cache, worker processes, and environment tuning.
|
||||
- **Desktop Integration Hooks:** This README ensures inclusion in the Web App Desktop overview.
|
||||
|
||||
## Further Resources
|
||||
|
||||
- [SocialHome Project](https://socialhome.network/)
|
||||
- [ActivityPub (W3C)](https://www.w3.org/TR/activitypub/)
|
219
tests/integration/test_handlers_invoked.py
Normal file
219
tests/integration/test_handlers_invoked.py
Normal file
@@ -0,0 +1,219 @@
|
||||
import os
|
||||
import glob
|
||||
import re
|
||||
import unittest
|
||||
import yaml
|
||||
from typing import Any, Dict, Iterable, List, Set
|
||||
|
||||
|
||||
# ---------- YAML helpers ----------
|
||||
|
||||
def load_yaml_documents(path: str) -> List[Any]:
|
||||
"""
|
||||
Load one or more YAML documents from a file and return them as a list.
|
||||
Raises AssertionError with a helpful message on parse errors.
|
||||
"""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
try:
|
||||
docs = list(yaml.safe_load_all(f))
|
||||
return [d for d in docs if d is not None]
|
||||
except yaml.YAMLError as e:
|
||||
raise AssertionError(f"YAML parsing error in {path}: {e}")
|
||||
|
||||
|
||||
def _iter_task_like_entries(node: Any) -> Iterable[Dict[str, Any]]:
|
||||
"""
|
||||
Recursively yield task/handler-like dict entries from a YAML node.
|
||||
Handles top-level lists and dict-wrapped lists, and also drills into
|
||||
Ansible blocks ('block', 'rescue', 'always') or any list of dicts.
|
||||
"""
|
||||
if isinstance(node, list):
|
||||
for item in node:
|
||||
yield from _iter_task_like_entries(item)
|
||||
elif isinstance(node, dict):
|
||||
# If this dict looks like a task (has common task keys), yield it.
|
||||
# We are liberal and treat any dict as a potential task entry.
|
||||
yield node
|
||||
# Recurse into any list-of-dicts values (blocks, etc.)
|
||||
for v in node.values():
|
||||
if isinstance(v, list):
|
||||
if any(isinstance(x, dict) for x in v):
|
||||
yield from _iter_task_like_entries(v)
|
||||
|
||||
|
||||
def iter_task_like_entries(docs: List[Any]) -> Iterable[Dict[str, Any]]:
|
||||
for doc in docs:
|
||||
yield from _iter_task_like_entries(doc)
|
||||
|
||||
|
||||
def as_str_list(val: Any) -> List[str]:
|
||||
"""Normalize a YAML value (string or list) into a list of strings."""
|
||||
if val is None:
|
||||
return []
|
||||
if isinstance(val, str):
|
||||
return [val]
|
||||
if isinstance(val, list):
|
||||
return [str(v) for v in val]
|
||||
return [str(val)]
|
||||
|
||||
|
||||
# ---------- Notify extraction helpers ----------
|
||||
|
||||
# Extract quoted literals inside a string (e.g. from Jinja conditionals)
|
||||
_QUOTED_RE = re.compile(r"""(['"])(.+?)\1""")
|
||||
|
||||
def _expand_dynamic_notify(value: str) -> List[str]:
|
||||
"""
|
||||
If 'value' is a Jinja expression like:
|
||||
"{{ 'reload system daemon' if cond else 'refresh systemctl service' }}"
|
||||
then extract all quoted literals as potential targets.
|
||||
Always include the raw value too (just in case it is a plain name).
|
||||
"""
|
||||
results = []
|
||||
s = value.strip()
|
||||
if s:
|
||||
results.append(s)
|
||||
if "{{" in s and "}}" in s:
|
||||
for m in _QUOTED_RE.finditer(s):
|
||||
literal = m.group(2).strip()
|
||||
if literal:
|
||||
results.append(literal)
|
||||
return results
|
||||
|
||||
|
||||
# ---------- Extraction from handlers/tasks ----------
|
||||
|
||||
def collect_handler_groups(handler_file: str) -> List[Set[str]]:
|
||||
"""
|
||||
Build groups of acceptable targets for each handler task from a handlers file.
|
||||
For each handler, collect its 'name' and all 'listen' aliases.
|
||||
A handler is considered covered if ANY alias in its group is notified.
|
||||
"""
|
||||
groups: List[Set[str]] = []
|
||||
docs = load_yaml_documents(handler_file)
|
||||
|
||||
for entry in iter_task_like_entries(docs):
|
||||
names: Set[str] = set()
|
||||
|
||||
# primary name
|
||||
if isinstance(entry.get("name"), str):
|
||||
nm = entry["name"].strip()
|
||||
if nm:
|
||||
names.add(nm)
|
||||
|
||||
# listen aliases (string or list)
|
||||
if "listen" in entry:
|
||||
for item in as_str_list(entry["listen"]):
|
||||
item = item.strip()
|
||||
if item:
|
||||
names.add(item)
|
||||
|
||||
if names:
|
||||
groups.append(names)
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def collect_notify_calls_from_tasks(task_file: str) -> Set[str]:
|
||||
"""
|
||||
From a task file, collect all notification targets via:
|
||||
- 'notify:' (string or list), including dynamic Jinja expressions with literals,
|
||||
- any occurrence of 'package_notify:' (string or list), anywhere in the task dict.
|
||||
Also traverses tasks nested inside 'block', 'rescue', 'always', etc.
|
||||
"""
|
||||
notified: Set[str] = set()
|
||||
docs = load_yaml_documents(task_file)
|
||||
|
||||
for entry in iter_task_like_entries(docs):
|
||||
# Standard notify:
|
||||
if "notify" in entry:
|
||||
for item in as_str_list(entry["notify"]):
|
||||
for expanded in _expand_dynamic_notify(item):
|
||||
expanded = expanded.strip()
|
||||
if expanded:
|
||||
notified.add(expanded)
|
||||
|
||||
# package_notify anywhere in the task (top-level or nested)
|
||||
def walk_for_package_notify(node: Any):
|
||||
if isinstance(node, dict):
|
||||
for k, v in node.items():
|
||||
if k == "package_notify":
|
||||
for item in as_str_list(v):
|
||||
for expanded in _expand_dynamic_notify(item):
|
||||
expanded = expanded.strip()
|
||||
if expanded:
|
||||
notified.add(expanded)
|
||||
else:
|
||||
walk_for_package_notify(v)
|
||||
elif isinstance(node, list):
|
||||
for v in node:
|
||||
walk_for_package_notify(v)
|
||||
|
||||
walk_for_package_notify(entry)
|
||||
|
||||
return notified
|
||||
|
||||
|
||||
# ---------- Test case ----------
|
||||
|
||||
class TestHandlersInvoked(unittest.TestCase):
|
||||
"""
|
||||
Ensures that every handler defined in roles/*/handlers/*.yml(.yaml)
|
||||
is referenced at least once via either:
|
||||
- tasks' 'notify:' fields (supports Jinja conditionals with quoted literals), or
|
||||
- any 'package_notify:' usage (e.g., include_role: vars: package_notify: "...").
|
||||
|
||||
A handler is considered covered if ANY of its {name + listen} aliases is notified.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
self.roles_dir = os.path.join(repo_root, "roles")
|
||||
|
||||
# Handlers: support .yml and .yaml
|
||||
self.handler_files = (
|
||||
glob.glob(os.path.join(self.roles_dir, "*/handlers/*.yml"))
|
||||
+ glob.glob(os.path.join(self.roles_dir, "*/handlers/*.yaml"))
|
||||
)
|
||||
|
||||
# Tasks: recurse under tasks for both .yml and .yaml
|
||||
self.task_files = (
|
||||
glob.glob(os.path.join(self.roles_dir, "*", "tasks", "**", "*.yml"), recursive=True)
|
||||
+ glob.glob(os.path.join(self.roles_dir, "*", "tasks", "**", "*.yaml"), recursive=True)
|
||||
)
|
||||
|
||||
def test_all_handlers_have_a_notifier(self):
|
||||
# 1) Collect handler groups (name + listen) for each handler task
|
||||
handler_groups: List[Set[str]] = []
|
||||
for hf in self.handler_files:
|
||||
handler_groups.extend(collect_handler_groups(hf))
|
||||
|
||||
# 2) Collect all notified targets (notify + package_notify) from tasks
|
||||
all_notified: Set[str] = set()
|
||||
for tf in self.task_files:
|
||||
all_notified |= collect_notify_calls_from_tasks(tf)
|
||||
|
||||
# 3) A handler group is covered if any alias is notified
|
||||
missing_groups: List[Set[str]] = [grp for grp in handler_groups if not (grp & all_notified)]
|
||||
|
||||
if missing_groups:
|
||||
representatives: List[str] = []
|
||||
for grp in missing_groups:
|
||||
representatives.append(sorted(grp)[0])
|
||||
representatives = sorted(set(representatives))
|
||||
|
||||
msg = [
|
||||
"The following handlers are defined but never notified (via 'notify:' or 'package_notify:'):",
|
||||
*[f" - {m}" for m in representatives],
|
||||
"",
|
||||
"Note:",
|
||||
" • A handler is considered covered if *any* of its {name + listen} aliases is notified.",
|
||||
" • Dynamic Jinja notify expressions are supported by extracting quoted literals.",
|
||||
" • Ensure 'notify:' uses the exact handler name or one of its 'listen' aliases.",
|
||||
" • If you trigger builds via roles/vars, set 'package_notify:' to the handler name.",
|
||||
]
|
||||
self.fail("\n".join(msg))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
66
tests/integration/test_web_app_roles_have_readme.py
Normal file
66
tests/integration/test_web_app_roles_have_readme.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
def find_repo_root(start: Path) -> Optional[Path]:
|
||||
"""
|
||||
Walk up from `start` until we find a directory containing 'roles'.
|
||||
Returns the repo root (the directory that contains 'roles') or None.
|
||||
"""
|
||||
for parent in [start] + list(start.parents):
|
||||
if (parent / "roles").is_dir():
|
||||
return parent
|
||||
return None
|
||||
|
||||
|
||||
def web_app_role_dirs(root: Path) -> List[Path]:
|
||||
"""Return all role directories that match roles/web-app-*."""
|
||||
roles_dir = root / "roles"
|
||||
return sorted([p for p in roles_dir.glob("web-app-*") if p.is_dir()])
|
||||
|
||||
|
||||
class TestWebAppRolesHaveReadme(unittest.TestCase):
|
||||
"""
|
||||
Ensures every role under roles/web-app-* contains a README.md.
|
||||
|
||||
Why: The README is required for the role to be shown in the Web App Desktop.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
here = Path(__file__).resolve()
|
||||
repo_root = find_repo_root(here.parent)
|
||||
if repo_root is None:
|
||||
raise RuntimeError(
|
||||
f"Could not locate the repository root from {here}. "
|
||||
"Expected to find a 'roles/' directory in one of the parent folders."
|
||||
)
|
||||
cls.repo_root = repo_root
|
||||
cls.roles = web_app_role_dirs(repo_root)
|
||||
|
||||
def test_roles_directory_present(self):
|
||||
self.assertTrue(
|
||||
(self.repo_root / "roles").is_dir(),
|
||||
f"'roles' directory not found at: {self.repo_root}",
|
||||
)
|
||||
|
||||
def test_every_web_app_role_has_readme(self):
|
||||
missing = []
|
||||
for role_dir in self.roles:
|
||||
with self.subTest(role=role_dir.name):
|
||||
readme = role_dir / "README.md"
|
||||
if not readme.is_file():
|
||||
missing.append(role_dir)
|
||||
|
||||
if missing:
|
||||
formatted = "\n".join(f"- {p.relative_to(self.repo_root)}" for p in missing)
|
||||
self.fail(
|
||||
"The following roles are missing a README.md:\n"
|
||||
f"{formatted}\n\n"
|
||||
"A README.md is required so the role can be displayed in the Web App Desktop."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Reference in New Issue
Block a user