From 648f08c150a638f192df37489f0fa97ad20b8464 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Sat, 2 May 2026 09:15:51 -0300 Subject: [PATCH] Adding automatisch --- .claude/settings.local.json | 5 +- inventories/production/group_vars/all.yml | 11 ++ molecule/shared/vars.yml | 9 ++ playbooks/site.yml | 2 + roles/automatisch/defaults/main.yml | 3 + roles/automatisch/handlers/main.yml | 7 + .../automatisch/molecule/default/converge.yml | 8 ++ .../automatisch/molecule/default/molecule.yml | 21 +++ roles/automatisch/molecule/default/verify.yml | 82 ++++++++++++ roles/automatisch/tasks/main.yml | 22 ++++ .../templates/docker-compose.yml.j2 | 120 ++++++++++++++++++ 11 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 roles/automatisch/defaults/main.yml create mode 100644 roles/automatisch/handlers/main.yml create mode 100644 roles/automatisch/molecule/default/converge.yml create mode 100644 roles/automatisch/molecule/default/molecule.yml create mode 100644 roles/automatisch/molecule/default/verify.yml create mode 100644 roles/automatisch/tasks/main.yml create mode 100644 roles/automatisch/templates/docker-compose.yml.j2 diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ad92ac3..07358d4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -35,7 +35,10 @@ "Bash(grep -E \"^\\(---$|base_domain:|traefik_domain:|graylog_root_password_sha2:|dns_server_ip:|dmarc_policy:|forgejo_ssh_port:|stalwart_dkim_selector:\\)\")", "Bash(python3 -c \"import sys, yaml; data = yaml.safe_load\\(sys.stdin\\); print\\(f'Valid YAML: {len\\(data\\)} keys'\\); print\\('changeme keys:', [k for k,v in data.items\\(\\) if isinstance\\(v,str\\) and 'changeme' in v]\\)\")", "Bash(python3 -c \"import sys, yaml; d = yaml.safe_load\\(sys.stdin\\); print\\('base_domain:', d['base_domain']\\); print\\('graylog_root_password_sha2 len:', len\\(d['graylog_root_password_sha2']\\)\\); print\\('authentik_secret_key len:', len\\(d['authentik_secret_key']\\)\\)\")", - "Bash(python3 -c \"import sys, yaml; d = yaml.safe_load\\(sys.stdin\\); print\\('base_domain type:', type\\(d['base_domain']\\).__name__, '=', repr\\(d['base_domain']\\)\\); print\\('All 93 keys present:', len\\(d\\) == 93\\)\")" + "Bash(python3 -c \"import sys, yaml; d = yaml.safe_load\\(sys.stdin\\); print\\('base_domain type:', type\\(d['base_domain']\\).__name__, '=', repr\\(d['base_domain']\\)\\); print\\('All 93 keys present:', len\\(d\\) == 93\\)\")", + "WebFetch(domain:automatisch.io)", + "WebFetch(domain:github.com)", + "WebFetch(domain:hub.docker.com)" ] } } diff --git a/inventories/production/group_vars/all.yml b/inventories/production/group_vars/all.yml index 85a58a2..3718d81 100644 --- a/inventories/production/group_vars/all.yml +++ b/inventories/production/group_vars/all.yml @@ -126,6 +126,17 @@ forgejo_admin_password: "changeme_forgejo_admin" forgejo_admin_email: "admin@{{ base_domain }}" forgejo_ssh_port: 2222 +# Automatisch +automatisch_domain: "automate.{{ base_domain }}" +automatisch_version: "latest" +automatisch_db_password: "changeme_automatisch_db" +# Generate each with: openssl rand -base64 36 +# WARNING: these keys encrypt stored credentials — changing them after first +# deployment will break all existing integrations. +automatisch_encryption_key: "changeme_automatisch_encryption_key_base64_36" +automatisch_webhook_secret_key: "changeme_automatisch_webhook_secret_base64_36" +automatisch_app_secret_key: "changeme_automatisch_app_secret_base64_36" + # Twenty CRM twenty_domain: "crm.{{ base_domain }}" twenty_version: "latest" diff --git a/molecule/shared/vars.yml b/molecule/shared/vars.yml index 314e961..3f6b1a7 100644 --- a/molecule/shared/vars.yml +++ b/molecule/shared/vars.yml @@ -136,6 +136,15 @@ forgejo_admin_email: "admin@test.example.com" forgejo_ssh_port: 2222 forgejo_data_dir: /tmp/sovereign_test/forgejo +# Automatisch +automatisch_domain: "automate.test.example.com" +automatisch_version: "latest" +automatisch_db_password: "test_automatisch_db" +automatisch_encryption_key: "test_automatisch_encryption_key" +automatisch_webhook_secret_key: "test_automatisch_webhook_secret" +automatisch_app_secret_key: "test_automatisch_app_secret" +automatisch_data_dir: /tmp/sovereign_test/automatisch + # Twenty CRM twenty_domain: "crm.test.example.com" twenty_version: "latest" diff --git a/playbooks/site.yml b/playbooks/site.yml index dda8b0d..5c6c078 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -31,6 +31,8 @@ tags: [vaultwarden, passwords, vault] - role: forgejo tags: [forgejo, git, vcs] + - role: automatisch + tags: [automatisch, automation, workflows] - role: twenty tags: [twenty, crm] - role: website diff --git a/roles/automatisch/defaults/main.yml b/roles/automatisch/defaults/main.yml new file mode 100644 index 0000000..8531111 --- /dev/null +++ b/roles/automatisch/defaults/main.yml @@ -0,0 +1,3 @@ +--- +automatisch_data_dir: "{{ sovereign_base_dir }}/automatisch" +automatisch_domain: "automate.{{ base_domain }}" diff --git a/roles/automatisch/handlers/main.yml b/roles/automatisch/handlers/main.yml new file mode 100644 index 0000000..f2872df --- /dev/null +++ b/roles/automatisch/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: restart automatisch + community.docker.docker_compose_v2: + project_src: "{{ automatisch_data_dir }}" + state: present + recreate: always + when: not (molecule_test_mode | default(false)) diff --git a/roles/automatisch/molecule/default/converge.yml b/roles/automatisch/molecule/default/converge.yml new file mode 100644 index 0000000..c5879a3 --- /dev/null +++ b/roles/automatisch/molecule/default/converge.yml @@ -0,0 +1,8 @@ +--- +- name: Converge + hosts: localhost + gather_facts: false + vars_files: + - ../../../molecule/shared/vars.yml + roles: + - role: automatisch diff --git a/roles/automatisch/molecule/default/molecule.yml b/roles/automatisch/molecule/default/molecule.yml new file mode 100644 index 0000000..5e0827d --- /dev/null +++ b/roles/automatisch/molecule/default/molecule.yml @@ -0,0 +1,21 @@ +--- +dependency: + name: galaxy + options: + requirements-file: requirements.yml +driver: + name: default +platforms: + - name: localhost + groups: + - sovereign +provisioner: + name: ansible + env: + ANSIBLE_ROLES_PATH: "${MOLECULE_PROJECT_DIRECTORY}/.." + inventory: + host_vars: + localhost: + ansible_connection: local +verifier: + name: ansible diff --git a/roles/automatisch/molecule/default/verify.yml b/roles/automatisch/molecule/default/verify.yml new file mode 100644 index 0000000..aaa29f4 --- /dev/null +++ b/roles/automatisch/molecule/default/verify.yml @@ -0,0 +1,82 @@ +--- +- name: Verify automatisch role + hosts: localhost + gather_facts: false + vars: + automatisch_data_dir: /tmp/sovereign_test/automatisch + automatisch_domain: automate.test.example.com + automatisch_version: latest + automatisch_encryption_key: test_automatisch_encryption_key + automatisch_webhook_secret_key: test_automatisch_webhook_secret + automatisch_app_secret_key: test_automatisch_app_secret + automatisch_db_password: test_automatisch_db + + tasks: + - name: Check automatisch data directory exists + ansible.builtin.stat: + path: "/tmp/sovereign_test/automatisch" + register: data_dir_stat + + - name: Assert automatisch data directory is present + ansible.builtin.assert: + that: data_dir_stat.stat.isdir + fail_msg: "Data directory /tmp/sovereign_test/automatisch was not created" + + - name: Check docker-compose.yml exists + ansible.builtin.stat: + path: "/tmp/sovereign_test/automatisch/docker-compose.yml" + register: compose_stat + + - name: Assert docker-compose.yml was rendered + ansible.builtin.assert: + that: compose_stat.stat.exists + fail_msg: "docker-compose.yml was not rendered for automatisch" + + - name: Read docker-compose.yml + ansible.builtin.slurp: + src: "/tmp/sovereign_test/automatisch/docker-compose.yml" + register: compose_raw + + - name: Set compose content fact + ansible.builtin.set_fact: + compose: "{{ compose_raw.content | b64decode }}" + + - name: Assert automatisch image is present + ansible.builtin.assert: + that: "'automatischio/automatisch' in compose" + fail_msg: "automatischio/automatisch image not found in docker-compose.yml" + + - name: Assert automatisch domain traefik rule is present + ansible.builtin.assert: + that: "'Host(`automate.test.example.com`)' in compose" + fail_msg: "Traefik rule for automate.test.example.com not found in docker-compose.yml" + + - name: Assert Authentik forward auth middleware is present + ansible.builtin.assert: + that: "'outpost.goauthentik.io/auth/traefik' in compose" + fail_msg: "Authentik forward auth address not found in docker-compose.yml" + + - name: Assert encryption key is present in compose + ansible.builtin.assert: + that: "'test_automatisch_encryption_key' in compose" + fail_msg: "automatisch_encryption_key not found in docker-compose.yml" + + - name: Assert GELF logging address is present + ansible.builtin.assert: + that: "'udp://127.0.0.1:12201' in compose" + fail_msg: "GELF logging address udp://127.0.0.1:12201 not found in docker-compose.yml" + + - name: Assert sovereign network is external + ansible.builtin.assert: + that: "'external: true' in compose" + fail_msg: "external: true not found in docker-compose.yml networks section" + + - name: Assert worker service is present + ansible.builtin.assert: + that: "'automatisch-worker' in compose" + fail_msg: "automatisch-worker service not found in docker-compose.yml" + + - name: Assert Redis service is present + ansible.builtin.assert: + that: "'automatisch-redis' in compose" + fail_msg: "automatisch-redis service not found in docker-compose.yml" diff --git a/roles/automatisch/tasks/main.yml b/roles/automatisch/tasks/main.yml new file mode 100644 index 0000000..25f4c5d --- /dev/null +++ b/roles/automatisch/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: Create Automatisch directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ automatisch_data_dir }}" + - "{{ automatisch_data_dir }}/storage" + +- name: Deploy Automatisch docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ automatisch_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart automatisch + +- name: Start Automatisch + community.docker.docker_compose_v2: + project_src: "{{ automatisch_data_dir }}" + state: present + when: not (molecule_test_mode | default(false)) diff --git a/roles/automatisch/templates/docker-compose.yml.j2 b/roles/automatisch/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..673b384 --- /dev/null +++ b/roles/automatisch/templates/docker-compose.yml.j2 @@ -0,0 +1,120 @@ +services: + automatisch-db: + image: postgres:16-alpine + container_name: automatisch-db + restart: unless-stopped + environment: + POSTGRES_DB: automatisch + POSTGRES_USER: automatisch + POSTGRES_PASSWORD: "{{ automatisch_db_password }}" + volumes: + - {{ automatisch_data_dir }}/db:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "automatisch-db" + + automatisch-redis: + image: redis:7-alpine + container_name: automatisch-redis + restart: unless-stopped + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "automatisch-redis" + + automatisch: + image: automatischio/automatisch:{{ automatisch_version }} + container_name: automatisch + restart: unless-stopped + depends_on: + - automatisch-db + - automatisch-redis + environment: + HOST: "{{ automatisch_domain }}" + PROTOCOL: https + PORT: "3000" + APP_ENV: production + POSTGRES_HOST: automatisch-db + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: automatisch + POSTGRES_USERNAME: automatisch + POSTGRES_PASSWORD: "{{ automatisch_db_password }}" + REDIS_HOST: automatisch-redis + REDIS_PORT: "6379" + ENCRYPTION_KEY: "{{ automatisch_encryption_key }}" + WEBHOOK_SECRET_KEY: "{{ automatisch_webhook_secret_key }}" + APP_SECRET_KEY: "{{ automatisch_app_secret_key }}" + SMTP_HOST: "{{ smtp_host }}" + SMTP_PORT: "{{ smtp_port }}" + SMTP_USER: "{{ smtp_user }}" + SMTP_PASSWORD: "{{ smtp_password }}" + FROM_EMAIL: "{{ smtp_from }}" + TELEMETRY_ENABLED: "false" + volumes: + - {{ automatisch_data_dir }}/storage:/automatisch/packages/backend/storage + labels: + - "traefik.enable=true" + - "traefik.http.routers.automatisch.rule=Host(`{{ automatisch_domain }}`)" + - "traefik.http.routers.automatisch.tls=true" + - "traefik.http.routers.automatisch.tls.certresolver=letsencrypt" + - "traefik.http.routers.automatisch.middlewares=automatisch-auth@docker" + - "traefik.http.services.automatisch.loadbalancer.server.port=3000" + # Authentik forward auth — protects the entire app with Authentik SSO. + # Pre-requisite: create a Proxy Provider (Forward Auth, single application) + # in Authentik pointing to https://{{ automatisch_domain }}, then add it + # to the embedded outpost. + - "traefik.http.middlewares.automatisch-auth.forwardauth.address=https://{{ authentik_domain }}/outpost.goauthentik.io/auth/traefik" + - "traefik.http.middlewares.automatisch-auth.forwardauth.trustForwardHeader=true" + - "traefik.http.middlewares.automatisch-auth.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,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" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "automatisch" + + automatisch-worker: + image: automatischio/automatisch:{{ automatisch_version }} + container_name: automatisch-worker + restart: unless-stopped + depends_on: + - automatisch + environment: + WORKER: "true" + HOST: "{{ automatisch_domain }}" + PROTOCOL: https + APP_ENV: production + POSTGRES_HOST: automatisch-db + POSTGRES_PORT: "5432" + POSTGRES_DATABASE: automatisch + POSTGRES_USERNAME: automatisch + POSTGRES_PASSWORD: "{{ automatisch_db_password }}" + REDIS_HOST: automatisch-redis + REDIS_PORT: "6379" + ENCRYPTION_KEY: "{{ automatisch_encryption_key }}" + WEBHOOK_SECRET_KEY: "{{ automatisch_webhook_secret_key }}" + APP_SECRET_KEY: "{{ automatisch_app_secret_key }}" + TELEMETRY_ENABLED: "false" + volumes: + - {{ automatisch_data_dir }}/storage:/automatisch/packages/backend/storage + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "automatisch-worker" + +networks: + internal: + {{ sovereign_network_name }}: + external: true