Compare commits

...

3 Commits

14 changed files with 333 additions and 53 deletions

View File

@@ -0,0 +1,39 @@
def merge_with_defaults(defaults, customs):
"""
Recursively merge two dicts (customs into defaults).
For each top-level key in customs, ensure all dict keys from defaults are present (at least empty dict).
Customs always take precedence.
"""
def merge_dict(d1, d2):
# Recursively merge d2 into d1, d2 wins
result = dict(d1) if d1 else {}
for k, v in (d2 or {}).items():
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
result[k] = merge_dict(result[k], v)
else:
result[k] = v
return result
merged = {}
# Union of all app-keys
all_keys = set(defaults or {}).union(set(customs or {}))
for app_key in all_keys:
base = (defaults or {}).get(app_key, {})
override = (customs or {}).get(app_key, {})
# Step 1: merge override into base
result = merge_dict(base, override)
# Step 2: ensure all dict keys from base exist in result (at least {})
for k, v in (base or {}).items():
if isinstance(v, dict) and k not in result:
result[k] = {}
merged[app_key] = result
return merged
class FilterModule(object):
'''Custom merge filter for CyMaIS: merge_with_defaults'''
def filters(self):
return {
'merge_with_defaults': merge_with_defaults,
}

View File

@@ -49,7 +49,7 @@ ports:
web-app-akaunting: 8025
web-app-moodle: 8026
taiga: 8027
friendica: 8028
web-app-friendica: 8028
web-app-port-ui: 8029
bluesky_api: 8030
bluesky_web: 8031

View File

@@ -18,7 +18,7 @@ defaults_networks:
subnet: 192.168.101.48/28
bluesky:
subnet: 192.168.101.64/28
friendica:
web-app-friendica:
subnet: 192.168.101.80/28
funkwhale:
subnet: 192.168.101.96/28

View File

@@ -0,0 +1,36 @@
# CopyQ Role for Ansible
## Overview
This role installs the CopyQ clipboard manager on Pacman-based systems (e.g. Arch Linux) and ensures it is started automatically for the current user.
## Requirements
- Ansible 2.9 or higher
- Pacman package manager (Arch Linux or derivative)
- X11/Wayland desktop environment (for GUI)
## Role Variables
No additional role variables are required.
## Dependencies
No external dependencies.
## Example Playbook
```yaml
- hosts: all
roles:
- desk-copyq
```
## Further Resources
- [CopyQ official site](https://hluk.github.io/CopyQ/)
- [Arch Wiki: Clipboard](https://wiki.archlinux.org/title/Clipboard)
## Contributing
Contributions are welcome. Please follow best practices.
## Other Resources
This role was created as part of a larger playbook. For more context on this role, you can refer to the related ChatGPT conversation [here](https://chat.openai.com/share/ae168ca0-5191-4bec-96a0-ffcfabca0024).

View File

@@ -0,0 +1,27 @@
---
galaxy_info:
author: "Kevin Veen-Birchenbach"
description: "Installs CopyQ clipboard manager on Pacman-based systems and configures autostart for the current user."
license: "CyMaIS NonCommercial License (CNCL)"
license_url: "https://s.veen.world/cncl"
company: |
Kevin Veen-Birchenbach
Consulting & Coaching Solutions
https://www.veen.world
galaxy_tags:
- copyq
- clipboard
- manager
- gui
- cli
logo:
class: fa fa-clipboard
repository: "https://github.com/kevinveenbirkenbach/cymais"
issue_tracker_url: "https://github.com/kevinveenbirkenbach/cymais/issues"
documentation: "https://github.com/kevinveenbirkenbach/cymais/tree/main/roles/desk-copyq"
min_ansible_version: "2.9"
platforms:
- name: Archlinux
versions:
- all
dependencies: []

View File

@@ -0,0 +1,27 @@
- name: Install CopyQ clipboard manager
community.general.pacman:
name:
- copyq
state: present
- name: Ensure autostart directory exists
file:
path: "{{ ansible_env.HOME }}/.config/autostart"
state: directory
mode: '0755'
become: false
- name: Add CopyQ to user autostart
copy:
dest: "{{ ansible_env.HOME }}/.config/autostart/copyq.desktop"
content: |
[Desktop Entry]
Type=Application
Exec=copyq
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
Name=CopyQ Clipboard Manager
Comment=Advanced clipboard manager with searchable and editable history
mode: '0644'
become: false

View File

@@ -0,0 +1 @@
application_id: desk-copyq

View File

@@ -0,0 +1,9 @@
- name: "create {{ friendica_host_ldap_config }}"
template:
src: "ldapauth.config.php.j2"
dest: "{{ friendica_host_ldap_config }}"
mode: '644'
owner: root
group: 33
force: yes
notify: docker compose up

View File

@@ -0,0 +1,34 @@
- name: flush handlers to ensure that friendica is up before friendica addon configuration
meta: flush_handlers
- name: Check if Friendica local.config.php exists
command: docker exec --user {{ friendica_user }} {{ friendica_container }} test -f {{ friendica_config_file }}
register: friendica_config_exists
changed_when: false
failed_when: false
- name: Patch Friendica local.config.php with updated DB credentials
when: friendica_config_exists.rc == 0
block:
- name: Update DB host
command: >
docker exec --user {{ friendica_user }} {{ friendica_container }}
sed -i "s/'hostname' => .*/'hostname' => '{{ database_host }}:{{ database_port }}',/" {{ friendica_config_file }}
notify: docker compose up
- name: Update DB name
command: >
docker exec --user {{ friendica_user }} {{ friendica_container }}
sed -i "s/'database' => .*/'database' => '{{ database_name }}',/" {{ friendica_config_file }}
notify: docker compose up
- name: Update DB user
command: >
docker exec --user {{ friendica_user }} {{ friendica_container }}
sed -i "s/'username' => .*/'username' => '{{ database_username }}',/" {{ friendica_config_file }}
notify: docker compose up
- name: Update DB password
command: >
docker exec --user {{ friendica_user }} {{ friendica_container }}
sed -i "s/'password' => .*/'password' => '{{ database_password }}',/" {{ friendica_config_file }}
notify: docker compose up

View File

@@ -0,0 +1,35 @@
- name: flush handlers to ensure that friendica is up before friendica addon configuration
meta: flush_handlers
- name: Build friendica_addons based on features
set_fact:
friendica_addons: >-
{{
friendica_addons | default([])
+ [{
'name': item.key,
'enabled': (
applications | get_app_conf(application_id, 'features.oidc', True)
if item.key == 'keycloakpassword'
else applications | get_app_conf(application_id, 'features.ldap', True)
if item.key == 'ldapauth'
else (item.value.enabled if item.value is mapping and 'enabled' in item.value else False)
)
}]
}}
loop: "{{ applications | get_app_conf(application_id, 'addons', True) | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Ensure Friendica addons are in sync
command: >
docker compose exec --user {{ friendica_user }}
application
bin/console addon
{{ 'enable' if item.enabled else 'disable' }}
{{ item.name }}
args:
chdir: "{{ docker_compose.directories.instance }}"
loop: "{{ friendica_addons }}"
loop_control:
label: "{{ item.name }}"

View File

@@ -3,50 +3,12 @@
include_role:
name: cmp-db-docker-proxy
- name: "create {{ friendica_host_ldap_config }}"
template:
src: "ldapauth.config.php.j2"
dest: "{{ friendica_host_ldap_config }}"
mode: '644'
owner: root
group: 33
force: yes
notify: docker compose up
- name: Integrate LDAP
include_tasks: 01_ldap.yml
when: applications | get_app_conf(application_id, 'features.ldap', False)
- name: Build friendica_addons based on features
set_fact:
friendica_addons: >-
{{
friendica_addons | default([])
+ [{
'name': item.key,
'enabled': (
applications | get_app_conf(application_id, 'features.oidc', True)
if item.key == 'keycloakpassword'
else applications | get_app_conf(application_id, 'features.ldap', True)
if item.key == 'ldapauth'
else (item.value.enabled if item.value is mapping and 'enabled' in item.value else False)
)
}]
}}
loop: "{{ applications | get_app_conf(application_id, 'addons', True) | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: flush handlers to ensure that friendica is up before friendica addon configuration
meta: flush_handlers
- name: Ensure Friendica addons are in sync
command: >
docker compose exec --user www-data
application
bin/console addon
{{ 'enable' if item.enabled else 'disable' }}
{{ item.name }}
args:
chdir: "{{ docker_compose.directories.instance }}"
loop: "{{ friendica_addons }}"
loop_control:
label: "{{ item.name }}"
- name: Update Friendica DB credentials
include_tasks: 02_database.yml
- name: Add Friendica Add Ons
include_tasks: 03_addons.yml

View File

@@ -1,8 +1,11 @@
application_id: "friendica"
application_id: "web-app-friendica"
database_type: "mariadb"
friendica_no_validation: "{{ applications | get_app_conf(application_id, 'features.oidc', True) }}" # Email validation is not neccessary if OIDC is active
friendica_application_base: "/var/www/html"
friendica_docker_ldap_config: "{{friendica_application_base}}/config/ldapauth.config.php"
friendica_host_ldap_config: "{{ docker_compose.directories.volumes }}ldapauth.config.php"
friendica_container: "application"
friendica_no_validation: "{{ applications | get_app_conf(application_id, 'features.oidc', True) }}" # Email validation is not neccessary if OIDC is active
friendica_application_base: "/var/www/html"
friendica_docker_ldap_config: "{{friendica_application_base}}/config/ldapauth.config.php"
friendica_host_ldap_config: "{{ docker_compose.directories.volumes }}ldapauth.config.php"
friendica_config_dir: "{{ friendica_application_base }}/config"
friendica_config_file: "{{ friendica_config_dir }}/local.config.php"
friendica_user: "www-data"

View File

@@ -19,7 +19,7 @@ galaxy_info:
issue_tracker_url: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/issues"
documentation: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/"
logo:
class: ""
class: "fa-solid fa-infinity"
run_after: []
dependencies:
- sys-cli

View File

@@ -0,0 +1,107 @@
import unittest
import sys
import os
# Allow import from project filter_plugins directory
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../filter_plugins')))
from merge_with_defaults import merge_with_defaults
class TestMergeWithDefaultsFilter(unittest.TestCase):
def test_basic_merge(self):
defaults = {
"app1": {
"docker": {
"network": "default",
"services": {"foo": "bar"},
"volumes": {"data": "/mnt"}
},
"features": {"ldap": True, "sso": False},
"version": 1
}
}
customs = {
"app1": {
"docker": {
"network": "customnet"
},
"version": 2
},
"app2": {
"docker": {
"network": "other"
}
}
}
expected = {
"app1": {
"docker": {
"network": "customnet",
"services": {"foo": "bar"},
"volumes": {"data": "/mnt"}
},
"features": {"ldap": True, "sso": False},
"version": 2
},
"app2": {
"docker": {
"network": "other"
}
}
}
result = merge_with_defaults(defaults, customs)
self.assertEqual(result, expected)
def test_keys_from_defaults_only(self):
defaults = {
"foo": {"docker": {"a": 1, "b": 2}, "features": {"x": True}},
}
customs = {
"foo": {},
}
expected = {
"foo": {"docker": {"a": 1, "b": 2}, "features": {"x": True}}
}
result = merge_with_defaults(defaults, customs)
self.assertEqual(result, expected)
def test_custom_overrides_nested_dict(self):
defaults = {"x": {"docker": {"bar": 1, "baz": 2}}}
customs = {"x": {"docker": {"bar": 99}}}
expected = {"x": {"docker": {"bar": 99, "baz": 2}}}
result = merge_with_defaults(defaults, customs)
self.assertEqual(result, expected)
def test_only_defaults_present(self):
defaults = {"only": {"value": 1}}
customs = {}
expected = {"only": {"value": 1}}
result = merge_with_defaults(defaults, customs)
self.assertEqual(result, expected)
def test_only_customs_present(self):
defaults = {}
customs = {"x": {"foo": 42}}
expected = {"x": {"foo": 42}}
result = merge_with_defaults(defaults, customs)
self.assertEqual(result, expected)
def test_deep_merge_multiple_levels(self):
defaults = {
"a": {"outer": {"mid": {"inner": 1, "keep": True}}, "plain": "test"}
}
customs = {
"a": {"outer": {"mid": {"inner": 99}}, "plain": "changed"}
}
expected = {
"a": {"outer": {"mid": {"inner": 99, "keep": True}}, "plain": "changed"}
}
result = merge_with_defaults(defaults, customs)
self.assertEqual(result, expected)
if __name__ == "__main__":
unittest.main()