Инициализация канона общих ролей с ролью eget

This commit is contained in:
av
2026-07-16 21:03:25 +03:00
commit aca0d03772
7 changed files with 265 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
# ansible-roles
Канон общих Ansible-ролей для изолированных серверных репозиториев
(`pet-project-server`, `buckland`, …).
Это **рабочая директория-канон**, а не сервис. Серверные репозитории
держат закоммиченные копии ролей в своём `roles/` и синхронизируются с
этим каноном через invoke-задачи (rsync, git здесь не участвует):
- `inv roles-pull [-- <role>]` — канон → репозиторий;
- `inv roles-push [-- <role>]` — репозиторий → канон (с подтверждением
при расхождении);
- `inv roles-status` — сравнить подписанные роли с каноном.
Каждый репозиторий объявляет подписку в своём `tasks.py`
(`SYNCED_ROLES`). «Пином версии» служит сама закоммиченная копия в
репозитории: деплоится ровно то, что лежит в его git.
Git в этой директории — опциональный бэкап истории канона, он не стоит
в рабочей петле синхронизации.
Спецификация подхода: `pet-project-server/docs/drafts/shared-roles-sync.md`.
## Роли
- `eget` — установка утилиты [eget](https://github.com/zyedidia/eget).
+88
View File
@@ -0,0 +1,88 @@
# eget
Устанавливает утилиту [eget](https://github.com/zyedidia/eget) и через неё —
пиновые бинарники релизов с GitHub (restic, rclone, btop, zellij и т.п.).
Роль общая для всех серверных репозиториев; канон живёт в `ansible-roles`,
копии вендорятся через `inv roles-pull`.
## Что делает
1. **Bootstrap eget.** Если нужной версии `eget` нет — качает архив релиза с
GitHub напрямую (сам себя eget обновить не может — chicken-and-egg),
распаковывает во временную директорию, ставит бинарник и убирает мусор.
Идемпотентно: сверяет `eget --version` с `eget_version`.
2. **Установка пакетов.** Идёт по декларативному списку `eget_packages` и для
каждого вызывает `eget <repo> --tag <tag> --upgrade-only …`.
## Идемпотентность и статус изменений
`--upgrade-only` заставляет eget скачивать бинарник, только если релиз новее
установленного файла (сравнение по mtime). Когда обновление не нужно, eget
пишет в stderr `… requested release is not more recent than current version`
и выходит с кодом 0.
Роль **не** глушит вывод (`--quiet` убран намеренно) и по наличию этой
подстроки (`eget_no_upgrade_marker`) выставляет `changed_when`: реальная
установка → `changed`, пропуск → `ok`. Ненулевой код выхода eget (реальная
ошибка) роняет таск штатно.
> Ограничения `--upgrade-only` (сравнение по дате релиза vs mtime бинарника, а
> не по версии — поэтому это **не** строгий пиннинг тега):
>
> - Бамп тега на релиз, опубликованный раньше текущей установки, и любой
> **даунгрейд** eget сочтёт «не новее» и не поставит (роль покажет `ok`).
> На практике теги двигают только вперёд на свежие релизы; откат версии —
> вручную (удалить бинарник, затем прогнать роль).
> - Если имя бинарника не совпадает с последним сегментом `repo` (напр.
> `BurntSushi/ripgrep` → `rg`), eget не находит установленный файл, и роль
> будет **переустанавливать пакет каждый прогон** (рапортуя `changed`). Все
> текущие пакеты этим не задеты; для таких репозиториев ставь бинарник в
> отдельную директорию либо не полагайся на статус `changed`.
## Переменные
Полный список и значения по умолчанию — в `defaults/main.yml`.
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `eget_version` | `1.3.4` | Версия самой утилиты eget. |
| `eget_install_path` | `/usr/bin/eget` | Куда ставить бинарник eget. |
| `eget_download_url` | ссылка на релиз | URL архива eget (собирается из версии). |
| `eget_download_checksum` | `""` | Необязательная проверка архива eget (формат `get_url.checksum`). |
| `eget_packages_to` | `/usr/local/bin` | Директория для бинарников пакетов по умолчанию. |
| `eget_packages` | `[]` | Декларативный список пакетов (см. ниже). |
### Схема элемента `eget_packages`
| Ключ | Обяз. | Назначение |
|------|:----:|------------|
| `repo` | да | `owner/repo` на GitHub, напр. `restic/restic`. |
| `tag` | да | Пиновка версии, напр. `v0.18.1`. |
| `to` | нет | Целевая директория; по умолчанию `eget_packages_to`. |
| `asset` | нет | Один фильтр ассета, напр. `zip` или `^musl` (`^` — анти-матч). |
| `assets` | нет | Список фильтров (каждый → свой `--asset`); приоритетнее `asset`. |
| `file` | нет | `--file` glob для выбора файла из архива. |
## Пример
```yaml
- name: "Install eget and pinned binaries"
hosts: all
tasks:
- name: "Install eget and tools"
ansible.builtin.import_role:
name: eget
vars:
eget_install_path: "/usr/local/bin/eget"
eget_packages_to: "/usr/local/bin"
eget_packages:
- {repo: "restic/restic", tag: "v0.18.1"}
- {repo: "rclone/rclone", tag: "v1.73.4", asset: "zip"}
- {repo: "bootandy/dust", tag: "v1.2.4", asset: "gnu"}
- {repo: "creativeprojects/resticprofile", tag: "v0.32.0", asset: "^no_self_update"}
```
## License
MIT
+32
View File
@@ -0,0 +1,32 @@
---
# defaults file for eget
# --- Сам eget ---
# Версия утилиты eget и путь установки её бинарника.
eget_version: "1.3.4"
eget_install_path: "/usr/bin/eget"
eget_download_url: "https://github.com/zyedidia/eget/releases/download/v{{ eget_version }}/eget-{{ eget_version }}-linux_amd64.tar.gz" # yamllint disable-line rule:line-length
# Необязательная проверка целостности архива eget при bootstrap.
# Формат — как у аргумента checksum модуля get_url
# (напр. "sha256:<hex>"). Пусто — проверка пропускается.
eget_download_checksum: ""
# --- Пакеты, устанавливаемые через eget ---
# Директория по умолчанию для бинарников пакетов
# (переопределяется в eget_packages[*].to).
eget_packages_to: "/usr/local/bin"
# Декларативный список пакетов. Каждый элемент — объект:
# repo (обязательно) — "owner/repo" на GitHub, напр. "restic/restic".
# tag (обязательно) — пиновка версии, напр. "v0.18.1".
# to (опц.) — целевая директория; по умолчанию eget_packages_to.
# asset (опц.) — один фильтр ассета, напр. "zip" или "^musl" (^ = анти-матч).
# assets (опц.) — список фильтров (каждый добавляет свой --asset); имеет
# приоритет над asset, если заданы оба.
# file (опц.) — --file glob для выбора файла из архива.
# Пример:
# eget_packages:
# - {repo: "restic/restic", tag: "v0.18.1"}
# - {repo: "bootandy/dust", tag: "v1.2.4", asset: "gnu"}
eget_packages: []
+21
View File
@@ -0,0 +1,21 @@
---
galaxy_info:
role_name: eget
author: "Anton Vakhrushev"
description: "Установка утилиты eget и загрузка через неё пиновых бинарников с GitHub"
license: "MIT"
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions:
- all
- name: Debian
versions:
- all
galaxy_tags:
- eget
- binaries
- github
- cli
dependencies: []
+46
View File
@@ -0,0 +1,46 @@
---
# Bootstrap самого eget: скачать релиз с GitHub, распаковать, поставить бинарник.
# Идемпотентность — по версии уже установленного eget; чужой eget нельзя обновить
# им самим (chicken-and-egg), поэтому качаем архив релиза напрямую.
- name: "Check installed eget version"
ansible.builtin.command:
cmd: "{{ eget_install_path }} --version"
register: eget_version_check
changed_when: false
failed_when: false
check_mode: false # read-only проба: нужна и в --check, чтобы gate ниже был честным
- name: "Install eget from GitHub release"
when: >-
eget_version_check.rc != 0
or eget_version not in eget_version_check.stdout
block:
- name: "Download eget release archive"
ansible.builtin.get_url:
url: "{{ eget_download_url }}"
dest: "{{ eget_download_dest }}"
checksum: "{{ eget_download_checksum or omit }}"
mode: "0600"
- name: "Extract eget release archive"
ansible.builtin.unarchive:
src: "{{ eget_download_dest }}"
dest: "/tmp"
remote_src: true
- name: "Install eget binary"
ansible.builtin.copy:
src: '{{ (eget_unarchive_dest, "eget") | path_join }}'
dest: "{{ eget_install_path }}"
mode: "0755"
remote_src: true
always:
- name: "Remove eget bootstrap artifacts"
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ eget_download_dest }}"
- "{{ eget_unarchive_dest }}"
+40
View File
@@ -0,0 +1,40 @@
---
# tasks file for eget
- name: "Validate eget_packages entries"
ansible.builtin.assert:
that:
- item.repo is defined and item.repo is string and item.repo | length > 0
- item.tag is defined and item.tag is string and item.tag | length > 0
- item.to is not defined or item.to is string
- item.file is not defined or item.file is string
- item.asset is not defined or item.asset is string
- item.assets is not defined or (item.assets is sequence and item.assets is not string)
fail_msg: >-
Некорректный элемент eget_packages (repo/tag — непустые строки;
asset/file/to — строки; assets — список строк): {{ item }}
quiet: true
loop: "{{ eget_packages }}"
loop_control:
label: "{{ item.repo | default('?') }}"
- name: "Ensure eget is installed"
ansible.builtin.import_tasks: install_eget.yml
- name: "Install pinned binaries with eget"
ansible.builtin.command:
argv: "{{ _eget_argv }}"
vars:
_eget_to: "{{ item.to | default(eget_packages_to) }}"
_eget_assets: >-
{{ item.assets | default([item.asset] if item.asset is defined else []) }}
_eget_argv: >-
{{ [eget_install_path, item.repo, '--upgrade-only',
'--to=' ~ _eget_to, '--tag=' ~ item.tag]
+ (['--file=' ~ item.file] if item.file is defined else [])
+ (['--asset'] | product(_eget_assets) | map('join', '=') | list) }}
register: eget_install
changed_when: eget_no_upgrade_marker not in eget_install.stderr
loop: "{{ eget_packages }}"
loop_control:
label: "{{ item.repo }} {{ item.tag }}"
+12
View File
@@ -0,0 +1,12 @@
---
# vars file for eget — внутренние значения роли, не предназначены для переопределения.
# Куда скачивается и распаковывается архив eget при bootstrap.
eget_download_dest: '/tmp/{{ eget_download_url | split("/") | last }}'
eget_unarchive_dest: '{{ eget_download_dest | regex_replace("(\.tar\.gz|\.zip)$", "") }}'
# Подстрока из текста ошибки ErrNoUpgrade в eget: печатается в stderr, когда
# установленный бинарник уже не старше запрошенного релиза (--upgrade-only).
# По ней роль отличает «ничего не делали» от реальной установки.
# См. find.go: errors.New("requested release is not more recent than current version").
eget_no_upgrade_marker: "not more recent than current version"