Compare commits

...

12 Commits

Author SHA1 Message Date
8059f272d5 Refactor Confluence and Jira env templates to use official Atlassian ATL_* database variables instead of unused custom placeholders. Ensures containers connect directly to PostgreSQL without relying on CONFLUENCE_DATABASE_* or JIRA_DATABASE_* vars. See conversation: https://chatgpt.com/share/68b6ddfd-3c44-800f-a57e-244dbd7ceeb5 2025-09-02 14:07:38 +02:00
7c814e6e83 BookWyrm: update Dockerfile and env handling
- Remove ARG BOOKWYRM_VERSION default, use Jinja variable directly
- Add proper SMTP environment variables mapping (EMAIL_HOST, EMAIL_PORT, TLS/SSL flags, user, password, default_from)
- Ensure env.j2 uses BookWyrm-expected names only
Ref: ChatGPT conversation 2025-09-02 https://chatgpt.com/share/68b6dc73-3784-800f-9a7e-340be498a412
2025-09-02 14:01:04 +02:00
d760c042c2 Atlassian JVM sizing: cast memory vars to int before floor-division
Apply |int to TOTAL_MB and dependent values to prevent 'unsupported operand type(s) for //' during templating in Confluence and Jira roles.

Context: discussion on 2025-09-02 — https://chatgpt.com/share/68b6d386-4490-800f-9bad-aa7be1571ebe
2025-09-02 13:22:59 +02:00
6cac8085a8 feat(web-app-chess): add castling.club role with ports, networks, and build setup
- Added network subnet (192.168.103.192/28) and port 8050 for web-app-chess
- Replaced stub README with usability-focused description of castling.club
- Implemented config, vars, meta, and tasks for web-app-chess
- Added Dockerfile, docker-compose.yml, env, and docker-entrypoint.sh templates
- Integrated entrypoint asset placement
- Updated meta to reflect usability and software features

Ref: https://chatgpt.com/share/68b6c65a-3de8-800f-86b2-a110920cd50e
2025-09-02 13:21:15 +02:00
3a83f3d14e Refactor BookWyrm role: switch to source-built Dockerfile, update README/meta for usability, add env improvements (ALLOWED_HOSTS, Redis vars, Celery broker), and pin version v0.7.5. See https://chatgpt.com/share/68b6d273-abc4-800f-ad3f-e1a5b9f8dad0 2025-09-02 13:18:32 +02:00
61d852c508 Added ports and networks for bookwyrm, jira, confluence 2025-09-02 12:08:20 +02:00
188b098503 Confluence/Jira roles: add READMEs, switch to custom images, proxy/JVM envs, and integer-safe heap sizing
Confluence: README added; demo disables OIDC/LDAP; Dockerfile overlay; docker-compose now uses CONFLUENCE_CUSTOM_IMAGE and DB depends include; env.j2 adds ATL_* and JVM_*; vars use integer math (//) for Xmx/Xms and expose CUSTOM_IMAGE.

Jira: initial role skeleton with README, config/meta/tasks; Dockerfile overlay; docker-compose using JIRA_CUSTOM_IMAGE and DB depends include; env.j2 with proxy + JVM envs; vars with integer-safe memory sizing.

Context: https://chatgpt.com/share/68b6b592-2250-800f-b68e-b37ae98dbe70
2025-09-02 12:07:34 +02:00
bc56940e55 Implement initial BookWyrm role
- Removed obsolete TODO.md
- Added config/main.yml with service, feature, CSP, and registration settings
- Added schema/main.yml defining vaulted SECRET_KEY (alphanumeric)
- Added tasks/main.yml to load stateful stack
- Added Dockerfile.j2 ensuring data/media dirs
- Added docker-compose.yml.j2 with application, worker, redis, volumes
- Added env.j2 with registration, secrets, DB, Redis, OIDC support
- Extended vars/main.yml with BookWyrm variables and OIDC, Docker, Redis settings
- Updated meta/main.yml with logo and run_after dependencies

Ref: https://chatgpt.com/share/68b6c060-3a0c-800f-89f8-e114a16a4a80
2025-09-02 12:03:11 +02:00
5dfc2efb5a Used port variable 2025-09-02 11:59:50 +02:00
7f9dc65b37 Add README.md files for web-app-bookwyrm, web-app-postmarks, and web-app-socialhome roles
Introduce integration test to ensure all web-app-* roles contain a README.md (required for Web App Desktop visibility)

See: https://chatgpt.com/share/68b6be49-7b78-800f-a3ff-bf922b4b083f
2025-09-02 11:52:34 +02:00
163a925096 fix(docker-compose): proper lock path + robust pull for buildable services
- Store pull lock under ${PATH_DOCKER_COMPOSE_PULL_LOCK_DIR}/<hash>.lock so global cleanup removes it reliably
- If any service defines `build:`, run `docker compose build --pull` before pulling
- Use `docker compose pull --ignore-buildable` when supported; otherwise tolerate pull failures for locally built images

This prevents failures when images are meant to be built locally (e.g., custom images) and ensures lock handling is consistent.

Ref: https://chatgpt.com/share/68b6b592-2250-800f-b68e-b37ae98dbe70
2025-09-02 11:15:28 +02:00
a8c88634b5 cleanup: remove unused handlers and add integration test for unused handlers
Removed obsolete handlers from roles (VirtualBox, backup-to-USB, OpenLDAP)
and introduced an integration test under tests/integration/test_handlers_invoked.py
that ensures all handlers defined in roles/*/handlers are actually notified
somewhere in the code base. This keeps the repository clean by preventing
unused or forgotten handlers from accumulating.

Ref: https://chatgpt.com/share/68b6b28e-4388-800f-87d2-34dfb34b8d36
2025-09-02 11:02:30 +02:00
46 changed files with 1127 additions and 131 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -1,4 +0,0 @@
---
- name: reload virtualbox kernel modules
become: true
command: vboxreload

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}"

View 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)

View File

@@ -1,2 +0,0 @@
# Todo
- Implement https://joinbookwyrm.com/de/

View 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

View File

@@ -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: []

View 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

View 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

View 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

View 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' %}

View 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 %}

View File

@@ -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 }}>"

View File

@@ -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/)

View 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"

View File

@@ -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: []

View 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

View 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

View 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"]

View 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' %}

View 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

View 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 }}"

View File

@@ -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"

View File

@@ -0,0 +1,25 @@
# Confluence
## Description
Confluence is Atlassians 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)

View File

@@ -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: {}

View File

@@ -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

View File

@@ -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' %}

View File

@@ -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 %}

View File

@@ -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"

View File

@@ -0,0 +1,25 @@
# Jira
## Description
Jira Software is Atlassians 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)

View 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: {}

View File

@@ -0,0 +1,20 @@
galaxy_info:
author: "Kevin Veen-Birkenbach"
description: "Jira Software is Atlassians 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: []

View File

View 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

View 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

View 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' %}

View 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 %}

View 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"

View File

@@ -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 %}

View 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)

View 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/)

View 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()

View 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()