From c060d6136ab4c2b87ace631cf60c0b7007fbf4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:15:06 +0200 Subject: [PATCH 1/9] fix(homarr): salt column, bcrypt newline, transaction safety --- roles/homarr/tasks/main.yml | 219 +++++++++++++----------------------- 1 file changed, 78 insertions(+), 141 deletions(-) diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index f8dd3df..22bfe97 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -3,7 +3,22 @@ # tasks file for homarr # ===================================================================== -# 1. VORBEREITUNG: Pakete und Verzeichnisse VOR Container-Start +# 0. VALIDATION +# ===================================================================== + +- name: Validate encryption key + ansible.builtin.assert: + that: + - homarr_secret_encryption_key | length == 64 + - homarr_secret_encryption_key is match('^[a-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 + +# ===================================================================== +# 1. PREPARATION: packages and directories before container start # ===================================================================== - name: Ensure required packages are installed @@ -11,7 +26,6 @@ name: - sqlite3 - python3-docker - - python3-bcrypt state: present - name: Create docker compose directory @@ -37,7 +51,7 @@ register: db_exists # ===================================================================== -# 2. CONTAINER STARTEN +# 2. START CONTAINER # ===================================================================== - name: Create docker-compose file for homarr @@ -52,7 +66,7 @@ state: present # ===================================================================== -# 3. AUF DATENBANK WARTEN +# 3. WAIT FOR DATABASE # ===================================================================== - name: Wait for database to be created by Homarr @@ -63,71 +77,69 @@ when: not db_exists.stat.exists - name: Wait for database schema to be initialized - ansible.builtin.shell: | - i=0 - while [ $i -lt 30 ]; do - if sqlite3 "{{ homarr_db_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" 2>/dev/null | grep -q board; then - exit 0 - fi - sleep 2 - i=$((i + 1)) - done - exit 1 + ansible.builtin.command: + cmd: sqlite3 "{{ homarr_db_dir }}" "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 -- name: Ensure python3-bcrypt is installed - ansible.builtin.package: - name: python3-bcrypt - state: present +# ===================================================================== +# 4. GENERATE BCRYPT HASH (on controller, not on target) +# ===================================================================== - name: Generate bcrypt hash for admin password - ansible.builtin.shell: | - python3 -c " - import bcrypt - password = '{{ homarr_admin_password }}'.encode() - salt = bcrypt.gensalt(rounds=10) - hashed = bcrypt.hashpw(password, salt) - print(salt.decode()) - print(hashed.decode()) - " - register: bcrypt_output + ansible.builtin.shell: + cmd: python3 -c "import bcrypt, sys; print(bcrypt.hashpw(sys.stdin.read().encode(), bcrypt.gensalt(rounds=10)).decode())" + stdin: "{{ homarr_admin_password }}" + stdin_add_newline: false + delegate_to: localhost + become: false + register: bcrypt_result changed_when: false no_log: true +- name: Set bcrypt hash fact + ansible.builtin.set_fact: + homarr_bcrypt_hash: "{{ bcrypt_result.stdout }}" + no_log: true + # ===================================================================== -# 4. DATENBANK SEEDEN (nur wenn Onboarding noch nicht abgeschlossen) +# 5. SEED DATABASE (only if local admin user does not exist yet) # ===================================================================== -- name: Check if onboarding is already completed - ansible.builtin.shell: | - sqlite3 "{{ homarr_db_dir }}" "SELECT step FROM onboarding WHERE step='finish';" 2>/dev/null - register: onboarding_status +- name: Check if local admin user exists + ansible.builtin.command: + cmd: sqlite3 "{{ homarr_db_dir }}" "SELECT id FROM user WHERE id='user-local-admin';" + register: admin_exists changed_when: false failed_when: false - name: Seed Homarr database ansible.builtin.shell: | sqlite3 "{{ homarr_db_dir }}" << 'SEEDSQL' + BEGIN TRANSACTION; + -- SERVER SETTINGS INSERT OR REPLACE INTO serverSetting (setting_key, value) - VALUES - ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false}}'), + VALUES + ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false, "enableUserData": false}}'), ('culture', '{"json": {"defaultLocale": "de"}}'), ('crawling', '{"json": {"crawlingEnabled": false}}'), - ('board', '{"json": {"defaultBoardId": "board-default"}}'); + ('board', '{"json": {"homeBoardId": "board-default", "mobileHomeBoardId": "board-default", "enableStatusByDefault": true, "forceDisableStatus": false, "defaultBoardId": "board-default"}}'); - -- ONBOARDING ÜBERSPRINGEN + -- SKIP ONBOARDING UPDATE onboarding SET step = 'finish', previous_step = 'settings'; - -- ===================================================================== - -- GRUPPEN (müssen VOR groupMember existieren) - -- ===================================================================== + -- ================================================================= + -- GROUPS (must exist before groupMember) + -- ================================================================= - -- OIDC-ADMIN GRUPPE + -- OIDC admin group INSERT OR IGNORE INTO "group" (id, name, owner_id, position) - VALUES ('group-oidc-admins', '{{ oidc_admin_group | default("homarr-admins") }}', NULL, 0); + VALUES ('group-oidc-admins', '{{ homarr_oidc_admin_group }}', NULL, 0); INSERT OR IGNORE INTO groupPermission (group_id, permission) VALUES @@ -137,7 +149,7 @@ ('group-oidc-admins', 'integration-create'), ('group-oidc-admins', 'integration-full-access'); - -- CREDENTIALS-ADMIN GRUPPE + -- Credentials admin group INSERT OR IGNORE INTO "group" (id, name, owner_id, position) VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1); @@ -149,31 +161,29 @@ ('group-credentials-admin', 'integration-create'), ('group-credentials-admin', 'integration-full-access'); - -- ===================================================================== - -- LOKALER ADMIN USER (Passwort wird via CLI gesetzt) - -- ===================================================================== + -- ================================================================= + -- LOCAL ADMIN USER + -- ================================================================= - INSERT OR IGNORE INTO user (id, name, email, password, salt, email_verified, provider) + INSERT OR IGNORE INTO user (id, name, email, password, email_verified, provider) VALUES ( 'user-local-admin', - '{{ homarr_admin_username | default("admin") }}', - '{{ homarr_admin_email | default("admin@digitalboard.ch") }}', - '{{ bcrypt_output.stdout_lines[1] }}', - '{{ bcrypt_output.stdout_lines[0] }}', + '{{ homarr_admin_username }}', + '{{ homarr_admin_email }}', + '{{ homarr_bcrypt_hash }}', 1, 'credentials' ); - -- ADMIN-USER DEN GRUPPEN ZUWEISEN (Gruppen existieren jetzt) - + -- 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, @@ -183,8 +193,8 @@ ) VALUES ( 'board-default', - '{{ default_board_name | default("Dashboard") }}', - {% if default_board_public | default(true) %}1{% else %}0{% endif %}, + '{{ homarr_default_board_name }}', + {% if homarr_default_board_public %}1{% else %}0{% endif %}, '#fa5252', '#fd7e14', 100, @@ -195,18 +205,18 @@ 0 ); - -- LAYOUTS + -- 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); - -- HOME BOARD FÜR ADMIN SETZEN (Board existiert jetzt) + -- 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'; - -- SEKTION + -- 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'; @@ -218,7 +228,7 @@ 'empty', 0, 0, - 'Anwendungen', + 'Applications', '{"json": {}}' ); @@ -228,15 +238,15 @@ ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); - -- BOARD-BERECHTIGUNG + -- 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 - -- ===================================================================== + -- ================================================================= -- Nextcloud INSERT OR IGNORE INTO app (id, name, description, icon_url, href) @@ -312,85 +322,12 @@ ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); + + COMMIT; SEEDSQL args: executable: /bin/bash register: seed_result changed_when: seed_result.rc == 0 - when: onboarding_status.stdout is not defined or 'finish' not in onboarding_status.stdout - -# ===================================================================== -# 5. RESTART, HEALTH-CHECK, DANN CLI -# ===================================================================== - -- name: Restart Homarr to apply database changes - ansible.builtin.shell: - cmd: docker compose restart - chdir: "{{ homarr_docker_compose_dir }}" - when: seed_result is changed - -- name: Wait for Homarr to be ready - ansible.builtin.shell: - cmd: docker compose exec -T {{ homarr_service_name }} wget -qO /dev/null "http://localhost:7575" 2>&1 - chdir: "{{ homarr_docker_compose_dir }}" - retries: 30 - delay: 5 - register: homarr_ready - until: homarr_ready.rc == 0 - changed_when: false - -- name: Display completion message - ansible.builtin.debug: - msg: | - ============================================================ - Homarr deployed! - URL: {{ homarr_base_url }} - Admin-User: {{ homarr_admin_username }} - Admin-Passwort: {{ homarr_admin_password }} - (Bitte sofort ändern!) - ============================================================ - when: not db_exists.stat.exists - -# ===================================================================== -# 6. ABSCHLUSS -# ===================================================================== - -- name: Display admin credentials - ansible.builtin.debug: - msg: | - ============================================ - LOKALER ADMIN PASSWORT (bitte sofort ändern!) - {{ admin_password_output.stdout }} - ============================================ - when: - - admin_password_output is defined - - admin_password_output.stdout is defined - - admin_password_output.stdout | length > 0 - -- name: Display completion message - ansible.builtin.debug: - msg: | - ============================================================ - Homarr wurde erfolgreich deployed! - ============================================================ - - URL: {{ homarr_base_url }} - - OIDC Konfiguration: - - Issuer: {{ oidc_issuer }} - - Client ID: {{ oidc_client_id }} - - Admin-Gruppe: {{ oidc_admin_group }} - - Nächste Schritte: - 1. Stelle sicher, dass in Keycloak die Gruppe "{{ oidc_admin_group }}" existiert - 2. Füge deinen User zur Gruppe "{{ oidc_admin_group }}" hinzu - 3. Öffne {{ homarr_base_url }} - 4. Du solltest automatisch via OIDC eingeloggt werden - - Das Standard-Board "{{ default_board_name }}" wurde erstellt mit: - - Nextcloud App - - Keycloak App - - Mailman App - - Weitere Apps kannst du direkt in der Homarr UI hinzufügen. - ============================================================ \ No newline at end of file + when: admin_exists.stdout == "" + notify: restart homarr \ No newline at end of file From bdb1b03a1890a65fe7b5c14c8a4c113db8f036cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:15:53 +0200 Subject: [PATCH 2/9] refactor(homarr): align vars with homarr_ prefix, EN-only strings --- roles/homarr/defaults/main.yml | 41 ++++++++++++-------- roles/homarr/templates/docker-compose.yml.j2 | 19 +++++---- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 78b32ab..dce7501 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -17,34 +17,43 @@ homarr_db_dir: "{{ homarr_appdata_dir }}/db/db.sqlite" # Service configuration homarr_domain: "homarr.local.test" homarr_image: "ghcr.io/homarr-labs/homarr:latest" -homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" homarr_port: 7575 homarr_use_docker: false -# URL – wird für BASE_URL, NEXTAUTH_URL und die Completion-Message verwendet +# 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: "" +homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" + +# URL — used for BASE_URL, NEXTAUTH_URL and the completion message homarr_base_url: "https://home.local.test" -# OIDC Konfiguration -oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" -oidc_client_id: "homarr-digitalboard" -oidc_client_name: "Digitalboard" -oidc_scopes: "openid profile email groups" -oidc_groups_attribute: "groups" -oidc_client_secret: "mein-test-secret-aus-keycloak" -oidc_auto_login: "false" +# Auth providers (comma-separated): credentials, oidc, ldap +homarr_auth_providers: "credentials,oidc" -# OIDC Admin-Gruppe (muss in Keycloak existieren) -oidc_admin_group: "homarr-admins" +# OIDC configuration +homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" +homarr_oidc_client_id: "homarr-digitalboard" +homarr_oidc_client_name: "Digitalboard" +homarr_oidc_scopes: "openid profile email groups" +homarr_oidc_groups_attribute: "groups" +homarr_oidc_client_secret: "" +homarr_oidc_auto_login: "false" -# Board Konfiguration -default_board_name: "Home" -default_board_public: true +# 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 -# Lokaler Admin +# Local admin homarr_admin_username: "admin" homarr_admin_email: "admin@digitalboard.ch" homarr_admin_password: "ChangeMe123!" \ No newline at end of file diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index b953ed6..96c203d 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -1,5 +1,5 @@ #---------------------------------------------------------------------# -# Homarr - A simple, yet powerful dashboard for your server. # +# Homarr — A simple, yet powerful dashboard for your server. # #---------------------------------------------------------------------# services: {{ homarr_service_name }}: @@ -16,15 +16,14 @@ services: BASE_URL: "{{ homarr_base_url }}" NEXTAUTH_URL: "{{ homarr_base_url }}" SECRET_ENCRYPTION_KEY: "{{ homarr_secret_encryption_key }}" - # Auth: Credentials + OIDC - AUTH_PROVIDERS: "credentials,oidc" - AUTH_OIDC_ISSUER: "{{ oidc_issuer }}" - AUTH_OIDC_CLIENT_ID: "{{ oidc_client_id }}" - AUTH_OIDC_CLIENT_SECRET: "{{ oidc_client_secret }}" - AUTH_OIDC_CLIENT_NAME: "{{ oidc_client_name | default('Keycloak') }}" - AUTH_OIDC_SCOPE_OVERWRITE: "{{ oidc_scopes | default('openid email profile groups') }}" - AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ oidc_groups_attribute | default('groups') }}" - AUTH_OIDC_AUTO_LOGIN: "{{ oidc_auto_login | default('false') }}" + 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 }} labels: From 123769a4f4d03f7f2bf18861f1a1bb8cfa868b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:16:28 +0200 Subject: [PATCH 3/9] feat(homarr): use handler for restart, validate encryption key --- roles/homarr/handlers/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/roles/homarr/handlers/main.yml b/roles/homarr/handlers/main.yml index 56f5283..dedb949 100644 --- a/roles/homarr/handlers/main.yml +++ b/roles/homarr/handlers/main.yml @@ -1,3 +1,8 @@ #SPDX-License-Identifier: MIT-0 --- -# handlers file for homarr \ No newline at end of file +# handlers file for homarr + +- name: restart homarr + community.docker.docker_compose_v2: + project_src: "{{ homarr_docker_compose_dir }}" + state: restarted \ No newline at end of file From f4084ba078fd8fb82c079e9008c6fbf2c9db8a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:34:33 +0200 Subject: [PATCH 4/9] refactor(homarr): drop service_name var and rename db_dir to db - homarr_service_name removed, replaced with fixed "homarr" string - homarr_db_dir renamed to homarr_db (variable points to a file, not a dir) --- roles/homarr/defaults/main.yml | 9 ++++----- roles/homarr/tasks/main.yml | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index dce7501..2afb9f9 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -8,11 +8,10 @@ docker_volume_base_dir: /srv/data # homarr-specific configuration homarr_base_path: /srv/data/homarr -homarr_service_name: homarr -homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ homarr_service_name }}" -homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ homarr_service_name }}" -homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/{{ homarr_service_name }}/appdata" -homarr_db_dir: "{{ homarr_appdata_dir }}/db/db.sqlite" +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" diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index 22bfe97..b292408 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -47,7 +47,7 @@ - name: Check if database already exists ansible.builtin.stat: - path: "{{ homarr_db_dir }}" + path: "{{ homarr_db }}" register: db_exists # ===================================================================== @@ -71,14 +71,14 @@ - name: Wait for database to be created by Homarr ansible.builtin.wait_for: - path: "{{ homarr_db_dir }}" + 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_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" + 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 @@ -112,14 +112,14 @@ - name: Check if local admin user exists ansible.builtin.command: - cmd: sqlite3 "{{ homarr_db_dir }}" "SELECT id FROM user WHERE id='user-local-admin';" + 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.shell: | - sqlite3 "{{ homarr_db_dir }}" << 'SEEDSQL' + sqlite3 "{{ homarr_db }}" << 'SEEDSQL' BEGIN TRANSACTION; -- SERVER SETTINGS From 3c35b8782ee75216d034ac63fa5f89fb912512c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 13:58:15 +0200 Subject: [PATCH 5/9] fix: reomved remnants of removed env / fixed encription key validatiion --- roles/homarr/defaults/main.yml | 3 +-- roles/homarr/tasks/main.yml | 2 +- roles/homarr/templates/docker-compose.yml.j2 | 14 +++++++------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 2afb9f9..a371bdf 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -23,8 +23,7 @@ homarr_use_docker: false # 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: "" -homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" +homarr_secret_encryption_key: "9981080f7eb054b90c7be80622608c3b63ba58408171ef3fbcdce9ba9c0554dc" # URL — used for BASE_URL, NEXTAUTH_URL and the completion message homarr_base_url: "https://home.local.test" diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index b292408..d3e9d75 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -10,7 +10,7 @@ ansible.builtin.assert: that: - homarr_secret_encryption_key | length == 64 - - homarr_secret_encryption_key is match('^[a-f0-9]+$') + - 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 diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index 96c203d..2d81063 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -2,8 +2,8 @@ # Homarr — A simple, yet powerful dashboard for your server. # #---------------------------------------------------------------------# services: - {{ homarr_service_name }}: - container_name: {{ homarr_service_name }} + homarr: + container_name: homarr image: {{ homarr_image }} restart: unless-stopped volumes: @@ -29,14 +29,14 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ homarr_traefik_network }} - - traefik.http.routers.{{ homarr_service_name }}.rule=Host(`{{ homarr_domain }}`) + - traefik.http.routers.homarr.rule=Host(`{{ homarr_domain }}`) {% if homarr_use_ssl %} - - traefik.http.routers.{{ homarr_service_name }}.entrypoints=websecure - - traefik.http.routers.{{ homarr_service_name }}.tls=true + - traefik.http.routers.homarr.entrypoints=websecure + - traefik.http.routers.homarr.tls=true {% else %} - - traefik.http.routers.{{ homarr_service_name }}.entrypoints=web + - traefik.http.routers.homarr.entrypoints=web {% endif %} - - traefik.http.services.{{ homarr_service_name }}.loadbalancer.server.port={{ homarr_port }} + - traefik.http.services.homarr.loadbalancer.server.port={{ homarr_port }} networks: {{ homarr_traefik_network }}: external: true \ No newline at end of file From d4eaa5f12c814cc674b1cb7beca307f71c252cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 14:51:36 +0200 Subject: [PATCH 6/9] refactor(homarr): extract seed SQL into template --- roles/homarr/defaults/main.yml | 2 +- roles/homarr/tasks/main.yml | 221 ++-------------------- roles/homarr/templates/homarr_seed.sql.j2 | 210 ++++++++++++++++++++ 3 files changed, 223 insertions(+), 210 deletions(-) create mode 100644 roles/homarr/templates/homarr_seed.sql.j2 diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index a371bdf..e981b1f 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -37,7 +37,7 @@ homarr_oidc_client_id: "homarr-digitalboard" homarr_oidc_client_name: "Digitalboard" homarr_oidc_scopes: "openid profile email groups" homarr_oidc_groups_attribute: "groups" -homarr_oidc_client_secret: "" +homarr_oidc_client_secret: "a91c9ec370f75fe34f7df20f50a70ff2d761ebd74c336a3f9e4640b49521cee2" homarr_oidc_auto_login: "false" # OIDC admin group (must exist in the identity provider) diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index d3e9d75..a3ff991 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -17,6 +17,15 @@ 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" + # ===================================================================== # 1. PREPARATION: packages and directories before container start # ===================================================================== @@ -118,215 +127,9 @@ failed_when: false - name: Seed Homarr database - ansible.builtin.shell: | - sqlite3 "{{ homarr_db }}" << 'SEEDSQL' - 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 - 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": {}}' - ); - - 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, 3), - ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), - ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); - - -- 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 - -- ================================================================= - - -- Nextcloud - INSERT OR IGNORE INTO app (id, name, description, icon_url, href) - VALUES ( - 'app-nextcloud', - 'Nextcloud', - 'Cloud Storage & Collaboration', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', - 'https://cloud.digitalboard.ch' - ); - - INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) - VALUES ( - 'item-nextcloud', - 'board-default', - 'app', - '{"json": {"appId": "app-nextcloud"}}', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) - VALUES - ('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); - - -- Keycloak - INSERT OR IGNORE INTO app (id, name, description, icon_url, href) - VALUES ( - 'app-keycloak', - 'Keycloak', - 'Identity & Access Management', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', - 'https://auth.digitalboard.ch' - ); - - INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) - VALUES ( - 'item-keycloak', - 'board-default', - 'app', - '{"json": {"appId": "app-keycloak"}}', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) - VALUES - ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); - - -- Mailman - INSERT OR IGNORE INTO app (id, name, description, icon_url, href) - VALUES ( - 'app-mailman', - 'Mailman', - 'Mailing List Manager', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', - 'https://lists.digitalboard.ch' - ); - - INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) - VALUES ( - 'item-mailman', - 'board-default', - 'app', - '{"json": {"appId": "app-mailman"}}', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) - VALUES - ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); - - COMMIT; - SEEDSQL - args: - executable: /bin/bash + 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 == "" diff --git a/roles/homarr/templates/homarr_seed.sql.j2 b/roles/homarr/templates/homarr_seed.sql.j2 new file mode 100644 index 0000000..0490c04 --- /dev/null +++ b/roles/homarr/templates/homarr_seed.sql.j2 @@ -0,0 +1,210 @@ +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": {}}' +); + +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, 3), + ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), + ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); + +-- 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 +-- ===================================================================== + +-- Nextcloud +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + 'app-nextcloud', + 'Nextcloud', + 'Cloud Storage & Collaboration', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', + 'https://cloud.digitalboard.ch' +); + +INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) +VALUES ( + 'item-nextcloud', + 'board-default', + 'app', + '{"json": {"appId": "app-nextcloud"}}', + '{"json": {}}' +); + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES + ('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), + ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), + ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); + +-- Keycloak +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + 'app-keycloak', + 'Keycloak', + 'Identity & Access Management', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', + 'https://auth.digitalboard.ch' +); + +INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) +VALUES ( + 'item-keycloak', + 'board-default', + 'app', + '{"json": {"appId": "app-keycloak"}}', + '{"json": {}}' +); + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES + ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), + ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), + ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); + +-- Mailman +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + 'app-mailman', + 'Mailman', + 'Mailing List Manager', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', + 'https://lists.digitalboard.ch' +); + +INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) +VALUES ( + 'item-mailman', + 'board-default', + 'app', + '{"json": {"appId": "app-mailman"}}', + '{"json": {}}' +); + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES + ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), + ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), + ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); + +COMMIT; From c1c1a8459100ea201f46e5fcf9cb02f16b81ab76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 15:02:32 +0200 Subject: [PATCH 7/9] feat(homarr): make apps list configurable with auto-layout --- roles/homarr/defaults/main.yml | 30 +++++- roles/homarr/tasks/main.yml | 12 +++ roles/homarr/templates/homarr_seed.sql.j2 | 119 +++++++++++----------- 3 files changed, 101 insertions(+), 60 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index e981b1f..2ef6df3 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -54,4 +54,32 @@ homarr_use_ssl: true # Local admin homarr_admin_username: "admin" homarr_admin_email: "admin@digitalboard.ch" -homarr_admin_password: "ChangeMe123!" \ No newline at end of file +homarr_admin_password: "ChangeMe123!" + +# Applications shown on the default board. +# Each app needs id, name, description, icon, href and a width (1-10). +# Height defaults to 1, can be increased for taller tiles. +# Apps are automatically packed left-to-right into the desktop grid (10 cols), +# scaled to tablet (6 cols) and mobile (2 cols). +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.digitalboard.ch + width: 2 + height: 1 + - 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 + height: 1 + - id: mailman + name: Mailman + description: Mailing List Manager + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png + href: https://lists.digitalboard.ch + width: 2 + height: 1 \ No newline at end of file diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index a3ff991..06488f4 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -26,6 +26,18 @@ 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 # ===================================================================== diff --git a/roles/homarr/templates/homarr_seed.sql.j2 b/roles/homarr/templates/homarr_seed.sql.j2 index 0490c04..fdb1a2f 100644 --- a/roles/homarr/templates/homarr_seed.sql.j2 +++ b/roles/homarr/templates/homarr_seed.sql.j2 @@ -1,3 +1,35 @@ +{#- + Auto-layout packing macro. + + Greedy left-to-right packing of apps into a grid with `cols` columns. + Returns the list of apps with computed x/y/w/h fields. + + Width is clamped to cols (so an app wider than the grid is downsized + rather than overflowing). Height is taken as-is. +-#} +{%- macro pack(apps, cols) -%} + {%- set ns = namespace(x=0, y=0, row_h=0, out=[]) -%} + {%- for app in apps -%} + {%- set w = [app.width, cols] | min -%} + {%- set h = app.height | default(1) -%} + {%- if ns.x + w > cols -%} + {%- set ns.x = 0 -%} + {%- set ns.y = ns.y + ns.row_h -%} + {%- set ns.row_h = 0 -%} + {%- endif -%} + {%- set _ = ns.out.append({'id': app.id, 'x': ns.x, 'y': ns.y, 'w': w, 'h': h}) -%} + {%- set ns.x = ns.x + w -%} + {%- if h > ns.row_h -%} + {%- set ns.row_h = h -%} + {%- endif -%} + {%- endfor -%} + {{- ns.out | to_json -}} +{%- endmacro -%} + +{%- set desktop_layout = pack(homarr_apps, 10) | from_json -%} +{%- set tablet_layout = pack(homarr_apps, 6) | from_json -%} +{%- set mobile_layout = pack(homarr_apps, 2) | from_json -%} + BEGIN TRANSACTION; -- ===================================================================== @@ -129,82 +161,51 @@ VALUES ('board-default', 'group-credentials-admin', 'full-access'); -- ===================================================================== --- APPS +-- APPS (auto-generated from homarr_apps variable) -- ===================================================================== --- Nextcloud +{% if homarr_apps | length > 0 %} +{% for app in homarr_apps %} INSERT OR IGNORE INTO app (id, name, description, icon_url, href) VALUES ( - 'app-nextcloud', - 'Nextcloud', - 'Cloud Storage & Collaboration', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', - 'https://cloud.digitalboard.ch' + '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-nextcloud', + 'item-{{ app.id }}', 'board-default', 'app', - '{"json": {"appId": "app-nextcloud"}}', + '{"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-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); - --- Keycloak -INSERT OR IGNORE INTO app (id, name, description, icon_url, href) -VALUES ( - 'app-keycloak', - 'Keycloak', - 'Identity & Access Management', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', - 'https://auth.digitalboard.ch' -); - -INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) -VALUES ( - 'item-keycloak', - 'board-default', - 'app', - '{"json": {"appId": "app-keycloak"}}', - '{"json": {}}' -); +{% endfor %} INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) VALUES - ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); - --- Mailman -INSERT OR IGNORE INTO app (id, name, description, icon_url, href) -VALUES ( - 'app-mailman', - 'Mailman', - 'Mailing List Manager', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', - 'https://lists.digitalboard.ch' -); - -INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) -VALUES ( - 'item-mailman', - 'board-default', - 'app', - '{"json": {"appId": "app-mailman"}}', - '{"json": {}}' -); +{% for entry in desktop_layout %} + ('item-{{ entry.id }}', 'section-apps', 'layout-desktop', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %} +{% endfor %} +; INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) VALUES - ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); +{% for entry in tablet_layout %} + ('item-{{ entry.id }}', 'section-apps', 'layout-tablet', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %} +{% endfor %} +; -COMMIT; +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES +{% for entry in mobile_layout %} + ('item-{{ entry.id }}', 'section-apps', 'layout-mobile', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %} +{% endfor %} +; +{% endif %} + +COMMIT; \ No newline at end of file From 308bf5012222e149e4e2fce9a27619ea24029433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 15:12:55 +0200 Subject: [PATCH 8/9] chore(homarr): remove digitalboard-specific defaults --- roles/homarr/defaults/main.yml | 37 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 2ef6df3..24e6b70 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -57,29 +57,18 @@ homarr_admin_email: "admin@digitalboard.ch" homarr_admin_password: "ChangeMe123!" # Applications shown on the default board. -# Each app needs id, name, description, icon, href and a width (1-10). -# Height defaults to 1, can be increased for taller tiles. +# 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). -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.digitalboard.ch - width: 2 - height: 1 - - 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 - height: 1 - - id: mailman - name: Mailman - description: Mailing List Manager - icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png - href: https://lists.digitalboard.ch - width: 2 - height: 1 \ No newline at end of file +# +# 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: [ ] \ No newline at end of file From 2aa1df861405c0e4e5f4e6d8371e4970381874af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 15:24:51 +0200 Subject: [PATCH 9/9] chore(homarr): added readme and removed test env contents --- roles/homarr/README.md | 206 +++++++++++++++++++++++++++++---- roles/homarr/defaults/main.yml | 21 ++-- 2 files changed, 193 insertions(+), 34 deletions(-) diff --git a/roles/homarr/README.md b/roles/homarr/README.md index da76bcd..db0ed4d 100644 --- a/roles/homarr/README.md +++ b/roles/homarr/README.md @@ -1,38 +1,196 @@ -Role Name -========= +# homarr -A brief description of the role goes here. +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. -Requirements ------------- +## What this role 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. +- 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 +- Skips the onboarding wizard so the instance is usable right after deploy +- Restarts the container via handler when the seed or compose file changes -Role Variables --------------- +## What this role does NOT do -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. +- 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) -Dependencies ------------- +## Required variables -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. +Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real +secrets to version control.** -Example Playbook ----------------- +| 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 | — | -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: +The `assert` task at the top of the role will fail fast if the encryption +key is missing or malformed. - - hosts: servers - roles: - - { role: username.rolename, x: 42 } +## Configurable variables -License -------- +See `defaults/main.yml` for the full list. Most useful overrides: -BSD +| Variable | Default | Purpose | +|---|---|---| +| `homarr_domain` | `homarr.local.test` | Traefik Host rule | +| `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 | -Author Information ------------------- +## Application tiles -An optional section for the role authors to include contact information, or a website (HTML is not allowed). \ No newline at end of file +`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-` and `item-` | +| `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 +``` + +## 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//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. \ No newline at end of file diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 24e6b70..728e3b3 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -1,3 +1,4 @@ +homarr_apps: [ ] #SPDX-License-Identifier: MIT-0 --- # defaults file for homarr @@ -23,21 +24,21 @@ homarr_use_docker: false # 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: "9981080f7eb054b90c7be80622608c3b63ba58408171ef3fbcdce9ba9c0554dc" +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" +homarr_auth_providers: "credentials" -# OIDC configuration -homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" -homarr_oidc_client_id: "homarr-digitalboard" -homarr_oidc_client_name: "Digitalboard" +# 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: "a91c9ec370f75fe34f7df20f50a70ff2d761ebd74c336a3f9e4640b49521cee2" +homarr_oidc_client_secret: "" homarr_oidc_auto_login: "false" # OIDC admin group (must exist in the identity provider) @@ -51,9 +52,9 @@ homarr_default_board_public: true homarr_traefik_network: "proxy" homarr_use_ssl: true -# Local admin +# Local admin (override in inventory or via vault) homarr_admin_username: "admin" -homarr_admin_email: "admin@digitalboard.ch" +homarr_admin_email: "admin@example.com" homarr_admin_password: "ChangeMe123!" # Applications shown on the default board. @@ -71,4 +72,4 @@ homarr_admin_password: "ChangeMe123!" # href: https://cloud.example.com # width: 2 # height: 1 -homarr_apps: [ ] \ No newline at end of file +homarr_apps: [] \ No newline at end of file