- 20 приложений её не получают: контейнерами рулит ansible от root, а членство в группе docker равносильно root на хосте - у gitea, miniflux и outline группа осталась — их backup.sh дампит базу через docker compose exec из-под самого приложения - задача service-users-docker-group закрыта, остаток вынесен в новую backup-dump-without-docker-group
164 lines
7.0 KiB
YAML
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
|