Compare commits
1 commit
main
...
feature/va
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ac6596063 |
154 changed files with 996 additions and 7991 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,8 +1 @@
|
||||||
/.idea/
|
/.idea/
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
plugins/lookup/__pycache__/
|
|
||||||
|
|
||||||
# Local Ansible collection cache (galaxy/collection resolver)
|
|
||||||
/.ansible/
|
|
||||||
|
|
|
||||||
69
README.md
69
README.md
|
|
@ -1,68 +1,3 @@
|
||||||
# Ansible Collection — digitalboard.core
|
# Ansible Collection - digitalboard.core
|
||||||
|
|
||||||
This collection bundles the Ansible roles used to deploy the
|
Documentation for the collection.
|
||||||
[Digitalboard](https://git.digitalboard.ch/Digitalboard) platform: a set of
|
|
||||||
self-hosted, Docker-Compose-based services running behind Traefik, with
|
|
||||||
single sign-on provided by authentik or Keycloak.
|
|
||||||
|
|
||||||
Each role provisions one service (or building block) as a self-contained
|
|
||||||
Docker Compose stack. Roles are consumed from the deployment repository
|
|
||||||
[reference-ansible](https://git.digitalboard.ch/Digitalboard/reference-ansible),
|
|
||||||
where inventories and playbooks tie the roles to concrete hosts.
|
|
||||||
|
|
||||||
## Roles
|
|
||||||
|
|
||||||
| Role | Description |
|
|
||||||
| --- | --- |
|
|
||||||
| `base` | Host baseline: Docker, apt packages and convenience tooling on Debian/Ubuntu. |
|
|
||||||
| `traefik` | Traefik v3 reverse proxy as a public DMZ proxy (file provider) or backend proxy (docker provider). |
|
|
||||||
| `authentik` | [authentik](https://goauthentik.io) IdP (server + worker + Postgres); resources via blueprints. |
|
|
||||||
| `authentik_outpost_ldap` | authentik LDAP outpost exposing an LDAP interface for apps that cannot speak OIDC. |
|
|
||||||
| `keycloak` | [Keycloak](https://www.keycloak.org/) IdP with a PostgreSQL backend. |
|
|
||||||
| `389ds` | [389 Directory Server](https://www.port389.org/) LDAP directory via Docker Compose. |
|
|
||||||
| `nextcloud` | [Nextcloud](https://nextcloud.com/) (fpm) + Postgres + Redis, optional Collabora/draw.io/notify_push. |
|
|
||||||
| `opencloud` | [OpenCloud](https://opencloud.eu/) file platform via Docker Compose. |
|
|
||||||
| `collabora` | [Collabora Online](https://www.collaboraonline.com/) (CODE), used as the WOPI backend for Nextcloud. |
|
|
||||||
| `bookstack` | [BookStack](https://www.bookstackapp.com/) wiki (LSIO + MariaDB) with OIDC SSO and daily backups. |
|
|
||||||
| `drawio` | [draw.io](https://www.drawio.com/) diagram editor, with optional authentik ForwardAuth gating. |
|
|
||||||
| `homarr` | [Homarr](https://github.com/homarr-labs/homarr) dashboard with seeded admin user and OIDC group. |
|
|
||||||
| `opnform` | [OpnForm](https://github.com/OpnForm/OpnForm) self-hosted form builder (api + ui + db + redis). |
|
|
||||||
| `send` | [Send](https://github.com/timvisee/send) (timvisee fork) file sharing with a Redis backend. |
|
|
||||||
| `garage` | [Garage](https://garagehq.deuxfleurs.fr/) S3-compatible object storage with key/bucket provisioning. |
|
|
||||||
| `httpbin` | [httpbin](https://httpbin.org/) HTTP request/response testing service for validating Traefik ingress. |
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Roles are not run from this repository directly. They are consumed from the
|
|
||||||
deployment repository
|
|
||||||
[reference-ansible](https://git.digitalboard.ch/Digitalboard/reference-ansible),
|
|
||||||
which holds the inventories, group/host variables and playbooks. See that
|
|
||||||
repository's `docs/` directory for getting-started instructions, how to run
|
|
||||||
Ansible and how secrets are managed.
|
|
||||||
|
|
||||||
Per-role variables and their defaults are documented in each role's own
|
|
||||||
`README.md` and `meta/argument_specs.yml`.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- A Debian/Ubuntu target host (the `base` role bootstraps Docker there).
|
|
||||||
- ansible-core 2.15 or newer on the controller.
|
|
||||||
- The `community.docker` collection (used by nearly every role) and
|
|
||||||
`community.general` (used by the `keycloak` role). Both are declared as
|
|
||||||
`dependencies` in `galaxy.yml` and pulled in automatically when this
|
|
||||||
collection is installed via `ansible-galaxy`.
|
|
||||||
|
|
||||||
The role READMEs use `community.hashi_vault` lookups in their examples to source
|
|
||||||
secrets from HashiCorp Vault. That is a documented convention, not a hard
|
|
||||||
dependency of the roles — supply the variables however you prefer.
|
|
||||||
|
|
||||||
## Role ordering
|
|
||||||
|
|
||||||
Within a play, apply the roles in dependency order: `base` first (Docker and the
|
|
||||||
host baseline), then `traefik` (the shared reverse proxy and its Docker network),
|
|
||||||
then the individual service roles (`authentik`, `keycloak`, `nextcloud`, …),
|
|
||||||
which attach to Traefik's network and expect Docker to be present.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT-0. See individual roles for per-role license metadata.
|
|
||||||
|
|
|
||||||
26
galaxy.yml
26
galaxy.yml
|
|
@ -19,16 +19,15 @@ readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Bert-Jan Fikse <bert-jan@whatwedo.ch>
|
- Bert-Jan Fikse <bert-jan@whatwedo.ch>
|
||||||
- Tobias Wüst <tobias.wuest@wksbern.ch>
|
- Tobias Wüst <tobias.wuest@wksbern.ch>
|
||||||
- Simon Bärlocher <simon@whatwedo.ch>
|
|
||||||
|
|
||||||
### OPTIONAL but strongly recommended
|
### OPTIONAL but strongly recommended
|
||||||
# A short summary description of the collection
|
# A short summary description of the collection
|
||||||
description: Ansible roles to deploy the Digitalboard self-hosted service platform (Docker Compose + Traefik + SSO)
|
description: your collection description
|
||||||
|
|
||||||
# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only
|
# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only
|
||||||
# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file'
|
# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file'
|
||||||
license:
|
license:
|
||||||
- MIT-0
|
- GPL-2.0-or-later
|
||||||
|
|
||||||
# The path to the license file for the collection. This path is relative to the root of the collection. This key is
|
# The path to the license file for the collection. This path is relative to the root of the collection. This key is
|
||||||
# mutually exclusive with 'license'
|
# mutually exclusive with 'license'
|
||||||
|
|
@ -36,36 +35,25 @@ license_file: ''
|
||||||
|
|
||||||
# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
|
# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
|
||||||
# requirements as 'namespace' and 'name'
|
# requirements as 'namespace' and 'name'
|
||||||
tags:
|
tags: []
|
||||||
- digitalboard
|
|
||||||
- docker
|
|
||||||
- traefik
|
|
||||||
- sso
|
|
||||||
- selfhosted
|
|
||||||
|
|
||||||
# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
|
# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
|
||||||
# collection label 'namespace.name'. The value is a version range
|
# collection label 'namespace.name'. The value is a version range
|
||||||
# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
|
# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
|
||||||
# range specifiers can be set and are separated by ','
|
# range specifiers can be set and are separated by ','
|
||||||
dependencies:
|
dependencies: {}
|
||||||
# Used by nearly every role: docker_compose_v2, docker_container,
|
|
||||||
# docker_container_exec, docker_network. Hard runtime dependency.
|
|
||||||
community.docker: '>=3.0.0'
|
|
||||||
# Used by the keycloak role (keycloak_realm/client/group/user and
|
|
||||||
# related modules) in roles/keycloak/tasks/provisioning.yml.
|
|
||||||
community.general: '>=7.0.0'
|
|
||||||
|
|
||||||
# The URL of the originating SCM repository
|
# The URL of the originating SCM repository
|
||||||
repository: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
repository: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
||||||
|
|
||||||
# The URL to any online docs
|
# The URL to any online docs
|
||||||
documentation: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
documentation: http://docs.example.com
|
||||||
|
|
||||||
# The URL to the homepage of the collection/project
|
# The URL to the homepage of the collection/project
|
||||||
homepage: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
homepage: http://example.com
|
||||||
|
|
||||||
# The URL to the collection issue tracker
|
# The URL to the collection issue tracker
|
||||||
issues: https://git.digitalboard.ch/Digitalboard/digitalboard.core/issues
|
issues: http://example.com/issue/tracker
|
||||||
|
|
||||||
# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
|
# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
|
||||||
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
|
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# Collections must specify a minimum required ansible version to upload
|
# Collections must specify a minimum required ansible version to upload
|
||||||
# to galaxy. Aligned with the highest min_ansible_version declared by the
|
# to galaxy
|
||||||
# roles (the traefik role requires ansible-core 2.15).
|
# requires_ansible: '>=2.9.10'
|
||||||
requires_ansible: '>=2.15.0'
|
|
||||||
|
|
||||||
# Content that Ansible needs to load from another location or that has
|
# Content that Ansible needs to load from another location or that has
|
||||||
# been deprecated/removed
|
# been deprecated/removed
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
# Collection Plugins — digitalboard.core
|
# Collections Plugins Directory
|
||||||
|
|
||||||
This collection ships a small number of custom plugins that support the roles.
|
This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
|
||||||
They are addressed by their fully qualified name, `digitalboard.core.<name>`.
|
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
|
||||||
|
would contain module utils and modules respectively.
|
||||||
|
|
||||||
## Filter plugins (`filter/`)
|
Here is an example directory of the majority of plugins currently supported by Ansible:
|
||||||
|
|
||||||
`homarr_layout` — computes Homarr dashboard grid layouts (desktop / tablet /
|
```
|
||||||
mobile breakpoints) from a list of apps, returning a ready-to-render data
|
└── plugins
|
||||||
structure for the SQL seed. Used by the `homarr` role.
|
├── action
|
||||||
|
├── become
|
||||||
```yaml
|
├── cache
|
||||||
- name: Compute Homarr app layouts
|
├── callback
|
||||||
ansible.builtin.set_fact:
|
├── cliconf
|
||||||
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
├── connection
|
||||||
|
├── filter
|
||||||
|
├── httpapi
|
||||||
|
├── inventory
|
||||||
|
├── lookup
|
||||||
|
├── module_utils
|
||||||
|
├── modules
|
||||||
|
├── netconf
|
||||||
|
├── shell
|
||||||
|
├── strategy
|
||||||
|
├── terminal
|
||||||
|
├── test
|
||||||
|
└── vars
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lookup plugins (`lookup/`)
|
A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.19/plugins/plugins.html).
|
||||||
|
|
||||||
`garage_credentials` — returns S3 credentials (`key_id`, `secret_key`) for a
|
|
||||||
named Garage key by executing a docker command on the target host. Used to wire
|
|
||||||
Garage object storage into consuming roles such as `nextcloud`.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
nextcloud_s3_key: >-
|
|
||||||
{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='backend')['key_id'] }}
|
|
||||||
nextcloud_s3_secret: >-
|
|
||||||
{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='backend')['secret_key'] }}
|
|
||||||
```
|
|
||||||
|
|
||||||
No other plugin types (modules, action, callback, inventory, etc.) are currently
|
|
||||||
shipped by this collection.
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +1,38 @@
|
||||||
# 389ds
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
Deploys [389 Directory Server](https://www.port389.org/) (`389ds/dirsrv`)
|
A brief description of the role goes here.
|
||||||
as an LDAP directory via Docker Compose. After the container starts, the
|
|
||||||
role creates the configured suffix and a set of base organizational
|
|
||||||
units (e.g. `users`, `groups`).
|
|
||||||
|
|
||||||
## Requirements
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
- Docker and Docker Compose on the target host (e.g. via
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
`digitalboard.core.base`)
|
|
||||||
- Ansible collection: `community.docker`
|
|
||||||
|
|
||||||
## Role variables
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
| Variable | Default | Description |
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
| --- | --- | --- |
|
|
||||||
| `ds389_image` | `docker.io/389ds/dirsrv:3.1` | Container image. |
|
|
||||||
| `ds389_suffix` | `dc=example,dc=com` | Root suffix of the directory. |
|
|
||||||
| `ds389_root_dn` | `cn=Directory Manager` | Directory Manager bind DN. |
|
|
||||||
| `ds389_root_password` | `changeme` | Directory Manager password — **override this**. |
|
|
||||||
| `ds389_instance_name` | `localhost` | Directory server instance name (slapd config dir). |
|
|
||||||
| `ds389_hostname` | `389ds` | Container hostname (defaults to `ds389_service_name`). |
|
|
||||||
| `ds389_backend_network` | `backend` | Docker network LDAP clients connect over (created by Compose). |
|
|
||||||
| `ds389_ldap_port` | `3389` | Published LDAP port (container port 3389). |
|
|
||||||
| `ds389_ldaps_port` | `3636` | Published LDAPS port (container port 3636). |
|
|
||||||
| `ds389_base_ous` | `[users, groups]` | Base OUs created after startup. |
|
|
||||||
|
|
||||||
## Example
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
```yaml
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
- hosts: directory
|
|
||||||
become: true
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
|
|
||||||
|
- hosts: servers
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.389ds
|
- { role: username.rolename, x: 42 }
|
||||||
vars:
|
|
||||||
ds389_suffix: "dc=example,dc=org"
|
|
||||||
ds389_root_password: "{{ vault_ds389_root_password }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy 389 Directory Server (LDAP) via Docker Compose
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.2
|
||||||
- 389ds
|
|
||||||
- ldap
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- directory
|
# min_ansible_container_version:
|
||||||
- docker
|
|
||||||
- digitalboard
|
galaxy_tags: []
|
||||||
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -1,136 +1,28 @@
|
||||||
# Authentik
|
# Authentik
|
||||||
|
|
||||||
Deploys [authentik](https://goauthentik.io) (server + worker + Postgres)
|
Deploys Authentik identity provider with Docker Compose.
|
||||||
as a Docker Compose stack behind Traefik, with all resources provisioned
|
|
||||||
via templated blueprints.
|
|
||||||
|
|
||||||
## What this role does
|
|
||||||
|
|
||||||
- Renders the Compose stack with traefik labels and an optional
|
|
||||||
split-horizon host rewrite (see below)
|
|
||||||
- Provisions local users, groups, OIDC apps, Proxy/ForwardAuth apps,
|
|
||||||
LDAP apps and outposts, and Entra ID OAuth sources via blueprints
|
|
||||||
- Configures the login screen (visible sources, local login fields)
|
|
||||||
- Supports declarative cleanup via `authentik_removed_*` lists
|
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
See `defaults/main.yml` for all available variables.
|
||||||
common overrides:
|
|
||||||
|
|
||||||
### Service
|
## Blueprints
|
||||||
|
|
||||||
- `authentik_domains` (required, list): FQDNs the router accepts. First
|
|
||||||
entry is the canonical hostname; further entries cover internal
|
|
||||||
`*.int.*` names for server-to-server traffic.
|
|
||||||
- `authentik_secret_key` (required): PG fernet / signing secret.
|
|
||||||
Generate with `openssl rand -base64 60`.
|
|
||||||
- `authentik_postgres_password` (required).
|
|
||||||
- `authentik_image`, `authentik_port`, `authentik_log_level`.
|
|
||||||
|
|
||||||
### Split-horizon host rewrite
|
|
||||||
|
|
||||||
`authentik_host_rewrite_domains` lists hostnames that should reach the
|
|
||||||
authentik container but make it generate URLs (OIDC issuer, password
|
|
||||||
reset links, etc.) as if the request had arrived on
|
|
||||||
`authentik_domains[0]`.
|
|
||||||
|
|
||||||
For each entry the role:
|
|
||||||
|
|
||||||
- Creates a dedicated traefik router on that hostname
|
|
||||||
- Routes it to a URL-based loadbalancer service that disables
|
|
||||||
`passHostHeader`, so the upstream Host header becomes the canonical
|
|
||||||
FQDN
|
|
||||||
- Pins `X-Forwarded-Host` via middleware so the iss claim stays aligned
|
|
||||||
with the public hostname browsers see
|
|
||||||
|
|
||||||
Use case: an internal `auth.int.example.com` keeps server-to-server
|
|
||||||
traffic in the LAN, but Keycloak/Nextcloud/etc. still receive issuer
|
|
||||||
URLs matching `auth.example.com`.
|
|
||||||
|
|
||||||
### Blueprints
|
|
||||||
|
|
||||||
The role renders blueprints for:
|
The role renders blueprints for:
|
||||||
|
|
||||||
- Local users (`authentik_local_users`)
|
- Local users (`authentik_local_users`)
|
||||||
- Groups (`authentik_groups`)
|
|
||||||
- OIDC applications (`authentik_oidc_apps`)
|
- OIDC applications (`authentik_oidc_apps`)
|
||||||
- Proxy applications (`authentik_proxy_apps`)
|
- Proxy applications (`authentik_proxy_apps`)
|
||||||
- Proxy outposts (`authentik_proxy_outposts`)
|
- Proxy outposts (`authentik_proxy_outposts`)
|
||||||
- LDAP applications (`authentik_ldap_apps`)
|
|
||||||
- LDAP outpost (`authentik_ldap_outpost`)
|
|
||||||
- Entra ID sources (`authentik_entra_sources`)
|
- Entra ID sources (`authentik_entra_sources`)
|
||||||
- Login-screen source visibility (`authentik_login_sources`)
|
- Login screen sources (`authentik_login_source_ids`)
|
||||||
|
|
||||||
Secrets are passed via the `authentik_blueprint_env` env-var indirection
|
Secrets are passed via `authentik_blueprint_env` using environment variable references.
|
||||||
so they never land in rendered blueprint YAML on disk.
|
|
||||||
|
|
||||||
#### Proxy apps: mode and group restrictions
|
|
||||||
|
|
||||||
Each entry in `authentik_proxy_apps` supports:
|
|
||||||
|
|
||||||
- `mode` (default `forward_single`): one of `proxy`, `forward_single`,
|
|
||||||
`forward_domain`
|
|
||||||
- `allowed_groups`: when set, a `PolicyBinding` is emitted per group on
|
|
||||||
the application. authentik OR-evaluates bindings, so users in any
|
|
||||||
listed group pass and users in none are denied.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
authentik_proxy_apps:
|
|
||||||
- slug: drawio
|
|
||||||
name: drawio
|
|
||||||
external_host: "https://drawio.example.com"
|
|
||||||
mode: forward_single
|
|
||||||
allowed_groups:
|
|
||||||
- drawio-users
|
|
||||||
- admins
|
|
||||||
```
|
|
||||||
|
|
||||||
## Removing resources
|
## Removing resources
|
||||||
|
|
||||||
Move slugs from the active list to the matching removal list:
|
To remove resources from Authentik, move slugs to the removal lists:
|
||||||
|
|
||||||
- `authentik_removed_oidc_apps`
|
- `authentik_removed_oidc_apps`
|
||||||
- `authentik_removed_proxy_apps`
|
- `authentik_removed_proxy_apps`
|
||||||
- `authentik_removed_local_users`
|
- `authentik_removed_local_users`
|
||||||
|
|
||||||
After authentik has applied the deletion blueprint, remove the slug
|
After confirming deletion, remove the slug from the list.
|
||||||
from the list to keep state clean.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Run `digitalboard.core.base` first (Docker) and have the `community.docker`
|
|
||||||
collection installed; the role drives the stack via
|
|
||||||
`community.docker.docker_compose_v2`.
|
|
||||||
- Traefik network (`authentik_traefik_network`, default `proxy`) must exist
|
|
||||||
beforehand (e.g. created by the traefik role); it is referenced as an
|
|
||||||
external network in the Compose file.
|
|
||||||
- Internal backend network (`authentik_backend_network`, default `backend`).
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- hosts: identity_servers
|
|
||||||
roles:
|
|
||||||
- role: digitalboard.core.authentik
|
|
||||||
vars:
|
|
||||||
authentik_domains:
|
|
||||||
- "auth.example.com"
|
|
||||||
- "auth.int.example.com"
|
|
||||||
authentik_host_rewrite_domains:
|
|
||||||
- "auth.int.example.com"
|
|
||||||
authentik_secret_key: "{{ vault_authentik_secret_key }}"
|
|
||||||
authentik_postgres_password: "{{ vault_authentik_pg_password }}"
|
|
||||||
authentik_proxy_apps:
|
|
||||||
- slug: drawio
|
|
||||||
name: drawio
|
|
||||||
external_host: "https://drawio.example.com"
|
|
||||||
mode: forward_single
|
|
||||||
allowed_groups: [drawio-users]
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT-0
|
|
||||||
|
|
@ -12,21 +12,8 @@ authentik_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ authentik_servic
|
||||||
authentik_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ authentik_service_name }}"
|
authentik_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ authentik_service_name }}"
|
||||||
|
|
||||||
# Authentik service configuration
|
# Authentik service configuration
|
||||||
# FQDNs the authentik router accepts. The first entry is the canonical
|
authentik_domain: "authentik.local.test"
|
||||||
# domain; further entries cover internal *.int.* names used for
|
authentik_image: "ghcr.io/goauthentik/server:2025.12.0"
|
||||||
# server-to-server traffic so backend calls don't hairpin via DMZ.
|
|
||||||
authentik_domains:
|
|
||||||
- "authentik.local.test"
|
|
||||||
|
|
||||||
# Hostnames that should reach authentik but make it generate URLs (OIDC
|
|
||||||
# issuer, password reset links, etc.) as if requested from the canonical
|
|
||||||
# `authentik_domains[0]` instead. Used for split-horizon setups where an
|
|
||||||
# internal FQDN (e.g. `auth.int.example.com`) keeps server-to-server
|
|
||||||
# traffic in the LAN but the iss claim must still match the public
|
|
||||||
# hostname that browsers see. Traefik handles each entry via a separate
|
|
||||||
# router that rewrites the Host header before forwarding to authentik.
|
|
||||||
authentik_host_rewrite_domains: []
|
|
||||||
authentik_image: "ghcr.io/goauthentik/server:2026.2.2"
|
|
||||||
authentik_port: 9000
|
authentik_port: 9000
|
||||||
authentik_secret_key: "changeme-generate-a-random-string"
|
authentik_secret_key: "changeme-generate-a-random-string"
|
||||||
|
|
||||||
|
|
@ -70,29 +57,11 @@ authentik_proxy_outposts: []
|
||||||
# authentik_host_browser: "https://authentik.local.test/"
|
# authentik_host_browser: "https://authentik.local.test/"
|
||||||
# log_level: "info"
|
# log_level: "info"
|
||||||
|
|
||||||
authentik_ldap_apps: []
|
|
||||||
# - slug: ldap
|
|
||||||
# name: LDAP
|
|
||||||
# base_dn: "dc=local,dc=test"
|
|
||||||
# search_mode: cached # cached | direct
|
|
||||||
# bind_mode: cached # cached | direct
|
|
||||||
# search_group: null # optional: group name whose members can search
|
|
||||||
# certificate: null # optional: certificate name for LDAPS
|
|
||||||
# uid_start_number: 2000
|
|
||||||
# gid_start_number: 4000
|
|
||||||
|
|
||||||
authentik_ldap_outpost: {}
|
|
||||||
# name: "ldap-outpost"
|
|
||||||
# token: "changeme" # known token for outpost authentication
|
|
||||||
# config:
|
|
||||||
# authentik_host: "https://authentik.local.test/"
|
|
||||||
# log_level: "info"
|
|
||||||
|
|
||||||
authentik_oidc_apps: []
|
authentik_oidc_apps: []
|
||||||
# - slug: grafana
|
# - slug: grafana
|
||||||
# name: Grafana
|
# name: Grafana
|
||||||
# client_id: "grafana"
|
# client_id_env: GRAFANA_OIDC_CLIENT_ID
|
||||||
# client_secret: "changeme"
|
# client_secret_env: GRAFANA_OIDC_CLIENT_SECRET
|
||||||
# redirect_uris:
|
# redirect_uris:
|
||||||
# - url: "https://grafana.example.com/login/generic_oauth"
|
# - url: "https://grafana.example.com/login/generic_oauth"
|
||||||
# matching_mode: strict
|
# matching_mode: strict
|
||||||
|
|
@ -102,14 +71,21 @@ authentik_oidc_apps: []
|
||||||
# invalidation_slug: default-provider-invalidation-flow
|
# invalidation_slug: default-provider-invalidation-flow
|
||||||
# scopes: [openid, email, profile, offline_access]
|
# scopes: [openid, email, profile, offline_access]
|
||||||
|
|
||||||
|
authentik_blueprint_env: []
|
||||||
|
# GRAFANA_OIDC_CLIENT_ID: "grafana"
|
||||||
|
# GRAFANA_OIDC_CLIENT_SECRET: "{{ vault_grafana_oidc_secret }}"
|
||||||
|
# ENTRA_TENANT_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
|
# ENTRA_CLIENT_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
|
# ENTRA_CLIENT_SECRET: "{{ vault_entra_client_secret }}"
|
||||||
|
|
||||||
# Oauth sources
|
# Oauth sources
|
||||||
authentik_entra_sources: []
|
authentik_entra_sources: []
|
||||||
# - slug: entra-id
|
# - slug: entra-id
|
||||||
# name: "Login with Entra"
|
# name: "Login with Entra"
|
||||||
# tenant_mode: single # single | common
|
# tenant_mode: single # single | common
|
||||||
# tenant_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
# tenant_id_env: ENTRA_TENANT_ID
|
||||||
# client_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
# client_id_env: ENTRA_CLIENT_ID
|
||||||
# client_secret: "changeme"
|
# client_secret_env: ENTRA_CLIENT_SECRET
|
||||||
# scopes:
|
# scopes:
|
||||||
# - openid
|
# - openid
|
||||||
# - profile
|
# - profile
|
||||||
|
|
@ -129,19 +105,12 @@ authentik_login_user_fields:
|
||||||
- username
|
- username
|
||||||
- email
|
- email
|
||||||
|
|
||||||
# Groups to provision
|
|
||||||
authentik_groups: []
|
|
||||||
# - name: admins
|
|
||||||
# - name: editors
|
|
||||||
# is_superuser: false
|
|
||||||
# parent: null
|
|
||||||
|
|
||||||
# Local users to provision
|
# Local users to provision
|
||||||
authentik_local_users: []
|
authentik_local_users: []
|
||||||
# - username: admin
|
# - username: admin
|
||||||
# name: "Admin User"
|
# name: "Admin User"
|
||||||
# email: "admin@example.com"
|
# email: "admin@example.com"
|
||||||
# password: "changeme"
|
# password_env: AUTHENTIK_ADMIN_PASSWORD # reference env var in authentik_blueprint_env
|
||||||
# is_active: true
|
# is_active: true
|
||||||
# groups:
|
# groups:
|
||||||
# - authentik Admins
|
# - authentik Admins
|
||||||
|
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy authentik (server + worker + Postgres) via Docker Compose.
|
|
||||||
description:
|
|
||||||
- Renders a Compose stack for authentik with traefik labels, optional
|
|
||||||
TLS and a configurable split-horizon host-rewrite that keeps the OIDC
|
|
||||||
issuer URL on the canonical public hostname even when traffic enters
|
|
||||||
on an internal FQDN.
|
|
||||||
- Provisions resources through templated blueprints
|
|
||||||
(local users, groups, OIDC/Proxy/LDAP apps, outposts, OAuth sources).
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
docker_volume_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /srv/data
|
|
||||||
authentik_service_name:
|
|
||||||
type: str
|
|
||||||
default: authentik
|
|
||||||
authentik_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
description: Defaults to C({{ docker_compose_base_dir }}/{{ authentik_service_name }}).
|
|
||||||
authentik_docker_volume_dir:
|
|
||||||
type: path
|
|
||||||
description: Defaults to C({{ docker_volume_base_dir }}/{{ authentik_service_name }}).
|
|
||||||
|
|
||||||
authentik_domains:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
required: true
|
|
||||||
description:
|
|
||||||
- FQDNs the authentik router accepts. The first entry is the
|
|
||||||
canonical (public) hostname and is used for the network alias,
|
|
||||||
the X-Forwarded-Host rewrite target, and as the default OIDC
|
|
||||||
issuer. Further entries cover internal C(*.int.*) names used
|
|
||||||
for server-to-server traffic.
|
|
||||||
authentik_host_rewrite_domains:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Hostnames that should reach authentik but make it generate URLs
|
|
||||||
(OIDC issuer, password reset links, etc.) as if the request had
|
|
||||||
arrived on C(authentik_domains[0]).
|
|
||||||
- Each entry gets its own traefik router and a URL-based
|
|
||||||
loadbalancer service that disables passHostHeader and pins
|
|
||||||
X-Forwarded-Host via middleware. Used for split-horizon setups
|
|
||||||
where the LAN keeps server-to-server traffic but the iss claim
|
|
||||||
must match the public hostname browsers see.
|
|
||||||
authentik_image:
|
|
||||||
type: str
|
|
||||||
default: ghcr.io/goauthentik/server:2026.2.2
|
|
||||||
authentik_port:
|
|
||||||
type: int
|
|
||||||
default: 9000
|
|
||||||
authentik_secret_key:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: PG fernet key / signing secret. Generate with C(openssl rand -base64 60).
|
|
||||||
|
|
||||||
authentik_postgres_image:
|
|
||||||
type: str
|
|
||||||
default: postgres:16-alpine
|
|
||||||
authentik_postgres_db:
|
|
||||||
type: str
|
|
||||||
default: authentik
|
|
||||||
authentik_postgres_user:
|
|
||||||
type: str
|
|
||||||
default: authentik
|
|
||||||
authentik_postgres_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
|
|
||||||
authentik_traefik_network:
|
|
||||||
type: str
|
|
||||||
default: proxy
|
|
||||||
authentik_backend_network:
|
|
||||||
type: str
|
|
||||||
default: backend
|
|
||||||
authentik_use_ssl:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
|
|
||||||
authentik_log_level:
|
|
||||||
type: str
|
|
||||||
choices: [trace, debug, info, warning, error]
|
|
||||||
default: info
|
|
||||||
authentik_error_reporting_enabled:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
|
|
||||||
authentik_proxy_apps:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Proxy/ForwardAuth applications rendered via the
|
|
||||||
C(blueprint-proxy-app.yaml.j2) template.
|
|
||||||
options:
|
|
||||||
slug:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
name:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
internal_host:
|
|
||||||
type: str
|
|
||||||
description: Required when C(mode=proxy).
|
|
||||||
external_host:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
mode:
|
|
||||||
type: str
|
|
||||||
choices: [proxy, forward_single, forward_domain]
|
|
||||||
default: forward_single
|
|
||||||
description:
|
|
||||||
- "C(proxy): the outpost itself proxies traffic to internal_host."
|
|
||||||
- "C(forward_single): a single app behind an external reverse
|
|
||||||
proxy via ForwardAuth."
|
|
||||||
- "C(forward_domain): wildcard mode — one provider guards every
|
|
||||||
host on a cookie domain."
|
|
||||||
allowed_groups:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
description:
|
|
||||||
- If set, PolicyBindings are emitted (one per group, OR-evaluated).
|
|
||||||
Users in none of the listed groups are denied.
|
|
||||||
skip_path_regex:
|
|
||||||
type: str
|
|
||||||
flows:
|
|
||||||
type: dict
|
|
||||||
description: Authentication / authorization / invalidation flow slugs.
|
|
||||||
|
|
||||||
authentik_proxy_outposts:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
|
|
||||||
authentik_ldap_apps:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
authentik_ldap_outpost:
|
|
||||||
type: dict
|
|
||||||
default: {}
|
|
||||||
|
|
||||||
authentik_oidc_apps:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
|
|
||||||
authentik_entra_sources:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
authentik_login_sources:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
authentik_identification_stage_name:
|
|
||||||
type: str
|
|
||||||
default: default-authentication-identification
|
|
||||||
authentik_login_user_fields:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
choices: [username, email, upn]
|
|
||||||
default: [username, email]
|
|
||||||
description: Local login fields shown on the login screen. Empty list hides local login.
|
|
||||||
|
|
||||||
authentik_groups:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
authentik_local_users:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
|
|
||||||
authentik_removed_oidc_apps:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description: OIDC application slugs scheduled for deletion.
|
|
||||||
authentik_removed_proxy_apps:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
authentik_removed_local_users:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy authentik (server + worker + Postgres) via Docker Compose with blueprint-provisioned resources
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.2
|
||||||
- authentik
|
|
||||||
- oidc
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- sso
|
# min_ansible_container_version:
|
||||||
- idp
|
|
||||||
- docker
|
galaxy_tags: []
|
||||||
- traefik
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
- digitalboard
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,17 @@
|
||||||
register: existing_blueprints
|
register: existing_blueprints
|
||||||
|
|
||||||
- name: Build list of expected blueprint files
|
- name: Build list of expected blueprint files
|
||||||
vars:
|
|
||||||
_oidc: "{{ authentik_oidc_apps | map(attribute='slug') | map('regex_replace', '^', '50-oidc-') | map('regex_replace', '$', '.yaml') | list }}"
|
|
||||||
_ldap: "{{ authentik_ldap_apps | map(attribute='slug') | map('regex_replace', '^', '55-ldap-') | map('regex_replace', '$', '.yaml') | list }}"
|
|
||||||
_proxy: "{{ authentik_proxy_apps | map(attribute='slug') | map('regex_replace', '^', '60-proxy-') | map('regex_replace', '$', '.yaml') | list }}"
|
|
||||||
_outpost: "{{ authentik_proxy_outposts | map(attribute='name') | map('regex_replace', '^', '70-outpost-') | map('regex_replace', '$', '.yaml') | list }}"
|
|
||||||
_entra: "{{ authentik_entra_sources | map(attribute='slug') | map('regex_replace', '^', '40-source-entra-') | map('regex_replace', '$', '.yaml') | list }}"
|
|
||||||
_ldap_out: "{{ ['75-outpost-ldap.yaml'] if authentik_ldap_outpost.name is defined else [] }}"
|
|
||||||
_users: "{{ ['10-local-users.yaml'] if (authentik_local_users | length > 0 or authentik_groups | length > 0) else [] }}"
|
|
||||||
_cleanup: "{{ ['00-cleanup.yaml'] if (authentik_removed_oidc_apps + authentik_removed_proxy_apps + authentik_removed_local_users) | length > 0 else [] }}"
|
|
||||||
set_fact:
|
set_fact:
|
||||||
expected_blueprints: "{{ _oidc + _ldap + _proxy + _outpost + _entra + ['45-login-sources.yaml'] + _ldap_out + _users + _cleanup }}"
|
expected_blueprints: >-
|
||||||
|
{{
|
||||||
|
(authentik_oidc_apps | map(attribute='slug') | map('regex_replace', '^(.*)$', '50-oidc-\1.yaml') | list) +
|
||||||
|
(authentik_proxy_apps | map(attribute='slug') | map('regex_replace', '^(.*)$', '60-proxy-\1.yaml') | list) +
|
||||||
|
(authentik_proxy_outposts | map(attribute='name') | map('regex_replace', '^(.*)$', '70-outpost-\1.yaml') | list) +
|
||||||
|
(authentik_entra_sources | map(attribute='slug') | map('regex_replace', '^(.*)$', '40-source-entra-\1.yaml') | list) +
|
||||||
|
['45-login-sources.yaml'] +
|
||||||
|
((authentik_local_users | length > 0) | ternary(['10-local-users.yaml'], [])) +
|
||||||
|
(((authentik_removed_oidc_apps | length > 0) or (authentik_removed_proxy_apps | length > 0) or (authentik_removed_local_users | length > 0)) | ternary(['00-cleanup.yaml'], []))
|
||||||
|
}}
|
||||||
|
|
||||||
- name: Remove stale blueprint files
|
- name: Remove stale blueprint files
|
||||||
file:
|
file:
|
||||||
|
|
@ -36,14 +36,6 @@
|
||||||
loop: "{{ authentik_oidc_apps }}"
|
loop: "{{ authentik_oidc_apps }}"
|
||||||
register: oidc_templates
|
register: oidc_templates
|
||||||
|
|
||||||
- name: Render LDAP blueprints
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: blueprints/blueprint-ldap-app.yaml.j2
|
|
||||||
dest: "{{ authentik_docker_volume_dir }}/blueprints/55-ldap-{{ item.slug }}.yaml"
|
|
||||||
mode: "0644"
|
|
||||||
loop: "{{ authentik_ldap_apps }}"
|
|
||||||
register: ldap_templates
|
|
||||||
|
|
||||||
- name: Render Proxy blueprints
|
- name: Render Proxy blueprints
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: blueprints/blueprint-proxy-app.yaml.j2
|
src: blueprints/blueprint-proxy-app.yaml.j2
|
||||||
|
|
@ -60,14 +52,6 @@
|
||||||
loop: "{{ authentik_proxy_outposts }}"
|
loop: "{{ authentik_proxy_outposts }}"
|
||||||
register: outpost_bp
|
register: outpost_bp
|
||||||
|
|
||||||
- name: Render LDAP outpost blueprint
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: blueprints/outpost-ldap.yaml.j2
|
|
||||||
dest: "{{ authentik_docker_volume_dir }}/blueprints/75-outpost-ldap.yaml"
|
|
||||||
mode: "0644"
|
|
||||||
when: authentik_ldap_outpost.name is defined
|
|
||||||
register: ldap_outpost_bp
|
|
||||||
|
|
||||||
- name: Render Entra source blueprints
|
- name: Render Entra source blueprints
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: blueprints/blueprint-source-entra.yaml.j2
|
src: blueprints/blueprint-source-entra.yaml.j2
|
||||||
|
|
@ -88,7 +72,7 @@
|
||||||
src: blueprints/blueprint-local-users.yaml.j2
|
src: blueprints/blueprint-local-users.yaml.j2
|
||||||
dest: "{{ authentik_docker_volume_dir }}/blueprints/10-local-users.yaml"
|
dest: "{{ authentik_docker_volume_dir }}/blueprints/10-local-users.yaml"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
when: authentik_local_users | length > 0 or authentik_groups | length > 0
|
when: authentik_local_users | length > 0
|
||||||
register: local_users_bp
|
register: local_users_bp
|
||||||
|
|
||||||
- name: Render cleanup blueprint
|
- name: Render cleanup blueprint
|
||||||
|
|
@ -104,10 +88,8 @@
|
||||||
blueprints_changed: >-
|
blueprints_changed: >-
|
||||||
{{
|
{{
|
||||||
(oidc_templates is defined and (oidc_templates.results | selectattr('changed') | list | length > 0))
|
(oidc_templates is defined and (oidc_templates.results | selectattr('changed') | list | length > 0))
|
||||||
or (ldap_templates is defined and (ldap_templates.results | selectattr('changed') | list | length > 0))
|
|
||||||
or (proxy_templates is defined and (proxy_templates.results | selectattr('changed') | list | length > 0))
|
or (proxy_templates is defined and (proxy_templates.results | selectattr('changed') | list | length > 0))
|
||||||
or (outpost_bp is defined and (outpost_bp.results | selectattr('changed') | list | length > 0))
|
or (outpost_bp is defined and (outpost_bp.results | selectattr('changed') | list | length > 0))
|
||||||
or (ldap_outpost_bp.changed | default(false))
|
|
||||||
or (entra_bp is defined and (entra_bp.results | selectattr('changed') | list | length > 0))
|
or (entra_bp is defined and (entra_bp.results | selectattr('changed') | list | length > 0))
|
||||||
or (login_bp is defined and login_bp.changed)
|
or (login_bp is defined and login_bp.changed)
|
||||||
or (local_users_bp.changed | default(false))
|
or (local_users_bp.changed | default(false))
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,44 @@
|
||||||
---
|
---
|
||||||
# tasks file for authentik
|
# tasks file for authentik
|
||||||
|
|
||||||
- name: Create authentik directories
|
- name: Create docker compose directory
|
||||||
file:
|
file:
|
||||||
path: "{{ item }}"
|
path: "{{ authentik_docker_compose_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
loop:
|
|
||||||
- "{{ authentik_docker_compose_dir }}"
|
- name: Create authentik data directory
|
||||||
- "{{ authentik_docker_volume_dir }}/data"
|
file:
|
||||||
- "{{ authentik_docker_volume_dir }}/certs"
|
path: "{{ authentik_docker_volume_dir }}/data"
|
||||||
- "{{ authentik_docker_volume_dir }}/templates"
|
state: directory
|
||||||
- "{{ authentik_docker_volume_dir }}/postgresql"
|
mode: '0755'
|
||||||
- "{{ authentik_docker_volume_dir }}/blueprints"
|
|
||||||
|
- name: Create authentik certs directory
|
||||||
|
file:
|
||||||
|
path: "{{ authentik_docker_volume_dir }}/certs"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Create authentik templates directory
|
||||||
|
file:
|
||||||
|
path: "{{ authentik_docker_volume_dir }}/templates"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Create postgres data directory
|
||||||
|
file:
|
||||||
|
path: "{{ authentik_docker_volume_dir }}/postgresql"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Create blueprints directory
|
||||||
|
file:
|
||||||
|
path: "{{ authentik_docker_volume_dir }}/blueprints"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Render blueprints
|
||||||
|
import_tasks: blueprints.yml
|
||||||
|
|
||||||
- name: Create docker-compose file for authentik
|
- name: Create docker-compose file for authentik
|
||||||
template:
|
template:
|
||||||
|
|
@ -25,46 +51,6 @@
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ authentik_docker_compose_dir }}"
|
project_src: "{{ authentik_docker_compose_dir }}"
|
||||||
state: present
|
state: present
|
||||||
|
recreate: "{{ blueprints_changed | ternary('always', 'auto') }}"
|
||||||
wait: true
|
wait: true
|
||||||
wait_timeout: 300
|
wait_timeout: 300
|
||||||
|
|
||||||
- name: Render blueprints
|
|
||||||
import_tasks: blueprints.yml
|
|
||||||
|
|
||||||
- name: Render blueprint wait script
|
|
||||||
template:
|
|
||||||
src: wait-for-blueprints.py.j2
|
|
||||||
dest: "{{ authentik_docker_volume_dir }}/data/wait-for-blueprints.py"
|
|
||||||
mode: '0644'
|
|
||||||
|
|
||||||
- name: Wait for custom blueprints to be applied
|
|
||||||
community.docker.docker_compose_v2_exec:
|
|
||||||
project_src: "{{ authentik_docker_compose_dir }}"
|
|
||||||
service: server
|
|
||||||
command: ak shell -c "exec(open('/data/wait-for-blueprints.py').read())"
|
|
||||||
register: blueprint_wait_result
|
|
||||||
changed_when: "'changed' in blueprint_wait_result.stdout"
|
|
||||||
retries: 30
|
|
||||||
delay: 10
|
|
||||||
until: blueprint_wait_result.rc == 0
|
|
||||||
when: blueprints_changed
|
|
||||||
|
|
||||||
- name: Render LDAP outpost token script
|
|
||||||
template:
|
|
||||||
src: set-outpost-token.py.j2
|
|
||||||
dest: "{{ authentik_docker_volume_dir }}/data/set-outpost-token.py"
|
|
||||||
mode: '0644'
|
|
||||||
when: authentik_ldap_outpost.name is defined
|
|
||||||
register: ldap_token_script
|
|
||||||
|
|
||||||
- name: Set known token for LDAP outpost
|
|
||||||
community.docker.docker_compose_v2_exec:
|
|
||||||
project_src: "{{ authentik_docker_compose_dir }}"
|
|
||||||
service: server
|
|
||||||
command: ak shell -c "exec(open('/data/set-outpost-token.py').read())"
|
|
||||||
register: ldap_token_result
|
|
||||||
changed_when: "'changed' in ldap_token_result.stdout"
|
|
||||||
retries: 30
|
|
||||||
delay: 10
|
|
||||||
until: ldap_token_result.rc == 0
|
|
||||||
when: authentik_ldap_outpost.name is defined and (blueprints_changed or ldap_token_script.changed)
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
|
||||||
version: 1
|
|
||||||
metadata:
|
|
||||||
name: "ldap-{{ item.slug }}"
|
|
||||||
labels:
|
|
||||||
blueprints.goauthentik.io/instantiate: "true"
|
|
||||||
blueprints.goauthentik.io/description: "LDAP provider + application for {{ item.slug }}"
|
|
||||||
|
|
||||||
entries:
|
|
||||||
# Simple password-only flow for LDAP bind (no browser policies)
|
|
||||||
- model: authentik_stages_password.passwordstage
|
|
||||||
id: ldap-password-stage
|
|
||||||
identifiers:
|
|
||||||
name: ldap-bind-password
|
|
||||||
attrs:
|
|
||||||
name: ldap-bind-password
|
|
||||||
backends:
|
|
||||||
- authentik.core.auth.InbuiltBackend
|
|
||||||
- authentik.core.auth.TokenBackend
|
|
||||||
- authentik.sources.ldap.auth.LDAPBackend
|
|
||||||
|
|
||||||
- model: authentik_stages_identification.identificationstage
|
|
||||||
id: ldap-identification-stage
|
|
||||||
identifiers:
|
|
||||||
name: ldap-bind-identification
|
|
||||||
attrs:
|
|
||||||
name: ldap-bind-identification
|
|
||||||
user_fields:
|
|
||||||
- username
|
|
||||||
- email
|
|
||||||
password_stage: !KeyOf ldap-password-stage
|
|
||||||
|
|
||||||
- model: authentik_stages_user_login.userloginstage
|
|
||||||
id: ldap-login-stage
|
|
||||||
identifiers:
|
|
||||||
name: ldap-bind-login
|
|
||||||
attrs:
|
|
||||||
name: ldap-bind-login
|
|
||||||
|
|
||||||
- model: authentik_flows.flow
|
|
||||||
id: ldap-bind-flow
|
|
||||||
identifiers:
|
|
||||||
slug: ldap-bind
|
|
||||||
attrs:
|
|
||||||
name: LDAP Bind
|
|
||||||
slug: ldap-bind
|
|
||||||
title: LDAP Bind
|
|
||||||
designation: authentication
|
|
||||||
authentication: none
|
|
||||||
|
|
||||||
- model: authentik_flows.flowstagebinding
|
|
||||||
identifiers:
|
|
||||||
target: !KeyOf ldap-bind-flow
|
|
||||||
stage: !KeyOf ldap-identification-stage
|
|
||||||
order: 0
|
|
||||||
attrs:
|
|
||||||
target: !KeyOf ldap-bind-flow
|
|
||||||
stage: !KeyOf ldap-identification-stage
|
|
||||||
order: 0
|
|
||||||
|
|
||||||
- model: authentik_flows.flowstagebinding
|
|
||||||
identifiers:
|
|
||||||
target: !KeyOf ldap-bind-flow
|
|
||||||
stage: !KeyOf ldap-login-stage
|
|
||||||
order: 10
|
|
||||||
attrs:
|
|
||||||
target: !KeyOf ldap-bind-flow
|
|
||||||
stage: !KeyOf ldap-login-stage
|
|
||||||
order: 10
|
|
||||||
|
|
||||||
{% if item.search_group is defined and item.search_group %}
|
|
||||||
- model: authentik_rbac.role
|
|
||||||
id: ldap-search-role-{{ item.slug }}
|
|
||||||
identifiers:
|
|
||||||
name: ldap-search-{{ item.slug }}
|
|
||||||
attrs:
|
|
||||||
name: ldap-search-{{ item.slug }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
- model: authentik_providers_ldap.ldapprovider
|
|
||||||
id: ldap-provider-{{ item.slug }}
|
|
||||||
identifiers:
|
|
||||||
name: {{ item.name }}
|
|
||||||
attrs:
|
|
||||||
name: {{ item.name }}
|
|
||||||
base_dn: "{{ item.base_dn }}"
|
|
||||||
authorization_flow: !KeyOf ldap-bind-flow
|
|
||||||
invalidation_flow: !Find [authentik_flows.flow, [slug, {{ item.invalidation_flow_slug | default('default-provider-invalidation-flow') }}]]
|
|
||||||
authentication_flow: !KeyOf ldap-bind-flow
|
|
||||||
search_mode: {{ item.search_mode | default('cached') }}
|
|
||||||
bind_mode: {{ item.bind_mode | default('direct') }}
|
|
||||||
{% if item.certificate is defined and item.certificate %}
|
|
||||||
certificate: !Find [authentik_crypto.certificatekeypair, [name, {{ item.certificate }}]]
|
|
||||||
{% endif %}
|
|
||||||
{% if item.uid_start_number is defined %}
|
|
||||||
uid_start_number: {{ item.uid_start_number }}
|
|
||||||
{% endif %}
|
|
||||||
{% if item.gid_start_number is defined %}
|
|
||||||
gid_start_number: {{ item.gid_start_number }}
|
|
||||||
{% endif %}
|
|
||||||
{% if item.search_group is defined and item.search_group %}
|
|
||||||
permissions:
|
|
||||||
- permission: authentik_providers_ldap.search_full_directory
|
|
||||||
role: !KeyOf ldap-search-role-{{ item.slug }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if item.search_group is defined and item.search_group %}
|
|
||||||
# Assign the LDAP search role to the search group
|
|
||||||
- model: authentik_core.group
|
|
||||||
identifiers:
|
|
||||||
name: {{ item.search_group }}
|
|
||||||
attrs:
|
|
||||||
roles:
|
|
||||||
- !KeyOf ldap-search-role-{{ item.slug }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
- model: authentik_core.application
|
|
||||||
id: app-{{ item.slug }}
|
|
||||||
identifiers:
|
|
||||||
slug: {{ item.slug }}
|
|
||||||
attrs:
|
|
||||||
name: "{{ item.name | default(item.slug) }}"
|
|
||||||
slug: {{ item.slug }}
|
|
||||||
provider: !KeyOf ldap-provider-{{ item.slug }}
|
|
||||||
|
|
@ -4,24 +4,9 @@ metadata:
|
||||||
name: "local-users"
|
name: "local-users"
|
||||||
labels:
|
labels:
|
||||||
blueprints.goauthentik.io/instantiate: "true"
|
blueprints.goauthentik.io/instantiate: "true"
|
||||||
blueprints.goauthentik.io/description: "Local groups and user accounts"
|
blueprints.goauthentik.io/description: "Local user accounts"
|
||||||
|
|
||||||
entries:
|
entries:
|
||||||
{% for group in authentik_groups %}
|
|
||||||
- model: authentik_core.group
|
|
||||||
id: group-{{ group.name | regex_replace('[^a-zA-Z0-9]', '-') }}
|
|
||||||
identifiers:
|
|
||||||
name: {{ group.name }}
|
|
||||||
attrs:
|
|
||||||
name: {{ group.name }}
|
|
||||||
{% if group.is_superuser is defined %}
|
|
||||||
is_superuser: {{ group.is_superuser | lower }}
|
|
||||||
{% endif %}
|
|
||||||
{% if group.parent is defined and group.parent %}
|
|
||||||
parent: !Find [authentik_core.group, [name, {{ group.parent }}]]
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
{% for user in authentik_local_users %}
|
{% for user in authentik_local_users %}
|
||||||
- model: authentik_core.user
|
- model: authentik_core.user
|
||||||
id: user-{{ user.username }}
|
id: user-{{ user.username }}
|
||||||
|
|
@ -32,8 +17,8 @@ entries:
|
||||||
name: "{{ user.name | default(user.username) }}"
|
name: "{{ user.name | default(user.username) }}"
|
||||||
email: "{{ user.email | default('') }}"
|
email: "{{ user.email | default('') }}"
|
||||||
is_active: {{ user.is_active | default(true) | lower }}
|
is_active: {{ user.is_active | default(true) | lower }}
|
||||||
{% if user.password is defined %}
|
{% if user.password_env is defined %}
|
||||||
password: "{{ user.password }}"
|
password: !Env {{ user.password_env }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.groups is defined and user.groups | length > 0 %}
|
{% if user.groups is defined and user.groups | length > 0 %}
|
||||||
groups:
|
groups:
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,8 @@ entries:
|
||||||
{% for field in authentik_login_user_fields %}
|
{% for field in authentik_login_user_fields %}
|
||||||
- {{ field }}
|
- {{ field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if authentik_login_sources %}
|
|
||||||
# OAuth/social login sources (use !Find to reference sources from other blueprints)
|
# OAuth/social login sources (use !Find to reference sources from other blueprints)
|
||||||
sources:
|
sources:
|
||||||
{% for src in authentik_login_sources %}
|
{% for src in authentik_login_sources %}
|
||||||
- !Find [authentik_sources_oauth.oauthsource, [slug, {{ src.slug }}]]
|
- !Find [authentik_sources_oauth.oauthsource, [slug, {{ src.slug }}]]
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,9 @@ entries:
|
||||||
name: {{ item.slug }}
|
name: {{ item.slug }}
|
||||||
attrs:
|
attrs:
|
||||||
name: {{ item.slug }}
|
name: {{ item.slug }}
|
||||||
client_type: {{ item.client_type | default('confidential') }}
|
client_type: confidential
|
||||||
client_id: "{{ item.client_id }}"
|
client_id: !Env {{ item.client_id_env }}
|
||||||
{% if item.client_type | default('confidential') == 'confidential' %}
|
client_secret: !Env {{ item.client_secret_env }}
|
||||||
client_secret: "{{ item.client_secret }}"
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
{% for ru in item.redirect_uris %}
|
{% for ru in item.redirect_uris %}
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ entries:
|
||||||
|
|
||||||
internal_host: "{{ item.internal_host }}"
|
internal_host: "{{ item.internal_host }}"
|
||||||
external_host: "{{ item.external_host }}"
|
external_host: "{{ item.external_host }}"
|
||||||
{# Provider mode controls how authentik treats the proxy app:
|
|
||||||
- proxy : the outpost itself proxies traffic to internal_host
|
|
||||||
- forward_single : a single app behind an external reverse proxy
|
|
||||||
(traefik forwardauth talks to authentik per-domain)
|
|
||||||
- forward_domain : wildcard mode — one provider guards every host on a
|
|
||||||
cookie domain; configure forward_auth_mode=domain on
|
|
||||||
the outpost in that case. Default to forward_single
|
|
||||||
since that's the common ForwardAuth-with-traefik
|
|
||||||
pattern. #}
|
|
||||||
mode: {{ item.mode | default('forward_single') }}
|
|
||||||
|
|
||||||
{% if item.skip_path_regex is defined and item.skip_path_regex|length > 0 %}
|
{% if item.skip_path_regex is defined and item.skip_path_regex|length > 0 %}
|
||||||
skip_path_regex: |
|
skip_path_regex: |
|
||||||
|
|
@ -44,20 +34,3 @@ entries:
|
||||||
name: "{{ item.name | default(item.slug) }}"
|
name: "{{ item.name | default(item.slug) }}"
|
||||||
slug: {{ item.slug }}
|
slug: {{ item.slug }}
|
||||||
provider: !KeyOf proxy-provider-{{ item.slug }}
|
provider: !KeyOf proxy-provider-{{ item.slug }}
|
||||||
|
|
||||||
{% if item.allowed_groups is defined and item.allowed_groups | length > 0 %}
|
|
||||||
{# Restrict access to listed groups: one PolicyBinding per group, all bound
|
|
||||||
to the application. Authentik treats multiple bindings on the same target
|
|
||||||
as OR (a user matching any binding passes), and a request from a user in
|
|
||||||
none of the bound groups is denied. #}
|
|
||||||
{% for group_name in item.allowed_groups %}
|
|
||||||
- model: authentik_policies.policybinding
|
|
||||||
identifiers:
|
|
||||||
target: !KeyOf app-{{ item.slug }}
|
|
||||||
order: {{ loop.index0 }}
|
|
||||||
group: !Find [authentik_core.group, [name, "{{ group_name }}"]]
|
|
||||||
attrs:
|
|
||||||
enabled: true
|
|
||||||
negate: false
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ entries:
|
||||||
name: "{{ item.name | default('Microsoft Entra ID') }}"
|
name: "{{ item.name | default('Microsoft Entra ID') }}"
|
||||||
slug: {{ item.slug }}
|
slug: {{ item.slug }}
|
||||||
|
|
||||||
# Authentik's OAuth sources support vendor-specific types.
|
# Authentik’s OAuth sources support vendor-specific types.
|
||||||
# Entra guide calls it "Entra ID OAuth Source".
|
# Entra guide calls it “Entra ID OAuth Source”.
|
||||||
provider_type: entraid
|
provider_type: entraid
|
||||||
|
|
||||||
consumer_key: "{{ item.client_id }}"
|
consumer_key: !Env {{ item.client_id_env }}
|
||||||
consumer_secret: "{{ item.client_secret }}"
|
consumer_secret: !Env {{ item.client_secret_env }}
|
||||||
|
|
||||||
scopes:
|
scopes:
|
||||||
{% for s in (item.scopes | default(['openid','profile','email'])) %}
|
{% for s in (item.scopes | default(['openid','profile','email'])) %}
|
||||||
|
|
@ -28,10 +28,10 @@ entries:
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if (item.tenant_mode | default('single')) == 'single' %}
|
{% if (item.tenant_mode | default('single')) == 'single' %}
|
||||||
authorization_url: "https://login.microsoftonline.com/{{ item.tenant_id }}/oauth2/v2.0/authorize"
|
authorization_url: !Format ["https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", !Env {{ item.tenant_id_env }}]
|
||||||
access_token_url: "https://login.microsoftonline.com/{{ item.tenant_id }}/oauth2/v2.0/token"
|
access_token_url: !Format ["https://login.microsoftonline.com/%s/oauth2/v2.0/token", !Env {{ item.tenant_id_env }}]
|
||||||
profile_url: "https://graph.microsoft.com/v1.0/me"
|
profile_url: "https://graph.microsoft.com/v1.0/me"
|
||||||
oidc_jwks_url: "https://login.microsoftonline.com/{{ item.tenant_id }}/discovery/v2.0/keys"
|
oidc_jwks_url: !Format ["https://login.microsoftonline.com/%s/discovery/v2.0/keys", !Env {{ item.tenant_id_env }}]
|
||||||
{% else %}
|
{% else %}
|
||||||
authorization_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
authorization_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||||
access_token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
access_token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
|
||||||
version: 1
|
|
||||||
metadata:
|
|
||||||
name: "outpost-{{ authentik_ldap_outpost.name }}"
|
|
||||||
labels:
|
|
||||||
blueprints.goauthentik.io/instantiate: "true"
|
|
||||||
|
|
||||||
entries:
|
|
||||||
- model: authentik_outposts.outpost
|
|
||||||
identifiers:
|
|
||||||
name: "{{ authentik_ldap_outpost.name }}"
|
|
||||||
attrs:
|
|
||||||
name: "{{ authentik_ldap_outpost.name }}"
|
|
||||||
type: ldap
|
|
||||||
service_connection: null
|
|
||||||
|
|
||||||
providers:
|
|
||||||
{% for app in authentik_ldap_apps %}
|
|
||||||
- !Find [authentik_providers_ldap.ldapprovider, [name, {{ app.name }}]]
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if authentik_ldap_outpost.config is defined %}
|
|
||||||
config:
|
|
||||||
{% for k, v in authentik_ldap_outpost.config.items() %}
|
|
||||||
{{ k }}: {{ v | tojson }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
@ -35,6 +35,11 @@ services:
|
||||||
AUTHENTIK_POSTGRESQL__PASSWORD: {{ authentik_postgres_password }}
|
AUTHENTIK_POSTGRESQL__PASSWORD: {{ authentik_postgres_password }}
|
||||||
AUTHENTIK_LOG_LEVEL: {{ authentik_log_level }}
|
AUTHENTIK_LOG_LEVEL: {{ authentik_log_level }}
|
||||||
AUTHENTIK_ERROR_REPORTING__ENABLED: "{{ authentik_error_reporting_enabled | lower }}"
|
AUTHENTIK_ERROR_REPORTING__ENABLED: "{{ authentik_error_reporting_enabled | lower }}"
|
||||||
|
{% if authentik_blueprint_env|length > 0 %}
|
||||||
|
{% for k, v in authentik_blueprint_env.items() %}
|
||||||
|
{{ k }}: "{{ v }}"
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
volumes:
|
volumes:
|
||||||
- {{ authentik_docker_volume_dir }}/blueprints:/blueprints/custom
|
- {{ authentik_docker_volume_dir }}/blueprints:/blueprints/custom
|
||||||
- {{ authentik_docker_volume_dir }}/data:/data
|
- {{ authentik_docker_volume_dir }}/data:/data
|
||||||
|
|
@ -43,58 +48,19 @@ services:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
{{ authentik_backend_network }}: {}
|
- {{ authentik_backend_network }}
|
||||||
# No alias for the public FQDN here: that would shadow `/etc/hosts`
|
- {{ authentik_traefik_network }}
|
||||||
# pins (extra_hosts) in other containers sharing this network and
|
|
||||||
# break OIDC discovery for Node-based clients (c-ares-based
|
|
||||||
# resolvers consult Docker DNS before /etc/hosts). The URL-based
|
|
||||||
# service below addresses this container by its compose service
|
|
||||||
# name `server`, which Docker exposes as an alias on every network
|
|
||||||
# the container joins.
|
|
||||||
{{ authentik_traefik_network }}: {}
|
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ authentik_traefik_network }}
|
- traefik.docker.network={{ authentik_traefik_network }}
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.rule={% for d in authentik_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
- traefik.http.routers.{{ authentik_service_name }}.rule=Host(`{{ authentik_domain }}`)
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.service={{ authentik_service_name }}
|
|
||||||
{% if authentik_use_ssl %}
|
{% if authentik_use_ssl %}
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.tls=true
|
- traefik.http.routers.{{ authentik_service_name }}.tls=true
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- traefik.http.services.{{ authentik_service_name }}.loadbalancer.server.port={{ authentik_port }}
|
- traefik.http.services.{{ authentik_service_name }}.loadbalancer.server.port={{ authentik_port }}
|
||||||
{% if authentik_host_rewrite_domains | length > 0 %}
|
|
||||||
# Server-to-server entry: a separate service points at this very
|
|
||||||
# container by its compose service name `server` and disables
|
|
||||||
# passHostHeader so the upstream Host header becomes
|
|
||||||
# `{{ authentik_domains[0] }}`. Authentik builds OIDC issuer URLs
|
|
||||||
# from X-Forwarded-Host (not Host), so we also pin that header via
|
|
||||||
# middleware. Together this keeps the iss claim aligned with the
|
|
||||||
# public hostname browsers see during login, even when the request
|
|
||||||
# itself arrived on an internal *.int.* FQDN.
|
|
||||||
- traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.server.url=http://server:{{ authentik_port }}
|
|
||||||
- traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.passhostheader=false
|
|
||||||
- traefik.http.middlewares.{{ authentik_service_name }}-xfh-rewrite.headers.customrequestheaders.X-Forwarded-Host={{ authentik_domains[0] }}
|
|
||||||
{% for d in authentik_host_rewrite_domains %}
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.rule=Host(`{{ d }}`)
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.priority=100
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.service={{ authentik_service_name }}-rewrite
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.middlewares={{ authentik_service_name }}-xfh-rewrite
|
|
||||||
{% if authentik_use_ssl %}
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.entrypoints=websecure
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.tls=true
|
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.entrypoints=web
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
image: {{ authentik_image }}
|
image: {{ authentik_image }}
|
||||||
|
|
@ -109,6 +75,11 @@ services:
|
||||||
AUTHENTIK_POSTGRESQL__PASSWORD: {{ authentik_postgres_password }}
|
AUTHENTIK_POSTGRESQL__PASSWORD: {{ authentik_postgres_password }}
|
||||||
AUTHENTIK_LOG_LEVEL: {{ authentik_log_level }}
|
AUTHENTIK_LOG_LEVEL: {{ authentik_log_level }}
|
||||||
AUTHENTIK_ERROR_REPORTING__ENABLED: "{{ authentik_error_reporting_enabled | lower }}"
|
AUTHENTIK_ERROR_REPORTING__ENABLED: "{{ authentik_error_reporting_enabled | lower }}"
|
||||||
|
{% if authentik_blueprint_env|length > 0 %}
|
||||||
|
{% for k, v in authentik_blueprint_env.items() %}
|
||||||
|
{{ k }}: "{{ v }}"
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
volumes:
|
volumes:
|
||||||
- {{ authentik_docker_volume_dir }}/data:/data
|
- {{ authentik_docker_volume_dir }}/data:/data
|
||||||
- {{ authentik_docker_volume_dir }}/certs:/certs
|
- {{ authentik_docker_volume_dir }}/certs:/certs
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from authentik.outposts.models import Outpost
|
|
||||||
from authentik.core.models import Token
|
|
||||||
o = Outpost.objects.get(name='{{ authentik_ldap_outpost.name }}')
|
|
||||||
t = Token.objects.get(identifier=o.token_identifier)
|
|
||||||
if t.key != '{{ authentik_ldap_outpost.token }}':
|
|
||||||
t.key = '{{ authentik_ldap_outpost.token }}'
|
|
||||||
t.save(update_fields=['key'])
|
|
||||||
print('changed')
|
|
||||||
else:
|
|
||||||
print('ok')
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
from authentik.blueprints.models import BlueprintInstance
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
|
||||||
|
|
||||||
failed = list(BlueprintInstance.objects.filter(enabled=True, path__startswith="custom/").exclude(status="successful").order_by("path"))
|
|
||||||
if not failed:
|
|
||||||
print("ok")
|
|
||||||
else:
|
|
||||||
for bp in failed:
|
|
||||||
content = bp.retrieve()
|
|
||||||
importer = Importer.from_string(content)
|
|
||||||
valid, _ = importer.validate()
|
|
||||||
if valid:
|
|
||||||
importer.apply()
|
|
||||||
bp.status = "successful"
|
|
||||||
bp.save()
|
|
||||||
still_failed = BlueprintInstance.objects.filter(enabled=True, path__startswith="custom/").exclude(status="successful")
|
|
||||||
if still_failed.exists():
|
|
||||||
names = ", ".join(bp.name for bp in still_failed)
|
|
||||||
raise Exception(f"Blueprints still failing: {names}")
|
|
||||||
print("changed")
|
|
||||||
|
|
@ -1,44 +1,38 @@
|
||||||
# authentik_outpost_ldap
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
Deploys an [authentik](https://goauthentik.io) LDAP outpost via Docker
|
A brief description of the role goes here.
|
||||||
Compose. The outpost exposes an LDAP interface backed by authentik, so
|
|
||||||
applications that cannot speak OIDC (e.g. Nextcloud or OpenCloud LDAP
|
|
||||||
backends) can still authenticate against the central IdP.
|
|
||||||
|
|
||||||
The outpost connects back to an authentik server using an outpost token
|
Requirements
|
||||||
issued in the authentik admin interface. The image version must match
|
------------
|
||||||
the authentik server version.
|
|
||||||
|
|
||||||
## Requirements
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
|
|
||||||
- Docker and Docker Compose on the target host (e.g. via
|
Role Variables
|
||||||
`digitalboard.core.base`)
|
--------------
|
||||||
- Ansible collection: `community.docker`
|
|
||||||
|
|
||||||
## Role variables
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
|
|
||||||
| Variable | Default | Description |
|
Dependencies
|
||||||
| --- | --- | --- |
|
------------
|
||||||
| `authentik_outpost_ldap_image` | `ghcr.io/goauthentik/ldap:2026.2.2` | Outpost image (match the server version). |
|
|
||||||
| `authentik_outpost_ldap_host` | `https://authentik.local.test` | URL of the authentik server. |
|
|
||||||
| `authentik_outpost_ldap_token` | `changeme` | Outpost token — **override this**. |
|
|
||||||
| `authentik_outpost_ldap_insecure` | `"true"` | Skip TLS verification toward the authentik server. |
|
|
||||||
| `authentik_outpost_ldap_network` | `ldap` | Docker network LDAP clients connect over (created by the role). |
|
|
||||||
| `authentik_outpost_ldap_authentik_network` | _unset_ | Optional extra external network to the authentik server. |
|
|
||||||
| `authentik_outpost_ldap_extra_hosts` | `[]` | Extra `host:ip` entries for in-container DNS. |
|
|
||||||
|
|
||||||
## Example
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
|
|
||||||
```yaml
|
Example Playbook
|
||||||
- hosts: directory
|
----------------
|
||||||
become: true
|
|
||||||
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
|
|
||||||
|
- hosts: servers
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.authentik_outpost_ldap
|
- { role: username.rolename, x: 42 }
|
||||||
vars:
|
|
||||||
authentik_outpost_ldap_host: "https://auth.example.com"
|
|
||||||
authentik_outpost_ldap_token: "{{ vault_authentik_ldap_outpost_token }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# defaults file for authentik_outpost_ldap
|
# defaults file for authentik_outpost_ldap
|
||||||
|
|
||||||
# Base directory configuration (inherited from base role or defined here)
|
|
||||||
docker_compose_base_dir: /etc/docker/compose
|
|
||||||
docker_volume_base_dir: /srv/data
|
|
||||||
|
|
||||||
# Service configuration
|
|
||||||
authentik_outpost_ldap_service_name: authentik-outpost-ldap
|
|
||||||
authentik_outpost_ldap_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ authentik_outpost_ldap_service_name }}"
|
|
||||||
|
|
||||||
# Container image (must match authentik server version)
|
|
||||||
authentik_outpost_ldap_image: "ghcr.io/goauthentik/ldap:2026.2.2"
|
|
||||||
|
|
||||||
# Connection to authentik server
|
|
||||||
authentik_outpost_ldap_host: "https://authentik.local.test"
|
|
||||||
authentik_outpost_ldap_token: "changeme"
|
|
||||||
authentik_outpost_ldap_insecure: "true"
|
|
||||||
|
|
||||||
# Dedicated network for LDAP clients (nextcloud, opencloud, etc.)
|
|
||||||
authentik_outpost_ldap_network: "ldap"
|
|
||||||
|
|
||||||
# Extra hosts for DNS resolution within the container
|
|
||||||
authentik_outpost_ldap_extra_hosts: []
|
|
||||||
# - "authentik.local.test:192.168.56.11"
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy an authentik LDAP outpost via Docker Compose for applications that cannot use OIDC
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.2
|
||||||
- authentik
|
|
||||||
- ldap
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- outpost
|
# min_ansible_container_version:
|
||||||
- sso
|
|
||||||
- docker
|
galaxy_tags: []
|
||||||
- digitalboard
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# tasks file for authentik_outpost_ldap
|
# tasks file for authentik_outpost_ldap
|
||||||
|
|
||||||
- name: Create LDAP network
|
|
||||||
community.docker.docker_network:
|
|
||||||
name: "{{ authentik_outpost_ldap_network }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create docker compose directory
|
|
||||||
file:
|
|
||||||
path: "{{ authentik_outpost_ldap_docker_compose_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: '0755'
|
|
||||||
|
|
||||||
- name: Create docker-compose file for authentik LDAP outpost
|
|
||||||
template:
|
|
||||||
src: docker-compose.yml.j2
|
|
||||||
dest: "{{ authentik_outpost_ldap_docker_compose_dir }}/docker-compose.yml"
|
|
||||||
mode: '0644'
|
|
||||||
|
|
||||||
- name: Start authentik LDAP outpost container
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ authentik_outpost_ldap_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
wait: true
|
|
||||||
wait_timeout: 120
|
|
||||||
retries: 3
|
|
||||||
delay: 15
|
|
||||||
register: result
|
|
||||||
until: result is not failed
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
services:
|
|
||||||
ldap:
|
|
||||||
image: {{ authentik_outpost_ldap_image }}
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
AUTHENTIK_HOST: {{ authentik_outpost_ldap_host }}
|
|
||||||
AUTHENTIK_TOKEN: {{ authentik_outpost_ldap_token }}
|
|
||||||
AUTHENTIK_INSECURE: "{{ authentik_outpost_ldap_insecure }}"
|
|
||||||
{% if authentik_outpost_ldap_extra_hosts | length > 0 %}
|
|
||||||
extra_hosts:
|
|
||||||
{% for host in authentik_outpost_ldap_extra_hosts %}
|
|
||||||
- "{{ host }}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
networks:
|
|
||||||
- {{ authentik_outpost_ldap_network }}
|
|
||||||
{% if authentik_outpost_ldap_authentik_network is defined %}
|
|
||||||
- {{ authentik_outpost_ldap_authentik_network }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
networks:
|
|
||||||
{{ authentik_outpost_ldap_network }}:
|
|
||||||
external: true
|
|
||||||
{% if authentik_outpost_ldap_authentik_network is defined %}
|
|
||||||
{{ authentik_outpost_ldap_authentik_network }}:
|
|
||||||
external: true
|
|
||||||
{% endif %}
|
|
||||||
|
|
@ -1,45 +1,38 @@
|
||||||
# base
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
Host baseline for the Digitalboard platform. Installs Docker (engine,
|
A brief description of the role goes here.
|
||||||
CLI, containerd, buildx, compose plugin) and a small set of apt and
|
|
||||||
convenience packages on Debian/Ubuntu, and sets the shared directory
|
|
||||||
layout every other role builds on.
|
|
||||||
|
|
||||||
This role is intended to run first on every host, before any
|
Requirements
|
||||||
service role.
|
------------
|
||||||
|
|
||||||
## What it does
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
|
|
||||||
- Installs Docker prerequisites (`apt-transport-https`, `ca-certificates`,
|
Role Variables
|
||||||
`curl`, `gnupg`, `lsb-release`, `apache2-utils` for `htpasswd`) plus
|
--------------
|
||||||
convenience packages (`htop`, `ncdu`, `vim`) and Docker itself
|
|
||||||
(`docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`,
|
|
||||||
`docker-compose-plugin`).
|
|
||||||
- Optionally configures Docker registry mirrors via `/etc/docker/daemon.json`.
|
|
||||||
- Starts and enables the Docker service and writes a custom `/etc/motd`.
|
|
||||||
|
|
||||||
This role defines the shared directory-layout variables
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
(`docker_compose_base_dir`, `docker_volume_base_dir`) that every service
|
|
||||||
role consumes, but the per-service subdirectories are created by the
|
|
||||||
respective service roles, not here.
|
|
||||||
|
|
||||||
## Role variables
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
| Variable | Default | Description |
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
| --- | --- | --- |
|
|
||||||
| `docker_compose_base_dir` | `/etc/docker/compose` | Root directory for per-service Compose projects. |
|
|
||||||
| `docker_volume_base_dir` | `/srv/data` | Root directory for per-service persistent volumes. |
|
|
||||||
| `docker_registry_mirrors` | `[]` | Optional list of registry mirror URLs; empty disables mirrors. |
|
|
||||||
|
|
||||||
## Example
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
```yaml
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
- hosts: all
|
|
||||||
become: true
|
- hosts: servers
|
||||||
roles:
|
roles:
|
||||||
- digitalboard.core.base
|
- { role: username.rolename, x: 42 }
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Host baseline — install Docker, required apt packages and convenience tooling on Debian/Ubuntu
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.1
|
||||||
- base
|
|
||||||
- docker
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- bootstrap
|
# min_ansible_container_version:
|
||||||
- digitalboard
|
|
||||||
|
galaxy_tags: []
|
||||||
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
# Ansible Role: bookstack
|
|
||||||
|
|
||||||
Deploys [BookStack](https://www.bookstackapp.com/) as a self-contained Docker
|
|
||||||
Compose stack behind Traefik, with its own MariaDB container, OIDC SSO
|
|
||||||
(Entra ID by default) and a daily systemd-timer driven backup of database
|
|
||||||
and uploads.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Docker Engine + Compose plugin on the target host
|
|
||||||
- Traefik already running, with the external network referenced by
|
|
||||||
`bookstack_traefik_network` (default: `proxy`)
|
|
||||||
- `community.docker` collection on the controller
|
|
||||||
- DNS for `bookstack_domain` pointing at the Traefik host
|
|
||||||
|
|
||||||
## Required variables
|
|
||||||
|
|
||||||
The role asserts these are set; the play fails fast if any is empty:
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|---|---|
|
|
||||||
| `bookstack_db_root_password` | MariaDB root password |
|
|
||||||
| `bookstack_db_password` | MariaDB user password |
|
|
||||||
| `bookstack_admin_password` | Initial local admin password |
|
|
||||||
| `bookstack_oidc_client_id` | OIDC client ID (if OIDC on) |
|
|
||||||
| `bookstack_oidc_client_secret` | OIDC client secret (if OIDC on) |
|
|
||||||
|
|
||||||
When OIDC is on, the role also asserts that `bookstack_oidc_issuer`
|
|
||||||
resolves to a concrete URL. For Entra ID this means setting
|
|
||||||
`bookstack_entra_tenant_id` (the default issuer interpolates it; an unset
|
|
||||||
tenant leaves `//v2.0` and fails the assert). For other IdPs (Authentik,
|
|
||||||
Keycloak) set `bookstack_oidc_issuer` directly instead.
|
|
||||||
|
|
||||||
Provide via OpenBao lookup, Ansible Vault or `--extra-vars`. Never commit
|
|
||||||
real secrets.
|
|
||||||
|
|
||||||
## Optional variables
|
|
||||||
|
|
||||||
See `defaults/main.yml`. Frequently overridden:
|
|
||||||
|
|
||||||
- `bookstack_domain`, `bookstack_base_url`
|
|
||||||
- `bookstack_extra_domains` (extra Host-rule hostnames, e.g. an internal
|
|
||||||
`*.int.*` FQDN for a DMZ reverseproxy)
|
|
||||||
- `bookstack_extra_hosts` (container `/etc/hosts` overrides for
|
|
||||||
split-horizon IdP access; entries as `host:ip`)
|
|
||||||
- `bookstack_image`, `bookstack_db_image` (pin in production)
|
|
||||||
- `bookstack_oidc_enabled` (set `false` to disable OIDC entirely)
|
|
||||||
- `bookstack_oidc_auto_initiate` (`true` redirects straight to IdP)
|
|
||||||
- `bookstack_oidc_user_to_groups` (`true` syncs roles from Entra groups)
|
|
||||||
- `bookstack_backup_enabled`, `bookstack_backup_schedule`,
|
|
||||||
`bookstack_backup_retention_days`
|
|
||||||
|
|
||||||
## Entra ID app registration
|
|
||||||
|
|
||||||
1. Azure Portal → Entra ID → App registrations → New registration
|
|
||||||
2. Redirect URI (Web): `https://<bookstack_domain>/oidc/callback`
|
|
||||||
3. Front-channel logout URL: `https://<bookstack_domain>/logout`
|
|
||||||
4. Certificates & secrets → New client secret →
|
|
||||||
`bookstack_oidc_client_secret`
|
|
||||||
5. For group sync (`bookstack_oidc_user_to_groups: true`):
|
|
||||||
- Token configuration → Add groups claim → Security groups
|
|
||||||
- In BookStack, create roles whose **External Auth ID** equals the
|
|
||||||
Entra group Object ID, so the mapping resolves on first login.
|
|
||||||
|
|
||||||
## What the role does
|
|
||||||
|
|
||||||
| Phase | Action |
|
|
||||||
|---|---|
|
|
||||||
| Validate | `assert` all required secrets are set |
|
|
||||||
| Prepare | install packages, create volume dirs, generate persistent `APP_KEY`, verify Traefik network |
|
|
||||||
| Deploy | render `docker-compose.yml`, pull images, bring stack up |
|
|
||||||
| Configure | wait for the app, create the initial local admin via `php artisan bookstack:create-admin` (idempotent) |
|
|
||||||
| Backup | render `/usr/local/bin/bookstack-backup.sh` + systemd timer (daily 03:00, 14-day retention) |
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Deploy BookStack service
|
|
||||||
hosts: bookstack_servers
|
|
||||||
become: true
|
|
||||||
roles:
|
|
||||||
- digitalboard.core.bookstack
|
|
||||||
```
|
|
||||||
|
|
||||||
With inventory variables:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# group_vars/bookstack_servers.yml
|
|
||||||
bookstack_domain: wiki.digitalboard.ch
|
|
||||||
bookstack_base_url: "https://wiki.digitalboard.ch"
|
|
||||||
bookstack_entra_tenant_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.tenant_id }}"
|
|
||||||
bookstack_oidc_client_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.client_id }}"
|
|
||||||
bookstack_oidc_client_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.client_secret }}"
|
|
||||||
bookstack_db_root_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.db_root_password }}"
|
|
||||||
bookstack_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.db_password }}"
|
|
||||||
bookstack_admin_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.admin_password }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup / restore
|
|
||||||
|
|
||||||
Backups land in `{{ bookstack_backup_dir }}` (default
|
|
||||||
`/srv/data/bookstack/backup`) with three files per run:
|
|
||||||
|
|
||||||
- `bookstack-db-<stamp>.sql.gz` — mariadb-dump
|
|
||||||
- `bookstack-files-<stamp>.tar.gz` — uploads, attachments
|
|
||||||
- `bookstack-appkey-<stamp>.txt` — APP_KEY (required for restore!)
|
|
||||||
|
|
||||||
Manual trigger: `systemctl start bookstack-backup.service`
|
|
||||||
Timer status: `systemctl list-timers bookstack-backup.timer`
|
|
||||||
|
|
||||||
Restore procedure:
|
|
||||||
|
|
||||||
1. Stop the stack: `docker compose down` in `bookstack_docker_compose_dir`
|
|
||||||
2. Restore the APP_KEY: copy the `.txt` content to
|
|
||||||
`{{ bookstack_docker_volume_dir }}/.app_key` (the key MUST match or
|
|
||||||
encrypted DB values become unreadable)
|
|
||||||
3. Start only the DB container, then load the dump:
|
|
||||||
```bash
|
|
||||||
gunzip -c bookstack-db-<stamp>.sql.gz \
|
|
||||||
| docker exec -i bookstack-db \
|
|
||||||
mariadb -u root -p"<root-pw>" bookstack
|
|
||||||
```
|
|
||||||
4. Extract the files: `tar -xzf bookstack-files-<stamp>.tar.gz -C
|
|
||||||
{{ bookstack_appdata_dir }}/www/`
|
|
||||||
5. Bring the stack back up: `docker compose up -d`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- `bookstack_oidc_auto_initiate: false` (default) shows a login page
|
|
||||||
with an SSO button alongside the local login form. With `true`, users
|
|
||||||
go straight to the IdP — the local admin then has to use
|
|
||||||
`https://<domain>/login?email_login=1`.
|
|
||||||
- `bookstack_oidc_user_to_groups: true` only makes sense once BookStack
|
|
||||||
roles with the correct **External Auth IDs** (= Entra group Object
|
|
||||||
IDs) exist; otherwise users lose their role assignment on every login.
|
|
||||||
- Image tags default to pinned versions; bump them deliberately rather
|
|
||||||
than chasing `latest`.
|
|
||||||
- BookStack officially supports MySQL/MariaDB only — no PostgreSQL.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT-0
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# defaults file for bookstack
|
|
||||||
|
|
||||||
# Base directory configuration (inherited from base role or defined here)
|
|
||||||
docker_compose_base_dir: /etc/docker/compose
|
|
||||||
docker_volume_base_dir: /srv/data
|
|
||||||
|
|
||||||
# bookstack-specific configuration
|
|
||||||
bookstack_service_name: bookstack
|
|
||||||
bookstack_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ bookstack_service_name }}"
|
|
||||||
bookstack_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ bookstack_service_name }}"
|
|
||||||
bookstack_appdata_dir: "{{ bookstack_docker_volume_dir }}/appdata"
|
|
||||||
bookstack_db_data_dir: "{{ bookstack_docker_volume_dir }}/db"
|
|
||||||
bookstack_backup_dir: "{{ bookstack_docker_volume_dir }}/backup"
|
|
||||||
|
|
||||||
# Service configuration
|
|
||||||
bookstack_domain: "wiki.local.test"
|
|
||||||
# Additional hostnames the bookstack router answers on (e.g. an internal
|
|
||||||
# *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered
|
|
||||||
# by the cert).
|
|
||||||
bookstack_extra_domains: []
|
|
||||||
# Container-level /etc/hosts overrides — useful in split-horizon setups
|
|
||||||
# where the BookStack container needs to reach an IdP's public FQDN
|
|
||||||
# (used in the OIDC `iss` claim) over the LAN rather than via the DMZ.
|
|
||||||
bookstack_extra_hosts: []
|
|
||||||
bookstack_base_url: "https://{{ bookstack_domain }}"
|
|
||||||
|
|
||||||
# Images — pin via inventory in production
|
|
||||||
bookstack_image: "lscr.io/linuxserver/bookstack:version-v26.03.3"
|
|
||||||
bookstack_db_image: "lscr.io/linuxserver/mariadb:11.4.9"
|
|
||||||
|
|
||||||
# Traefik configuration
|
|
||||||
bookstack_traefik_network: "proxy"
|
|
||||||
bookstack_traefik_certresolver: "le"
|
|
||||||
|
|
||||||
# Timezone / UID
|
|
||||||
bookstack_tz: "Europe/Zurich"
|
|
||||||
bookstack_puid: "1000"
|
|
||||||
bookstack_pgid: "1000"
|
|
||||||
|
|
||||||
# Database configuration
|
|
||||||
bookstack_db_name: "bookstack"
|
|
||||||
bookstack_db_user: "bookstack"
|
|
||||||
|
|
||||||
# REQUIRED SECRETS — empty defaults force `assert` to fail until set.
|
|
||||||
# Provide via OpenBao lookup, Ansible Vault, or extra-vars.
|
|
||||||
# Never commit real secrets to version control.
|
|
||||||
#
|
|
||||||
# Generate with:
|
|
||||||
# bookstack_db_root_password: openssl rand -base64 32 | tr -d '/+='
|
|
||||||
# bookstack_db_password: openssl rand -base64 32 | tr -d '/+='
|
|
||||||
# bookstack_admin_password: openssl rand -base64 24 | tr -d '/+='
|
|
||||||
bookstack_db_root_password: ""
|
|
||||||
bookstack_db_password: ""
|
|
||||||
bookstack_admin_password: ""
|
|
||||||
bookstack_oidc_client_secret: ""
|
|
||||||
|
|
||||||
# APP_KEY is generated automatically on first run and persisted on the host.
|
|
||||||
# Set explicitly only if restoring an existing instance.
|
|
||||||
bookstack_app_key: ""
|
|
||||||
|
|
||||||
# Initial local admin (fallback account, lives alongside OIDC)
|
|
||||||
bookstack_admin_name: "Admin"
|
|
||||||
bookstack_admin_email: "admin@local.test"
|
|
||||||
bookstack_artisan_path: "/app/www/artisan"
|
|
||||||
|
|
||||||
# Mail configuration
|
|
||||||
bookstack_mail_driver: "smtp"
|
|
||||||
bookstack_mail_host: "smtp.local.test"
|
|
||||||
bookstack_mail_port: 587
|
|
||||||
bookstack_mail_encryption: "tls"
|
|
||||||
bookstack_mail_from: "bookstack@local.test"
|
|
||||||
bookstack_mail_from_name: "BookStack"
|
|
||||||
bookstack_mail_username: ""
|
|
||||||
bookstack_mail_password: ""
|
|
||||||
|
|
||||||
# OIDC configuration (Entra ID by default; override `bookstack_oidc_issuer`
|
|
||||||
# for Keycloak or any other provider)
|
|
||||||
bookstack_oidc_enabled: false
|
|
||||||
bookstack_oidc_name: "SSO"
|
|
||||||
bookstack_entra_tenant_id: ""
|
|
||||||
bookstack_oidc_issuer: "https://login.microsoftonline.com/{{ bookstack_entra_tenant_id }}/v2.0"
|
|
||||||
bookstack_oidc_client_id: ""
|
|
||||||
bookstack_oidc_auto_initiate: false
|
|
||||||
bookstack_oidc_user_to_groups: false
|
|
||||||
bookstack_oidc_groups_claim: "groups"
|
|
||||||
bookstack_oidc_additional_scopes: "openid profile email"
|
|
||||||
|
|
||||||
# Backup configuration
|
|
||||||
bookstack_backup_enabled: true
|
|
||||||
bookstack_backup_retention_days: 14
|
|
||||||
bookstack_backup_schedule: "*-*-* 03:00:00"
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# handlers file for bookstack
|
|
||||||
|
|
||||||
- name: stop bookstack
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: stopped
|
|
||||||
listen: restart bookstack
|
|
||||||
|
|
||||||
- name: start bookstack
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
listen: restart bookstack
|
|
||||||
|
|
||||||
- name: reload systemd
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
daemon_reload: true
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy BookStack (LSIO image + MariaDB) via Docker Compose.
|
|
||||||
description:
|
|
||||||
- Renders a Compose stack for the linuxserver.io BookStack image
|
|
||||||
with a sibling MariaDB container behind Traefik, then bootstraps
|
|
||||||
the initial admin user via C(php artisan bookstack:create-admin)
|
|
||||||
and optionally enables OIDC SSO (Entra ID by default).
|
|
||||||
- "Persists the Laravel C(APP_KEY) on the host so the same key is
|
|
||||||
re-used across deploys (a fresh key would orphan all encrypted
|
|
||||||
database values: 2FA secrets, API tokens, OIDC client_secret)."
|
|
||||||
- Ships an optional systemd timer that backs up the database dump,
|
|
||||||
uploads tarball and APP_KEY daily with configurable retention.
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
docker_volume_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /srv/data
|
|
||||||
bookstack_service_name:
|
|
||||||
type: str
|
|
||||||
default: bookstack
|
|
||||||
bookstack_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_docker_volume_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_appdata_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_db_data_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_backup_dir:
|
|
||||||
type: path
|
|
||||||
|
|
||||||
bookstack_domain:
|
|
||||||
type: str
|
|
||||||
default: wiki.local.test
|
|
||||||
description: Hostname used in the Traefik Host rule.
|
|
||||||
bookstack_extra_domains:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Additional hostnames the Traefik router answers on, OR-combined
|
|
||||||
with C(bookstack_domain). Useful for an internal C(*.int.*) FQDN
|
|
||||||
so a DMZ reverseproxy can reach a backend hostname covered by the
|
|
||||||
cert.
|
|
||||||
bookstack_extra_hosts:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Container-level C(/etc/hosts) overrides (Compose C(extra_hosts)
|
|
||||||
entries, C("host:ip")). Useful in split-horizon setups where the
|
|
||||||
BookStack container must reach an IdP's public FQDN (used in the
|
|
||||||
OIDC C(iss) claim) over the LAN rather than via the DMZ.
|
|
||||||
bookstack_base_url:
|
|
||||||
type: str
|
|
||||||
description: Defaults to C("https://{{ bookstack_domain }}").
|
|
||||||
|
|
||||||
bookstack_image:
|
|
||||||
type: str
|
|
||||||
default: "lscr.io/linuxserver/bookstack:version-v26.03.3"
|
|
||||||
bookstack_db_image:
|
|
||||||
type: str
|
|
||||||
default: "lscr.io/linuxserver/mariadb:11.4.9"
|
|
||||||
|
|
||||||
bookstack_traefik_network:
|
|
||||||
type: str
|
|
||||||
default: proxy
|
|
||||||
bookstack_traefik_certresolver:
|
|
||||||
type: str
|
|
||||||
default: le
|
|
||||||
|
|
||||||
bookstack_tz:
|
|
||||||
type: str
|
|
||||||
default: Europe/Zurich
|
|
||||||
bookstack_puid:
|
|
||||||
type: str
|
|
||||||
default: "1000"
|
|
||||||
bookstack_pgid:
|
|
||||||
type: str
|
|
||||||
default: "1000"
|
|
||||||
|
|
||||||
bookstack_db_name:
|
|
||||||
type: str
|
|
||||||
default: bookstack
|
|
||||||
bookstack_db_user:
|
|
||||||
type: str
|
|
||||||
default: bookstack
|
|
||||||
bookstack_db_root_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: MariaDB C(root) password. Override per-inventory.
|
|
||||||
bookstack_db_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: MariaDB C(bookstack_db_user) password. Override per-inventory.
|
|
||||||
|
|
||||||
bookstack_admin_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description:
|
|
||||||
- Password for the local admin user that the role creates via
|
|
||||||
C(bookstack:create-admin). Lives alongside any OIDC users.
|
|
||||||
|
|
||||||
bookstack_app_key:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
- When empty the role generates a persistent C(APP_KEY) on first
|
|
||||||
run and stores it under C({{ bookstack_docker_volume_dir }}/.app_key).
|
|
||||||
Override only when restoring an existing instance — a mismatching
|
|
||||||
key orphans all encrypted database values.
|
|
||||||
|
|
||||||
bookstack_admin_name:
|
|
||||||
type: str
|
|
||||||
default: Admin
|
|
||||||
bookstack_admin_email:
|
|
||||||
type: str
|
|
||||||
default: admin@local.test
|
|
||||||
bookstack_artisan_path:
|
|
||||||
type: path
|
|
||||||
default: /app/www/artisan
|
|
||||||
description:
|
|
||||||
- Path to BookStack's C(artisan) script inside the container. The
|
|
||||||
LSIO image's C(WORKDIR) is not the app directory, so this must
|
|
||||||
be absolute.
|
|
||||||
|
|
||||||
bookstack_mail_driver:
|
|
||||||
type: str
|
|
||||||
choices: [smtp, log, sendmail, mailgun, ses, postmark]
|
|
||||||
default: smtp
|
|
||||||
bookstack_mail_host:
|
|
||||||
type: str
|
|
||||||
default: smtp.local.test
|
|
||||||
bookstack_mail_port:
|
|
||||||
type: int
|
|
||||||
default: 587
|
|
||||||
bookstack_mail_encryption:
|
|
||||||
type: str
|
|
||||||
choices: [tls, ssl, '']
|
|
||||||
default: tls
|
|
||||||
bookstack_mail_from:
|
|
||||||
type: str
|
|
||||||
default: bookstack@local.test
|
|
||||||
bookstack_mail_from_name:
|
|
||||||
type: str
|
|
||||||
default: BookStack
|
|
||||||
bookstack_mail_username:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
bookstack_mail_password:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
bookstack_oidc_enabled:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
bookstack_oidc_name:
|
|
||||||
type: str
|
|
||||||
default: SSO
|
|
||||||
description: Display name of the SSO button on the login page.
|
|
||||||
bookstack_entra_tenant_id:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Entra tenant UUID. Required when C(bookstack_oidc_enabled=true).
|
|
||||||
bookstack_oidc_issuer:
|
|
||||||
type: str
|
|
||||||
description:
|
|
||||||
- OIDC issuer URL. Defaults to the Entra v2 issuer template
|
|
||||||
built from C(bookstack_entra_tenant_id). Override for
|
|
||||||
Keycloak or any other provider.
|
|
||||||
bookstack_oidc_client_id:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Required when C(bookstack_oidc_enabled=true).
|
|
||||||
bookstack_oidc_client_secret:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Required when C(bookstack_oidc_enabled=true).
|
|
||||||
bookstack_oidc_auto_initiate:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description:
|
|
||||||
- When true users are redirected straight to the IdP and the
|
|
||||||
local login is reachable only via C(?email_login=1).
|
|
||||||
bookstack_oidc_user_to_groups:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description:
|
|
||||||
- When true BookStack syncs roles from the IdP groups claim
|
|
||||||
on every login. Requires BookStack roles whose
|
|
||||||
C(External Auth ID) matches the IdP group's Object ID.
|
|
||||||
bookstack_oidc_groups_claim:
|
|
||||||
type: str
|
|
||||||
default: groups
|
|
||||||
bookstack_oidc_additional_scopes:
|
|
||||||
type: str
|
|
||||||
default: openid profile email
|
|
||||||
|
|
||||||
bookstack_backup_enabled:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
bookstack_backup_retention_days:
|
|
||||||
type: int
|
|
||||||
default: 14
|
|
||||||
bookstack_backup_schedule:
|
|
||||||
type: str
|
|
||||||
default: "*-*-* 03:00:00"
|
|
||||||
description: systemd C(OnCalendar) expression for the backup timer.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
galaxy_info:
|
|
||||||
author: digitalboard
|
|
||||||
description: Deploy BookStack as a self-contained Docker Compose stack behind Traefik
|
|
||||||
company: digitalboard
|
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
|
||||||
|
|
||||||
platforms:
|
|
||||||
- name: Debian
|
|
||||||
versions:
|
|
||||||
- bookworm
|
|
||||||
- name: Ubuntu
|
|
||||||
versions:
|
|
||||||
- jammy
|
|
||||||
- noble
|
|
||||||
|
|
||||||
galaxy_tags:
|
|
||||||
- docker
|
|
||||||
- bookstack
|
|
||||||
- wiki
|
|
||||||
- documentation
|
|
||||||
- digitalboard
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# tasks file for bookstack
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 1. VALIDATE REQUIRED SECRETS
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Assert required secrets are set
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- bookstack_db_root_password | length > 0
|
|
||||||
- bookstack_db_password | length > 0
|
|
||||||
- bookstack_admin_password | length > 0
|
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_id | length > 0)
|
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_secret | length > 0)
|
|
||||||
# Issuer URL must resolve to something concrete. The Entra default
|
|
||||||
# interpolates bookstack_entra_tenant_id; an unset tenant leaves
|
|
||||||
# "//v2.0" in the URL. Allow non-Entra IdPs (Authentik, Keycloak)
|
|
||||||
# that override bookstack_oidc_issuer directly.
|
|
||||||
- (not bookstack_oidc_enabled) or
|
|
||||||
(bookstack_oidc_issuer | length > 0 and
|
|
||||||
'//v2.0' not in bookstack_oidc_issuer)
|
|
||||||
fail_msg: >-
|
|
||||||
One or more required secrets are unset. Provide them via OpenBao
|
|
||||||
lookup, Ansible Vault or --extra-vars. See README for the full list.
|
|
||||||
quiet: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 2. PREPARATION: Packages, directories, APP_KEY
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Ensure required packages are installed
|
|
||||||
ansible.builtin.package:
|
|
||||||
name:
|
|
||||||
- python3-docker
|
|
||||||
- python3-requests
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create docker compose directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: '0755'
|
|
||||||
|
|
||||||
- name: Create BookStack data directories
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ bookstack_puid }}"
|
|
||||||
group: "{{ bookstack_pgid }}"
|
|
||||||
mode: '0755'
|
|
||||||
loop:
|
|
||||||
- "{{ bookstack_docker_volume_dir }}"
|
|
||||||
- "{{ bookstack_appdata_dir }}"
|
|
||||||
- "{{ bookstack_db_data_dir }}"
|
|
||||||
- "{{ bookstack_backup_dir }}"
|
|
||||||
|
|
||||||
- name: Verify Traefik network exists
|
|
||||||
community.docker.docker_network_info:
|
|
||||||
name: "{{ bookstack_traefik_network }}"
|
|
||||||
register: _traefik_net
|
|
||||||
failed_when: not _traefik_net.exists
|
|
||||||
|
|
||||||
- name: Check whether APP_KEY has been generated before
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
register: _app_key_file
|
|
||||||
|
|
||||||
- name: Generate persistent APP_KEY on first run
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
set -o pipefail
|
|
||||||
umask 077
|
|
||||||
echo "base64:$(openssl rand -base64 32)" > {{ bookstack_docker_volume_dir }}/.app_key
|
|
||||||
args:
|
|
||||||
executable: /bin/bash
|
|
||||||
creates: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
when:
|
|
||||||
- not _app_key_file.stat.exists
|
|
||||||
- bookstack_app_key | length == 0
|
|
||||||
|
|
||||||
- name: Write inventory-provided APP_KEY
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ bookstack_app_key }}\n"
|
|
||||||
dest: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
mode: '0600'
|
|
||||||
when:
|
|
||||||
- not _app_key_file.stat.exists
|
|
||||||
- bookstack_app_key | length > 0
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Read APP_KEY back into a fact
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
register: _app_key_slurp
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Register APP_KEY fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
bookstack_resolved_app_key: "{{ _app_key_slurp.content | b64decode | trim }}"
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 3. DEPLOY: Render compose, bring stack up
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Render docker-compose.yml for BookStack
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: docker-compose.yml.j2
|
|
||||||
dest: "{{ bookstack_docker_compose_dir }}/docker-compose.yml"
|
|
||||||
mode: '0640'
|
|
||||||
notify: restart bookstack
|
|
||||||
|
|
||||||
- name: Start BookStack containers
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
pull: always
|
|
||||||
wait: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 4. CONFIGURE: Wait for app and seed initial admin user
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Wait for BookStack to be ready
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: docker exec {{ bookstack_service_name }} curl -sf -o /dev/null -w "%{http_code}" http://localhost/login
|
|
||||||
register: _bookstack_health
|
|
||||||
retries: 30
|
|
||||||
delay: 5
|
|
||||||
until: _bookstack_health.stdout == "200"
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Wait for BookStack migrations to be complete
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ bookstack_service_name }}-db"
|
|
||||||
argv:
|
|
||||||
- mariadb
|
|
||||||
- --protocol=tcp
|
|
||||||
- -h
|
|
||||||
- 127.0.0.1
|
|
||||||
- -u
|
|
||||||
- "{{ bookstack_db_user }}"
|
|
||||||
- "-p{{ bookstack_db_password }}"
|
|
||||||
- "{{ bookstack_db_name }}"
|
|
||||||
- -Nse
|
|
||||||
- "SHOW TABLES LIKE 'users';"
|
|
||||||
register: _users_table
|
|
||||||
retries: 30
|
|
||||||
delay: 5
|
|
||||||
until: _users_table.stdout | trim == 'users'
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Check whether the initial admin already exists
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ bookstack_service_name }}-db"
|
|
||||||
argv:
|
|
||||||
- mariadb
|
|
||||||
- --protocol=tcp
|
|
||||||
- -h
|
|
||||||
- 127.0.0.1
|
|
||||||
- -u
|
|
||||||
- "{{ bookstack_db_user }}"
|
|
||||||
- "-p{{ bookstack_db_password }}"
|
|
||||||
- "{{ bookstack_db_name }}"
|
|
||||||
- -Nse
|
|
||||||
- "SELECT COUNT(*) FROM users WHERE email = '{{ bookstack_admin_email }}';"
|
|
||||||
register: _admin_exists
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Create initial admin user
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ bookstack_service_name }}"
|
|
||||||
argv:
|
|
||||||
- php
|
|
||||||
- "{{ bookstack_artisan_path }}"
|
|
||||||
- bookstack:create-admin
|
|
||||||
- "--email={{ bookstack_admin_email }}"
|
|
||||||
- "--name={{ bookstack_admin_name }}"
|
|
||||||
- "--password={{ bookstack_admin_password }}"
|
|
||||||
when: (_admin_exists.stdout | trim | int) == 0
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 5. BACKUP: systemd timer for daily DB + uploads dump
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Render backup script
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: backup.sh.j2
|
|
||||||
dest: /usr/local/bin/bookstack-backup.sh
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: '0750'
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
|
|
||||||
- name: Render backup systemd service
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: bookstack-backup.service.j2
|
|
||||||
dest: /etc/systemd/system/bookstack-backup.service
|
|
||||||
mode: '0644'
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
notify: reload systemd
|
|
||||||
|
|
||||||
- name: Render backup systemd timer
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: bookstack-backup.timer.j2
|
|
||||||
dest: /etc/systemd/system/bookstack-backup.timer
|
|
||||||
mode: '0644'
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
notify: reload systemd
|
|
||||||
|
|
||||||
- name: Enable and start backup timer
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: bookstack-backup.timer
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
|
|
||||||
- name: Disable backup timer when feature is off
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: bookstack-backup.timer
|
|
||||||
enabled: false
|
|
||||||
state: stopped
|
|
||||||
when: not (bookstack_backup_enabled | bool)
|
|
||||||
failed_when: false
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# {{ ansible_managed }}
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BACKUP_DIR="{{ bookstack_backup_dir }}"
|
|
||||||
RETENTION_DAYS={{ bookstack_backup_retention_days }}
|
|
||||||
APPDATA_DIR="{{ bookstack_appdata_dir }}"
|
|
||||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
|
||||||
|
|
||||||
# --- DB dump (mariadb-dump from inside the DB container) ---
|
|
||||||
# Use the app user via TCP because root@localhost is unix_socket-auth only
|
|
||||||
# in the LSIO MariaDB image and root@% does not exist.
|
|
||||||
docker exec {{ bookstack_service_name }}-db \
|
|
||||||
mariadb-dump \
|
|
||||||
--protocol=tcp -h 127.0.0.1 \
|
|
||||||
-u "{{ bookstack_db_user }}" -p"{{ bookstack_db_password }}" \
|
|
||||||
--single-transaction --routines --triggers --quick \
|
|
||||||
"{{ bookstack_db_name }}" \
|
|
||||||
| gzip -9 > "$BACKUP_DIR/bookstack-db-$STAMP.sql.gz"
|
|
||||||
|
|
||||||
# --- File uploads (images, attachments) ---
|
|
||||||
# LSIO BookStack stores user uploads under /config/www/{uploads,storage/uploads,files}.
|
|
||||||
tar --warning=no-file-changed \
|
|
||||||
-czf "$BACKUP_DIR/bookstack-files-$STAMP.tar.gz" \
|
|
||||||
-C "$APPDATA_DIR/www" \
|
|
||||||
uploads storage/uploads files 2>/dev/null || true
|
|
||||||
|
|
||||||
# --- APP_KEY backup (critical for restore!) ---
|
|
||||||
install -m 0600 "{{ bookstack_docker_volume_dir }}/.app_key" \
|
|
||||||
"$BACKUP_DIR/bookstack-appkey-$STAMP.txt"
|
|
||||||
|
|
||||||
# --- Retention ---
|
|
||||||
find "$BACKUP_DIR" -type f \
|
|
||||||
\( -name 'bookstack-db-*.sql.gz' \
|
|
||||||
-o -name 'bookstack-files-*.tar.gz' \
|
|
||||||
-o -name 'bookstack-appkey-*.txt' \) \
|
|
||||||
-mtime +"$RETENTION_DAYS" -delete
|
|
||||||
|
|
||||||
echo "Backup complete: $STAMP"
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
[Unit]
|
|
||||||
Description=BookStack backup (DB + uploads)
|
|
||||||
Requires=docker.service
|
|
||||||
After=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/bookstack-backup.sh
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=best-effort
|
|
||||||
IOSchedulingPriority=7
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
[Unit]
|
|
||||||
Description=Daily BookStack backup
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar={{ bookstack_backup_schedule }}
|
|
||||||
Persistent=true
|
|
||||||
RandomizedDelaySec=300
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
#---------------------------------------------------------------------#
|
|
||||||
# BookStack - Self-hosted wiki / knowledge base. #
|
|
||||||
#---------------------------------------------------------------------#
|
|
||||||
---
|
|
||||||
services:
|
|
||||||
{{ bookstack_service_name }}:
|
|
||||||
image: {{ bookstack_image }}
|
|
||||||
container_name: {{ bookstack_service_name }}
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PUID: "{{ bookstack_puid }}"
|
|
||||||
PGID: "{{ bookstack_pgid }}"
|
|
||||||
TZ: "{{ bookstack_tz }}"
|
|
||||||
APP_URL: "{{ bookstack_base_url }}"
|
|
||||||
APP_KEY: "{{ bookstack_resolved_app_key }}"
|
|
||||||
DB_HOST: "{{ bookstack_service_name }}-db"
|
|
||||||
DB_PORT: "3306"
|
|
||||||
DB_DATABASE: "{{ bookstack_db_name }}"
|
|
||||||
DB_USERNAME: "{{ bookstack_db_user }}"
|
|
||||||
DB_PASSWORD: "{{ bookstack_db_password }}"
|
|
||||||
MAIL_DRIVER: "{{ bookstack_mail_driver }}"
|
|
||||||
MAIL_HOST: "{{ bookstack_mail_host }}"
|
|
||||||
MAIL_PORT: "{{ bookstack_mail_port }}"
|
|
||||||
MAIL_USERNAME: "{{ bookstack_mail_username }}"
|
|
||||||
MAIL_PASSWORD: "{{ bookstack_mail_password }}"
|
|
||||||
MAIL_ENCRYPTION: "{{ bookstack_mail_encryption }}"
|
|
||||||
MAIL_FROM: "{{ bookstack_mail_from }}"
|
|
||||||
MAIL_FROM_NAME: "{{ bookstack_mail_from_name }}"
|
|
||||||
{% if bookstack_oidc_enabled %}
|
|
||||||
AUTH_METHOD: "oidc"
|
|
||||||
AUTH_AUTO_INITIATE: "{{ bookstack_oidc_auto_initiate | string | lower }}"
|
|
||||||
OIDC_NAME: "{{ bookstack_oidc_name }}"
|
|
||||||
OIDC_DISPLAY_NAME_CLAIMS: "name"
|
|
||||||
OIDC_CLIENT_ID: "{{ bookstack_oidc_client_id }}"
|
|
||||||
OIDC_CLIENT_SECRET: "{{ bookstack_oidc_client_secret }}"
|
|
||||||
OIDC_ISSUER: "{{ bookstack_oidc_issuer }}"
|
|
||||||
OIDC_ISSUER_DISCOVER: "true"
|
|
||||||
OIDC_END_SESSION_ENDPOINT: "true"
|
|
||||||
OIDC_ADDITIONAL_SCOPES: "{{ bookstack_oidc_additional_scopes }}"
|
|
||||||
OIDC_USER_TO_GROUPS: "{{ bookstack_oidc_user_to_groups | string | lower }}"
|
|
||||||
OIDC_GROUPS_CLAIM: "{{ bookstack_oidc_groups_claim }}"
|
|
||||||
{% endif %}
|
|
||||||
volumes:
|
|
||||||
- {{ bookstack_appdata_dir }}:/config
|
|
||||||
networks:
|
|
||||||
- {{ bookstack_traefik_network }}
|
|
||||||
- internal
|
|
||||||
{% if bookstack_extra_hosts | length > 0 %}
|
|
||||||
extra_hosts:
|
|
||||||
{% for host in bookstack_extra_hosts %}
|
|
||||||
- "{{ host }}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
depends_on:
|
|
||||||
{{ bookstack_service_name }}-db:
|
|
||||||
condition: service_healthy
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ bookstack_traefik_network }}"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.rule={% set _all_domains = [bookstack_domain] + (bookstack_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.tls=true"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.tls.certresolver={{ bookstack_traefik_certresolver }}"
|
|
||||||
- "traefik.http.services.{{ bookstack_service_name }}.loadbalancer.server.port=80"
|
|
||||||
|
|
||||||
{{ bookstack_service_name }}-db:
|
|
||||||
image: {{ bookstack_db_image }}
|
|
||||||
container_name: {{ bookstack_service_name }}-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PUID: "{{ bookstack_puid }}"
|
|
||||||
PGID: "{{ bookstack_pgid }}"
|
|
||||||
TZ: "{{ bookstack_tz }}"
|
|
||||||
MYSQL_ROOT_PASSWORD: "{{ bookstack_db_root_password }}"
|
|
||||||
MYSQL_DATABASE: "{{ bookstack_db_name }}"
|
|
||||||
MYSQL_USER: "{{ bookstack_db_user }}"
|
|
||||||
MYSQL_PASSWORD: "{{ bookstack_db_password }}"
|
|
||||||
volumes:
|
|
||||||
- {{ bookstack_db_data_dir }}:/config
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u root --password=\"$$MYSQL_ROOT_PASSWORD\" --silent"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 12
|
|
||||||
start_period: 60s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
{{ bookstack_traefik_network }}:
|
|
||||||
external: true
|
|
||||||
internal:
|
|
||||||
driver: bridge
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
localhost
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
- hosts: localhost
|
|
||||||
remote_user: root
|
|
||||||
roles:
|
|
||||||
- bookstack
|
|
||||||
|
|
@ -1,42 +1,38 @@
|
||||||
# collabora
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
Deploys [Collabora Online](https://www.collaboraonline.com/) (CODE,
|
A brief description of the role goes here.
|
||||||
`collabora/code`) via Docker Compose behind Traefik. Collabora is the
|
|
||||||
WOPI backend that renders office documents for Nextcloud and OpenCloud.
|
|
||||||
|
|
||||||
The role templates `coolwsd.xml` to declare which WOPI hosts may call
|
Requirements
|
||||||
Collabora and which origins may embed it in an iframe.
|
------------
|
||||||
|
|
||||||
## Role variables
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
|
|
||||||
| Variable | Default | Description |
|
Role Variables
|
||||||
| --- | --- | --- |
|
--------------
|
||||||
| `collabora_domains` | `[office.local.test]` | FQDNs the router accepts; first is canonical. |
|
|
||||||
| `collabora_image` | `collabora/code:latest` | Container image. |
|
|
||||||
| `collabora_port` | `9980` | Container port Traefik forwards to. |
|
|
||||||
| `collabora_traefik_network` | `proxy` | Docker network shared with Traefik. |
|
|
||||||
| `collabora_use_ssl` | `true` | Enable the TLS resolver on the router. |
|
|
||||||
| `collabora_ssl_verification` | `true` | Verify TLS on WOPI callbacks (false for self-signed). |
|
|
||||||
| `collabora_allowed_domains` | `[nextcloud.local.test]` | WOPI hosts allowed to call Collabora (regex). |
|
|
||||||
| `collabora_frame_ancestors` | `[nextcloud.local.test]` | Origins allowed to embed Collabora in an iframe. |
|
|
||||||
| `collabora_extra_hosts` | `[]` | Extra `host:ip` entries for in-container DNS. |
|
|
||||||
|
|
||||||
## Example
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
|
|
||||||
```yaml
|
Dependencies
|
||||||
- hosts: services
|
------------
|
||||||
become: true
|
|
||||||
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
|
|
||||||
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
|
|
||||||
|
- hosts: servers
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.collabora
|
- { role: username.rolename, x: 42 }
|
||||||
vars:
|
|
||||||
collabora_domains:
|
|
||||||
- "office.example.com"
|
|
||||||
collabora_allowed_domains:
|
|
||||||
- "cloud.example.com"
|
|
||||||
collabora_frame_ancestors:
|
|
||||||
- "cloud.example.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ collabora_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ collabora_servic
|
||||||
collabora_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ collabora_service_name }}"
|
collabora_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ collabora_service_name }}"
|
||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
# FQDNs the collabora router accepts. The first entry is the canonical
|
collabora_domain: "office.local.test"
|
||||||
# domain; further entries cover internal *.int.* names used for
|
|
||||||
# server-to-server WOPI discovery.
|
|
||||||
collabora_domains:
|
|
||||||
- "office.local.test"
|
|
||||||
collabora_image: "collabora/code:latest"
|
collabora_image: "collabora/code:latest"
|
||||||
collabora_port: 9980
|
collabora_port: 9980
|
||||||
collabora_extra_hosts: []
|
collabora_extra_hosts: []
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@
|
||||||
- name: restart collabora
|
- name: restart collabora
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ collabora_docker_compose_dir }}"
|
project_src: "{{ collabora_docker_compose_dir }}"
|
||||||
state: present
|
state: restarted
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy Collabora Online (CODE) as a WOPI backend via Docker Compose behind Traefik
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.2
|
||||||
- collabora
|
|
||||||
- office
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- wopi
|
# min_ansible_container_version:
|
||||||
- nextcloud
|
|
||||||
- docker
|
galaxy_tags: []
|
||||||
- digitalboard
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,11 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ collabora_traefik_network }}
|
- traefik.docker.network={{ collabora_traefik_network }}
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.rule={% for d in collabora_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
- traefik.http.routers.{{ collabora_service_name }}.rule=Host(`{{ collabora_domain }}`)
|
||||||
- traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }}
|
- traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }}
|
||||||
{% if collabora_use_ssl %}
|
{% if collabora_use_ssl %}
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.tls=true
|
- traefik.http.routers.{{ collabora_service_name }}.tls=true
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# coturn
|
|
||||||
|
|
||||||
Deploys a [coturn](https://github.com/coturn/coturn) TURN/STUN server with `network_mode: host`,
|
|
||||||
optionally accompanied by an `acme.sh` sidecar that obtains and renews a public TLS certificate
|
|
||||||
via RFC2136 (`nsupdate`) and restarts coturn on renewal.
|
|
||||||
|
|
||||||
This is the recommended pairing for `digitalboard.core.talk` (Nextcloud Talk HPB).
|
|
||||||
|
|
||||||
## What it does
|
|
||||||
|
|
||||||
- Renders `/etc/docker/compose/coturn/docker-compose.yml`
|
|
||||||
- (acme mode) Deploys the TSIG key from `playbooks/secrets/{{ inventory_hostname }}/nsupdate.key`
|
|
||||||
- (selfsigned mode) Generates an ECC keypair + selfsigned cert in `{{ coturn_cert_dir }}`
|
|
||||||
- Starts the stack via `community.docker.docker_compose_v2`
|
|
||||||
|
|
||||||
## Required variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|---|---|
|
|
||||||
| `coturn_realm` | Public DNS name used as realm + cert CN (e.g. `stun.digitalboard.ch`) |
|
|
||||||
| `coturn_external_ip` | Mapping for `--external-ip`, format `PUBLIC[/PRIVATE]` |
|
|
||||||
| `coturn_static_auth_secret` | Shared secret for HMAC-based credentials; **must match** `talk_turn_secret` on the HPB host |
|
|
||||||
|
|
||||||
## Important variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `coturn_cert_mode` | `file` | One of `acme`, `file`, `selfsigned` |
|
|
||||||
| `coturn_listening_port` | `443` | TCP/UDP non-TLS port |
|
|
||||||
| `coturn_tls_listening_port` | `443` | TLS port (shared with non-TLS via STUN mux) |
|
|
||||||
| `coturn_min_relay_port` / `coturn_max_relay_port` | `49160` / `49200` | UDP relay range |
|
|
||||||
| `coturn_internal_realm` | `""` | Optional second SAN for split-horizon DNS |
|
|
||||||
| `coturn_image` | `coturn/coturn:4.6.2-r5-alpine` | Pinned by default; override as needed |
|
|
||||||
|
|
||||||
## ACME / nsupdate mode
|
|
||||||
|
|
||||||
When `coturn_cert_mode: acme` is set, also configure:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
coturn_acme_email: "admin@digitalboard.ch"
|
|
||||||
coturn_acme_nsupdate_server: "ns1.digitalboard.ch"
|
|
||||||
coturn_acme_nsupdate_server_ip: "172.16.9.169" # optional pin
|
|
||||||
coturn_acme_nsupdate_zone: "digitalboard._acme.digitalboard.ch"
|
|
||||||
# optional: override the auto-built challenge alias mapping
|
|
||||||
coturn_acme_challenge_aliases:
|
|
||||||
- name: stun.digitalboard.ch
|
|
||||||
alias: stun.digitalboard._acme.digitalboard.ch
|
|
||||||
- name: stun.int.digitalboard.ch
|
|
||||||
alias: stun.int.digitalboard._acme.digitalboard.ch
|
|
||||||
```
|
|
||||||
|
|
||||||
Place your TSIG key at `playbooks/secrets/{{ inventory_hostname }}/nsupdate.key` (mode 0600).
|
|
||||||
|
|
||||||
## Secrets
|
|
||||||
|
|
||||||
Place the static auth secret at:
|
|
||||||
|
|
||||||
```
|
|
||||||
playbooks/secrets/{{ inventory_hostname }}/coturn_static_auth_secret
|
|
||||||
```
|
|
||||||
|
|
||||||
Mode 0600. The same value must be deployed to the HPB host as `talk_turn_secret`.
|
|
||||||
|
|
||||||
## Firewall
|
|
||||||
|
|
||||||
The role does not manage firewall rules. Ensure the host has:
|
|
||||||
|
|
||||||
- `443/tcp` and `443/udp` reachable from the internet
|
|
||||||
- UDP `{{ coturn_min_relay_port }}-{{ coturn_max_relay_port }}` reachable from the internet
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# defaults file for coturn
|
|
||||||
|
|
||||||
# Base directories (inherited from base role)
|
|
||||||
docker_compose_base_dir: /etc/docker/compose
|
|
||||||
docker_volume_base_dir: /srv/data
|
|
||||||
|
|
||||||
# Service-specific paths
|
|
||||||
coturn_service_name: coturn
|
|
||||||
coturn_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ coturn_service_name }}"
|
|
||||||
coturn_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ coturn_service_name }}"
|
|
||||||
|
|
||||||
# Container images (pin per host_vars in production)
|
|
||||||
coturn_image: "coturn/coturn:4.6.2-r5-alpine"
|
|
||||||
coturn_acme_image: "neilpang/acme.sh:3.1.0"
|
|
||||||
|
|
||||||
# Public DNS name used for the realm and the public certificate
|
|
||||||
coturn_realm: "stun.example.test"
|
|
||||||
# Optional second DNS name issued on the same certificate (for split-horizon "internal" name)
|
|
||||||
coturn_internal_realm: "" # e.g. "stun.int.example.test"
|
|
||||||
|
|
||||||
# Ports
|
|
||||||
# Defaults follow IANA standards (3478/TURN, 5349/TURNS) so coturn can
|
|
||||||
# co-exist with a Traefik instance on the same host. Override to 443/443
|
|
||||||
# in restrictive-network environments where punching through firewalls matters.
|
|
||||||
coturn_listening_port: 3478 # TURN / STUN (TCP+UDP)
|
|
||||||
coturn_tls_listening_port: 5349 # TURNS (TCP+UDP)
|
|
||||||
coturn_min_relay_port: 49160
|
|
||||||
coturn_max_relay_port: 49200
|
|
||||||
|
|
||||||
# IP advertisement: must be set in host_vars for production
|
|
||||||
# Format follows coturn's --external-ip: "PUBLIC_IP" or "PUBLIC_IP/PRIVATE_IP"
|
|
||||||
coturn_external_ip: "" # e.g. "203.0.113.10/172.18.0.2"
|
|
||||||
coturn_listening_ip: "0.0.0.0"
|
|
||||||
|
|
||||||
# Shared secret used by HPB to mint short-lived TURN credentials.
|
|
||||||
# Loaded by default from a plain file in playbooks/secrets/{host}/coturn_static_auth_secret
|
|
||||||
# Override per host_vars if you want to use a vault or different lookup.
|
|
||||||
coturn_static_auth_secret: "{{ lookup('file', playbook_dir ~ '/secrets/' ~ inventory_hostname ~ '/coturn_static_auth_secret') }}"
|
|
||||||
|
|
||||||
# Additional CLI flags (list of strings, appended verbatim to command:)
|
|
||||||
coturn_extra_args: []
|
|
||||||
|
|
||||||
# --- TLS certificate ---
|
|
||||||
# 'acme' : run an acme.sh sidecar that issues + renews via RFC2136 / nsupdate, restarts coturn
|
|
||||||
# 'file' : assume a certificate already lives at {{ coturn_cert_dir }}/{{ coturn_cert_file }} on the host (you manage it)
|
|
||||||
# 'selfsigned' : generate a selfsigned cert on first run (for vagrant/dev only)
|
|
||||||
coturn_cert_mode: "file"
|
|
||||||
|
|
||||||
coturn_cert_dir: "{{ docker_volume_base_dir }}/acme/certs"
|
|
||||||
coturn_cert_file: "fullchain.cer"
|
|
||||||
coturn_key_file: "{{ coturn_realm }}.key"
|
|
||||||
|
|
||||||
# --- acme.sh sidecar (only used when coturn_cert_mode == 'acme') ---
|
|
||||||
coturn_acme_email: "admin@example.test"
|
|
||||||
coturn_acme_directory: "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
# Stage URL for testing: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
||||||
coturn_acme_keylength: "ec-256"
|
|
||||||
coturn_acme_dnssleep: 60
|
|
||||||
coturn_acme_data_dir: "{{ docker_volume_base_dir }}/acme/acme"
|
|
||||||
|
|
||||||
# DNS-01 RFC2136 / nsupdate configuration
|
|
||||||
coturn_acme_nsupdate_server: "" # e.g. "ns1.example.test"
|
|
||||||
coturn_acme_nsupdate_server_ip: "" # optional extra_hosts pin (string IP) for the server
|
|
||||||
coturn_acme_nsupdate_zone: "" # e.g. "example._acme.example.test"
|
|
||||||
# Per-name challenge alias zones (one entry per SAN)
|
|
||||||
# When empty (default), built automatically as "{{ realm }}._acme.{{ zone-tail }}"
|
|
||||||
coturn_acme_challenge_aliases: []
|
|
||||||
# Example:
|
|
||||||
# - name: stun.example.test
|
|
||||||
# alias: stun.example._acme.example.test
|
|
||||||
# - name: stun.int.example.test
|
|
||||||
# alias: stun.int.example._acme.example.test
|
|
||||||
|
|
||||||
# Path of the TSIG key file inside the container (mounted from secrets)
|
|
||||||
coturn_acme_nsupdate_key_src: "{{ playbook_dir }}/secrets/{{ inventory_hostname }}/nsupdate.key"
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# handlers file for coturn
|
|
||||||
|
|
||||||
- name: Restart coturn container
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ coturn_docker_compose_dir }}"
|
|
||||||
state: restarted
|
|
||||||
services:
|
|
||||||
- coturn
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy a coturn TURN/STUN server with optional acme.sh sidecar.
|
|
||||||
description:
|
|
||||||
- "Renders a Docker Compose stack for coturn running in
|
|
||||||
C(network_mode: host), with an optional C(acme.sh) sidecar that
|
|
||||||
issues + renews a public TLS certificate via RFC2136 / nsupdate
|
|
||||||
and restarts coturn on renewal."
|
|
||||||
- Designed to be paired with the C(digitalboard.core.talk) role
|
|
||||||
(Nextcloud Talk High Performance Backend).
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
docker_volume_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /srv/data
|
|
||||||
coturn_service_name:
|
|
||||||
type: str
|
|
||||||
default: coturn
|
|
||||||
coturn_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
coturn_docker_volume_dir:
|
|
||||||
type: path
|
|
||||||
|
|
||||||
coturn_image:
|
|
||||||
type: str
|
|
||||||
default: "coturn/coturn:4.6.2-r5-alpine"
|
|
||||||
coturn_acme_image:
|
|
||||||
type: str
|
|
||||||
default: "neilpang/acme.sh:3.1.0"
|
|
||||||
|
|
||||||
coturn_realm:
|
|
||||||
type: str
|
|
||||||
default: stun.example.test
|
|
||||||
description: Public DNS name used for the TURN realm and the public certificate.
|
|
||||||
coturn_internal_realm:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
- Optional second DNS name issued on the same certificate, used for
|
|
||||||
split-horizon internal access (e.g. C(stun.int.example.test)).
|
|
||||||
|
|
||||||
coturn_listening_port:
|
|
||||||
type: int
|
|
||||||
default: 3478
|
|
||||||
description: TURN/STUN port (TCP + UDP). IANA standard is 3478.
|
|
||||||
coturn_tls_listening_port:
|
|
||||||
type: int
|
|
||||||
default: 5349
|
|
||||||
description: TURNS port (TCP + UDP). IANA standard is 5349.
|
|
||||||
coturn_min_relay_port:
|
|
||||||
type: int
|
|
||||||
default: 49160
|
|
||||||
coturn_max_relay_port:
|
|
||||||
type: int
|
|
||||||
default: 49200
|
|
||||||
|
|
||||||
coturn_external_ip:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
- coturn C(--external-ip) value. Format C("PUBLIC_IP") or
|
|
||||||
C("PUBLIC_IP/PRIVATE_IP"). Must be set in host_vars for production.
|
|
||||||
coturn_listening_ip:
|
|
||||||
type: str
|
|
||||||
default: '0.0.0.0'
|
|
||||||
|
|
||||||
coturn_static_auth_secret:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description:
|
|
||||||
- Shared secret used by the HPB signaling server to mint short-lived
|
|
||||||
TURN credentials. Default lookup reads
|
|
||||||
C(playbooks/secrets/<host>/coturn_static_auth_secret).
|
|
||||||
|
|
||||||
coturn_extra_args:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description: Additional CLI flags appended verbatim to the container C(command:).
|
|
||||||
|
|
||||||
coturn_cert_mode:
|
|
||||||
type: str
|
|
||||||
choices: [acme, file, selfsigned]
|
|
||||||
default: file
|
|
||||||
description:
|
|
||||||
- C(acme) runs an acme.sh sidecar that issues + renews via RFC2136
|
|
||||||
and restarts coturn. C(file) assumes a certificate already lives
|
|
||||||
on the host (you manage it). C(selfsigned) generates one on first
|
|
||||||
run (vagrant/dev only).
|
|
||||||
coturn_cert_dir:
|
|
||||||
type: path
|
|
||||||
coturn_cert_file:
|
|
||||||
type: str
|
|
||||||
default: fullchain.cer
|
|
||||||
coturn_key_file:
|
|
||||||
type: str
|
|
||||||
description: Defaults to C("{{ coturn_realm }}.key").
|
|
||||||
|
|
||||||
coturn_acme_email:
|
|
||||||
type: str
|
|
||||||
default: admin@example.test
|
|
||||||
coturn_acme_directory:
|
|
||||||
type: str
|
|
||||||
default: https://acme-v02.api.letsencrypt.org/directory
|
|
||||||
coturn_acme_keylength:
|
|
||||||
type: str
|
|
||||||
default: ec-256
|
|
||||||
choices: [ec-256, ec-384, '2048', '3072', '4096']
|
|
||||||
coturn_acme_dnssleep:
|
|
||||||
type: int
|
|
||||||
default: 60
|
|
||||||
coturn_acme_data_dir:
|
|
||||||
type: path
|
|
||||||
|
|
||||||
coturn_acme_nsupdate_server:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Authoritative nameserver acme.sh sends C(nsupdate) packets to.
|
|
||||||
coturn_acme_nsupdate_server_ip:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Optional C(extra_hosts) pin (string IP) for the nsupdate server.
|
|
||||||
coturn_acme_nsupdate_zone:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Delegated challenge zone (e.g. C(example._acme.example.test)).
|
|
||||||
coturn_acme_challenge_aliases:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Per-name challenge alias zones (one entry per SAN). When empty,
|
|
||||||
built automatically as C({{ realm }}._acme.{{ zone-tail }}).
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: SAN the challenge is for.
|
|
||||||
alias:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: CNAME target where the C(_acme-challenge) TXT lives.
|
|
||||||
coturn_acme_nsupdate_key_src:
|
|
||||||
type: path
|
|
||||||
description: Path of the TSIG key file on the controller, mounted into the acme container.
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
galaxy_info:
|
|
||||||
author: Digital Board Team
|
|
||||||
description: Deploy a coturn TURN/STUN server with optional acme.sh sidecar (RFC2136/nsupdate)
|
|
||||||
company: digitalboard.ch
|
|
||||||
license: GPL-2.0-or-later
|
|
||||||
min_ansible_version: "2.14"
|
|
||||||
galaxy_tags:
|
|
||||||
- turn
|
|
||||||
- stun
|
|
||||||
- coturn
|
|
||||||
- webrtc
|
|
||||||
- nextcloud
|
|
||||||
- talk
|
|
||||||
dependencies: []
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# tasks file for coturn
|
|
||||||
|
|
||||||
- name: Assert minimum configuration
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- coturn_realm | length > 0
|
|
||||||
- coturn_external_ip | length > 0
|
|
||||||
- coturn_static_auth_secret | length > 0
|
|
||||||
fail_msg: >
|
|
||||||
coturn_realm, coturn_external_ip and coturn_static_auth_secret must be set.
|
|
||||||
Provide them in host_vars or via a secrets file.
|
|
||||||
|
|
||||||
- name: Create coturn compose directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ coturn_docker_compose_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Create coturn data directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ coturn_docker_volume_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Create certificate directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ coturn_cert_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
# --- TLS certificate provisioning -------------------------------------------------
|
|
||||||
|
|
||||||
- name: Configure acme.sh sidecar (TSIG key + acme data dir)
|
|
||||||
when: coturn_cert_mode == 'acme'
|
|
||||||
block:
|
|
||||||
- name: Create acme.sh data directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ coturn_acme_data_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
- name: Deploy nsupdate TSIG key
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ coturn_acme_nsupdate_key_src }}"
|
|
||||||
dest: "{{ coturn_docker_compose_dir }}/nsupdate.key"
|
|
||||||
mode: "0600"
|
|
||||||
no_log: true
|
|
||||||
notify: Restart coturn container
|
|
||||||
|
|
||||||
- name: Build effective challenge alias list (default if not provided)
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_coturn_challenge_aliases: >-
|
|
||||||
{{ coturn_acme_challenge_aliases
|
|
||||||
if coturn_acme_challenge_aliases | length > 0
|
|
||||||
else (
|
|
||||||
[{'name': coturn_realm,
|
|
||||||
'alias': (coturn_realm.split('.')[:-2] | join('.')) ~ '.' ~ coturn_acme_nsupdate_zone }]
|
|
||||||
+ ([{'name': coturn_internal_realm,
|
|
||||||
'alias': (coturn_internal_realm.split('.')[:-2] | join('.')) ~ '.' ~ coturn_acme_nsupdate_zone }]
|
|
||||||
if coturn_internal_realm | length > 0 else [])
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
|
|
||||||
- name: Generate selfsigned certificate (vagrant / dev only)
|
|
||||||
when: coturn_cert_mode == 'selfsigned'
|
|
||||||
block:
|
|
||||||
- name: Ensure openssl is available
|
|
||||||
ansible.builtin.package:
|
|
||||||
name: openssl
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Generate selfsigned private key
|
|
||||||
community.crypto.openssl_privatekey:
|
|
||||||
path: "{{ coturn_cert_dir }}/{{ coturn_key_file }}"
|
|
||||||
type: ECC
|
|
||||||
curve: secp256r1
|
|
||||||
mode: "0600"
|
|
||||||
|
|
||||||
- name: Generate selfsigned CSR
|
|
||||||
community.crypto.openssl_csr:
|
|
||||||
path: "{{ coturn_cert_dir }}/{{ coturn_realm }}.csr"
|
|
||||||
privatekey_path: "{{ coturn_cert_dir }}/{{ coturn_key_file }}"
|
|
||||||
common_name: "{{ coturn_realm }}"
|
|
||||||
subject_alt_name:
|
|
||||||
- "DNS:{{ coturn_realm }}"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Issue selfsigned certificate
|
|
||||||
community.crypto.x509_certificate:
|
|
||||||
path: "{{ coturn_cert_dir }}/{{ coturn_cert_file }}"
|
|
||||||
privatekey_path: "{{ coturn_cert_dir }}/{{ coturn_key_file }}"
|
|
||||||
csr_path: "{{ coturn_cert_dir }}/{{ coturn_realm }}.csr"
|
|
||||||
provider: selfsigned
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
# --- Compose + start --------------------------------------------------------------
|
|
||||||
|
|
||||||
- name: Generate docker-compose.yml for coturn
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: docker-compose.yml.j2
|
|
||||||
dest: "{{ coturn_docker_compose_dir }}/docker-compose.yml"
|
|
||||||
mode: "0644"
|
|
||||||
notify: Restart coturn container
|
|
||||||
|
|
||||||
- name: Start coturn stack
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ coturn_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
services:
|
|
||||||
coturn:
|
|
||||||
image: {{ coturn_image }}
|
|
||||||
container_name: {{ coturn_service_name }}
|
|
||||||
restart: always
|
|
||||||
network_mode: host
|
|
||||||
volumes:
|
|
||||||
- {{ coturn_cert_dir }}:/certs:ro
|
|
||||||
command:
|
|
||||||
- --use-auth-secret
|
|
||||||
- --static-auth-secret={{ coturn_static_auth_secret }}
|
|
||||||
- --realm={{ coturn_realm }}
|
|
||||||
- --fingerprint
|
|
||||||
- --no-multicast-peers
|
|
||||||
- --no-cli
|
|
||||||
- --listening-ip={{ coturn_listening_ip }}
|
|
||||||
- --listening-port={{ coturn_listening_port }}
|
|
||||||
- --tls-listening-port={{ coturn_tls_listening_port }}
|
|
||||||
- --min-port={{ coturn_min_relay_port }}
|
|
||||||
- --max-port={{ coturn_max_relay_port }}
|
|
||||||
- --cert=/certs/{{ coturn_cert_file }}
|
|
||||||
- --pkey=/certs/{{ coturn_key_file }}
|
|
||||||
- --external-ip={{ coturn_external_ip }}
|
|
||||||
{% for arg in coturn_extra_args %}
|
|
||||||
- {{ arg }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if coturn_cert_mode == 'acme' %}
|
|
||||||
acme:
|
|
||||||
image: {{ coturn_acme_image }}
|
|
||||||
container_name: acme-{{ coturn_service_name }}
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
NSUPDATE_SERVER: "{{ coturn_acme_nsupdate_server }}"
|
|
||||||
NSUPDATE_KEY: "/acme.sh/nsupdate.key"
|
|
||||||
ACME_DIRECTORY: "{{ coturn_acme_directory }}"
|
|
||||||
NSUPDATE_ZONE: "{{ coturn_acme_nsupdate_zone }}"
|
|
||||||
{% if coturn_acme_nsupdate_server_ip | length > 0 %}
|
|
||||||
extra_hosts:
|
|
||||||
- "{{ coturn_acme_nsupdate_server }}:{{ coturn_acme_nsupdate_server_ip }}"
|
|
||||||
{% endif %}
|
|
||||||
volumes:
|
|
||||||
- {{ coturn_cert_dir }}:/certs
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- {{ coturn_docker_compose_dir }}/nsupdate.key:/acme.sh/nsupdate.key:ro
|
|
||||||
- {{ coturn_acme_data_dir }}:/acme.sh
|
|
||||||
entrypoint:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
set -eu
|
|
||||||
acme.sh --set-default-ca --server "$$ACME_DIRECTORY"
|
|
||||||
acme.sh --register-account -m {{ coturn_acme_email }} || true
|
|
||||||
set +e
|
|
||||||
acme.sh --issue \
|
|
||||||
{% for san in _coturn_challenge_aliases %}
|
|
||||||
-d {{ san.name }} \
|
|
||||||
--challenge-alias {{ san.alias }} \
|
|
||||||
{% endfor %}
|
|
||||||
--dns dns_nsupdate \
|
|
||||||
--keylength {{ coturn_acme_keylength }} \
|
|
||||||
--dnssleep {{ coturn_acme_dnssleep }}
|
|
||||||
rc=$$?
|
|
||||||
set -e
|
|
||||||
if [ "$$rc" -eq 0 ]; then
|
|
||||||
echo "Issue: success"
|
|
||||||
elif [ "$$rc" -eq 2 ]; then
|
|
||||||
echo "Issue: not due, continuing"
|
|
||||||
else
|
|
||||||
echo "Issue: failed with rc=$$rc"
|
|
||||||
exit "$$rc"
|
|
||||||
fi
|
|
||||||
acme.sh --install-cert -d {{ coturn_realm }} --ecc \
|
|
||||||
--fullchain-file /certs/{{ coturn_cert_file }} \
|
|
||||||
--key-file /certs/{{ coturn_key_file }} \
|
|
||||||
--reloadcmd 'curl --fail --silent --show-error --unix-socket /var/run/docker.sock -X POST http://localhost/v1.41/containers/{{ coturn_service_name }}/restart' || true
|
|
||||||
exec crond -f
|
|
||||||
{% endif %}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
localhost
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- hosts: localhost
|
|
||||||
remote_user: root
|
|
||||||
roles:
|
|
||||||
- coturn
|
|
||||||
|
|
@ -1,60 +1,38 @@
|
||||||
# Drawio
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
Ansible role to deploy [draw.io](https://www.drawio.com/) (the
|
A brief description of the role goes here.
|
||||||
self-hosted `jgraph/drawio` container) via Docker Compose behind
|
|
||||||
Traefik, with optional authentik ForwardAuth gating.
|
|
||||||
|
|
||||||
## Requirements
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
- Docker and Docker Compose installed on the target host
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
- Ansible collection: `community.docker`
|
|
||||||
- Traefik with a shared `drawio_traefik_network` (default `proxy`)
|
|
||||||
- For ForwardAuth: a reachable authentik embedded outpost endpoint
|
|
||||||
|
|
||||||
## Role variables
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
common overrides:
|
|
||||||
|
|
||||||
### Service
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
- `drawio_domain`: canonical hostname used in the traefik Host rule
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
(default `drawio.local.test`).
|
|
||||||
- `drawio_extra_domains`: additional hostnames the same container
|
|
||||||
should answer on (e.g. an internal `*.int.*` FQDN so a DMZ proxy
|
|
||||||
can reach drawio via a backend hostname).
|
|
||||||
- `drawio_image`, `drawio_port`, `drawio_use_ssl`.
|
|
||||||
|
|
||||||
### Authentik ForwardAuth
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
- `drawio_authentik_forward_auth`: set to `true` to gate the editor
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
behind authentik.
|
|
||||||
- `drawio_authentik_forward_auth_url`: full URL of the embedded
|
|
||||||
outpost ForwardAuth endpoint, e.g.
|
|
||||||
`https://auth.example.com/outpost.goauthentik.io/auth/traefik`.
|
|
||||||
|
|
||||||
When enabled, traefik redirects unauthenticated requests to authentik
|
- hosts: servers
|
||||||
for login and forwards the resulting `X-Authentik-*` identity headers
|
|
||||||
downstream.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Traefik network (`drawio_traefik_network`, default `proxy`)
|
|
||||||
- Optional: authentik with a Proxy/ForwardAuth provider for drawio
|
|
||||||
(see the `authentik` role's `authentik_proxy_apps`).
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- hosts: app_servers
|
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.drawio
|
- { role: username.rolename, x: 42 }
|
||||||
vars:
|
|
||||||
drawio_domain: "drawio.example.com"
|
|
||||||
drawio_authentik_forward_auth: true
|
|
||||||
drawio_authentik_forward_auth_url: "https://auth.example.com/outpost.goauthentik.io/auth/traefik"
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ drawio_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ drawio_service_name
|
||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
drawio_domain: "drawio.local.test"
|
drawio_domain: "drawio.local.test"
|
||||||
# Additional hostnames the same drawio container should answer on
|
|
||||||
# (e.g. an internal *.int.* FQDN so a DMZ reverseproxy can reach
|
|
||||||
# drawio via a backend hostname covered by the local traefik cert).
|
|
||||||
drawio_extra_domains: []
|
|
||||||
drawio_image: "jgraph/drawio:latest"
|
drawio_image: "jgraph/drawio:latest"
|
||||||
drawio_port: 8080
|
drawio_port: 8080
|
||||||
drawio_extra_hosts: []
|
drawio_extra_hosts: []
|
||||||
|
|
@ -22,10 +18,3 @@ drawio_extra_hosts: []
|
||||||
# Traefik configuration
|
# Traefik configuration
|
||||||
drawio_traefik_network: "proxy"
|
drawio_traefik_network: "proxy"
|
||||||
drawio_use_ssl: true
|
drawio_use_ssl: true
|
||||||
|
|
||||||
# Optional Authentik ForwardAuth (set to true and provide the URL to gate
|
|
||||||
# drawio behind an authentik proxy provider). Expects the authentik
|
|
||||||
# embedded outpost to expose the /outpost.goauthentik.io/auth/traefik
|
|
||||||
# endpoint on the configured URL (typically the public auth.* FQDN).
|
|
||||||
drawio_authentik_forward_auth: false
|
|
||||||
drawio_authentik_forward_auth_url: ""
|
|
||||||
|
|
@ -5,4 +5,4 @@
|
||||||
- name: restart drawio
|
- name: restart drawio
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ drawio_docker_compose_dir }}"
|
project_src: "{{ drawio_docker_compose_dir }}"
|
||||||
state: present
|
state: restarted
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy draw.io diagram editor via Docker Compose behind Traefik.
|
|
||||||
description:
|
|
||||||
- Renders a Compose stack for jgraph/drawio with traefik labels, optional
|
|
||||||
TLS and optional authentik ForwardAuth gating.
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
drawio_service_name:
|
|
||||||
type: str
|
|
||||||
default: drawio
|
|
||||||
drawio_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
description: Defaults to C({{ docker_compose_base_dir }}/{{ drawio_service_name }}).
|
|
||||||
|
|
||||||
drawio_domain:
|
|
||||||
type: str
|
|
||||||
default: drawio.local.test
|
|
||||||
description: Canonical hostname used in the traefik Host rule.
|
|
||||||
drawio_extra_domains:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Additional hostnames the same drawio container should answer on,
|
|
||||||
e.g. an internal C(*.int.*) FQDN so a DMZ reverse-proxy can reach
|
|
||||||
drawio via a backend hostname covered by the local traefik cert.
|
|
||||||
drawio_image:
|
|
||||||
type: str
|
|
||||||
default: jgraph/drawio:latest
|
|
||||||
drawio_port:
|
|
||||||
type: int
|
|
||||||
default: 8080
|
|
||||||
drawio_extra_hosts:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
description: C(extra_hosts) entries injected into the container (Docker C(host:ip) syntax).
|
|
||||||
|
|
||||||
drawio_traefik_network:
|
|
||||||
type: str
|
|
||||||
default: proxy
|
|
||||||
drawio_use_ssl:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
|
|
||||||
drawio_authentik_forward_auth:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description:
|
|
||||||
- When true, traefik attaches a ForwardAuth middleware pointing at
|
|
||||||
the authentik embedded outpost. Unauthenticated requests are
|
|
||||||
redirected to authentik for login and the resulting
|
|
||||||
C(X-Authentik-*) identity headers are forwarded downstream.
|
|
||||||
drawio_authentik_forward_auth_url:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
- URL of the authentik ForwardAuth endpoint, typically
|
|
||||||
C(https://auth.example.com/outpost.goauthentik.io/auth/traefik).
|
|
||||||
Required when C(drawio_authentik_forward_auth=true).
|
|
||||||
|
|
@ -1,26 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy the draw.io diagram editor via Docker Compose behind Traefik, with optional authentik ForwardAuth
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.2
|
||||||
- drawio
|
|
||||||
- diagrams
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- docker
|
# min_ansible_container_version:
|
||||||
- traefik
|
|
||||||
- digitalboard
|
galaxy_tags: []
|
||||||
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -14,26 +14,14 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ drawio_traefik_network }}
|
- traefik.docker.network={{ drawio_traefik_network }}
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.rule={% set _all_domains = [drawio_domain] + (drawio_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
- traefik.http.routers.{{ drawio_service_name }}.rule=Host(`{{ drawio_domain }}`)
|
||||||
- traefik.http.services.{{ drawio_service_name }}.loadbalancer.server.port={{ drawio_port }}
|
- traefik.http.services.{{ drawio_service_name }}.loadbalancer.server.port={{ drawio_port }}
|
||||||
{% if drawio_use_ssl %}
|
{% if drawio_use_ssl %}
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.tls=true
|
- traefik.http.routers.{{ drawio_service_name }}.tls=true
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if drawio_authentik_forward_auth | default(false) %}
|
|
||||||
# ForwardAuth via the authentik embedded outpost. Unauthenticated
|
|
||||||
# requests get redirected to authentik to log in; authentik then
|
|
||||||
# sets X-Authentik-* headers traefik forwards downstream.
|
|
||||||
- traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.address={{ drawio_authentik_forward_auth_url }}
|
|
||||||
- traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.trustForwardHeader=true
|
|
||||||
- traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
|
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.middlewares={{ drawio_service_name }}-authentik
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
{{ drawio_traefik_network }}:
|
{{ drawio_traefik_network }}:
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,113 @@
|
||||||
# Garage
|
Garage
|
||||||
|
======
|
||||||
|
|
||||||
Ansible role to deploy [Garage](https://garagehq.deuxfleurs.fr/) S3-compatible
|
Ansible role to deploy Garage S3-compatible object storage using Docker Compose.
|
||||||
object storage via Docker Compose, with declarative key/bucket
|
|
||||||
provisioning and an optional WebUI behind htpasswd or authentik
|
|
||||||
ForwardAuth.
|
|
||||||
|
|
||||||
## Requirements
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
- Docker and Docker Compose installed on the target host
|
- Docker and Docker Compose installed on the target host
|
||||||
- Ansible collection: `community.docker`
|
- Ansible collection: `community.docker`
|
||||||
- `htpasswd` (from `apache2-utils` / `httpd-tools`) when the WebUI is
|
- Traefik reverse proxy (for external access)
|
||||||
enabled and authentik ForwardAuth is *not* used
|
|
||||||
- Traefik with a shared `garage_traefik_network` (default `proxy`)
|
|
||||||
|
|
||||||
## Role variables
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
Key variables defined in `defaults/main.yml`:
|
||||||
common overrides:
|
|
||||||
|
|
||||||
### Service
|
**Base Configuration:**
|
||||||
|
- `docker_compose_base_dir`: Base directory for Docker Compose files (default: `/etc/docker/compose`)
|
||||||
|
- `docker_volume_base_dir`: Base directory for Docker volumes (default: `/srv/data`)
|
||||||
|
|
||||||
- `garage_s3_domains`: FQDNs the S3 router accepts. The first entry is the
|
**Garage Configuration:**
|
||||||
canonical hostname; `garage.toml` derives the virtual-hosted-style S3
|
- `garage_service_name`: Service name (default: `garage`)
|
||||||
`root_domain` from it as `.s3.<first-entry>` (so buckets resolve under
|
- `garage_image`: Garage Docker image (default: `dxflrs/garage:v2.1.0`)
|
||||||
`<bucket>.s3.<first-entry>`).
|
- `garage_s3_domain`: Domain for S3 API endpoint (default: `storage.local.test`)
|
||||||
- `garage_web_domain`, `garage_webui_domain`: separate hostnames for
|
- `garage_web_domain`: Domain for S3 web endpoint (default: `web.storage.local.test`)
|
||||||
the S3-website endpoint and the console.
|
- `garage_webui_domain`: Domain for web console (default: `console.storage.local.test`)
|
||||||
- `garage_image`, `garage_replication_factor`, `garage_db_engine`,
|
|
||||||
`garage_s3_region`.
|
|
||||||
|
|
||||||
### Required secrets
|
**Garage Storage Configuration:**
|
||||||
|
- `garage_replication_factor`: Replication factor (default: `1`)
|
||||||
|
- `garage_compression_level`: Compression level (default: `1`)
|
||||||
|
- `garage_db_engine`: Database engine (default: `lmdb`)
|
||||||
|
- `garage_s3_region`: S3 region (default: `us-east-1`)
|
||||||
|
|
||||||
Generate with `openssl rand -hex 32` (32 bytes / 64 hex chars):
|
**Garage Ports:**
|
||||||
|
- `garage_s3_api_port`: S3 API port (default: `3900`)
|
||||||
|
- `garage_s3_web_port`: S3 web port (default: `3902`)
|
||||||
|
- `garage_admin_port`: Admin API port (default: `3903`)
|
||||||
|
- `garage_rpc_port`: RPC port (default: `3901`)
|
||||||
|
|
||||||
- `garage_rpc_secret`: node-to-node RPC secret
|
**Garage Security:**
|
||||||
- `garage_admin_token`: admin API token
|
- `garage_rpc_secret`: RPC secret for node communication
|
||||||
- `garage_metrics_token`: metrics endpoint token
|
- `garage_admin_token`: Admin API token
|
||||||
|
- `garage_metrics_token`: Metrics API token
|
||||||
|
|
||||||
### WebUI authentication
|
**Garage WebUI Configuration:**
|
||||||
|
- `garage_webui_enabled`: Enable web UI (default: `true`)
|
||||||
|
- `garage_webui_image`: WebUI Docker image (default: `khairul169/garage-webui:latest`)
|
||||||
|
- `garage_webui_port`: WebUI port (default: `3909`)
|
||||||
|
- `garage_webui_username`: WebUI username (default: `admin`)
|
||||||
|
- `garage_webui_password`: WebUI password in plaintext (default: `admin`)
|
||||||
|
|
||||||
Three modes:
|
**Traefik Configuration:**
|
||||||
|
- `garage_traefik_network`: Traefik network name (default: `proxy`)
|
||||||
|
- `garage_internal_network`: Internal network name (default: `internal`)
|
||||||
|
- `garage_use_ssl`: Enable SSL (default: `true`)
|
||||||
|
|
||||||
1. **htpasswd** (default): `garage_webui_username` / `garage_webui_password`
|
Dependencies
|
||||||
in plaintext. The role hashes the password with
|
------------
|
||||||
`htpasswd -nbBC 10`, persists the hash on disk, and re-verifies with
|
|
||||||
`htpasswd -vbB` so unchanged passwords don't churn the play.
|
|
||||||
2. **authentik ForwardAuth**: set
|
|
||||||
`garage_webui_authentik_forward_auth: true` and
|
|
||||||
`garage_webui_authentik_forward_auth_url:
|
|
||||||
"https://auth.example.com/outpost.goauthentik.io/auth/traefik"`.
|
|
||||||
`AUTH_USER_PASS` is dropped from the container env so authentik is
|
|
||||||
the only gate.
|
|
||||||
3. **Disabled**: `garage_webui_enabled: false`.
|
|
||||||
|
|
||||||
### Layout bootstrap
|
This role requires:
|
||||||
|
- Traefik reverse proxy to be configured and the `proxy` network to be created
|
||||||
|
- `htpasswd` utility (from `apache2-utils` package) for generating bcrypt password hashes
|
||||||
|
|
||||||
Setting `garage_bootstrap_enabled: true` runs the bootstrap task, which
|
Example Playbook
|
||||||
joins the local node to the layout (`zone: garage_bootstrap_zone`,
|
----------------
|
||||||
capacity: `garage_bootstrap_capacity`) on the first run. The check
|
|
||||||
tolerates the 16-char truncation that `garage layout show` performs.
|
|
||||||
|
|
||||||
### Declarative S3 keys and buckets
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
garage_s3_keys:
|
|
||||||
- name: nextcloud
|
|
||||||
buckets:
|
|
||||||
- name: nextcloud-data
|
|
||||||
permissions: [read, write]
|
|
||||||
- name: backup
|
|
||||||
buckets:
|
|
||||||
- name: restic-prod
|
|
||||||
permissions: [read, write, owner]
|
|
||||||
```
|
|
||||||
|
|
||||||
The role:
|
|
||||||
|
|
||||||
- Lists existing keys (`garage key list`), creates missing ones
|
|
||||||
- Lists existing buckets (`garage bucket list`), creates missing ones
|
|
||||||
- Reads current permissions via `garage bucket info` and runs
|
|
||||||
`garage bucket allow` only when the current RWO flags for the key
|
|
||||||
don't already match the desired permissions
|
|
||||||
|
|
||||||
`stdout` parsing is hardened against ANSI escapes and interleaved INFO
|
|
||||||
log lines, so probe noise no longer produces spurious changes.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Traefik network (`garage_traefik_network`, default `proxy`)
|
|
||||||
- Internal network (`garage_internal_network`, default `internal`)
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- hosts: storage_servers
|
- hosts: storage_servers
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.garage
|
- role: garage
|
||||||
vars:
|
vars:
|
||||||
garage_s3_domains:
|
garage_s3_domain: "storage.example.com"
|
||||||
- "storage.example.com"
|
garage_rpc_secret: "your-secure-rpc-secret"
|
||||||
- "storage.int.example.com"
|
garage_admin_token: "your-admin-token"
|
||||||
garage_rpc_secret: "{{ vault_garage_rpc_secret }}"
|
garage_webui_enabled: true
|
||||||
garage_admin_token: "{{ vault_garage_admin_token }}"
|
garage_webui_username: "admin"
|
||||||
garage_metrics_token: "{{ vault_garage_metrics_token }}"
|
garage_webui_password: "secure-password"
|
||||||
garage_bootstrap_enabled: true
|
|
||||||
garage_webui_authentik_forward_auth: true
|
|
||||||
garage_webui_authentik_forward_auth_url: "https://auth.example.com/outpost.goauthentik.io/auth/traefik"
|
|
||||||
garage_s3_keys:
|
|
||||||
- name: nextcloud
|
|
||||||
buckets:
|
|
||||||
- name: nextcloud-data
|
|
||||||
permissions: [read, write]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
**Note:** The WebUI password is specified in plaintext and will be automatically hashed using bcrypt during deployment. The role uses `htpasswd` to generate a secure bcrypt hash that is then properly escaped for use in Docker Compose.
|
||||||
|
|
||||||
|
Post-Installation
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
After deployment, you need to configure the Garage cluster:
|
||||||
|
|
||||||
|
1. Connect to the node and get the node ID:
|
||||||
|
```bash
|
||||||
|
docker exec -ti garage /garage node id
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configure the node layout:
|
||||||
|
```bash
|
||||||
|
docker exec -ti garage /garage layout assign -z dc1 -c 1G <node-id>
|
||||||
|
docker exec -ti garage /garage layout apply --version 1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a key for S3 access:
|
||||||
|
```bash
|
||||||
|
docker exec -ti garage /garage key create my-key
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create a bucket:
|
||||||
|
```bash
|
||||||
|
docker exec -ti garage /garage bucket create my-bucket
|
||||||
|
docker exec -ti garage /garage bucket allow my-bucket --read --write --key my-key
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
MIT-0
|
||||||
|
|
@ -13,12 +13,7 @@ garage_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ garage_service_name }
|
||||||
|
|
||||||
# Garage service configuration
|
# Garage service configuration
|
||||||
garage_image: "dxflrs/garage:v2.1.0"
|
garage_image: "dxflrs/garage:v2.1.0"
|
||||||
# FQDNs the garage S3 router accepts. The first entry is the canonical
|
garage_s3_domain: "storage.local.test"
|
||||||
# domain; garage.toml derives the virtual-hosted-style S3 root_domain
|
|
||||||
# from it as ".s3.<first-entry>"; further entries cover internal
|
|
||||||
# *.int.* names.
|
|
||||||
garage_s3_domains:
|
|
||||||
- "storage.local.test"
|
|
||||||
garage_web_domain: "web.storage.local.test"
|
garage_web_domain: "web.storage.local.test"
|
||||||
garage_webui_domain: "console.storage.local.test"
|
garage_webui_domain: "console.storage.local.test"
|
||||||
|
|
||||||
|
|
@ -26,20 +21,10 @@ garage_webui_domain: "console.storage.local.test"
|
||||||
garage_webui_enabled: true
|
garage_webui_enabled: true
|
||||||
garage_webui_image: "khairul169/garage-webui:latest"
|
garage_webui_image: "khairul169/garage-webui:latest"
|
||||||
garage_webui_port: 3909
|
garage_webui_port: 3909
|
||||||
# WebUI basic auth credentials (plaintext, will be hashed automatically).
|
# WebUI basic auth credentials (plaintext, will be hashed automatically)
|
||||||
# Ignored when garage_webui_authentik_forward_auth is true — in that case
|
|
||||||
# authentik handles authentication via the ForwardAuth middleware below.
|
|
||||||
garage_webui_username: "admin"
|
garage_webui_username: "admin"
|
||||||
garage_webui_password: "admin"
|
garage_webui_password: "admin"
|
||||||
|
|
||||||
# Optional Authentik ForwardAuth in front of the WebUI. When true:
|
|
||||||
# - the AUTH_USER_PASS env-var is dropped from the container so htpasswd
|
|
||||||
# isn't enforced; authentik is the only gate.
|
|
||||||
# - traefik attaches a ForwardAuth middleware pointing at the URL below.
|
|
||||||
# Leave false to keep classic htpasswd protection.
|
|
||||||
garage_webui_authentik_forward_auth: false
|
|
||||||
garage_webui_authentik_forward_auth_url: ""
|
|
||||||
|
|
||||||
# Garage ports
|
# Garage ports
|
||||||
garage_s3_api_port: 3900
|
garage_s3_api_port: 3900
|
||||||
garage_s3_web_port: 3902
|
garage_s3_web_port: 3902
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy Garage S3-compatible object storage via Docker Compose.
|
|
||||||
description:
|
|
||||||
- Renders a Compose stack for Garage with traefik labels, configures the
|
|
||||||
node layout on first run, and (optionally) provisions S3 keys, buckets
|
|
||||||
and per-key permissions declaratively.
|
|
||||||
- The optional WebUI can be protected by classic htpasswd or by
|
|
||||||
authentik ForwardAuth.
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
docker_volume_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /srv/data
|
|
||||||
garage_service_name:
|
|
||||||
type: str
|
|
||||||
default: garage
|
|
||||||
garage_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
description: Defaults to C({{ docker_compose_base_dir }}/{{ garage_service_name }}).
|
|
||||||
garage_docker_volume_dir:
|
|
||||||
type: path
|
|
||||||
description: Defaults to C({{ docker_volume_base_dir }}/{{ garage_service_name }}).
|
|
||||||
|
|
||||||
garage_image:
|
|
||||||
type: str
|
|
||||||
default: dxflrs/garage:v2.1.0
|
|
||||||
|
|
||||||
garage_s3_domains:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: ['storage.local.test']
|
|
||||||
description:
|
|
||||||
- FQDNs the garage S3 router accepts. The first entry is the
|
|
||||||
canonical domain; C(garage.toml) derives the virtual-hosted-style
|
|
||||||
S3 C(root_domain) from it as C(.s3.<first-entry>). Further entries
|
|
||||||
cover internal C(*.int.*) names.
|
|
||||||
garage_web_domain:
|
|
||||||
type: str
|
|
||||||
default: web.storage.local.test
|
|
||||||
description: Hostname serving the S3-website endpoint.
|
|
||||||
garage_webui_domain:
|
|
||||||
type: str
|
|
||||||
default: console.storage.local.test
|
|
||||||
description: Hostname serving the WebUI console.
|
|
||||||
|
|
||||||
garage_webui_enabled:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
garage_webui_image:
|
|
||||||
type: str
|
|
||||||
default: khairul169/garage-webui:latest
|
|
||||||
garage_webui_port:
|
|
||||||
type: int
|
|
||||||
default: 3909
|
|
||||||
garage_webui_username:
|
|
||||||
type: str
|
|
||||||
default: admin
|
|
||||||
description: htpasswd username. Ignored when C(garage_webui_authentik_forward_auth=true).
|
|
||||||
garage_webui_password:
|
|
||||||
type: str
|
|
||||||
default: admin
|
|
||||||
description:
|
|
||||||
- Plaintext password; hashed with C(htpasswd -nbBC 10) and persisted
|
|
||||||
on disk so re-runs don't churn. Ignored when authentik ForwardAuth
|
|
||||||
is enabled.
|
|
||||||
garage_webui_authentik_forward_auth:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description:
|
|
||||||
- When true the C(AUTH_USER_PASS) env-var is dropped from the WebUI
|
|
||||||
container and traefik attaches a ForwardAuth middleware pointing
|
|
||||||
at the URL below. authentik is then the only gate; htpasswd is
|
|
||||||
disabled.
|
|
||||||
garage_webui_authentik_forward_auth_url:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
- Required when C(garage_webui_authentik_forward_auth=true).
|
|
||||||
Typically C(https://auth.example.com/outpost.goauthentik.io/auth/traefik).
|
|
||||||
|
|
||||||
garage_s3_api_port:
|
|
||||||
type: int
|
|
||||||
default: 3900
|
|
||||||
garage_s3_web_port:
|
|
||||||
type: int
|
|
||||||
default: 3902
|
|
||||||
garage_admin_port:
|
|
||||||
type: int
|
|
||||||
default: 3903
|
|
||||||
garage_rpc_port:
|
|
||||||
type: int
|
|
||||||
default: 3901
|
|
||||||
|
|
||||||
garage_replication_factor:
|
|
||||||
type: int
|
|
||||||
default: 1
|
|
||||||
garage_compression_level:
|
|
||||||
type: int
|
|
||||||
default: 1
|
|
||||||
garage_db_engine:
|
|
||||||
type: str
|
|
||||||
choices: [lmdb, sqlite, sled]
|
|
||||||
default: lmdb
|
|
||||||
garage_s3_region:
|
|
||||||
type: str
|
|
||||||
default: us-east-1
|
|
||||||
garage_rpc_secret:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: Hex secret for node-to-node RPC. Generate with C(openssl rand -hex 32).
|
|
||||||
garage_admin_token:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
garage_metrics_token:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
|
|
||||||
garage_traefik_network:
|
|
||||||
type: str
|
|
||||||
default: proxy
|
|
||||||
garage_internal_network:
|
|
||||||
type: str
|
|
||||||
default: internal
|
|
||||||
garage_use_ssl:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
|
|
||||||
garage_bootstrap_enabled:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description: When true the bootstrap task ensures the node is in the layout.
|
|
||||||
garage_bootstrap_zone:
|
|
||||||
type: str
|
|
||||||
default: dc1
|
|
||||||
description: Zone label assigned during layout bootstrap.
|
|
||||||
garage_bootstrap_capacity:
|
|
||||||
type: str
|
|
||||||
default: 1G
|
|
||||||
description: Capacity string passed to C(garage layout assign -c).
|
|
||||||
|
|
||||||
garage_s3_keys:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
description:
|
|
||||||
- Declarative key + bucket + permission provisioning. The role
|
|
||||||
creates missing keys, missing buckets, and runs C(bucket allow)
|
|
||||||
only when the current RWO flags for a given key don't match.
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
buckets:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
description: Buckets this key gets access to.
|
|
||||||
options:
|
|
||||||
name:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
permissions:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
choices: [read, write, owner]
|
|
||||||
required: true
|
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy Garage S3-compatible object storage via Docker Compose, with declarative key/bucket provisioning
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.1
|
||||||
- garage
|
|
||||||
- s3
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- storage
|
# min_ansible_container_version:
|
||||||
- object-storage
|
|
||||||
- docker
|
galaxy_tags: []
|
||||||
- digitalboard
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,21 @@
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage node id -q
|
command: /garage node id -q
|
||||||
register: _garage_node_id
|
register: _garage_node_id
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Extract short node ID
|
- name: Extract short node ID
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_garage_node_id_short: "{{ _garage_node_id.stdout.split('@')[0] }}"
|
_garage_node_id_short: "{{ _garage_node_id.stdout.split('@')[0] }}"
|
||||||
|
|
||||||
- name: Extract truncated node ID (first 16 chars, matches `garage layout show` output)
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_garage_node_id_truncated: "{{ _garage_node_id_short[:16] }}"
|
|
||||||
|
|
||||||
- name: Check if node layout is configured
|
- name: Check if node layout is configured
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage layout show
|
command: /garage layout show
|
||||||
register: _garage_layout_show
|
register: _garage_layout_show
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Check if node is in layout
|
- name: Check if node is in layout
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_node_in_layout: "{{ (_garage_node_id_truncated in _garage_layout_show.stdout) or (_garage_node_id_short in _garage_layout_show.stdout) }}"
|
_node_in_layout: "{{ _garage_node_id_short in _garage_layout_show.stdout }}"
|
||||||
|
|
||||||
- name: Configure garage node layout
|
- name: Configure garage node layout
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
|
|
|
||||||
|
|
@ -26,77 +26,12 @@
|
||||||
dest: "{{ garage_docker_compose_dir }}/garage.toml"
|
dest: "{{ garage_docker_compose_dir }}/garage.toml"
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
|
|
||||||
- name: Set webui htpasswd activation fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
# htpasswd only runs when the WebUI is enabled AND authentik ForwardAuth
|
|
||||||
# is not handling authentication. When authentik is in front, the
|
|
||||||
# compose template drops AUTH_USER_PASS so no hash is needed.
|
|
||||||
_garage_webui_htpasswd_active: >-
|
|
||||||
{{
|
|
||||||
garage_webui_enabled
|
|
||||||
and not (garage_webui_authentik_forward_auth | default(false))
|
|
||||||
}}
|
|
||||||
|
|
||||||
- name: Read cached webui htpasswd hash
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
|
||||||
register: _garage_webui_htpasswd_cached
|
|
||||||
failed_when: false
|
|
||||||
changed_when: false
|
|
||||||
when: _garage_webui_htpasswd_active
|
|
||||||
|
|
||||||
- name: Verify cached webui htpasswd hash still matches password
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv:
|
|
||||||
- htpasswd
|
|
||||||
- -vbB
|
|
||||||
- "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
|
||||||
- "{{ garage_webui_username }}"
|
|
||||||
- "{{ garage_webui_password }}"
|
|
||||||
register: _garage_webui_htpasswd_verify
|
|
||||||
failed_when: false
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
when:
|
|
||||||
- _garage_webui_htpasswd_active
|
|
||||||
- _garage_webui_htpasswd_cached.content is defined
|
|
||||||
|
|
||||||
- name: Generate bcrypt hash for webui password using htpasswd
|
- name: Generate bcrypt hash for webui password using htpasswd
|
||||||
ansible.builtin.command:
|
ansible.builtin.shell: |
|
||||||
argv:
|
htpasswd -nbBC 10 "{{ garage_webui_username }}" "{{ garage_webui_password }}"
|
||||||
- htpasswd
|
register: _garage_webui_password_hash
|
||||||
- -nbBC
|
|
||||||
- "10"
|
|
||||||
- "{{ garage_webui_username }}"
|
|
||||||
- "{{ garage_webui_password }}"
|
|
||||||
register: _garage_webui_password_hash_new
|
|
||||||
changed_when: true
|
|
||||||
when:
|
|
||||||
- _garage_webui_htpasswd_active
|
|
||||||
- (_garage_webui_htpasswd_cached.content is not defined)
|
|
||||||
or (_garage_webui_htpasswd_verify.rc | default(1) != 0)
|
|
||||||
|
|
||||||
- name: Persist webui htpasswd hash on disk
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ _garage_webui_password_hash_new.stdout }}\n"
|
|
||||||
dest: "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
|
||||||
mode: '0600'
|
|
||||||
when:
|
|
||||||
- _garage_webui_htpasswd_active
|
|
||||||
- _garage_webui_password_hash_new is changed
|
|
||||||
|
|
||||||
- name: Load current webui htpasswd hash
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
|
||||||
register: _garage_webui_htpasswd_current
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: _garage_webui_htpasswd_active
|
when: garage_webui_enabled
|
||||||
|
|
||||||
- name: Expose current webui htpasswd hash to template
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_garage_webui_password_hash:
|
|
||||||
stdout: "{{ (_garage_webui_htpasswd_current.content | b64decode).strip() }}"
|
|
||||||
when: _garage_webui_htpasswd_active
|
|
||||||
|
|
||||||
- name: Create docker-compose file for garage
|
- name: Create docker-compose file for garage
|
||||||
template:
|
template:
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,11 @@
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage key list
|
command: /garage key list
|
||||||
register: _existing_keys_output
|
register: _existing_keys_output
|
||||||
changed_when: false
|
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Parse existing key names
|
- name: Parse existing key names
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
# `garage key list` columns: ID Created Name Expiration.
|
_existing_keys: "{{ _existing_keys_output.stdout_lines[1:] | select('match', '^GK') | map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+)\\s+.*$', '\\1') | list }}"
|
||||||
# Data rows begin with a GK<hex> key ID; header is "ID Created ..."
|
|
||||||
# and INFO log lines may interleave on stderr (kept separate by
|
|
||||||
# docker_container_exec). Strip ANSI escapes defensively, filter to
|
|
||||||
# GK-prefixed rows, then take the 3rd whitespace-separated field.
|
|
||||||
_existing_keys: "{{ _existing_keys_output.stdout_lines | map('regex_replace', '\\x1b\\[[0-9;]*m', '') | select('match', '^GK[0-9a-fA-F]+') | map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+).*$', '\\1') | list }}"
|
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Create S3 keys
|
- name: Create S3 keys
|
||||||
|
|
@ -33,7 +27,6 @@
|
||||||
command: /garage key info {{ item.name }}
|
command: /garage key info {{ item.name }}
|
||||||
loop: "{{ garage_s3_keys }}"
|
loop: "{{ garage_s3_keys }}"
|
||||||
register: _key_info_results
|
register: _key_info_results
|
||||||
changed_when: false
|
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Extract key IDs from info
|
- name: Extract key IDs from info
|
||||||
|
|
@ -49,21 +42,11 @@
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage bucket list
|
command: /garage bucket list
|
||||||
register: _existing_buckets_output
|
register: _existing_buckets_output
|
||||||
changed_when: false
|
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Parse existing bucket names
|
- name: Parse existing bucket names
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
# `garage bucket list` columns: ID Created Global aliases Local aliases
|
_existing_buckets: "{{ _existing_buckets_output.stdout_lines[2:] | map('split') | map('first') | list }}"
|
||||||
# Data rows start with a hex bucket ID; filter to those and take the
|
|
||||||
# third whitespace-separated field (the global alias = bucket name).
|
|
||||||
_existing_buckets: >-
|
|
||||||
{{
|
|
||||||
_existing_buckets_output.stdout_lines
|
|
||||||
| select('match', '^[0-9a-f]{16}\\s')
|
|
||||||
| map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+).*$', '\\1')
|
|
||||||
| list
|
|
||||||
}}
|
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Get unique bucket names
|
- name: Get unique bucket names
|
||||||
|
|
@ -81,37 +64,12 @@
|
||||||
- item not in _existing_buckets
|
- item not in _existing_buckets
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Get current bucket permissions
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ garage_service_name }}"
|
|
||||||
command: /garage bucket info {{ item.1.name }}
|
|
||||||
loop: "{{ garage_s3_keys | subelements('buckets', skip_missing=True) }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.1.name }}"
|
|
||||||
register: _bucket_info_results
|
|
||||||
changed_when: false
|
|
||||||
when: garage_s3_keys | length > 0
|
|
||||||
|
|
||||||
- name: Set bucket permissions using key IDs
|
- name: Set bucket permissions using key IDs
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage bucket allow {{ item.item.1.name }} {% for perm in item.item.1.permissions %}--{{ perm }} {% endfor %}--key {{ _key_id_map[item.item.0.name] }}
|
command: /garage bucket allow {{ item.1.name }} {% for perm in item.1.permissions %}--{{ perm }} {% endfor %}--key {{ _key_id_map[item.0.name] }}
|
||||||
loop: "{{ _bucket_info_results.results }}"
|
loop: "{{ garage_s3_keys | subelements('buckets', skip_missing=True) }}"
|
||||||
loop_control:
|
when: garage_s3_keys | length > 0
|
||||||
label: "{{ item.item.1.name }} -> {{ item.item.0.name }}"
|
|
||||||
when:
|
|
||||||
- garage_s3_keys | length > 0
|
|
||||||
- >-
|
|
||||||
(item.stdout | regex_search(
|
|
||||||
'(?m)^\s*' ~ _wanted_flags ~ '\s+' ~ _key_id_map[item.item.0.name]
|
|
||||||
)) is none
|
|
||||||
vars:
|
|
||||||
_wanted_flags: >-
|
|
||||||
{{
|
|
||||||
('R' if 'read' in item.item.1.permissions else '-')
|
|
||||||
~ ('W' if 'write' in item.item.1.permissions else '-')
|
|
||||||
~ ('O' if 'owner' in item.item.1.permissions else '-')
|
|
||||||
}}
|
|
||||||
|
|
||||||
# Export key credentials for use by other roles
|
# Export key credentials for use by other roles
|
||||||
- name: Get detailed key information for all keys
|
- name: Get detailed key information for all keys
|
||||||
|
|
@ -120,7 +78,6 @@
|
||||||
command: /garage key info {{ item.name }} --show-secret
|
command: /garage key info {{ item.name }} --show-secret
|
||||||
loop: "{{ garage_s3_keys }}"
|
loop: "{{ garage_s3_keys }}"
|
||||||
register: _key_details_results
|
register: _key_details_results
|
||||||
changed_when: false
|
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Build garage S3 credentials map
|
- name: Build garage S3 credentials map
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,10 @@ services:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ garage_traefik_network }}
|
- traefik.docker.network={{ garage_traefik_network }}
|
||||||
# S3 API endpoint
|
# S3 API endpoint
|
||||||
- traefik.http.routers.{{ garage_service_name }}.rule={% for d in garage_s3_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
- traefik.http.routers.{{ garage_service_name }}.rule=Host(`{{ garage_s3_domain }}`)
|
||||||
{% if garage_use_ssl %}
|
{% if garage_use_ssl %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ garage_service_name }}.tls=true
|
- traefik.http.routers.{{ garage_service_name }}.tls=true
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.{{ garage_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ garage_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -38,9 +35,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
API_BASE_URL: "http://{{ garage_service_name }}:{{ garage_admin_port }}"
|
API_BASE_URL: "http://{{ garage_service_name }}:{{ garage_admin_port }}"
|
||||||
S3_ENDPOINT_URL: "http://{{ garage_service_name }}:{{ garage_s3_api_port }}"
|
S3_ENDPOINT_URL: "http://{{ garage_service_name }}:{{ garage_s3_api_port }}"
|
||||||
{% if not (garage_webui_authentik_forward_auth | default(false)) %}
|
|
||||||
AUTH_USER_PASS: '{{ _garage_webui_password_hash.stdout | replace("$", "$$") }}'
|
AUTH_USER_PASS: '{{ _garage_webui_password_hash.stdout | replace("$", "$$") }}'
|
||||||
{% endif %}
|
|
||||||
volumes:
|
volumes:
|
||||||
- {{ garage_docker_compose_dir }}/garage.toml:/etc/garage.toml:ro
|
- {{ garage_docker_compose_dir }}/garage.toml:/etc/garage.toml:ro
|
||||||
networks:
|
networks:
|
||||||
|
|
@ -53,25 +48,12 @@ services:
|
||||||
{% if garage_use_ssl %}
|
{% if garage_use_ssl %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure
|
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.tls=true
|
- traefik.http.routers.{{ garage_service_name }}-console.tls=true
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=web
|
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.service={{ garage_service_name }}-console
|
- traefik.http.routers.{{ garage_service_name }}-console.service={{ garage_service_name }}-console
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.priority=10
|
- traefik.http.routers.{{ garage_service_name }}-console.priority=10
|
||||||
- traefik.http.services.{{ garage_service_name }}-console.loadbalancer.server.port={{ garage_webui_port }}
|
- traefik.http.services.{{ garage_service_name }}-console.loadbalancer.server.port={{ garage_webui_port }}
|
||||||
{% if garage_webui_authentik_forward_auth | default(false) %}
|
|
||||||
# ForwardAuth via the authentik embedded outpost. Unauthenticated
|
|
||||||
# requests are redirected to authentik; authentik then forwards
|
|
||||||
# X-Authentik-* identity headers downstream. htpasswd is disabled
|
|
||||||
# in the env block above so authentik is the only gate.
|
|
||||||
- traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.address={{ garage_webui_authentik_forward_auth_url }}
|
|
||||||
- traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.trustForwardHeader=true
|
|
||||||
- traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
|
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.middlewares={{ garage_service_name }}-console-authentik
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ rpc_secret = "{{ garage_rpc_secret }}"
|
||||||
[s3_api]
|
[s3_api]
|
||||||
s3_region = "{{ garage_s3_region }}"
|
s3_region = "{{ garage_s3_region }}"
|
||||||
api_bind_addr = "[::]:{{ garage_s3_api_port }}"
|
api_bind_addr = "[::]:{{ garage_s3_api_port }}"
|
||||||
root_domain = ".s3.{{ garage_s3_domains[0] }}"
|
root_domain = ".s3.{{ garage_s3_domain }}"
|
||||||
|
|
||||||
[s3_web]
|
[s3_web]
|
||||||
bind_addr = "[::]:{{ garage_s3_web_port }}"
|
bind_addr = "[::]:{{ garage_s3_web_port }}"
|
||||||
|
|
|
||||||
|
|
@ -1,251 +0,0 @@
|
||||||
# homarr
|
|
||||||
|
|
||||||
Deploy [Homarr](https://github.com/homarr-labs/homarr) as a self-contained
|
|
||||||
Docker Compose stack behind Traefik, with seeded admin user, OIDC group
|
|
||||||
and customizable application tiles.
|
|
||||||
|
|
||||||
## What this role does
|
|
||||||
|
|
||||||
- Deploys the official Homarr container with Traefik labels
|
|
||||||
- Seeds the SQLite database with:
|
|
||||||
- server settings (locale, analytics, crawling, default board)
|
|
||||||
- a default board with the three layouts (desktop/tablet/mobile)
|
|
||||||
- a local admin user with bcrypt-hashed password
|
|
||||||
- OIDC and credentials admin groups with full permissions
|
|
||||||
- application tiles defined in `homarr_apps`, auto-laid-out across all
|
|
||||||
three screen sizes via the bundled `homarr_compute_layouts` filter
|
|
||||||
- Skips the onboarding wizard so the instance is usable right after deploy
|
|
||||||
- Restarts the container via handler when the seed or compose file changes
|
|
||||||
|
|
||||||
## What this role does NOT do
|
|
||||||
|
|
||||||
- Does not configure OIDC end-to-end — set `homarr_oidc_*` variables and
|
|
||||||
configure the corresponding client in your identity provider
|
|
||||||
- Does not migrate existing Homarr databases — only seeds empty ones
|
|
||||||
- Does not create users beyond the single local admin (OIDC users are
|
|
||||||
provisioned on first login)
|
|
||||||
|
|
||||||
## Required variables
|
|
||||||
|
|
||||||
Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real
|
|
||||||
secrets to version control.**
|
|
||||||
|
|
||||||
| Variable | Format | Generate with |
|
|
||||||
|---|---|---|
|
|
||||||
| `homarr_secret_encryption_key` | 64-char hex string | `openssl rand -hex 32` |
|
|
||||||
| `homarr_admin_password` | strong password | `openssl rand -base64 24` |
|
|
||||||
| `homarr_oidc_client_secret` | from your identity provider | — |
|
|
||||||
|
|
||||||
`homarr_oidc_client_secret` is only required when `oidc` is in
|
|
||||||
`homarr_auth_providers`; the role asserts it then. The encryption key is
|
|
||||||
always required — the `assert` task at the top of the role fails fast if it
|
|
||||||
is missing or malformed.
|
|
||||||
|
|
||||||
## Configurable variables
|
|
||||||
|
|
||||||
See `defaults/main.yml` for the full list. Most useful overrides:
|
|
||||||
|
|
||||||
| Variable | Default | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `homarr_domain` | `homarr.local.test` | Traefik Host rule |
|
|
||||||
| `homarr_extra_domains` | `[]` | Extra Host-rule hostnames (OR-combined), e.g. internal `*.int.*` FQDN |
|
|
||||||
| `homarr_extra_hosts` | `[]` | Container `/etc/hosts` overrides (`host:ip`) — pin IdP FQDN to LAN IP |
|
|
||||||
| `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL |
|
|
||||||
| `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both |
|
|
||||||
| `homarr_oidc_issuer` | empty | Identity provider issuer URL |
|
|
||||||
| `homarr_oidc_client_id` | empty | OIDC client id |
|
|
||||||
| `homarr_oidc_admin_group` | `homarr-admins` | Group granting admin role |
|
|
||||||
| `homarr_apps` | `[]` | List of application tiles, see below |
|
|
||||||
|
|
||||||
## Application tiles
|
|
||||||
|
|
||||||
`homarr_apps` is a list of tile definitions that are seeded into the
|
|
||||||
default board. Each entry needs:
|
|
||||||
|
|
||||||
| Field | Required | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | yes | Unique slug, used as `app-<id>` and `item-<id>` |
|
|
||||||
| `name` | yes | Display name |
|
|
||||||
| `icon` | yes | Icon URL |
|
|
||||||
| `href` | yes | Click target |
|
|
||||||
| `width` | yes | Tile width in grid cells (1–10) |
|
|
||||||
| `description` | no | Tooltip / subtitle |
|
|
||||||
| `height` | no | Tile height (default `1`) |
|
|
||||||
|
|
||||||
The role validates that all `id` values are unique.
|
|
||||||
|
|
||||||
### Auto-layout
|
|
||||||
|
|
||||||
Tiles are packed left-to-right into three layouts:
|
|
||||||
|
|
||||||
- **Desktop**: 10 columns
|
|
||||||
- **Tablet**: 6 columns
|
|
||||||
- **Mobile**: 2 columns
|
|
||||||
|
|
||||||
When a tile does not fit the remaining width of a row, it wraps to the
|
|
||||||
next row. Tile width is clamped to the grid width (a tile with
|
|
||||||
`width: 8` becomes `width: 6` on tablet and `width: 2` on mobile).
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
homarr_apps:
|
|
||||||
- id: nextcloud
|
|
||||||
name: Nextcloud
|
|
||||||
description: Cloud Storage & Collaboration
|
|
||||||
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png
|
|
||||||
href: https://cloud.example.com
|
|
||||||
width: 2
|
|
||||||
- id: keycloak
|
|
||||||
name: Keycloak
|
|
||||||
description: Identity & Access Management
|
|
||||||
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png
|
|
||||||
href: https://auth.example.com
|
|
||||||
width: 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layout filter plugin
|
|
||||||
|
|
||||||
The grid-packing algorithm that places tiles on the desktop, tablet
|
|
||||||
and mobile layouts lives in `filter_plugins/homarr_layout.py` rather
|
|
||||||
than inside the Jinja seed template. This keeps the SQL template
|
|
||||||
readable and lets the algorithm be unit-tested in isolation.
|
|
||||||
|
|
||||||
The filter is invoked once from `tasks/main.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Compute Homarr app layouts
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces a `homarr_layout` fact with two keys, both consumed by
|
|
||||||
`templates/homarr_seed.sql.j2`:
|
|
||||||
|
|
||||||
| Key | Shape | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `apps` | list, same order as `homarr_apps` | each entry gains `desktop`/`tablet`/`mobile` dicts of `{x, y, w, h}` |
|
|
||||||
| `section_height` | dict with `desktop`, `tablet`, `mobile` | minimum height of the parent section so all tiles fit |
|
|
||||||
|
|
||||||
The filter signature accepts custom column counts if Homarr ever
|
|
||||||
changes the breakpoint widths:
|
|
||||||
|
|
||||||
```jinja
|
|
||||||
{{ homarr_apps | digitalboard.core.homarr_compute_layouts(desktop_cols=12, tablet_cols=8, mobile_cols=4) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
To debug a layout without running the full deploy, run the play with
|
|
||||||
`-vv` — the `Show computed app layouts` task dumps the full
|
|
||||||
`homarr_layout` fact.
|
|
||||||
|
|
||||||
### Running the filter tests
|
|
||||||
|
|
||||||
The filter is covered by unit tests in
|
|
||||||
`filter_plugins/tests/test_homarr_layout.py`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install pytest ansible-core
|
|
||||||
pytest filter_plugins/tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
15 tests cover packing, width clamping, height/section-height,
|
|
||||||
input validation and custom grid sizes.
|
|
||||||
|
|
||||||
## First login
|
|
||||||
|
|
||||||
After the role completes, log in at `{{ homarr_base_url }}` with:
|
|
||||||
|
|
||||||
- Username: value of `homarr_admin_username` (default `admin`)
|
|
||||||
- Password: value of `homarr_admin_password`
|
|
||||||
|
|
||||||
OIDC users are provisioned on first login if their identity provider
|
|
||||||
group matches `homarr_oidc_admin_group`. They receive admin permissions
|
|
||||||
automatically through the seeded `group-oidc-admins` group.
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Deploy Homarr service
|
|
||||||
hosts: homarr_servers
|
|
||||||
become: true
|
|
||||||
roles:
|
|
||||||
- digitalboard.core.homarr
|
|
||||||
```
|
|
||||||
|
|
||||||
With inventory variables:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# inventories/<env>/group_vars/homarr_servers.yml
|
|
||||||
homarr_domain: home.digitalboard.ch
|
|
||||||
homarr_base_url: "https://home.digitalboard.ch"
|
|
||||||
|
|
||||||
homarr_auth_providers: "credentials,oidc"
|
|
||||||
homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard"
|
|
||||||
homarr_oidc_client_id: "homarr-digitalboard"
|
|
||||||
homarr_oidc_client_name: "Digitalboard"
|
|
||||||
|
|
||||||
homarr_secret_encryption_key: >-
|
|
||||||
{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/homarr',
|
|
||||||
mount_point='kv').data.data.encryption_key }}
|
|
||||||
homarr_admin_password: >-
|
|
||||||
{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/homarr',
|
|
||||||
mount_point='kv').data.data.admin_password }}
|
|
||||||
homarr_oidc_client_secret: >-
|
|
||||||
{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/homarr',
|
|
||||||
mount_point='kv').data.data.oidc_client_secret }}
|
|
||||||
|
|
||||||
homarr_apps:
|
|
||||||
- id: nextcloud
|
|
||||||
name: Nextcloud
|
|
||||||
description: Cloud Storage
|
|
||||||
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png
|
|
||||||
href: https://cloud.digitalboard.ch
|
|
||||||
width: 2
|
|
||||||
- id: keycloak
|
|
||||||
name: Keycloak
|
|
||||||
description: Identity & Access Management
|
|
||||||
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png
|
|
||||||
href: https://auth.digitalboard.ch
|
|
||||||
width: 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Re-running the role
|
|
||||||
|
|
||||||
The role is idempotent for the typical re-run case:
|
|
||||||
|
|
||||||
- The seed only runs when no local admin user exists in the database
|
|
||||||
- The compose file and seed template are deployed via `template`,
|
|
||||||
which only changes content when the inputs change
|
|
||||||
- The restart handler only fires when one of those templates changes
|
|
||||||
|
|
||||||
If you need to re-seed an existing database (for example after deleting
|
|
||||||
the database file to apply schema changes), the role will detect the
|
|
||||||
fresh database and seed it again on the next run.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Login fails after deploy.** Verify that the bcrypt hash was written
|
|
||||||
correctly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sqlite3 /srv/data/homarr/homarr/appdata/db/db.sqlite \
|
|
||||||
"SELECT id, name, email, length(password), provider FROM user;"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: one row with `user-local-admin`, password length 60,
|
|
||||||
provider `credentials`.
|
|
||||||
|
|
||||||
**Encryption key validation fails.** The key must be exactly 64
|
|
||||||
characters and contain only hex digits (`[a-fA-F0-9]`). Both upper-
|
|
||||||
and lowercase are accepted.
|
|
||||||
|
|
||||||
**App tiles overlap.** Check `homarr_apps` for duplicate `id` values.
|
|
||||||
The role validates this, but if you bypass the check, the seed will
|
|
||||||
still run and Homarr will display only one of the duplicates.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT-0
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# defaults file for homarr
|
|
||||||
|
|
||||||
# Base directory configuration (inherited from base role or defined here)
|
|
||||||
docker_compose_base_dir: /etc/docker/compose
|
|
||||||
docker_volume_base_dir: /srv/data
|
|
||||||
|
|
||||||
# homarr-specific configuration
|
|
||||||
homarr_base_path: /srv/data/homarr
|
|
||||||
homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/homarr"
|
|
||||||
homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/homarr"
|
|
||||||
homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/homarr/appdata"
|
|
||||||
homarr_db: "{{ homarr_appdata_dir }}/db/db.sqlite"
|
|
||||||
|
|
||||||
# Service configuration
|
|
||||||
homarr_domain: "homarr.local.test"
|
|
||||||
# Additional hostnames the homarr router answers on (e.g. an internal
|
|
||||||
# *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered
|
|
||||||
# by the cert).
|
|
||||||
homarr_extra_domains: []
|
|
||||||
# Extra /etc/hosts entries inside the homarr container (format "host:ip").
|
|
||||||
# Used to pin the IdP's public FQDN to a LAN IP so OIDC discovery stays
|
|
||||||
# in-network while the issuer URL matches what browsers see.
|
|
||||||
homarr_extra_hosts: []
|
|
||||||
homarr_image: "ghcr.io/homarr-labs/homarr:latest"
|
|
||||||
homarr_port: 7575
|
|
||||||
homarr_use_docker: false
|
|
||||||
|
|
||||||
# REQUIRED: 64-character hex string used to encrypt integration credentials.
|
|
||||||
# Generate with: openssl rand -hex 32
|
|
||||||
# Provide via OpenBao lookup, Ansible Vault, or extra-vars.
|
|
||||||
# Never commit a real key to version control.
|
|
||||||
homarr_secret_encryption_key: ""
|
|
||||||
|
|
||||||
# URL — used for BASE_URL, NEXTAUTH_URL and the completion message
|
|
||||||
homarr_base_url: "https://home.local.test"
|
|
||||||
|
|
||||||
# Auth providers (comma-separated): credentials, oidc, ldap
|
|
||||||
homarr_auth_providers: "credentials"
|
|
||||||
|
|
||||||
# OIDC configuration (only used when 'oidc' is in homarr_auth_providers)
|
|
||||||
homarr_oidc_issuer: ""
|
|
||||||
homarr_oidc_client_id: ""
|
|
||||||
homarr_oidc_client_name: ""
|
|
||||||
homarr_oidc_scopes: "openid profile email groups"
|
|
||||||
homarr_oidc_groups_attribute: "groups"
|
|
||||||
homarr_oidc_client_secret: ""
|
|
||||||
homarr_oidc_auto_login: "false"
|
|
||||||
|
|
||||||
# OIDC admin group (must exist in the identity provider)
|
|
||||||
homarr_oidc_admin_group: "homarr-admins"
|
|
||||||
|
|
||||||
# Board configuration
|
|
||||||
homarr_default_board_name: "Home"
|
|
||||||
homarr_default_board_public: true
|
|
||||||
|
|
||||||
# Traefik configuration
|
|
||||||
homarr_traefik_network: "proxy"
|
|
||||||
homarr_use_ssl: true
|
|
||||||
|
|
||||||
# Local admin (override in inventory or via vault)
|
|
||||||
homarr_admin_username: "admin"
|
|
||||||
homarr_admin_email: "admin@example.com"
|
|
||||||
homarr_admin_password: "ChangeMe123!"
|
|
||||||
|
|
||||||
# Applications shown on the default board.
|
|
||||||
# Override in your project/inventory vars. Each app needs:
|
|
||||||
# id, name, icon, href, width (1-10). Optional: description, height (default 1).
|
|
||||||
# Apps are automatically packed left-to-right into the desktop grid (10 cols),
|
|
||||||
# scaled to tablet (6 cols) and mobile (2 cols).
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# homarr_apps:
|
|
||||||
# - id: nextcloud
|
|
||||||
# name: Nextcloud
|
|
||||||
# description: Cloud Storage & Collaboration
|
|
||||||
# icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png
|
|
||||||
# href: https://cloud.example.com
|
|
||||||
# width: 2
|
|
||||||
# height: 1
|
|
||||||
homarr_apps: []
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
|
|
||||||
"""Unit tests for the homarr_layout filter plugin.
|
|
||||||
|
|
||||||
Run from the role root:
|
|
||||||
|
|
||||||
pytest filter_plugins/tests/
|
|
||||||
|
|
||||||
Requires `pytest` and `ansible-core` in the environment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
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__), '..', '..', '..', '..',
|
|
||||||
'plugins', 'filter')
|
|
||||||
)
|
|
||||||
|
|
||||||
import pytest # noqa: E402
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleFilterError # noqa: E402
|
|
||||||
from homarr_layout import homarr_compute_layouts # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _app(app_id, width, height=1):
|
|
||||||
"""Build a minimal app dict for tests."""
|
|
||||||
return {
|
|
||||||
'id': app_id,
|
|
||||||
'name': app_id.title(),
|
|
||||||
'icon': 'https://example.com/{0}.png'.format(app_id),
|
|
||||||
'href': 'https://{0}.example.com'.format(app_id),
|
|
||||||
'width': width,
|
|
||||||
'height': height,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Happy-path packing
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_empty_apps_returns_empty_list_and_min_height():
|
|
||||||
result = homarr_compute_layouts([])
|
|
||||||
assert result['apps'] == []
|
|
||||||
# Even an empty grid keeps section_height >= 1 so the section
|
|
||||||
# renders in the UI.
|
|
||||||
assert result['section_height'] == {
|
|
||||||
'desktop': 1, 'tablet': 1, 'mobile': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_app_positioned_at_origin_in_all_grids():
|
|
||||||
result = homarr_compute_layouts([_app('a', width=2)])
|
|
||||||
a = result['apps'][0]
|
|
||||||
assert a['desktop'] == {'x': 0, 'y': 0, 'w': 2, 'h': 1}
|
|
||||||
assert a['tablet'] == {'x': 0, 'y': 0, 'w': 2, 'h': 1}
|
|
||||||
assert a['mobile'] == {'x': 0, 'y': 0, 'w': 2, 'h': 1}
|
|
||||||
|
|
||||||
|
|
||||||
def test_original_app_keys_are_preserved():
|
|
||||||
apps = [_app('nextcloud', width=2)]
|
|
||||||
apps[0]['description'] = 'Cloud Storage'
|
|
||||||
result = homarr_compute_layouts(apps)
|
|
||||||
a = result['apps'][0]
|
|
||||||
# Original fields survive the layout enrichment.
|
|
||||||
assert a['name'] == 'Nextcloud'
|
|
||||||
assert a['description'] == 'Cloud Storage'
|
|
||||||
assert a['icon'] == 'https://example.com/nextcloud.png'
|
|
||||||
|
|
||||||
|
|
||||||
def test_desktop_wraps_after_filling_row():
|
|
||||||
# 5 apps of width 2 fill the 10-col desktop row exactly; 6th wraps.
|
|
||||||
apps = [_app('a{0}'.format(i), width=2) for i in range(6)]
|
|
||||||
result = homarr_compute_layouts(apps)
|
|
||||||
assert result['apps'][4]['desktop'] == {'x': 8, 'y': 0, 'w': 2, 'h': 1}
|
|
||||||
assert result['apps'][5]['desktop'] == {'x': 0, 'y': 1, 'w': 2, 'h': 1}
|
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_wraps_after_every_app():
|
|
||||||
# Mobile is only 2 cols wide → every app of width 2 starts a new row.
|
|
||||||
apps = [_app('a{0}'.format(i), width=2) for i in range(3)]
|
|
||||||
result = homarr_compute_layouts(apps)
|
|
||||||
assert [a['mobile']['y'] for a in result['apps']] == [0, 1, 2]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Width clamping
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_width_clamped_per_grid():
|
|
||||||
result = homarr_compute_layouts([_app('big', width=8)])
|
|
||||||
# Desktop has room (8 <= 10), tablet clamps to 6, mobile clamps to 2.
|
|
||||||
a = result['apps'][0]
|
|
||||||
assert a['desktop']['w'] == 8
|
|
||||||
assert a['tablet']['w'] == 6
|
|
||||||
assert a['mobile']['w'] == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_width_larger_than_desktop_still_clamps():
|
|
||||||
# A pathological width=20 still works — it just becomes a full-width
|
|
||||||
# tile on every grid.
|
|
||||||
result = homarr_compute_layouts([_app('huge', width=20)])
|
|
||||||
a = result['apps'][0]
|
|
||||||
assert a['desktop']['w'] == 10
|
|
||||||
assert a['tablet']['w'] == 6
|
|
||||||
assert a['mobile']['w'] == 2
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Height handling
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_section_height_grows_with_rows():
|
|
||||||
# 6 apps of width 2 on desktop → 5 in row 1, 1 in row 2.
|
|
||||||
apps = [_app('a{0}'.format(i), width=2) for i in range(6)]
|
|
||||||
result = homarr_compute_layouts(apps)
|
|
||||||
assert result['section_height']['desktop'] == 2
|
|
||||||
# On mobile every app is on its own row.
|
|
||||||
assert result['section_height']['mobile'] == 6
|
|
||||||
|
|
||||||
|
|
||||||
def test_tall_app_extends_row_height():
|
|
||||||
apps = [
|
|
||||||
_app('tall', width=2, height=3),
|
|
||||||
_app('short', width=2, height=1),
|
|
||||||
]
|
|
||||||
result = homarr_compute_layouts(apps)
|
|
||||||
# Both fit in row 0 horizontally, but the section must be 3 tall.
|
|
||||||
assert result['section_height']['desktop'] == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_tall_app_pushes_subsequent_row_down():
|
|
||||||
# tall (h=3) fills full desktop width → next app wraps to y=3.
|
|
||||||
result = homarr_compute_layouts([
|
|
||||||
_app('tall', width=10, height=3),
|
|
||||||
_app('next', width=2, height=1),
|
|
||||||
])
|
|
||||||
assert result['apps'][1]['desktop'] == {'x': 0, 'y': 3, 'w': 2, 'h': 1}
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Input validation
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_rejects_non_list_input():
|
|
||||||
with pytest.raises(AnsibleFilterError, match='expected a list'):
|
|
||||||
homarr_compute_layouts('not a list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_non_dict_entry():
|
|
||||||
with pytest.raises(AnsibleFilterError, match='not a dict'):
|
|
||||||
homarr_compute_layouts(['just a string'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_app_without_id():
|
|
||||||
with pytest.raises(AnsibleFilterError, match="missing required key 'id'"):
|
|
||||||
homarr_compute_layouts([{'name': 'no id', 'width': 2}])
|
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_app_without_width():
|
|
||||||
with pytest.raises(AnsibleFilterError,
|
|
||||||
match="missing required key 'width'"):
|
|
||||||
homarr_compute_layouts([{'id': 'no-width', 'name': 'x'}])
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Configurable grid sizes
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_custom_grid_sizes():
|
|
||||||
# If Homarr ever switches to 12-col desktop, the filter still works.
|
|
||||||
result = homarr_compute_layouts(
|
|
||||||
[_app('a', width=4), _app('b', width=4), _app('c', width=4)],
|
|
||||||
desktop_cols=12, tablet_cols=8, mobile_cols=4,
|
|
||||||
)
|
|
||||||
# All three fit in desktop row 0 (4+4+4 = 12).
|
|
||||||
assert [a['desktop']['x'] for a in result['apps']] == [0, 4, 8]
|
|
||||||
assert result['section_height']['desktop'] == 1
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# handlers file for homarr
|
|
||||||
|
|
||||||
- name: restart homarr
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ homarr_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
galaxy_info:
|
|
||||||
author: digitalboard
|
|
||||||
description: Deploy the Homarr dashboard via Docker Compose behind Traefik, with seeded admin user and OIDC group
|
|
||||||
company: Digitalboard
|
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
|
||||||
|
|
||||||
platforms:
|
|
||||||
- name: Debian
|
|
||||||
versions:
|
|
||||||
- bookworm
|
|
||||||
- name: Ubuntu
|
|
||||||
versions:
|
|
||||||
- jammy
|
|
||||||
- noble
|
|
||||||
|
|
||||||
galaxy_tags:
|
|
||||||
- homarr
|
|
||||||
- dashboard
|
|
||||||
- oidc
|
|
||||||
- docker
|
|
||||||
- traefik
|
|
||||||
- digitalboard
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# tasks file for homarr
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 0. VALIDATION
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Validate encryption key
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- homarr_secret_encryption_key | length == 64
|
|
||||||
- homarr_secret_encryption_key is match('^[a-fA-F0-9]+$')
|
|
||||||
fail_msg: >-
|
|
||||||
homarr_secret_encryption_key must be a 64-character hex string.
|
|
||||||
Generate with: openssl rand -hex 32
|
|
||||||
Provide via OpenBao, Ansible Vault or extra-vars.
|
|
||||||
success_msg: Encryption key validation passed
|
|
||||||
|
|
||||||
- name: Validate OIDC configuration when enabled
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- homarr_oidc_client_secret | length > 0
|
|
||||||
fail_msg: >-
|
|
||||||
homarr_oidc_client_secret must be set when 'oidc' is in homarr_auth_providers.
|
|
||||||
Set via OpenBao or remove 'oidc' from homarr_auth_providers.
|
|
||||||
when: "'oidc' in homarr_auth_providers"
|
|
||||||
|
|
||||||
- name: Validate homarr_apps have unique ids
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- homarr_apps | map(attribute='id') | list | length ==
|
|
||||||
homarr_apps | map(attribute='id') | unique | list | length
|
|
||||||
fail_msg: >-
|
|
||||||
homarr_apps contains duplicate ids.
|
|
||||||
Each app must have a unique 'id'. Got:
|
|
||||||
{{ homarr_apps | map(attribute='id') | list }}
|
|
||||||
success_msg: All app ids are unique
|
|
||||||
when: homarr_apps | length > 0
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 1. PREPARATION: packages and directories before container start
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Ensure required packages are installed
|
|
||||||
ansible.builtin.package:
|
|
||||||
name:
|
|
||||||
- sqlite3
|
|
||||||
- python3-docker
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create docker compose directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ homarr_docker_compose_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: '0755'
|
|
||||||
|
|
||||||
- name: Create Homarr data directories
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: directory
|
|
||||||
owner: "1000"
|
|
||||||
group: "1000"
|
|
||||||
mode: "0755"
|
|
||||||
loop:
|
|
||||||
- "{{ homarr_appdata_dir }}"
|
|
||||||
- "{{ homarr_appdata_dir }}/db"
|
|
||||||
|
|
||||||
- name: Check if database already exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ homarr_db }}"
|
|
||||||
register: db_exists
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 2. START CONTAINER
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Create docker-compose file for homarr
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: docker-compose.yml.j2
|
|
||||||
dest: "{{ homarr_docker_compose_dir }}/docker-compose.yml"
|
|
||||||
mode: '0644'
|
|
||||||
|
|
||||||
- name: Start homarr containers
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ homarr_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 3. WAIT FOR DATABASE
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Wait for database to be created by Homarr
|
|
||||||
ansible.builtin.wait_for:
|
|
||||||
path: "{{ homarr_db }}"
|
|
||||||
state: present
|
|
||||||
timeout: 60
|
|
||||||
when: not db_exists.stat.exists
|
|
||||||
|
|
||||||
- name: Wait for database schema to be initialized
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: sqlite3 "{{ homarr_db }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';"
|
|
||||||
register: schema_check
|
|
||||||
until: schema_check.stdout == "board"
|
|
||||||
retries: 30
|
|
||||||
delay: 2
|
|
||||||
changed_when: false
|
|
||||||
when: not db_exists.stat.exists
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 4. GENERATE BCRYPT HASH (on controller, not on target)
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Generate bcrypt hash for admin password
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
# Deterministic salt derived from the password's SHA-256 digest so the
|
|
||||||
# hash stays stable across runs (idempotent — no spurious template
|
|
||||||
# changes / container restarts when the password is unchanged). The
|
|
||||||
# bcrypt salt alphabet is [./A-Za-z0-9]; the digest's hex chars are
|
|
||||||
# a strict subset, so we just take the first 22.
|
|
||||||
homarr_bcrypt_hash: >-
|
|
||||||
{{ homarr_admin_password
|
|
||||||
| password_hash('bcrypt', rounds=10,
|
|
||||||
salt=(homarr_admin_password
|
|
||||||
| hash('sha256'))[:22]) }}
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 5. COMPUTE APP LAYOUTS
|
|
||||||
# =====================================================================
|
|
||||||
# Packing is done by the homarr_compute_layouts filter plugin (Python)
|
|
||||||
# rather than inline Jinja, so the seed template stays readable and the
|
|
||||||
# packing algorithm can be unit-tested in isolation.
|
|
||||||
|
|
||||||
- name: Compute Homarr app layouts
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
|
||||||
|
|
||||||
- name: Show computed app layouts
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: homarr_layout
|
|
||||||
verbosity: 1
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 6. SEED DATABASE (only if local admin user does not exist yet)
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Check if local admin user exists
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: sqlite3 "{{ homarr_db }}" "SELECT id FROM user WHERE id='user-local-admin';"
|
|
||||||
register: admin_exists
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Seed Homarr database
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: sqlite3 "{{ homarr_db }}"
|
|
||||||
stdin: "{{ lookup('template', 'homarr_seed.sql.j2') }}"
|
|
||||||
register: seed_result
|
|
||||||
changed_when: seed_result.rc == 0
|
|
||||||
when: admin_exists.stdout == ""
|
|
||||||
notify: restart homarr
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#---------------------------------------------------------------------#
|
|
||||||
# Homarr — A simple, yet powerful dashboard for your server. #
|
|
||||||
#---------------------------------------------------------------------#
|
|
||||||
services:
|
|
||||||
homarr:
|
|
||||||
container_name: homarr
|
|
||||||
image: {{ homarr_image }}
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
{% if homarr_use_docker %}
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
{% endif %}
|
|
||||||
- {{ homarr_docker_volume_dir }}/homarr/appdata:/appdata
|
|
||||||
environment:
|
|
||||||
TZ: "Europe/Zurich"
|
|
||||||
BASE_URL: "{{ homarr_base_url }}"
|
|
||||||
NEXTAUTH_URL: "{{ homarr_base_url }}"
|
|
||||||
SECRET_ENCRYPTION_KEY: "{{ homarr_secret_encryption_key }}"
|
|
||||||
AUTH_PROVIDERS: "{{ homarr_auth_providers }}"
|
|
||||||
AUTH_OIDC_ISSUER: "{{ homarr_oidc_issuer }}"
|
|
||||||
AUTH_OIDC_CLIENT_ID: "{{ homarr_oidc_client_id }}"
|
|
||||||
AUTH_OIDC_CLIENT_SECRET: "{{ homarr_oidc_client_secret }}"
|
|
||||||
AUTH_OIDC_CLIENT_NAME: "{{ homarr_oidc_client_name | default('Keycloak') }}"
|
|
||||||
AUTH_OIDC_SCOPE_OVERWRITE: "{{ homarr_oidc_scopes | default('openid email profile groups') }}"
|
|
||||||
AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ homarr_oidc_groups_attribute | default('groups') }}"
|
|
||||||
AUTH_OIDC_AUTO_LOGIN: "{{ homarr_oidc_auto_login | default('false') }}"
|
|
||||||
networks:
|
|
||||||
- {{ homarr_traefik_network }}
|
|
||||||
{% if homarr_extra_hosts | default([]) | length > 0 %}
|
|
||||||
extra_hosts:
|
|
||||||
{% for h in homarr_extra_hosts %}
|
|
||||||
- "{{ h }}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.docker.network={{ homarr_traefik_network }}
|
|
||||||
- traefik.http.routers.homarr.rule={% set _all_domains = [homarr_domain] + (homarr_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
|
||||||
{% if homarr_use_ssl %}
|
|
||||||
- traefik.http.routers.homarr.entrypoints=websecure
|
|
||||||
- traefik.http.routers.homarr.tls=true
|
|
||||||
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
||||||
- traefik.http.routers.homarr.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
- traefik.http.routers.homarr.entrypoints=web
|
|
||||||
{% endif %}
|
|
||||||
- traefik.http.services.homarr.loadbalancer.server.port={{ homarr_port }}
|
|
||||||
networks:
|
|
||||||
{{ homarr_traefik_network }}:
|
|
||||||
external: true
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
{#-
|
|
||||||
Homarr database seed.
|
|
||||||
|
|
||||||
The packing algorithm previously lived in this template as a Jinja
|
|
||||||
`pack()` macro with from_json/to_json round-trips. It has been
|
|
||||||
extracted to the `homarr_compute_layouts` filter plugin (see
|
|
||||||
filter_plugins/homarr_layout.py) and the result is provided as the
|
|
||||||
`homarr_layout` fact set in tasks/main.yml. This template therefore
|
|
||||||
only renders SQL — no logic.
|
|
||||||
-#}
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
-- =====================================================================
|
|
||||||
-- SERVER SETTINGS
|
|
||||||
-- =====================================================================
|
|
||||||
|
|
||||||
INSERT OR REPLACE INTO serverSetting (setting_key, value)
|
|
||||||
VALUES
|
|
||||||
('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false, "enableUserData": false}}'),
|
|
||||||
('culture', '{"json": {"defaultLocale": "de"}}'),
|
|
||||||
('crawling', '{"json": {"crawlingEnabled": false}}'),
|
|
||||||
('board', '{"json": {"homeBoardId": "board-default", "mobileHomeBoardId": "board-default", "enableStatusByDefault": true, "forceDisableStatus": false, "defaultBoardId": "board-default"}}');
|
|
||||||
|
|
||||||
-- Skip onboarding wizard
|
|
||||||
UPDATE onboarding SET step = 'finish', previous_step = 'settings';
|
|
||||||
|
|
||||||
-- =====================================================================
|
|
||||||
-- GROUPS (must exist before groupMember)
|
|
||||||
-- =====================================================================
|
|
||||||
|
|
||||||
-- OIDC admin group
|
|
||||||
INSERT OR IGNORE INTO "group" (id, name, owner_id, position)
|
|
||||||
VALUES ('group-oidc-admins', '{{ homarr_oidc_admin_group }}', NULL, 0);
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO groupPermission (group_id, permission)
|
|
||||||
VALUES
|
|
||||||
('group-oidc-admins', 'admin'),
|
|
||||||
('group-oidc-admins', 'board-create'),
|
|
||||||
('group-oidc-admins', 'board-full-access'),
|
|
||||||
('group-oidc-admins', 'integration-create'),
|
|
||||||
('group-oidc-admins', 'integration-full-access');
|
|
||||||
|
|
||||||
-- Credentials admin group
|
|
||||||
INSERT OR IGNORE INTO "group" (id, name, owner_id, position)
|
|
||||||
VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1);
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO groupPermission (group_id, permission)
|
|
||||||
VALUES
|
|
||||||
('group-credentials-admin', 'admin'),
|
|
||||||
('group-credentials-admin', 'board-create'),
|
|
||||||
('group-credentials-admin', 'board-full-access'),
|
|
||||||
('group-credentials-admin', 'integration-create'),
|
|
||||||
('group-credentials-admin', 'integration-full-access');
|
|
||||||
|
|
||||||
-- =====================================================================
|
|
||||||
-- LOCAL ADMIN USER
|
|
||||||
-- =====================================================================
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO user (id, name, email, password, email_verified, provider)
|
|
||||||
VALUES (
|
|
||||||
'user-local-admin',
|
|
||||||
'{{ homarr_admin_username }}',
|
|
||||||
'{{ homarr_admin_email }}',
|
|
||||||
'{{ homarr_bcrypt_hash }}',
|
|
||||||
1,
|
|
||||||
'credentials'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Assign admin user to groups
|
|
||||||
INSERT OR IGNORE INTO groupMember (group_id, user_id)
|
|
||||||
VALUES
|
|
||||||
('group-credentials-admin', 'user-local-admin'),
|
|
||||||
('group-oidc-admins', 'user-local-admin');
|
|
||||||
|
|
||||||
-- =====================================================================
|
|
||||||
-- BOARD
|
|
||||||
-- =====================================================================
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO board (
|
|
||||||
id, name, is_public,
|
|
||||||
primary_color, secondary_color, opacity,
|
|
||||||
background_image_attachment, background_image_repeat, background_image_size,
|
|
||||||
item_radius, disable_status
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
'board-default',
|
|
||||||
'{{ homarr_default_board_name }}',
|
|
||||||
{% if homarr_default_board_public %}1{% else %}0{% endif %},
|
|
||||||
'#fa5252',
|
|
||||||
'#fd7e14',
|
|
||||||
100,
|
|
||||||
'fixed',
|
|
||||||
'no-repeat',
|
|
||||||
'cover',
|
|
||||||
'lg',
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Layouts
|
|
||||||
INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint)
|
|
||||||
VALUES
|
|
||||||
('layout-desktop', 'Desktop', 'board-default', 10, 0),
|
|
||||||
('layout-tablet', 'Tablet', 'board-default', 6, 768),
|
|
||||||
('layout-mobile', 'Mobile', 'board-default', 2, 480);
|
|
||||||
|
|
||||||
-- Set home board for admin user (board exists now)
|
|
||||||
UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default'
|
|
||||||
WHERE id = 'user-local-admin';
|
|
||||||
|
|
||||||
-- =====================================================================
|
|
||||||
-- SECTION
|
|
||||||
-- =====================================================================
|
|
||||||
|
|
||||||
DELETE FROM section_layout WHERE section_id = 'section-apps';
|
|
||||||
DELETE FROM item_layout WHERE section_id = 'section-apps';
|
|
||||||
DELETE FROM section WHERE id = 'section-apps';
|
|
||||||
|
|
||||||
INSERT INTO section (id, board_id, kind, x_offset, y_offset, name, options)
|
|
||||||
VALUES (
|
|
||||||
'section-apps',
|
|
||||||
'board-default',
|
|
||||||
'empty',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'Applications',
|
|
||||||
'{"json": {}}'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Section height is sized to fit the computed layout (see
|
|
||||||
-- homarr_compute_layouts filter). It grows automatically when more
|
|
||||||
-- apps or taller tiles are added.
|
|
||||||
INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height)
|
|
||||||
VALUES
|
|
||||||
('section-apps', 'layout-desktop', NULL, 0, 0, 10, {{ homarr_layout.section_height.desktop }}),
|
|
||||||
('section-apps', 'layout-tablet', NULL, 0, 0, 6, {{ homarr_layout.section_height.tablet }}),
|
|
||||||
('section-apps', 'layout-mobile', NULL, 0, 0, 2, {{ homarr_layout.section_height.mobile }});
|
|
||||||
|
|
||||||
-- Board permissions
|
|
||||||
INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission)
|
|
||||||
VALUES
|
|
||||||
('board-default', 'group-oidc-admins', 'full-access'),
|
|
||||||
('board-default', 'group-credentials-admin', 'full-access');
|
|
||||||
|
|
||||||
-- =====================================================================
|
|
||||||
-- APPS (positions pre-computed by homarr_compute_layouts filter)
|
|
||||||
-- =====================================================================
|
|
||||||
|
|
||||||
{% for app in homarr_layout.apps %}
|
|
||||||
-- {{ app.name }}
|
|
||||||
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
|
|
||||||
VALUES (
|
|
||||||
'app-{{ app.id }}',
|
|
||||||
'{{ app.name }}',
|
|
||||||
'{{ app.description | default("") }}',
|
|
||||||
'{{ app.icon }}',
|
|
||||||
'{{ app.href }}'
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options)
|
|
||||||
VALUES (
|
|
||||||
'item-{{ app.id }}',
|
|
||||||
'board-default',
|
|
||||||
'app',
|
|
||||||
'{"json": {"appId": "app-{{ app.id }}"}}',
|
|
||||||
'{"json": {}}'
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
|
|
||||||
VALUES
|
|
||||||
('item-{{ app.id }}', 'section-apps', 'layout-desktop', {{ app.desktop.x }}, {{ app.desktop.y }}, {{ app.desktop.w }}, {{ app.desktop.h }}),
|
|
||||||
('item-{{ app.id }}', 'section-apps', 'layout-tablet', {{ app.tablet.x }}, {{ app.tablet.y }}, {{ app.tablet.w }}, {{ app.tablet.h }}),
|
|
||||||
('item-{{ app.id }}', 'section-apps', 'layout-mobile', {{ app.mobile.x }}, {{ app.mobile.y }}, {{ app.mobile.w }}, {{ app.mobile.h }});
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
COMMIT;
|
|
||||||
|
|
@ -1,30 +1,38 @@
|
||||||
# httpbin
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
Deploys [httpbin](https://httpbin.org/) (`kennethreitz/httpbin`) via
|
A brief description of the role goes here.
|
||||||
Docker Compose behind Traefik. Useful as a throwaway endpoint to verify
|
|
||||||
that the Traefik ingress path, TLS and routing work end to end.
|
|
||||||
|
|
||||||
## Role variables
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
| Variable | Default | Description |
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
| --- | --- | --- |
|
|
||||||
| `httpbin_domain` | `httpbin.local.test` | FQDN the Traefik router matches. |
|
|
||||||
| `httpbin_image` | `kennethreitz/httpbin` | Container image. |
|
|
||||||
| `httpbin_port` | `80` | Container port Traefik forwards to. |
|
|
||||||
| `httpbin_traefik_network` | `proxy` | Docker network shared with Traefik. |
|
|
||||||
| `httpbin_use_ssl` | `true` | Route via the `websecure` entrypoint with `tls=true` (otherwise `web`). |
|
|
||||||
|
|
||||||
## Example
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
```yaml
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
- hosts: services
|
|
||||||
become: true
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
|
|
||||||
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
|
|
||||||
|
- hosts: servers
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.httpbin
|
- { role: username.rolename, x: 42 }
|
||||||
vars:
|
|
||||||
httpbin_domain: "httpbin.example.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy httpbin HTTP request/response testing service via Docker Compose behind Traefik
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.1
|
||||||
- httpbin
|
|
||||||
- testing
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- debug
|
# min_ansible_container_version:
|
||||||
- docker
|
|
||||||
- traefik
|
galaxy_tags: []
|
||||||
- digitalboard
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,65 @@
|
||||||
# Keycloak
|
Keycloak
|
||||||
|
=========
|
||||||
|
|
||||||
Ansible role to deploy Keycloak with a PostgreSQL backend via Docker
|
Ansible role to deploy Keycloak with PostgreSQL database using Docker Compose.
|
||||||
Compose, published behind Traefik. Optionally provisions realm resources
|
|
||||||
(groups, users, OIDC clients, identity providers, LDAP user federations)
|
|
||||||
through the `community.general` Keycloak modules.
|
|
||||||
|
|
||||||
## Requirements
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
- Docker and Docker Compose on the target host (e.g. via
|
- Docker and Docker Compose installed on the target host
|
||||||
`digitalboard.core.base`)
|
- Ansible collection: `community.docker`
|
||||||
- Ansible collections: `community.docker`, and `community.general` when
|
- Traefik reverse proxy (for external access)
|
||||||
`keycloak_provisioning_enabled` is true
|
|
||||||
- Traefik reverse proxy with the `proxy` network already created (for
|
|
||||||
external access)
|
|
||||||
|
|
||||||
## Role variables
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
Key variables from `defaults/main.yml`:
|
Key variables defined in `defaults/main.yml`:
|
||||||
|
|
||||||
### Base configuration
|
**Base Configuration:**
|
||||||
|
- `docker_compose_base_dir`: Base directory for Docker Compose files (default: `/etc/docker/compose`)
|
||||||
|
- `docker_volume_base_dir`: Base directory for Docker volumes (default: `/srv/data`)
|
||||||
|
|
||||||
| Variable | Default | Description |
|
**Keycloak Configuration:**
|
||||||
| --- | --- | --- |
|
- `keycloak_service_name`: Service name (default: `keycloak`)
|
||||||
| `docker_compose_base_dir` | `/etc/docker/compose` | Base dir for Compose projects. |
|
- `keycloak_domain`: Domain name for Keycloak (default: `auth.digitalboard.ch`)
|
||||||
| `docker_volume_base_dir` | `/srv/data` | Base dir for persistent volumes. |
|
- `keycloak_image`: Keycloak Docker image (default: `quay.io/keycloak/keycloak:24.0.1`)
|
||||||
| `keycloak_service_name` | `keycloak` | Compose/service name; builds the per-service paths. |
|
- `keycloak_port`: Internal Keycloak port (default: `8080`)
|
||||||
|
- `keycloak_admin_user`: Admin username (default: `admin`)
|
||||||
|
- `keycloak_admin_password`: Admin password (default: `changeme`)
|
||||||
|
- `keycloak_log_level`: Log level (default: `INFO`)
|
||||||
|
- `keycloak_proxy_mode`: Proxy mode (default: `edge`)
|
||||||
|
|
||||||
### Keycloak
|
**PostgreSQL Configuration:**
|
||||||
|
- `keycloak_postgres_image`: PostgreSQL Docker image (default: `postgres:15`)
|
||||||
|
- `keycloak_postgres_db`: Database name (default: `keycloak`)
|
||||||
|
- `keycloak_postgres_user`: Database user (default: `keycloak`)
|
||||||
|
- `keycloak_postgres_password`: Database password (default: `changeme`)
|
||||||
|
|
||||||
| Variable | Default | Description |
|
**Traefik Configuration:**
|
||||||
| --- | --- | --- |
|
- `keycloak_traefik_network`: Traefik network name (default: `proxy`)
|
||||||
| `keycloak_domain` | `keycloak.local.test` | Host rule and `KC_HOSTNAME`. |
|
- `keycloak_backend_network`: Backend network name (default: `backend`)
|
||||||
| `keycloak_image` | `quay.io/keycloak/keycloak:24.0.1` | Keycloak image. |
|
- `keycloak_use_ssl`: Enable SSL (default: `true`)
|
||||||
| `keycloak_port` | `8080` | Internal HTTP port advertised to Traefik. |
|
- `keycloak_cert_resolver`: Certificate resolver name (default: `dns`)
|
||||||
| `keycloak_admin_user` | `admin` | Bootstrap admin user. |
|
|
||||||
| `keycloak_admin_password` | `changeme` | Admin password — **override this**. |
|
|
||||||
| `keycloak_log_level` | `INFO` | `KC_LOG_LEVEL`. |
|
|
||||||
| `keycloak_proxy_mode` | `edge` | `KC_PROXY` mode. |
|
|
||||||
| `keycloak_gzip_enabled` | `false` | Toggle Keycloak GZIP response encoding. |
|
|
||||||
| `keycloak_truststore_certificates` | `[]` | Host PEM paths mounted into the truststore (`KC_TRUSTSTORE_PATHS`). |
|
|
||||||
| `keycloak_extra_hosts` | `[]` | Extra `host:ip` entries for the container. |
|
|
||||||
|
|
||||||
### PostgreSQL
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
| Variable | Default | Description |
|
This role requires the Traefik reverse proxy to be configured and the `proxy` network to be created.
|
||||||
| --- | --- | --- |
|
|
||||||
| `keycloak_postgres_image` | `postgres:15` | PostgreSQL image. |
|
|
||||||
| `keycloak_postgres_db` | `keycloak` | Database name. |
|
|
||||||
| `keycloak_postgres_user` | `keycloak` | Database user. |
|
|
||||||
| `keycloak_postgres_password` | `changeme` | Database password — **override this**. |
|
|
||||||
|
|
||||||
### Traefik
|
Example Playbook
|
||||||
|
----------------
|
||||||
| Variable | Default | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `keycloak_traefik_network` | `proxy` | External Traefik network. |
|
|
||||||
| `keycloak_backend_network` | `backend` | Internal network to PostgreSQL. |
|
|
||||||
| `keycloak_use_ssl` | `true` | Route on `websecure` with `tls=true` instead of `web`. |
|
|
||||||
|
|
||||||
TLS is requested from Traefik via `tls=true`; the role does not set a
|
|
||||||
certificate resolver, so Traefik issues/serves the certificate according
|
|
||||||
to its own configuration.
|
|
||||||
|
|
||||||
### Provisioning (optional)
|
|
||||||
|
|
||||||
Provisioning runs only when `keycloak_provisioning_enabled` is true. The
|
|
||||||
tasks wait for the `/health/ready` endpoint and then call the
|
|
||||||
`community.general.keycloak_*` modules, delegated to `localhost` against
|
|
||||||
`keycloak_auth_url` (derived from `keycloak_use_ssl` + `keycloak_domain`).
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `keycloak_provisioning_enabled` | `false` | Enable realm provisioning. |
|
|
||||||
| `keycloak_realm` | `default` | Target realm; created unless `master`. |
|
|
||||||
| `keycloak_realm_display_name` | `Default Realm` | Realm display name. |
|
|
||||||
| `keycloak_auth_url` | derived | API base URL for provisioning. |
|
|
||||||
| `keycloak_groups` | `[]` | Groups to create. |
|
|
||||||
| `keycloak_local_users` | `[]` | Local users to create. |
|
|
||||||
| `keycloak_oidc_clients` | `[]` | OIDC clients to create. |
|
|
||||||
| `keycloak_identity_providers` | `[]` | Identity providers (e.g. Entra ID). |
|
|
||||||
| `keycloak_user_federations` | `[]` | LDAP user federations. |
|
|
||||||
| `keycloak_removed_users` | `[]` | Usernames to delete. |
|
|
||||||
| `keycloak_removed_groups` | `[]` | Group names to delete. |
|
|
||||||
| `keycloak_removed_clients` | `[]` | Client IDs to delete. |
|
|
||||||
| `keycloak_removed_identity_providers` | `[]` | IdP aliases to delete. |
|
|
||||||
| `keycloak_removed_user_federations` | `[]` | Federation names to delete. |
|
|
||||||
|
|
||||||
See `defaults/main.yml` for the full entry shape of each list.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
This role requires the Traefik reverse proxy to be configured and the
|
|
||||||
`proxy` network to be created beforehand (it is referenced as an external
|
|
||||||
network in the Compose file). The `backend` network is created by the
|
|
||||||
Compose project itself.
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- hosts: backend_servers
|
- hosts: backend_servers
|
||||||
roles:
|
roles:
|
||||||
- role: digitalboard.core.keycloak
|
- role: keycloak
|
||||||
vars:
|
vars:
|
||||||
keycloak_domain: "auth.example.com"
|
keycloak_domain: "auth.example.com"
|
||||||
keycloak_admin_password: "{{ vault_keycloak_admin_password }}"
|
keycloak_admin_password: "secure_password"
|
||||||
keycloak_postgres_password: "{{ vault_keycloak_pg_password }}"
|
keycloak_postgres_password: "secure_db_password"
|
||||||
keycloak_provisioning_enabled: true
|
|
||||||
keycloak_oidc_clients:
|
|
||||||
- client_id: nextcloud
|
|
||||||
name: "Nextcloud"
|
|
||||||
client_secret: "{{ vault_nextcloud_client_secret }}"
|
|
||||||
redirect_uris:
|
|
||||||
- "https://nextcloud.example.com/apps/user_oidc/code"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
MIT-0
|
MIT-0
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: digitalboard
|
author: your name
|
||||||
description: Deploy Keycloak with a PostgreSQL backend via Docker Compose behind Traefik
|
description: your role description
|
||||||
company: Digitalboard
|
company: your company (optional)
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
platforms:
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
- name: Debian
|
# - BSD-3-Clause (default)
|
||||||
versions:
|
# - MIT
|
||||||
- bookworm
|
# - GPL-2.0-or-later
|
||||||
- name: Ubuntu
|
# - GPL-3.0-only
|
||||||
versions:
|
# - Apache-2.0
|
||||||
- jammy
|
# - CC-BY-4.0
|
||||||
- noble
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
galaxy_tags:
|
min_ansible_version: 2.1
|
||||||
- keycloak
|
|
||||||
- oidc
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
- sso
|
# min_ansible_container_version:
|
||||||
- docker
|
|
||||||
- traefik
|
galaxy_tags: []
|
||||||
- digitalboard
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
|
||||||
38
roles/knot/README.md
Normal file
38
roles/knot/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
|
A brief description of the role goes here.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
|
|
||||||
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
|
|
||||||
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
|
|
||||||
|
- hosts: servers
|
||||||
|
roles:
|
||||||
|
- { role: username.rolename, x: 42 }
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# vars file for opnform
|
# defaults file for knot
|
||||||
3
roles/knot/handlers/main.yml
Normal file
3
roles/knot/handlers/main.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
|
# handlers file for knot
|
||||||
35
roles/knot/meta/main.yml
Normal file
35
roles/knot/meta/main.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
galaxy_info:
|
||||||
|
author: your name
|
||||||
|
description: your role description
|
||||||
|
company: your company (optional)
|
||||||
|
|
||||||
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
|
# - BSD-3-Clause (default)
|
||||||
|
# - MIT
|
||||||
|
# - GPL-2.0-or-later
|
||||||
|
# - GPL-3.0-only
|
||||||
|
# - Apache-2.0
|
||||||
|
# - CC-BY-4.0
|
||||||
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
|
min_ansible_version: 2.2
|
||||||
|
|
||||||
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
|
# min_ansible_container_version:
|
||||||
|
|
||||||
|
galaxy_tags: []
|
||||||
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# vars file for send
|
# tasks file for knot
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
localhost
|
localhost
|
||||||
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
- hosts: localhost
|
- hosts: localhost
|
||||||
remote_user: root
|
remote_user: root
|
||||||
roles:
|
roles:
|
||||||
- talk
|
- knot
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# vars file for talk
|
# vars file for knot
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
# Nextcloud
|
|
||||||
|
|
||||||
Ansible role to deploy [Nextcloud](https://nextcloud.com/) (fpm) with
|
|
||||||
Postgres and Redis via Docker Compose, optional Collabora WOPI
|
|
||||||
integration, optional draw.io integration, optional notify_push
|
|
||||||
companion, optional S3 primary storage, plus OIDC and LDAP user
|
|
||||||
backends.
|
|
||||||
|
|
||||||
## What this role does
|
|
||||||
|
|
||||||
- Renders the Compose stack with traefik labels and TLS
|
|
||||||
- Installs and enables a configurable list of Nextcloud apps idempotently
|
|
||||||
- Configures Collabora (richdocuments), draw.io, OIDC providers and
|
|
||||||
LDAP via `occ` — every setting is read first and only written when
|
|
||||||
the stored value differs, so re-runs don't churn
|
|
||||||
- Sets up notify_push (when enabled)
|
|
||||||
- Applies an in-container PHP source workaround for the upstream
|
|
||||||
`UserConfig::getValueBool` TypeError (nextcloud/server#59629, fixed in
|
|
||||||
master via PR #59646 with no stable33 backport before 33.0.4).
|
|
||||||
Idempotent via grep guard; remove the patch task once
|
|
||||||
`nextcloud_image` is >= 33.0.4.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Docker and Docker Compose installed on the target host
|
|
||||||
- Ansible collection: `community.docker`
|
|
||||||
- Traefik with a shared `nextcloud_traefik_network` (default `proxy`)
|
|
||||||
|
|
||||||
## Role variables
|
|
||||||
|
|
||||||
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
|
||||||
common overrides:
|
|
||||||
|
|
||||||
### Service
|
|
||||||
|
|
||||||
- `nextcloud_domains`: FQDNs the router accepts. First entry is the
|
|
||||||
canonical hostname (used for `OVERWRITEHOST` and notify_push setup).
|
|
||||||
Further entries cover internal `*.int.*` names so Collabora's WOPI
|
|
||||||
callback hits the instance on a name with a valid cert.
|
|
||||||
- `nextcloud_admin_password`, `nextcloud_postgres_password` (required).
|
|
||||||
- `nextcloud_memory_limit_mb`, `nextcloud_upload_limit_mb`.
|
|
||||||
|
|
||||||
### Collabora
|
|
||||||
|
|
||||||
- `nextcloud_enable_collabora`: toggle integration with a separately
|
|
||||||
deployed Collabora server (see the `collabora` role).
|
|
||||||
- `nextcloud_collabora_domain`: server-to-server hostname.
|
|
||||||
- `nextcloud_collabora_public_domain` (optional): browser-facing
|
|
||||||
hostname when split-horizon uses different names.
|
|
||||||
|
|
||||||
### Draw.io
|
|
||||||
|
|
||||||
- `nextcloud_enable_drawio`: enable the `integration_drawio` app.
|
|
||||||
- `nextcloud_drawio_url`: public draw.io URL.
|
|
||||||
- `nextcloud_drawio_theme`, `nextcloud_drawio_offline`.
|
|
||||||
|
|
||||||
### Notify push
|
|
||||||
|
|
||||||
- `nextcloud_enable_notify_push`: deploy the notify_push companion.
|
|
||||||
- `nextcloud_notify_push_domain` (optional): override the hostname
|
|
||||||
used by `occ notify_push:setup` to avoid hairpinning through the DMZ.
|
|
||||||
|
|
||||||
### S3 primary storage
|
|
||||||
|
|
||||||
Set `nextcloud_use_s3_storage: true` plus the `nextcloud_s3_*` block to
|
|
||||||
point Nextcloud at an external S3-compatible store (e.g. Garage, MinIO).
|
|
||||||
|
|
||||||
### OIDC
|
|
||||||
|
|
||||||
`nextcloud_oidc_providers` is a list of OIDC providers registered with
|
|
||||||
`user_oidc`. Required fields per entry: `identifier`, `display_name`,
|
|
||||||
`client_id`, `client_secret`, `discovery_url`.
|
|
||||||
|
|
||||||
### LDAP
|
|
||||||
|
|
||||||
Set `nextcloud_ldap_enabled: true` and provide `nextcloud_ldap_config`
|
|
||||||
as a dict of `occ ldap:set-config s01 KEY VALUE` pairs. The role reads
|
|
||||||
the current LDAP config via `occ ldap:show-config s01 --output=json`
|
|
||||||
and only calls `ldap:set-config` for keys whose stored value differs.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Traefik network (`nextcloud_traefik_network`, default `proxy`)
|
|
||||||
- Optional: `collabora`, `drawio`, `garage` roles for the corresponding
|
|
||||||
integrations
|
|
||||||
- Optional: an OIDC provider (Keycloak, authentik) reachable from
|
|
||||||
Nextcloud and a 389ds LDAP server when using `user_ldap`
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- hosts: app_servers
|
|
||||||
roles:
|
|
||||||
- role: digitalboard.core.nextcloud
|
|
||||||
vars:
|
|
||||||
nextcloud_domains:
|
|
||||||
- "cloud.example.com"
|
|
||||||
- "cloud.int.example.com"
|
|
||||||
nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}"
|
|
||||||
nextcloud_postgres_password: "{{ vault_nextcloud_pg_password }}"
|
|
||||||
|
|
||||||
nextcloud_enable_collabora: true
|
|
||||||
nextcloud_collabora_domain: "office.int.example.com"
|
|
||||||
nextcloud_collabora_public_domain: "office.example.com"
|
|
||||||
|
|
||||||
nextcloud_enable_notify_push: true
|
|
||||||
nextcloud_notify_push_domain: "cloud.int.example.com"
|
|
||||||
|
|
||||||
nextcloud_oidc_providers:
|
|
||||||
- identifier: authentik
|
|
||||||
display_name: "Login with Authentik"
|
|
||||||
client_id: nextcloud
|
|
||||||
client_secret: "{{ vault_nextcloud_oidc_secret }}"
|
|
||||||
discovery_url: "https://auth.example.com/application/o/nextcloud/.well-known/openid-configuration"
|
|
||||||
mapping:
|
|
||||||
uid: preferred_username
|
|
||||||
display_name: name
|
|
||||||
email: email
|
|
||||||
groups: groups
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT-0
|
|
||||||
|
|
@ -9,17 +9,11 @@ nextcloud_service_name: nextcloud
|
||||||
nextcloud_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ nextcloud_service_name }}"
|
nextcloud_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ nextcloud_service_name }}"
|
||||||
nextcloud_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ nextcloud_service_name }}"
|
nextcloud_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ nextcloud_service_name }}"
|
||||||
|
|
||||||
# FQDNs the nextcloud router accepts. The first entry is the canonical
|
nextcloud_domain: "nextcloud.local.test"
|
||||||
# domain (used for OVERWRITEHOST and the notify_push setup); further
|
|
||||||
# entries cover internal *.int.* names so collabora's WOPI callback
|
|
||||||
# hits us on a name with a valid cert.
|
|
||||||
nextcloud_domains:
|
|
||||||
- "nextcloud.local.test"
|
|
||||||
nextcloud_image: "nextcloud:fpm"
|
nextcloud_image: "nextcloud:fpm"
|
||||||
nextcloud_redis_image: "redis:latest"
|
nextcloud_redis_image: "redis:latest"
|
||||||
nextcloud_port: 80
|
nextcloud_port: 80
|
||||||
nextcloud_extra_hosts: []
|
nextcloud_extra_hosts: []
|
||||||
nextcloud_extra_networks: []
|
|
||||||
nextcloud_allow_local_remote_servers: false # Set to true to allow requests to local network (dev only)
|
nextcloud_allow_local_remote_servers: false # Set to true to allow requests to local network (dev only)
|
||||||
|
|
||||||
nextcloud_postgres_image: "postgres:15"
|
nextcloud_postgres_image: "postgres:15"
|
||||||
|
|
@ -64,33 +58,6 @@ nextcloud_trusted_proxies: "172.16.0.0/12"
|
||||||
|
|
||||||
# File locking and real-time push notifications
|
# File locking and real-time push notifications
|
||||||
nextcloud_enable_notify_push: false
|
nextcloud_enable_notify_push: false
|
||||||
nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1"
|
|
||||||
# Domain used when calling `occ notify_push:setup`. Defaults to the
|
|
||||||
# first nextcloud_domains entry (the canonical public name). Override
|
|
||||||
# with an internal FQDN to avoid hairpinning the setup check through
|
|
||||||
# the DMZ; the FQDN must also be in nextcloud_domains so the push
|
|
||||||
# router matches it.
|
|
||||||
# nextcloud_notify_push_domain: "cloud.int.example.com"
|
|
||||||
|
|
||||||
# Nextcloud Talk: register external HPB signaling + TURN + STUN
|
|
||||||
# Set to true to run tasks/talk.yml after Nextcloud is up.
|
|
||||||
nextcloud_enable_talk: false
|
|
||||||
|
|
||||||
# HPB signaling servers to register.
|
|
||||||
# Each item: { server: "https://signaling.example.test", secret: "<hpb_shared_secret>", verify: true }
|
|
||||||
nextcloud_talk_signaling_servers: []
|
|
||||||
# Hostnames/URLs to remove via `talk:signaling:delete` before adding the new set.
|
|
||||||
nextcloud_talk_signaling_servers_removed: []
|
|
||||||
|
|
||||||
# TURN servers to register.
|
|
||||||
# Each item: { server: "stun.example.test:443", secret: "<turn_shared_secret>", schemes: "turn,turns", protocols: "udp,tcp" }
|
|
||||||
nextcloud_talk_turn_servers: []
|
|
||||||
# Clear the spreed.turn_servers config key before re-adding (single source of truth)
|
|
||||||
nextcloud_talk_turn_reset_before_add: true
|
|
||||||
|
|
||||||
# Plain STUN endpoints (host:port). Optional; usually not needed if TURN servers handle STUN too.
|
|
||||||
nextcloud_talk_stun_servers: []
|
|
||||||
nextcloud_talk_stun_servers_removed: []
|
|
||||||
|
|
||||||
# Non-default apps to install and enable
|
# Non-default apps to install and enable
|
||||||
nextcloud_apps_to_install:
|
nextcloud_apps_to_install:
|
||||||
|
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy Nextcloud (fpm) + Redis + Postgres via Docker Compose.
|
|
||||||
description:
|
|
||||||
- Renders a Compose stack for Nextcloud with traefik labels, optional
|
|
||||||
Collabora WOPI integration, optional draw.io integration, optional
|
|
||||||
notify_push companion, optional S3 primary storage, OIDC providers
|
|
||||||
and LDAP user backend.
|
|
||||||
- "All C(occ)-driven configuration tasks are idempotent: each setting
|
|
||||||
is read with C(config:app:get) (or C(ldap:show-config)) first and
|
|
||||||
only written when the stored value differs."
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
docker_volume_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /srv/data
|
|
||||||
nextcloud_service_name:
|
|
||||||
type: str
|
|
||||||
default: nextcloud
|
|
||||||
nextcloud_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
nextcloud_docker_volume_dir:
|
|
||||||
type: path
|
|
||||||
|
|
||||||
nextcloud_domains:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: ['nextcloud.local.test']
|
|
||||||
description:
|
|
||||||
- FQDNs the nextcloud router accepts. The first entry is the
|
|
||||||
canonical domain (used for C(OVERWRITEHOST) and the
|
|
||||||
C(notify_push) setup). Further entries cover internal C(*.int.*)
|
|
||||||
names so Collabora's WOPI callback hits the instance on a name
|
|
||||||
with a valid certificate.
|
|
||||||
nextcloud_image:
|
|
||||||
type: str
|
|
||||||
default: nextcloud:fpm
|
|
||||||
nextcloud_redis_image:
|
|
||||||
type: str
|
|
||||||
default: redis:latest
|
|
||||||
nextcloud_port:
|
|
||||||
type: int
|
|
||||||
default: 80
|
|
||||||
nextcloud_extra_hosts:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
nextcloud_extra_networks:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
nextcloud_allow_local_remote_servers:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description: Allow requests to local network from Nextcloud (dev only).
|
|
||||||
|
|
||||||
nextcloud_postgres_image:
|
|
||||||
type: str
|
|
||||||
default: postgres:15
|
|
||||||
nextcloud_postgres_db:
|
|
||||||
type: str
|
|
||||||
default: nextcloud
|
|
||||||
nextcloud_postgres_user:
|
|
||||||
type: str
|
|
||||||
default: nextcloud
|
|
||||||
nextcloud_postgres_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
|
|
||||||
nextcloud_backend_network:
|
|
||||||
type: str
|
|
||||||
default: nextcloud-internal
|
|
||||||
nextcloud_traefik_network:
|
|
||||||
type: str
|
|
||||||
default: proxy
|
|
||||||
nextcloud_use_ssl:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
|
|
||||||
nextcloud_enable_collabora:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
nextcloud_collabora_domain:
|
|
||||||
type: str
|
|
||||||
default: office.local.test
|
|
||||||
description: Hostname Nextcloud uses to talk to Collabora server-to-server.
|
|
||||||
nextcloud_collabora_public_domain:
|
|
||||||
type: str
|
|
||||||
description:
|
|
||||||
- Optional browser-facing hostname for Collabora; defaults to
|
|
||||||
C(nextcloud_collabora_domain) when unset. Set when split-horizon
|
|
||||||
uses different names for browser and server traffic.
|
|
||||||
nextcloud_collabora_disable_cert_verification:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
|
|
||||||
nextcloud_enable_drawio:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description: Enable the integration_drawio Nextcloud app and configure the URL/theme.
|
|
||||||
nextcloud_drawio_url:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Public draw.io URL used by the integration_drawio app.
|
|
||||||
nextcloud_drawio_theme:
|
|
||||||
type: str
|
|
||||||
choices: [kennedy, atlas, dark, sketch, min]
|
|
||||||
default: kennedy
|
|
||||||
nextcloud_drawio_offline:
|
|
||||||
type: str
|
|
||||||
choices: ['yes', 'no']
|
|
||||||
default: 'yes'
|
|
||||||
|
|
||||||
nextcloud_use_s3_storage:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description: Use S3 primary object storage instead of the local data dir.
|
|
||||||
nextcloud_s3_key:
|
|
||||||
type: str
|
|
||||||
default: changeme
|
|
||||||
nextcloud_s3_secret:
|
|
||||||
type: str
|
|
||||||
default: changeme
|
|
||||||
nextcloud_s3_region:
|
|
||||||
type: str
|
|
||||||
default: us-east-1
|
|
||||||
nextcloud_s3_bucket:
|
|
||||||
type: str
|
|
||||||
default: nextcloud
|
|
||||||
nextcloud_s3_host:
|
|
||||||
type: str
|
|
||||||
default: s3.example.com
|
|
||||||
nextcloud_s3_port:
|
|
||||||
type: int
|
|
||||||
default: 443
|
|
||||||
nextcloud_s3_ssl:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
nextcloud_s3_usepath_style:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
nextcloud_s3_autocreate:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
|
|
||||||
nextcloud_admin_user:
|
|
||||||
type: str
|
|
||||||
default: admin
|
|
||||||
nextcloud_admin_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
nextcloud_memory_limit_mb:
|
|
||||||
type: int
|
|
||||||
default: 1024
|
|
||||||
nextcloud_upload_limit_mb:
|
|
||||||
type: int
|
|
||||||
default: 2048
|
|
||||||
nextcloud_scale_factor:
|
|
||||||
type: int
|
|
||||||
default: 2
|
|
||||||
|
|
||||||
nextcloud_trusted_proxies:
|
|
||||||
type: str
|
|
||||||
default: '172.16.0.0/12'
|
|
||||||
description: Trusted proxy CIDR(s) — by default the Docker internal range.
|
|
||||||
|
|
||||||
nextcloud_enable_notify_push:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
nextcloud_notify_push_image:
|
|
||||||
type: str
|
|
||||||
default: icewind1991/notify_push:1.3.1
|
|
||||||
nextcloud_notify_push_domain:
|
|
||||||
type: str
|
|
||||||
description:
|
|
||||||
- Hostname used when calling C(occ notify_push:setup). Defaults to
|
|
||||||
the first C(nextcloud_domains) entry. Override with an internal
|
|
||||||
FQDN to avoid hairpinning the setup check through the DMZ; the
|
|
||||||
FQDN must also be in C(nextcloud_domains).
|
|
||||||
|
|
||||||
nextcloud_apps_to_install:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default:
|
|
||||||
- groupfolders
|
|
||||||
- richdocuments
|
|
||||||
- spreed
|
|
||||||
- user_ldap
|
|
||||||
- user_oidc
|
|
||||||
- whiteboard
|
|
||||||
- files_lock
|
|
||||||
- notify_push
|
|
||||||
description:
|
|
||||||
- Non-default Nextcloud apps to install + enable.
|
|
||||||
Install/enable detection is idempotent — re-runs report C(ok)
|
|
||||||
when the app is already present and enabled.
|
|
||||||
|
|
||||||
nextcloud_oidc_allow_selfsigned:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
nextcloud_oidc_providers:
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
default: []
|
|
||||||
description: OIDC providers registered with the user_oidc app.
|
|
||||||
options:
|
|
||||||
identifier:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
display_name:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
client_id:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
client_secret:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
discovery_url:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
scope:
|
|
||||||
type: str
|
|
||||||
default: openid email profile
|
|
||||||
unique_uid:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
check_bearer:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
send_id_token_hint:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
mapping:
|
|
||||||
type: dict
|
|
||||||
nextcloud_oidc_providers_removed:
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
|
|
||||||
nextcloud_ldap_enabled:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
nextcloud_ldap_config:
|
|
||||||
type: dict
|
|
||||||
default: {}
|
|
||||||
description:
|
|
||||||
- Key/value pairs passed to C(occ ldap:set-config s01 KEY VALUE).
|
|
||||||
The role reads the current config first and only invokes
|
|
||||||
C(set-config) when a stored value differs.
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
galaxy_info:
|
|
||||||
author: digitalboard
|
|
||||||
description: Deploy Nextcloud (fpm) + Redis + Postgres via Docker Compose behind Traefik
|
|
||||||
company: Digitalboard
|
|
||||||
license: MIT-0
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
|
||||||
|
|
||||||
platforms:
|
|
||||||
- name: Debian
|
|
||||||
versions:
|
|
||||||
- bookworm
|
|
||||||
- name: Ubuntu
|
|
||||||
versions:
|
|
||||||
- jammy
|
|
||||||
- noble
|
|
||||||
|
|
||||||
galaxy_tags:
|
|
||||||
- nextcloud
|
|
||||||
- files
|
|
||||||
- collabora
|
|
||||||
- oidc
|
|
||||||
- docker
|
|
||||||
- traefik
|
|
||||||
- digitalboard
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
|
|
@ -1,55 +1,22 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# tasks file for configuring Collabora in Nextcloud
|
# tasks file for configuring Collabora in Nextcloud
|
||||||
- name: Read current richdocuments config values
|
- name: Configure Collabora WOPI URL
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
|
||||||
command: php /var/www/html/occ config:app:get richdocuments {{ item }}
|
|
||||||
loop:
|
|
||||||
- wopi_url
|
|
||||||
- public_wopi_url
|
|
||||||
- disable_certificate_verification
|
|
||||||
- wopi_allowlist
|
|
||||||
register: _richdocuments_current
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Build map of current richdocuments config
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_richdocuments_cfg: "{{ _richdocuments_cfg | default({}) | combine({item.item: (item.stdout | default('')).strip()}) }}"
|
|
||||||
loop: "{{ _richdocuments_current.results }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
|
|
||||||
- name: Configure Collabora WOPI URL (server-to-server)
|
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set richdocuments wopi_url --value=https://{{ nextcloud_collabora_domain }}
|
command: php /var/www/html/occ config:app:set richdocuments wopi_url --value=https://{{ nextcloud_collabora_domain }}
|
||||||
when: _richdocuments_cfg.wopi_url != ('https://' ~ nextcloud_collabora_domain)
|
|
||||||
|
|
||||||
- name: Configure Collabora public WOPI URL (browser-facing)
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
|
||||||
command: php /var/www/html/occ config:app:set richdocuments public_wopi_url --value=https://{{ nextcloud_collabora_public_domain }}
|
|
||||||
when:
|
|
||||||
- nextcloud_collabora_public_domain is defined
|
|
||||||
- nextcloud_collabora_public_domain != nextcloud_collabora_domain
|
|
||||||
- _richdocuments_cfg.public_wopi_url != ('https://' ~ nextcloud_collabora_public_domain)
|
|
||||||
|
|
||||||
- name: Configure certificate verification for Collabora
|
- name: Configure certificate verification for Collabora
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set richdocuments disable_certificate_verification --value={{ nextcloud_collabora_disable_cert_verification | ternary('yes', 'no') }}
|
command: php /var/www/html/occ config:app:set richdocuments disable_certificate_verification --value={{ nextcloud_collabora_disable_cert_verification | ternary('yes', 'no') }}
|
||||||
when: _richdocuments_cfg.disable_certificate_verification != (nextcloud_collabora_disable_cert_verification | ternary('yes', 'no'))
|
|
||||||
|
|
||||||
- name: Set Collabora WOPI allowlist
|
- name: Set Collabora WOPI allowlist
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value=''
|
command: php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value=''
|
||||||
when: _richdocuments_cfg.wopi_allowlist | default('') != ''
|
|
||||||
|
|
||||||
- name: Activate richdocuments configuration (fetch discovery from Collabora)
|
- name: Activate richdocuments configuration (fetch discovery from Collabora)
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ richdocuments:activate-config
|
command: php /var/www/html/occ richdocuments:activate-config
|
||||||
changed_when: false
|
|
||||||
|
|
@ -2,41 +2,18 @@
|
||||||
---
|
---
|
||||||
# tasks file for configuring draw.io in Nextcloud
|
# tasks file for configuring draw.io in Nextcloud
|
||||||
|
|
||||||
- name: Read current drawio config values
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
|
||||||
command: php /var/www/html/occ config:app:get drawio {{ item }}
|
|
||||||
loop:
|
|
||||||
- DrawioUrl
|
|
||||||
- DrawioTheme
|
|
||||||
- DrawioOffline
|
|
||||||
register: _drawio_current
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Build map of current drawio config
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_drawio_cfg: "{{ _drawio_cfg | default({}) | combine({item.item: (item.stdout | default('')).strip()}) }}"
|
|
||||||
loop: "{{ _drawio_current.results }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
|
|
||||||
- name: Configure draw.io URL
|
- name: Configure draw.io URL
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set drawio DrawioUrl --value={{ nextcloud_drawio_url }}
|
command: php /var/www/html/occ config:app:set drawio DrawioUrl --value={{ nextcloud_drawio_url }}
|
||||||
when:
|
when: nextcloud_drawio_url | length > 0
|
||||||
- nextcloud_drawio_url | length > 0
|
|
||||||
- _drawio_cfg.DrawioUrl != nextcloud_drawio_url
|
|
||||||
|
|
||||||
- name: Configure draw.io theme
|
- name: Configure draw.io theme
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set drawio DrawioTheme --value={{ nextcloud_drawio_theme }}
|
command: php /var/www/html/occ config:app:set drawio DrawioTheme --value={{ nextcloud_drawio_theme }}
|
||||||
when: _drawio_cfg.DrawioTheme != (nextcloud_drawio_theme | string)
|
|
||||||
|
|
||||||
- name: Configure draw.io offline mode
|
- name: Configure draw.io offline mode
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set drawio DrawioOffline --value={{ nextcloud_drawio_offline }}
|
command: php /var/www/html/occ config:app:set drawio DrawioOffline --value={{ nextcloud_drawio_offline }}
|
||||||
when: _drawio_cfg.DrawioOffline != (nextcloud_drawio_offline | string)
|
|
||||||
|
|
@ -15,24 +15,6 @@
|
||||||
command: php /var/www/html/occ ldap:create-empty-config
|
command: php /var/www/html/occ ldap:create-empty-config
|
||||||
when: "'s01' not in ldap_show_config.stdout"
|
when: "'s01' not in ldap_show_config.stdout"
|
||||||
|
|
||||||
- name: Read current LDAP config for s01
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ nextcloud_service_name }}-nextcloud-1"
|
|
||||||
command: php /var/www/html/occ ldap:show-config s01 --output=json
|
|
||||||
register: _ldap_show_s01
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Parse current LDAP config
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_ldap_current: >-
|
|
||||||
{{
|
|
||||||
(_ldap_show_s01.stdout | from_json) if (
|
|
||||||
(_ldap_show_s01.stdout | default('') | trim) is match('^[\\[{]')
|
|
||||||
) else {}
|
|
||||||
}}
|
|
||||||
when: _ldap_show_s01.rc | default(1) == 0
|
|
||||||
|
|
||||||
- name: Configure LDAP settings
|
- name: Configure LDAP settings
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_service_name }}-nextcloud-1"
|
container: "{{ nextcloud_service_name }}-nextcloud-1"
|
||||||
|
|
@ -47,7 +29,6 @@
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.key }}"
|
label: "{{ item.key }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
when: ((_ldap_current | default({})).get(item.key) | default(none) | string) != (item.value | string)
|
|
||||||
|
|
||||||
- name: Test LDAP configuration
|
- name: Test LDAP configuration
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,6 @@
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
|
|
||||||
- name: Ensure extra networks exist
|
|
||||||
community.docker.docker_network:
|
|
||||||
name: "{{ item }}"
|
|
||||||
state: present
|
|
||||||
loop: "{{ nextcloud_extra_networks }}"
|
|
||||||
|
|
||||||
- name: Create docker-compose file for nextcloud
|
- name: Create docker-compose file for nextcloud
|
||||||
template:
|
template:
|
||||||
src: docker-compose.yml.j2
|
src: docker-compose.yml.j2
|
||||||
|
|
@ -49,61 +43,6 @@
|
||||||
project_src: "{{ nextcloud_docker_compose_dir }}"
|
project_src: "{{ nextcloud_docker_compose_dir }}"
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
# nextcloud/server#59629: UserConfig::getValueBool() passes a non-string from
|
|
||||||
# getTypedValue() into strtolower() under PHP 8.x + OPcache, throwing a
|
|
||||||
# TypeError on every authenticated request once user_ldap is involved. Fix
|
|
||||||
# is in master (PR #59646) but no stable33 backport landed before 33.0.4.
|
|
||||||
# Apply the (string) cast in-container; idempotent via grep guard. Remove
|
|
||||||
# this block once nextcloud_image >= 33.0.4.
|
|
||||||
- name: Discover nextcloud php containers needing the UserConfig patch
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: >-
|
|
||||||
docker ps --filter "label=com.docker.compose.project={{ nextcloud_docker_compose_dir | basename }}"
|
|
||||||
--filter "label=com.docker.compose.service=nextcloud"
|
|
||||||
--format '{% raw %}{{.Names}}{% endraw %}'
|
|
||||||
register: _nextcloud_php_containers
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Check UserConfig.php patch status per container
|
|
||||||
ansible.builtin.shell:
|
|
||||||
# rc 0 -> already patched; rc 1 -> still the unpatched original; rc 2 ->
|
|
||||||
# neither marker present (upstream drift -> the guard task below fails loud).
|
|
||||||
cmd: >-
|
|
||||||
docker exec {{ item }} sh -c '
|
|
||||||
grep -q "strtolower((string)\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 0;
|
|
||||||
grep -q "strtolower(\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 1;
|
|
||||||
exit 2'
|
|
||||||
loop: "{{ _nextcloud_php_containers.stdout_lines }}"
|
|
||||||
register: _nextcloud_userconfig_check
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Fail if the UserConfig.php source drifted from the expected upstream line
|
|
||||||
ansible.builtin.fail:
|
|
||||||
msg: >-
|
|
||||||
Neither the patched nor the expected original strtolower($this->getTypedValue(...))
|
|
||||||
line was found in {{ item.item }}:/var/www/html/lib/private/Config/UserConfig.php.
|
|
||||||
The nextcloud/server#59629 workaround can no longer locate its target — the upstream
|
|
||||||
source likely changed. Re-verify whether the fix shipped (then drop this block) or
|
|
||||||
update the sed expression. Silently skipping would let the TypeError regress.
|
|
||||||
loop: "{{ _nextcloud_userconfig_check.results }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
when:
|
|
||||||
- item.rc | default(2) == 2
|
|
||||||
|
|
||||||
- name: Apply UserConfig::getValueBool string-cast workaround
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: >-
|
|
||||||
docker exec {{ item.item }}
|
|
||||||
sed -i 's|$b = strtolower($this->getTypedValue|$b = strtolower((string)$this->getTypedValue|'
|
|
||||||
/var/www/html/lib/private/Config/UserConfig.php
|
|
||||||
loop: "{{ _nextcloud_userconfig_check.results }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
when:
|
|
||||||
- item.rc | default(2) == 1
|
|
||||||
|
|
||||||
- name: Wait for Nextcloud to be ready
|
- name: Wait for Nextcloud to be ready
|
||||||
ansible.builtin.shell:
|
ansible.builtin.shell:
|
||||||
cmd: docker compose exec -T nextcloud php /var/www/html/occ status --output=json
|
cmd: docker compose exec -T nextcloud php /var/www/html/occ status --output=json
|
||||||
|
|
@ -146,7 +85,3 @@
|
||||||
- name: Configure OIDC providers
|
- name: Configure OIDC providers
|
||||||
ansible.builtin.include_tasks: oidc.yml
|
ansible.builtin.include_tasks: oidc.yml
|
||||||
when: nextcloud_oidc_providers | length > 0 or nextcloud_oidc_providers_removed | length > 0
|
when: nextcloud_oidc_providers | length > 0 or nextcloud_oidc_providers_removed | length > 0
|
||||||
|
|
||||||
- name: Configure Nextcloud Talk (HPB + TURN + STUN)
|
|
||||||
ansible.builtin.include_tasks: talk.yml
|
|
||||||
when: nextcloud_enable_talk
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue