From aca0d03772a4151110d1c6b0081408501730242d Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 16 Jul 2026 21:03:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=89=D0=B8=D1=85=20=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=D1=81=20=D1=80=D0=BE=D0=BB=D1=8C=D1=8E=20?= =?UTF-8?q?eget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +++++++++ roles/eget/README.md | 88 +++++++++++++++++++++++++++++++ roles/eget/defaults/main.yml | 32 +++++++++++ roles/eget/meta/main.yml | 21 ++++++++ roles/eget/tasks/install_eget.yml | 46 ++++++++++++++++ roles/eget/tasks/main.yml | 40 ++++++++++++++ roles/eget/vars/main.yml | 12 +++++ 7 files changed, 265 insertions(+) create mode 100644 README.md create mode 100644 roles/eget/README.md create mode 100644 roles/eget/defaults/main.yml create mode 100644 roles/eget/meta/main.yml create mode 100644 roles/eget/tasks/install_eget.yml create mode 100644 roles/eget/tasks/main.yml create mode 100644 roles/eget/vars/main.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..91174a1 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# ansible-roles + +Канон общих Ansible-ролей для изолированных серверных репозиториев +(`pet-project-server`, `buckland`, …). + +Это **рабочая директория-канон**, а не сервис. Серверные репозитории +держат закоммиченные копии ролей в своём `roles/` и синхронизируются с +этим каноном через invoke-задачи (rsync, git здесь не участвует): + +- `inv roles-pull [-- ]` — канон → репозиторий; +- `inv roles-push [-- ]` — репозиторий → канон (с подтверждением + при расхождении); +- `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). diff --git a/roles/eget/README.md b/roles/eget/README.md new file mode 100644 index 0000000..1ee847d --- /dev/null +++ b/roles/eget/README.md @@ -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 --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 diff --git a/roles/eget/defaults/main.yml b/roles/eget/defaults/main.yml new file mode 100644 index 0000000..bd6a9ae --- /dev/null +++ b/roles/eget/defaults/main.yml @@ -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:"). Пусто — проверка пропускается. +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: [] diff --git a/roles/eget/meta/main.yml b/roles/eget/meta/main.yml new file mode 100644 index 0000000..fee664c --- /dev/null +++ b/roles/eget/meta/main.yml @@ -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: [] diff --git a/roles/eget/tasks/install_eget.yml b/roles/eget/tasks/install_eget.yml new file mode 100644 index 0000000..2109991 --- /dev/null +++ b/roles/eget/tasks/install_eget.yml @@ -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 }}" diff --git a/roles/eget/tasks/main.yml b/roles/eget/tasks/main.yml new file mode 100644 index 0000000..5aa3591 --- /dev/null +++ b/roles/eget/tasks/main.yml @@ -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 }}" diff --git a/roles/eget/vars/main.yml b/roles/eget/vars/main.yml new file mode 100644 index 0000000..320022f --- /dev/null +++ b/roles/eget/vars/main.yml @@ -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"