From f282cacf27894248bd43f04ecec454e144427c41 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Sat, 2 May 2026 09:03:08 -0300 Subject: [PATCH] Adding Twenty CRM --- inventories/production/group_vars/all.yml | 7 ++ molecule/shared/vars.yml | 8 ++ playbooks/site.yml | 2 + roles/twenty/defaults/main.yml | 3 + roles/twenty/handlers/main.yml | 7 ++ roles/twenty/molecule/default/converge.yml | 8 ++ roles/twenty/molecule/default/molecule.yml | 21 +++++ roles/twenty/molecule/default/verify.yml | 75 ++++++++++++++++ roles/twenty/tasks/main.yml | 22 +++++ roles/twenty/templates/docker-compose.yml.j2 | 95 ++++++++++++++++++++ 10 files changed, 248 insertions(+) create mode 100644 roles/twenty/defaults/main.yml create mode 100644 roles/twenty/handlers/main.yml create mode 100644 roles/twenty/molecule/default/converge.yml create mode 100644 roles/twenty/molecule/default/molecule.yml create mode 100644 roles/twenty/molecule/default/verify.yml create mode 100644 roles/twenty/tasks/main.yml create mode 100644 roles/twenty/templates/docker-compose.yml.j2 diff --git a/inventories/production/group_vars/all.yml b/inventories/production/group_vars/all.yml index c1e2b0e..85a58a2 100644 --- a/inventories/production/group_vars/all.yml +++ b/inventories/production/group_vars/all.yml @@ -126,6 +126,13 @@ forgejo_admin_password: "changeme_forgejo_admin" forgejo_admin_email: "admin@{{ base_domain }}" forgejo_ssh_port: 2222 +# Twenty CRM +twenty_domain: "crm.{{ base_domain }}" +twenty_version: "latest" +twenty_app_secret: "changeme_twenty_app_secret_min_32_chars_random" +twenty_db_password: "changeme_twenty_db" +twenty_oidc_client_secret: "changeme_twenty_oidc_secret" # set in Authentik after creating the OAuth2 app + # Website website_nginx_version: "alpine" diff --git a/molecule/shared/vars.yml b/molecule/shared/vars.yml index fb80c5d..314e961 100644 --- a/molecule/shared/vars.yml +++ b/molecule/shared/vars.yml @@ -136,6 +136,14 @@ forgejo_admin_email: "admin@test.example.com" forgejo_ssh_port: 2222 forgejo_data_dir: /tmp/sovereign_test/forgejo +# Twenty CRM +twenty_domain: "crm.test.example.com" +twenty_version: "latest" +twenty_app_secret: "test_twenty_app_secret" +twenty_db_password: "test_twenty_db" +twenty_oidc_client_secret: "test_twenty_oidc_secret" +twenty_data_dir: /tmp/sovereign_test/twenty + # Website website_nginx_version: "alpine" website_data_dir: /tmp/sovereign_test/website diff --git a/playbooks/site.yml b/playbooks/site.yml index 547c721..dda8b0d 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -31,5 +31,7 @@ tags: [vaultwarden, passwords, vault] - role: forgejo tags: [forgejo, git, vcs] + - role: twenty + tags: [twenty, crm] - role: website tags: [website, web] diff --git a/roles/twenty/defaults/main.yml b/roles/twenty/defaults/main.yml new file mode 100644 index 0000000..0493509 --- /dev/null +++ b/roles/twenty/defaults/main.yml @@ -0,0 +1,3 @@ +--- +twenty_data_dir: "{{ sovereign_base_dir }}/twenty" +twenty_domain: "crm.{{ base_domain }}" diff --git a/roles/twenty/handlers/main.yml b/roles/twenty/handlers/main.yml new file mode 100644 index 0000000..d4d780f --- /dev/null +++ b/roles/twenty/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: restart twenty + community.docker.docker_compose_v2: + project_src: "{{ twenty_data_dir }}" + state: present + recreate: always + when: not (molecule_test_mode | default(false)) diff --git a/roles/twenty/molecule/default/converge.yml b/roles/twenty/molecule/default/converge.yml new file mode 100644 index 0000000..5134add --- /dev/null +++ b/roles/twenty/molecule/default/converge.yml @@ -0,0 +1,8 @@ +--- +- name: Converge + hosts: localhost + gather_facts: false + vars_files: + - ../../../molecule/shared/vars.yml + roles: + - role: twenty diff --git a/roles/twenty/molecule/default/molecule.yml b/roles/twenty/molecule/default/molecule.yml new file mode 100644 index 0000000..5e0827d --- /dev/null +++ b/roles/twenty/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/twenty/molecule/default/verify.yml b/roles/twenty/molecule/default/verify.yml new file mode 100644 index 0000000..e0618e3 --- /dev/null +++ b/roles/twenty/molecule/default/verify.yml @@ -0,0 +1,75 @@ +--- +- name: Verify twenty role + hosts: localhost + gather_facts: false + vars: + twenty_data_dir: /tmp/sovereign_test/twenty + twenty_domain: crm.test.example.com + twenty_version: latest + twenty_app_secret: test_twenty_app_secret + twenty_db_password: test_twenty_db + + tasks: + - name: Check twenty data directory exists + ansible.builtin.stat: + path: "/tmp/sovereign_test/twenty" + register: data_dir_stat + + - name: Assert twenty data directory is present + ansible.builtin.assert: + that: data_dir_stat.stat.isdir + fail_msg: "Data directory /tmp/sovereign_test/twenty was not created" + + - name: Check docker-compose.yml exists + ansible.builtin.stat: + path: "/tmp/sovereign_test/twenty/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 twenty" + + - name: Read docker-compose.yml + ansible.builtin.slurp: + src: "/tmp/sovereign_test/twenty/docker-compose.yml" + register: compose_raw + + - name: Set compose content fact + ansible.builtin.set_fact: + compose: "{{ compose_raw.content | b64decode }}" + + - name: Assert twenty server image is present + ansible.builtin.assert: + that: "'twentycrm/twenty' in compose" + fail_msg: "twentycrm/twenty image not found in docker-compose.yml" + + - name: Assert twenty domain traefik rule is present + ansible.builtin.assert: + that: "'Host(`crm.test.example.com`)' in compose" + fail_msg: "Traefik rule for crm.test.example.com not found in docker-compose.yml" + + - name: Assert app secret is present in compose + ansible.builtin.assert: + that: "'test_twenty_app_secret' in compose" + fail_msg: "twenty_app_secret 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: "'twenty-worker' in compose" + fail_msg: "twenty-worker service not found in docker-compose.yml" + + - name: Assert Redis service is present + ansible.builtin.assert: + that: "'twenty-redis' in compose" + fail_msg: "twenty-redis service not found in docker-compose.yml" diff --git a/roles/twenty/tasks/main.yml b/roles/twenty/tasks/main.yml new file mode 100644 index 0000000..7025ba0 --- /dev/null +++ b/roles/twenty/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: Create Twenty CRM directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ twenty_data_dir }}" + - "{{ twenty_data_dir }}/data" + +- name: Deploy Twenty CRM docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ twenty_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart twenty + +- name: Start Twenty CRM + community.docker.docker_compose_v2: + project_src: "{{ twenty_data_dir }}" + state: present + when: not (molecule_test_mode | default(false)) diff --git a/roles/twenty/templates/docker-compose.yml.j2 b/roles/twenty/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..b0e9516 --- /dev/null +++ b/roles/twenty/templates/docker-compose.yml.j2 @@ -0,0 +1,95 @@ +services: + twenty-db: + image: postgres:16-alpine + container_name: twenty-db + restart: unless-stopped + environment: + POSTGRES_DB: twenty + POSTGRES_USER: twenty + POSTGRES_PASSWORD: "{{ twenty_db_password }}" + volumes: + - {{ twenty_data_dir }}/db:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "twenty-db" + + twenty-redis: + image: redis:7-alpine + container_name: twenty-redis + restart: unless-stopped + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "twenty-redis" + + twenty-server: + image: twentycrm/twenty:{{ twenty_version }} + container_name: twenty-server + restart: unless-stopped + depends_on: + - twenty-db + - twenty-redis + environment: + SERVER_URL: "https://{{ twenty_domain }}" + APP_SECRET: "{{ twenty_app_secret }}" + PG_DATABASE_URL: "postgres://twenty:{{ twenty_db_password }}@twenty-db/twenty" + REDIS_URL: "redis://twenty-redis:6379" + STORAGE_TYPE: local + MESSAGE_QUEUE_TYPE: bull-mq + SIGN_IN_PREFILLED: "false" + # Authentik OIDC — after first login as admin go to: + # Settings → Security → SSO → Add provider + # Discovery URL: https://{{ authentik_domain }}/application/o/twenty/.well-known/openid-configuration + # Client ID: twenty + # Client Secret: {{ twenty_oidc_client_secret }} + volumes: + - {{ twenty_data_dir }}/data:/app/packages/twenty-server/.local-storage + labels: + - "traefik.enable=true" + - "traefik.http.routers.twenty.rule=Host(`{{ twenty_domain }}`)" + - "traefik.http.routers.twenty.tls=true" + - "traefik.http.routers.twenty.tls.certresolver=letsencrypt" + - "traefik.http.services.twenty.loadbalancer.server.port=3000" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "twenty-server" + + twenty-worker: + image: twentycrm/twenty:{{ twenty_version }} + container_name: twenty-worker + restart: unless-stopped + command: ["yarn", "worker:prod"] + depends_on: + - twenty-server + environment: + APP_SECRET: "{{ twenty_app_secret }}" + PG_DATABASE_URL: "postgres://twenty:{{ twenty_db_password }}@twenty-db/twenty" + REDIS_URL: "redis://twenty-redis:6379" + STORAGE_TYPE: local + MESSAGE_QUEUE_TYPE: bull-mq + volumes: + - {{ twenty_data_dir }}/data:/app/packages/twenty-server/.local-storage + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "twenty-worker" + +networks: + internal: + {{ sovereign_network_name }}: + external: true