Files
pet-project-server/playbook-goatcounter.yml
T
av c5217607b1
Linting / YAML Lint (push) Canceled after 0s
Linting / Ansible Lint (push) Canceled after 0s
owner: группа docker убрана у сервисных пользователей
- 20 приложений её не получают: контейнерами рулит ansible от root, а
  членство в группе docker равносильно root на хосте
- у gitea, miniflux и outline группа осталась — их backup.sh дампит базу
  через docker compose exec из-под самого приложения
- задача service-users-docker-group закрыта, остаток вынесен в новую
  backup-dump-without-docker-group
2026-08-22 19:40:57 +03:00

164 lines
7.0 KiB
YAML

---
- 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 }}"
- 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