--- - name: "Configure goatcounter application" hosts: all # goatcounter_sites приходит из group_vars/all/main.yml (его же читает # caddyproxy), goatcounter_admin_email/password — из vault-файла secrets.yml. vars: app_name: "goatcounter" # Родительский сайт: владелец пользователя, к нему линкуются остальные. goatcounter_parent_vhost: "{{ goatcounter_sites[0] }}" app_user: "{{ app_name }}" app_owner_uid: 1110 app_owner_gid: 1110 base_dir: "{{ (application_dir, app_name) | path_join }}" data_dir: "{{ (base_dir, 'data') | path_join }}" # Логин и пароль Postbox goatcounter принимает только внутри SMTP-URL, в # userinfo, — значит их надо процентно экранировать. Фильтр urlencode # оставляет `/` как есть (safe='/'), поэтому дожимаем его вручную. postbox_user_enc: "{{ postbox_user | urlencode | replace('/', '%2F') }}" postbox_pass_enc: "{{ postbox_pass | urlencode | replace('/', '%2F') }}" tasks: - name: "Create user and environment" ansible.builtin.import_role: name: owner vars: owner_name: "{{ app_user }}" owner_uid: "{{ app_owner_uid }}" owner_gid: "{{ app_owner_gid }}" owner_extra_groups: ["docker"] - name: "Create application internal directories" ansible.builtin.file: path: "{{ item }}" state: "directory" owner: "{{ app_user }}" group: "{{ app_user }}" mode: "0750" loop: - "{{ base_dir }}" - "{{ data_dir }}" # В data лежит только SQLite-база; бэкапим её restic'ом напрямую, без # промежуточного дампа — как у tududi. Записи редкие (пачка просмотров раз # в 10 секунд), ночной прогон почти наверняка застаёт базу в покое, а # retention страхует от редкого несогласованного снимка. - name: "Create backup targets file" ansible.builtin.copy: dest: "{{ base_dir }}/backup-targets" content: | {{ data_dir }} owner: "{{ app_user }}" group: "{{ app_user }}" mode: "0640" - name: "Copy docker compose file" ansible.builtin.template: src: "./files/{{ app_name }}/docker-compose.template.yml" dest: "{{ base_dir }}/docker-compose.yml" owner: "{{ app_user }}" group: "{{ app_user }}" mode: "0640" # SMTP-пароль стоит прямым значением в environment: — без no_log его # печатает --diff. Файловых секретов goatcounter не умеет. no_log: true - name: "Run application with docker compose" community.docker.docker_compose_v2: project_src: "{{ base_dir }}" state: "present" remove_orphans: true tags: - run-app # Сайты заводятся по разу, в том же прогоне, что поднимает контейнер: пока # сайта нет в базе, goatcounter отдаёт на его домене открытый мастер # регистрации, и это окно надо закрыть сразу. # # `db show site` различает три состояния по rc и stderr: rc=0 — сайт есть; # rc=1 + "nothing found" — база готова, сайта нет; rc=1 + "doesn't exist" — # serve ещё не накатил миграции. Последнее и есть гонка со стартом # контейнера, поэтому ждём в until, пока ответ не станет осмысленным. - name: "Check which goatcounter sites exist" community.docker.docker_container_exec: container: "{{ app_name }}_app" argv: - "goatcounter" - "db" - "show" - "site" - "-find" - "{{ item }}" loop: "{{ goatcounter_sites }}" register: "goatcounter_sites_state" changed_when: false failed_when: false until: "goatcounter_sites_state.rc == 0 or 'nothing found' in goatcounter_sites_state.stderr" retries: 10 delay: 3 tags: - run-app # Пароль нужен только когда заводим родительский сайт: дочерние идут с # -link и своих пользователей не получают. - name: "Check credentials for the first goatcounter user are set" ansible.builtin.assert: that: - "goatcounter_admin_email | default('') is match('[^@]+@[^@]+')" - "goatcounter_admin_password | default('') | length >= 8" fail_msg: >- Сайт {{ goatcounter_parent_vhost }} ещё не заведён, а goatcounter_admin_email / goatcounter_admin_password не заданы. Пропишите их в group_vars/all/secrets.yml и повторите прогон. when: "goatcounter_sites_state.results[0].rc != 0" tags: - run-app # Вход дальше — своей парой email/пароль на https://{{ goatcounter_parent_vhost }}; # Authelia тут не участвует, эндпоинт /count обязан быть публичным. - name: "Create the parent goatcounter site and its user" community.docker.docker_container_exec: container: "{{ app_name }}_app" argv: - "goatcounter" - "db" - "create" - "site" - "-vhost" - "{{ goatcounter_parent_vhost }}" - "-user.email" - "{{ goatcounter_admin_email }}" - "-password" - "{{ goatcounter_admin_password }}" when: "goatcounter_sites_state.results[0].rc != 0" changed_when: true # Пароль стоит прямым значением в argv — без no_log его печатает --diff. no_log: true tags: - run-app # -link отдаёт дочернему сайту пользователей родителя и ставит его в # переключалку сайтов в шапке. Данные при этом раздельные: всё, что видно # в дашборде, скоупится по site_id. - name: "Create linked goatcounter sites" community.docker.docker_container_exec: container: "{{ app_name }}_app" argv: - "goatcounter" - "db" - "create" - "site" - "-vhost" - "{{ item.item }}" - "-link" - "{{ goatcounter_parent_vhost }}" loop: "{{ goatcounter_sites_state.results[1:] }}" loop_control: label: "{{ item.item }}" when: "item.rc != 0" changed_when: true tags: - run-app