feat(bookstack): add role for self-hosted BookStack deployment
Deploy BookStack with linuxserver.io images behind Traefik, including Entra ID OIDC SSO support and a daily backup timer. Stack: - lscr.io/linuxserver/bookstack:version-v26.03.3 - lscr.io/linuxserver/mariadb:11.4.9 - Traefik labels for websecure entrypoint on internal network - Healthcheck via mariadb-admin ping (LSIO image lacks healthcheck.sh) Features: - Persistent APP_KEY generated on first run, stored in volume dir - Optional OIDC SSO via Microsoft Entra ID (configurable per-instance) - Idempotent admin user creation with DB-based existence check - Daily systemd timer backup (DB dump + uploads tar + APP_KEY) with configurable retention Implementation notes: - DB queries use --protocol=tcp with the app user because root@localhost uses unix_socket auth in the LSIO MariaDB image (no password) and root@% does not exist - docker_container_exec uses argv: (list) instead of command: (string) to avoid argument-splitting issues - Migration-wait task ensures users table exists before admin check, since /login returns 200 before Laravel migrations complete - no_log: true on all tasks that reference DB or admin passwords - artisan absolute path (/app/www/artisan) because LSIO image WORKDIR is not the app directory Adds bookstack route to DMZ Traefik service registry.
This commit is contained in:
parent
78095cca1d
commit
9d539d0da4
16 changed files with 659 additions and 2 deletions
|
|
@ -1,140 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: MIT-0
|
||||
|
||||
"""Custom Ansible filter plugin for computing Homarr grid layouts.
|
||||
|
||||
The Homarr SQL seed needs item_layout rows for three breakpoints
|
||||
(desktop / tablet / mobile). Rather than embedding the packing
|
||||
algorithm in Jinja with namespace gymnastics, this filter does the
|
||||
computation in Python and hands the seed template a ready-to-render
|
||||
data structure.
|
||||
|
||||
Usage in tasks:
|
||||
|
||||
- name: Compute Homarr app layouts
|
||||
ansible.builtin.set_fact:
|
||||
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
||||
|
||||
The result is a dict with two keys:
|
||||
|
||||
apps — original homarr_apps in order, each enriched with
|
||||
'desktop', 'tablet', 'mobile' sub-dicts of
|
||||
{'x', 'y', 'w', 'h'} ready for SQL templating.
|
||||
section_height — dict with 'desktop', 'tablet', 'mobile' keys
|
||||
giving the minimum height (in grid cells) the
|
||||
parent section must have to fit all tiles.
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
|
||||
def _pack(apps, cols):
|
||||
"""Greedy left-to-right packing into a fixed-column grid.
|
||||
|
||||
Width values larger than the grid are clamped to the grid width
|
||||
rather than overflowing — so a tile declared with width=8 still
|
||||
renders on the 6-column tablet grid (as a full-width tile) and on
|
||||
the 2-column mobile grid (as a full-width tile) without breaking
|
||||
the layout.
|
||||
|
||||
Returns (placements, total_height) where placements is a list of
|
||||
{'id', 'x', 'y', 'w', 'h'} dicts, one per input app, in the same
|
||||
order. total_height is the y-coordinate of the bottom of the last
|
||||
occupied row (i.e. max(y + h) across placements).
|
||||
"""
|
||||
x = 0
|
||||
y = 0
|
||||
row_h = 0
|
||||
max_y = 0
|
||||
placements = []
|
||||
|
||||
for app in apps:
|
||||
w = min(int(app.get('width', 1)), cols)
|
||||
h = int(app.get('height', 1))
|
||||
|
||||
# Wrap to the next row when the tile would overflow the grid.
|
||||
if x + w > cols:
|
||||
x = 0
|
||||
y += row_h
|
||||
row_h = 0
|
||||
|
||||
placements.append({
|
||||
'id': app['id'],
|
||||
'x': x,
|
||||
'y': y,
|
||||
'w': w,
|
||||
'h': h,
|
||||
})
|
||||
|
||||
x += w
|
||||
if h > row_h:
|
||||
row_h = h
|
||||
if y + h > max_y:
|
||||
max_y = y + h
|
||||
|
||||
return placements, max_y
|
||||
|
||||
|
||||
def homarr_compute_layouts(apps, desktop_cols=10, tablet_cols=6,
|
||||
mobile_cols=2):
|
||||
"""Compute responsive layouts for a list of Homarr apps.
|
||||
|
||||
Input validation is intentionally strict — a malformed apps list
|
||||
should fail the play with a clear message rather than produce a
|
||||
broken SQL seed and a silently misconfigured dashboard.
|
||||
|
||||
Note: uniqueness of app ids is NOT checked here. The role's
|
||||
`Validate homarr_apps have unique ids` assert task runs earlier
|
||||
and is the single source of truth for that check.
|
||||
"""
|
||||
if not isinstance(apps, list):
|
||||
raise AnsibleFilterError(
|
||||
"homarr_compute_layouts: expected a list of apps, "
|
||||
"got {0}".format(type(apps).__name__)
|
||||
)
|
||||
|
||||
for index, app in enumerate(apps):
|
||||
if not isinstance(app, dict):
|
||||
raise AnsibleFilterError(
|
||||
"homarr_compute_layouts: app at index {0} is not a "
|
||||
"dict (got {1})".format(index, type(app).__name__)
|
||||
)
|
||||
for required in ('id', 'width'):
|
||||
if required not in app:
|
||||
raise AnsibleFilterError(
|
||||
"homarr_compute_layouts: app at index {0} is "
|
||||
"missing required key '{1}'".format(index, required)
|
||||
)
|
||||
|
||||
desktop, h_desktop = _pack(apps, desktop_cols)
|
||||
tablet, h_tablet = _pack(apps, tablet_cols)
|
||||
mobile, h_mobile = _pack(apps, mobile_cols)
|
||||
|
||||
enriched = []
|
||||
for src, d, t, m in zip(apps, desktop, tablet, mobile):
|
||||
enriched.append({
|
||||
**src,
|
||||
'desktop': {'x': d['x'], 'y': d['y'], 'w': d['w'], 'h': d['h']},
|
||||
'tablet': {'x': t['x'], 'y': t['y'], 'w': t['w'], 'h': t['h']},
|
||||
'mobile': {'x': m['x'], 'y': m['y'], 'w': m['w'], 'h': m['h']},
|
||||
})
|
||||
|
||||
# Floor section height at 1 so the section stays visible even
|
||||
# when homarr_apps is empty.
|
||||
return {
|
||||
'apps': enriched,
|
||||
'section_height': {
|
||||
'desktop': max(h_desktop, 1),
|
||||
'tablet': max(h_tablet, 1),
|
||||
'mobile': max(h_mobile, 1),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
"""Ansible filter plugin entry point."""
|
||||
|
||||
def filters(self):
|
||||
return {
|
||||
'homarr_compute_layouts': homarr_compute_layouts,
|
||||
}
|
||||
|
|
@ -15,7 +15,11 @@ import sys
|
|||
|
||||
# Make the filter importable without having Ansible auto-discovery in
|
||||
# the way (it would only run during a real `ansible-playbook` invocation).
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(
|
||||
0,
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..',
|
||||
'plugins', 'filter')
|
||||
)
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@
|
|||
|
||||
- name: Compute Homarr app layouts
|
||||
ansible.builtin.set_fact:
|
||||
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
||||
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
||||
|
||||
- name: Show computed app layouts
|
||||
ansible.builtin.debug:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue