Skip to content
KiboMiboPublic

About

Свой репозиторий бинарников приложений: веб-интерфейс и JSON API для загрузки версий (semver, SHA-256), стабильные ссылки на скачивание и latest, роли для людей и CI. Один бинарник на Go, метаданные в SQLite, файлы на диске — без Docker, внешней базы и CGO.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

9 Commits

Folders and files

Repository files navigation

apprepo

Свой репозиторий бинарников: залил сборку — получил стабильную ссылку на скачивание.

Go Platforms CGO Release

Что это

Место, куда складывают сборки своих приложений и откуда их забирают: веб-интерфейс и JSON API, версии по semver, проверка SHA-256 при загрузке, постоянная ссылка на latest, роли отдельно для людей и для CI. Один бинарник на Go, метаданные в SQLite, файлы на диске — ни Docker, ни внешней базы, ни CGO.

Быстрый старт

# 1. Взять архив под свою платформу и распаковать
#    https://github.com/KiboMibo/mini-repo/releases/latest
tar -xzf apprepo_*_linux_amd64.tar.gz && cd apprepo_*_linux_amd64

# 2. Завести первого пользователя (он же admin) — пароль спросит интерактивно
./apprepo user add alice -data-dir ./data

# 3. Запустить и открыть http://localhost:8080
./apprepo serve -data-dir ./data -addr :8080

Из исходников — make build (нужен Go ≥ 1.24). Целостность скачанного архива проверяется файлом SHA256SUMS из того же релиза: sha256sum -c SHA256SUMS (на macOS — shasum -a 256 -c SHA256SUMS). Установка «по-взрослому», с systemd и отдельным пользователем, — в разделе Деплой (systemd).

Оглавление

Что умеет

Репозиторий бинарников приложений: веб-UI и JSON API для заведения приложений, загрузки версий (semver) с проверкой SHA-256 и скачивания по стабильным ссылкам, включая latest. Версия — один файл любого содержания: одиночный бинарник или архив многофайлового приложения (см. «Многофайловые приложения»); у каждой версии есть платформа — под какую ОС и архитектуру она собрана (см. «Платформа версии»). Приложения можно переименовывать и редактировать, приложения и отдельные версии — удалять (жёстко, вместе с файлами на диске). Доступ разграничен глобальными ролями (deployer, developer, maintainer, admin), учётки заводятся и блокируются из интерфейса, API и CLI. Один бинарник на Go, метаданные в SQLite (без CGO), файлы на диске.

Сборка и запуск

make build          # -> ./apprepo
./apprepo user add alice          # первый пользователь — admin; пароль спросит
                                  # интерактивно (или APPREPO_PASSWORD=…)
./apprepo serve -data-dir /opt/apps -addr :8080

serve — подкоманда по умолчанию: ./apprepo -addr :8080 тоже работает. Пока в базе нет ни одного пользователя, serve пишет в лог подсказку про user add. GET /healthz отвечает ok без аутентификации.

Требования: Go ≥ 1.24 (только для сборки). CGO не нужен (SQLite — чистый Go, WASM-сборка через wazero), кросс-компиляция свободная: GOOS=linux GOARCH=amd64 go build ./cmd/apprepo. Готовые архивы под все пять поддерживаемых платформ собирает make dist (см. Релизные сборки).

Сборка без доступа к golang.org/gitlab.com: в go.mod стоят replace зависимостей golang.org/x/* на официальные зеркала github.com/golang/* — проект собирается в средах, где наружу выпущен только github.com (GOPROXY=direct). В среде с нормальной сетью эти replace можно удалить, код от них не зависит.

Конфигурация

Флаг приоритетнее переменной окружения.

Флаг Env Дефолт
-addr APPREPO_ADDR :8080
-data-dir APPREPO_DATA_DIR /opt/apps
-db APPREPO_DB {data-dir}/apprepo.db
-base-url APPREPO_BASE_URL http://localhost:8080
-max-upload APPREPO_MAX_UPLOAD 2147483648 (2 GiB)

Доступ

Анонимного доступа нет. UI — логин/пароль (сессионная кука); API и скачивание — HTTP Basic Auth с теми же учётками. Завести пользователя можно тремя путями: из CLI (apprepo user add), из веб-интерфейса (страница /users) и через API (POST /api/users); первого пользователя — только из CLI, войти-то ещё некому.

Пароль — от 8 до 72 байт, одинаково во всех трёх интерфейсах (CLI, веб-интерфейс, API). Нижняя граница есть потому, что ограничения частоты входа сервис пока не делает и цена перебора равна стоимости bcrypt; верхняя — предел bcrypt, который длинный пароль отвергает, а не молча обрезает. Других требований — классов символов, словарей, несовпадения с именем — нет намеренно. Слишком короткий или слишком длинный пароль отвергается на входе: 400 в API, сообщение на форме в UI, выход с кодом 1 в CLI.

Важно: Basic Auth передаёт пароль в открытом виде — в проде обязателен reverse-proxy с TLS перед сервисом.

Роли и права

У каждой учётки ровно одна роль. Роль читается из базы на каждом запросе, так что выданное или отобранное право действует немедленно — перелогиниваться не надо.

Роль Что может
deployer только читать и скачивать по API и прямым ссылкам. Машинная учётка для CI: в веб-интерфейс не пускается вовсе
developer всё, что deployer, плюс заливать и удалять версии, закреплять latest, и работать в веб-интерфейсе
maintainer всё, что developer, плюс создавать, переименовывать и удалять приложения
admin всё, что maintainer, плюс управление учётками

Подробнее по каждому праву:

Право deployer developer maintainer admin
Смотреть списки по API, скачивать бинарники ✔ ✔ ✔ ✔
Залить версию, удалить версию, закрепить latest — ✔ ✔ ✔
Создать, переименовать, удалить приложение — — ✔ ✔
Управлять учётками — — — ✔
Заходить в веб-интерфейс — ✔ ✔ ✔
Сменить себе пароль ✔ (по API) ✔ ✔ ✔

Почему deployer не заходит в интерфейс. Это учётка для CI и прямых ссылок, её пароль лежит в переменных сборки и часто в нескольких местах сразу; страницы с кнопками «удалить приложение» ей не нужны. Если такой учёткой всё-таки войти на /login, вход пройдёт, но любая страница ответит 403 со страницей-объяснением: роль даёт доступ к /api/… и /download/… по Basic Auth, и только к ним. Свой пароль deployer меняет через API — POST /api/me/password.

Матрица прав задана в одном месте — rolePerms в internal/auth/roles.go; и маршруты, и кнопки в шаблонах спрашивают право, а не имя роли.

Последнего активного администратора нельзя разжаловать, заблокировать или удалить — API отвечает 409 last_admin, интерфейс показывает ошибку, CLI пишет refused: no active administrator would be left in the system. Чтобы снять с себя роль админа, сначала выдайте admin кому-то ещё. Свою учётку удалить нельзя ни при каком числе админов.

Полное описание прав по маршрутам API и коды ошибок — в docs/api.md.

Веб-UI

Серверные HTML-страницы, всё встроено в бинарник (без CDN и JS-зависимостей):

  • /login — вход; после логина возвращает на исходную страницу (?next=).
  • / — список приложений (имя, описание, latest, число версий) и форма создания приложения.
  • /apps/{name} — страница приложения: таблица версий (semver по убыванию, платформа, размер, дата, SHA-256, прямая ссылка на скачивание, бейдж latest), форма загрузки версии (файл, semver-версия, опциональный SHA-256 для сверки, выпадающие списки ОС и архитектуры) и управление latest: закрепить конкретную версию или вернуть auto (максимальный semver).
  • /users — управление учётками, только для роли admin (см. ниже).

На странице приложения и на /users слева от заголовка есть стрелка «назад» (<) — обычная ссылка на список приложений, а не history.back(): работает без JavaScript, открывается в новой вкладке и всегда ведёт в одно и то же место.

Платформа в интерфейсе. В таблице версий у неё своя колонка; неизвестная платформа показана прочерком —. В форме загрузки списки ОС и архитектуры по умолчанию стоят на detect automatically — для бинарника менять ничего не надо, для архива пару придётся выбрать (или any), иначе загрузка отклоняется. Кнопка Platform у строки версии открывает то же окно для уже залитой версии: там платформа проставляется задним числом и сбрасывается обратно в «неизвестно» пунктом unknown. Подробности — в разделе «Платформа версии».

Прогресс загрузки. У формы загрузки версии есть полоса прогресса с процентом и объёмом («42% — 350.1 MB of 812.4 MB») и кнопка Cancel upload, которая обрывает начатую загрузку — на архивах в сотни мегабайт без этого непонятно, идёт дело или зависло. Когда файл ушёл целиком и сервер досчитывает хеш, надпись меняется на «finishing on the server…», а кнопка отмены убирается: отменять с этого момента уже нечего. Если соединение оборвалось после того, как тело ушло, интерфейс не утверждает, что ничего не сохранено, — он честно просит перезагрузить страницу и посмотреть список версий.

Сделано это прогрессивным улучшением: форма остаётся обычной HTML-формой, а встроенный скрипт лишь перехватывает отправку и шлёт то же тело тем же адресом. Без JavaScript форма работает ровно как раньше — обычной отправкой, без полосы прогресса. Внешних библиотек и CDN по-прежнему нет.

Сессия — HttpOnly-кука на 7 суток, переживает рестарт сервиса (хранится в SQLite). Все POST-формы защищены CSRF-токеном (double-submit cookie); ошибки загрузки (несовпадение хеша, дубль версии, превышение лимита) показываются на странице приложения.

Интерфейс показывает только то, что роль может. Кнопки скрыты по праву, а не по имени роли: developer не видит «New application», «Edit» и «Delete application», maintainer не видит ссылку «Users». Скрытая кнопка — не защита: тот же запрос напрямую отвечает 403 со страницей «Not available for your role». В шапке всегда видно, кто вошёл и с какой ролью.

Смена своего пароля

Кнопка Change password в шапке — на любой странице, у любой роли, которую пускают в интерфейс. Форма спрашивает текущий пароль и новый дважды. После смены все сессии пользователя гасятся, и браузер уезжает на /login с пояснением «Your password has been changed. Please log in again.» — это нормально, а не ошибка. Неверный текущий пароль — 401 и текст на месте, введённое не пропадает; несовпадение повтора — 400.

Роль deployer в интерфейс не заходит и меняет пароль через POST /api/me/password (см. docs/api.md).

Экран управления пользователями (/users, только admin)

Таблица всех учёток: имя, роль, статус (активна/заблокирована), дата создания. По каждой строке доступны:

  • Set рядом с выпадающим списком ролей — сменить роль. Действует сразу, в том числе для того, кто прямо сейчас сидит в интерфейсе.
  • Block / Unblock — закрыть доступ, не удаляя учётку. Блокировка мгновенно гасит сессии пользователя и закрывает Basic Auth; имя, роль и история остаются. Unblock возвращает доступ с тем же паролем.
  • Reset password — админский сброс: текущий пароль не спрашивается. Сессии пользователя гасятся. Сброс пароля самому себе выкидывает на /login.
  • Delete — удаление с строгим подтверждением: нужно вручную ввести имя пользователя, как при удалении приложения. Не совпало — 400 и ничего не удаляется.
  • New user — модалка создания: имя, пароль, роль (роль обязательна).

Своей строке кнопка Delete не рисуется, а прямой запрос отвечает 409 («you cannot delete your own account»). Разжалование, блокировка и удаление последнего активного админа отвечают 409 с подсказкой «give the admin role to another account first». Все операции журналируются в лог сервиса с именем того, кто их выполнил; пароли и хеши в лог не попадают.

Правка и удаление

Формы живут в модальных окнах на странице приложения; при ошибке модалка открывается заново рядом с баннером ошибки, чтобы введённое не пропало.

  • Edit — правка приложения: имя и описание. Сохранение переименовывает и каталог на диске, после чего браузер уезжает на /apps/{новое-имя}. В форме висит предупреждение: переименование ломает старые ссылки — /download/старое-имя/… и /apps/старое-имя после этого отдают 404, алиасов и редиректов со старого имени нет. Занятое имя — 409 с текстом «application … already exists», невалидное — 400 с правилом именования; в обоих случаях приложение остаётся как было.
  • Delete у строки версии — удаляет версию с файлом на диске. Подтверждение простое (кнопка в модалке), вводить ничего не нужно: версия одна и её всегда можно залить заново. Если удалялась закреплённая latest, закрепление снимается и latest снова считается автоматически.
  • Delete application — удаляет приложение со всеми версиями и файлами. Подтверждение строгое: нужно вручную ввести имя приложения в поле формы; сравнение точное, лишний пробел не подойдёт. Не совпало — 400 и текст «type "…" exactly to confirm deletion — nothing was deleted», ничего не удаляется. Совпало — приложение и его каталог исчезают, браузер уходит на /.

Удаление жёсткое: soft-delete и корзины нет, файлы действительно уходят с диска. Все три операции требуют сессии и CSRF-токена и журналируются в лог сервиса с именем пользователя.

Управление учётками из CLI

CLI работает с базой напрямую, сессия ему не нужна — это аварийный вход в установку, в которую больше некому войти: забыт пароль единственного админа, все заблокированы, свежая установка без пользователей.

apprepo user add <username> [-role <role>] [flags]
apprepo user list [flags]
apprepo user role <username> <role> [flags]
apprepo user password <username> [flags]
apprepo user disable <username> [flags]
apprepo user enable <username> [flags]
apprepo user delete <username> -yes [flags]
  • [flags] — те же флаги конфигурации, что у serve (-data-dir, -db, …); их можно писать в любом порядке после позиционных аргументов.
  • -role — одна из deployer, developer, maintainer, admin. Опустить его можно только для самого первого пользователя в базе: он становится admin, иначе установка была бы неуправляемой. Для второго и дальше без -role — ошибка с подсказкой.
  • Пароль для add и password берётся из APPREPO_PASSWORD, а на терминале спрашивается интерактивно (дважды). Он обязан быть длиной 8–72 байта — та же политика, что в API и UI; короткий пароль даёт password must be at least 8 bytes, got … и выход с кодом 1, учётка не создаётся и не меняется. Пароли и хеши не печатаются никогда, в том числе в user list.
  • user delete требует -yes: CLI гоняют из скриптов, где спросить некого, а удаление необратимо.
  • Коды выхода: 0 — успех, 1 — ошибка выполнения (нет пользователя, дубль, негодный пароль, отказ по последнему админу), 2 — ошибка вызова (неизвестная подкоманда, нет -role, нет -yes) — вместе со справкой.
D=/opt/apps
APPREPO_PASSWORD=… apprepo user add alice -data-dir $D                  # первый — admin
APPREPO_PASSWORD=… apprepo user add ci -role deployer -data-dir $D      # учётка для CI
apprepo user list -data-dir $D
apprepo user role ci developer -data-dir $D
APPREPO_PASSWORD=… apprepo user password alice -data-dir $D             # сброс пароля
apprepo user disable ci -data-dir $D                                    # заблокировать
apprepo user enable ci -data-dir $D
apprepo user delete ci -yes -data-dir $D

user role, user disable и user delete отказываются тронуть последнего активного админа:

apprepo: refused: no active administrator would be left in the system

Платформа версии

У каждой версии есть платформа — под какую операционную систему и архитектуру собран её файл. Выглядит она как linux/amd64, darwin/arm64, windows/386: имена те же, что у GOOS/GOARCH, так что строку можно подставить в скрипт сборки без перевода. Нужна она затем, что «версия 1.4.0» у приложения, которое собирают под три платформы, — это три разных файла, и по имени файла отличить их можно только на честном слове.

Два значения стоят особняком:

  • any — файл от ОС и архитектуры не зависит: jar, набор скриптов, конфиги. Ставится только руками: угадать «переносимость» по содержимому нельзя.
  • darwin/universal — macOS universal binary (fat Mach-O), в котором лежит сразу несколько срезов; назвать одну пару значило бы соврать.

Полный словарь допустимых значений — в docs/api.md, раздел «Платформа версии». Платформа может быть и неизвестной (в интерфейсе — прочерк —): так живут версии, залитые до появления платформы, и всё, у чего её не удалось определить и никто не проставил.

Что определяется само. Голый исполняемый файл: ELF (Linux, FreeBSD), Mach-O (macOS), PE (Windows), включая universal binary. Сервис читает заголовок файла и берёт платформу оттуда — заливать бинарники можно ровно так же, как раньше, ничего указывать не нужно.

Что не определяется и почему. Архив (.tar.gz, .zip), jar, скрипт — в таком файле платформы просто нет: это контейнер, и сервис внутрь него не заглядывает (он вообще не распаковывает архивы, см. следующий раздел). Не определяется и бинарник под машину вне словаря. Угадывать сервис не пытается: молча приписанная неверная платформа хуже, чем честное «не знаю».

Как указать платформу для архива. В API — параметром ?platform= при заливке; без него архив залить не получится, ответ будет 400 platform_required (ломающее изменение, см. «Обновление существующей установки»):

# архив под конкретную платформу
curl -u $CRED -X PUT "$BASE/api/apps/myapp/versions/1.4.0?filename=myapp-1.4.0.tar.gz&platform=linux/amd64" \
  --data-binary @myapp-1.4.0.tar.gz
# архив, которому платформа безразлична
curl -u $CRED -X PUT "$BASE/api/apps/tools/versions/1.0.0?filename=tools-1.0.0.jar&platform=any" \
  --data-binary @tools-1.0.0.jar

Если указанная платформа спорит с тем, что сервис прочитал из файла (в артефакте с меткой linux/amd64 лежит windows-бинарник), заливка отклоняется с 409 platform_mismatch и на диске не остаётся ничего. Это же означает, что пометить настоящий бинарник как any нельзя.

В интерфейсе. На странице приложения у таблицы версий есть колонка Platform (у неизвестной — прочерк). В форме загрузки — два выпадающих списка, ОС и архитектура; по умолчанию выбрано detect automatically, и для бинарника их трогать не нужно. У каждой версии есть кнопка Platform: она открывает окно с теми же списками, где платформу можно проставить задним числом или сбросить обратно в «неизвестно» (пункт unknown). Интерфейс отвечает теми же кодами, что API: 400 на архив без платформы, 409 на расхождение с файлом.

Тем же занимается PATCH /api/apps/{name}/versions/{version} в API — единственная операция правки версии; право нужно то же, что на заливку (PermVersion).

Многофайловые приложения (архивы)

Версия — это ровно один файл, и его содержание сервису безразлично. Если приложение состоит из нескольких файлов (бинарник, конфиги, статика, миграции), упакуйте их в архив — .tar.gz, .zip, любой другой — и залейте архив как версию. Отдельной поддержки архивов не нужно и её нет: сервис хранит байты как есть и отдаёт их обратно байт в байт, вместе с посчитанным SHA-256.

Имя файла обязательно и обязано нести расширение. Именно на архивах это критично: имя, с которым версия залита, — это то самое имя, под которым файл уедет клиенту в Content-Disposition, и myapp-1.4.0 вместо myapp-1.4.0.tar.gz даёт скачанный файл, который ни tar, ни файловый менеджер не опознают. В API имя задаётся параметром ?filename= и он обязателен (до этого изменения было умолчательное {имя}-{версия} без расширения — см. раздел «Обновление существующей установки»). В веб-интерфейсе указывать ничего не нужно: имя берётся из выбранного файла.

Платформу у архива тоже придётся назвать. Внутрь архива сервис не заглядывает, читать платформу ему негде, а версию без платформы он молча не заводит: PUT без ?platform= отвечает 400 platform_required. Укажите пару os/arch, под которую собрано содержимое, или any, если оно от платформы не зависит (см. «Платформа версии»).

# собрать архив многофайлового приложения
tar -czf myapp-1.4.0.tar.gz -C build myapp

# залить через API: ?filename= и ?platform= обязательны,
# расширение и правдивость платформы — ваша ответственность
curl -u $CRED -X PUT "$BASE/api/apps/myapp/versions/1.4.0?filename=myapp-1.4.0.tar.gz&platform=linux/amd64" \
  -H "X-Checksum-Sha256: $(sha256sum myapp-1.4.0.tar.gz | cut -d' ' -f1)" \
  --data-binary @myapp-1.4.0.tar.gz

# скачать и распаковать: -J сохраняет файл под именем из Content-Disposition
curl -u $CRED -O -J "$BASE/download/myapp/1.4.0"
tar -xzf myapp-1.4.0.tar.gz

Через веб-интерфейс: страница приложения → Upload version → указать версию (semver), выбрать файл архива, выбрать ОС и архитектуру в выпадающих списках (или any), при желании вставить SHA-256 для сверки. Имя файла возьмётся из выбранного, ?filename= тут ни при чём.

Сервис архивы не распаковывает и не заглядывает внутрь. В списке версий и в API видно имя файла, размер, SHA-256 и дату — но не список файлов внутри архива. Скачать отдельный файл из архива нельзя: единица хранения и скачивания — версия целиком. Проверка SHA-256 (X-Checksum-Sha256 при заливке, тот же заголовок при скачивании) сверяет архив целиком, а не файлы в нём.

Размер. Тело загрузки ограничено APPREPO_MAX_UPLOAD (-max-upload), по умолчанию 2 GiB — сборки с зависимостями и статикой в этот предел упираются быстрее, чем одиночный бинарник. Превышение — 413 (в API too_large, в UI текст «file is too large» на странице), файл на диск не попадает. Лимит поднимается переменной окружения или флагом: APPREPO_MAX_UPLOAD=8589934592 (8 GiB). Загрузка потоковая — файл не буферится в памяти целиком, поэтому упирается это в диск, а не в RAM.

Примеры curl

Полное описание API с кодами ошибок — в docs/api.md.

BASE=http://localhost:8080
CRED=alice:s3cret-pass        # пароль не короче 8 байт — короче сервис не примет

# создать приложение
curl -u $CRED -X POST $BASE/api/apps \
  -H 'Content-Type: application/json' -d '{"name":"myapp","description":"demo"}'

# залить версию (тело — сырые байты файла; хеш опционален, но сверяется).
# ?filename= обязателен: под этим именем файл уедет клиенту при скачивании.
# ?platform= у голого бинарника не нужен — платформа читается из файла
curl -u $CRED -X PUT "$BASE/api/apps/myapp/versions/1.2.3?filename=myapp-linux-amd64" \
  -H "X-Checksum-Sha256: $(sha256sum myapp-linux-amd64 | cut -d' ' -f1)" \
  --data-binary @myapp-linux-amd64

# то же для архива многофайлового приложения — расширение и платформу
# указываем сами: у архива её читать неоткуда (any — если её вовсе нет)
curl -u $CRED -X PUT "$BASE/api/apps/myapp/versions/1.4.0?filename=myapp-1.4.0.tar.gz&platform=linux/amd64" \
  --data-binary @myapp-1.4.0.tar.gz

# проставить платформу задним числом или сбросить её в «неизвестно»
curl -u $CRED -X PATCH $BASE/api/apps/myapp/versions/1.4.0 \
  -H 'Content-Type: application/json' -d '{"platform":"linux/arm64"}'
curl -u $CRED -X PATCH $BASE/api/apps/myapp/versions/1.4.0 \
  -H 'Content-Type: application/json' -d '{"platform":""}'

# списки и latest
curl -u $CRED $BASE/api/apps                          # все приложения
curl -u $CRED $BASE/api/apps/myapp                    # приложение + его версии
curl -u $CRED $BASE/api/apps/myapp/versions           # только версии
curl -u $CRED $BASE/api/apps/myapp/versions/1.2.3     # конкретная версия
curl -u $CRED $BASE/api/apps/myapp/latest             # текущая latest

# скачать конкретную версию и latest
curl -u $CRED -O -J $BASE/download/myapp/1.2.3
curl -u $CRED -O -J $BASE/download/myapp/latest

# закрепить latest вручную / вернуть авто (максимальный semver)
curl -u $CRED -X POST $BASE/api/apps/myapp/latest \
  -H 'Content-Type: application/json' -d '{"version":"1.2.3"}'
curl -u $CRED -X POST $BASE/api/apps/myapp/latest \
  -H 'Content-Type: application/json' -d '{"version":"auto"}'

# изменить описание (поле name не передаём — имя не меняется)
curl -u $CRED -X PATCH $BASE/api/apps/myapp \
  -H 'Content-Type: application/json' -d '{"description":"new text"}'

# переименовать: каталог на диске переезжает, ссылки /download/myapp/... ломаются
curl -u $CRED -X PATCH $BASE/api/apps/myapp \
  -H 'Content-Type: application/json' -d '{"name":"myapp2"}'
curl -u $CRED -O -J $BASE/download/myapp2/1.2.3   # работает
curl -u $CRED -O -J $BASE/download/myapp/1.2.3    # 404: старого имени больше нет

# удалить версию (204; файл и каталог версии уходят с диска)
curl -u $CRED -X DELETE $BASE/api/apps/myapp2/versions/1.2.3 -o /dev/null -w '%{http_code}\n'

# удалить приложение со всеми версиями (204; подтверждения в API нет — оно только в UI)
curl -u $CRED -X DELETE $BASE/api/apps/myapp2 -o /dev/null -w '%{http_code}\n'

# кто я (любая роль) и смена своего пароля — работает и у deployer
curl -u $CRED $BASE/api/me
curl -u $CRED -X POST $BASE/api/me/password \
  -H 'Content-Type: application/json' \
  -d '{"current_password":"s3cret-pass","new_password":"new-s3cret-pass"}'

# управление учётками — только admin
curl -u $CRED $BASE/api/users
curl -u $CRED -X POST $BASE/api/users \
  -H 'Content-Type: application/json' -d '{"username":"ci","password":"s3cret-pass","role":"deployer"}'
curl -u $CRED -X PATCH $BASE/api/users/ci \
  -H 'Content-Type: application/json' -d '{"role":"developer"}'   # сменить роль
curl -u $CRED -X PATCH $BASE/api/users/ci \
  -H 'Content-Type: application/json' -d '{"disabled":true}'      # заблокировать
curl -u $CRED -X POST $BASE/api/users/ci/password \
  -H 'Content-Type: application/json' -d '{"password":"reset-pass"}'
curl -u $CRED -X DELETE $BASE/api/users/ci -o /dev/null -w '%{http_code}\n'

Удаление жёсткое и необратимое. Повторное удаление отдаёт 404, а не 204; 409 already_exists на удалении или переименовании означает, что путь в data-dir занят посторонним объектом и диск не тронут — подробности и полный список кодов в docs/api.md.

Деплой (systemd)

sudo useradd -r -s /usr/sbin/nologin apprepo
sudo install -m 700 -o apprepo -g apprepo -d /opt/apps
sudo install -m 755 apprepo /usr/local/bin/apprepo
sudo install -D -m 640 deploy/apprepo.env.example /etc/apprepo/apprepo.env
sudo install -m 644 deploy/apprepo.service /etc/systemd/system/apprepo.service
sudo -u apprepo APPREPO_PASSWORD=… apprepo user add alice -data-dir /opt/apps
sudo systemctl enable --now apprepo

Unit-файл deploy/apprepo.service целиком; hardening-директивы и UMask=0077 — часть модели прав из секьюрити-ревью (файлы данных недоступны другим локальным пользователям). EnvironmentFile указывает на /etc/apprepo/apprepo.env с переменными APPREPO_* (см. таблицу конфигурации выше). Отдельно стоит пара MemoryMax=1G и GOMEMLIMIT=768MiB — предел памяти процесса; зачем он и когда его поднимать, разобрано сразу после юнита:

[Unit]
Description=apprepo binary artifact repository
After=network.target

[Service]
Type=simple
DynamicUser=no
User=apprepo
Group=apprepo
EnvironmentFile=/etc/apprepo/apprepo.env
ExecStart=/usr/local/bin/apprepo serve
Restart=on-failure
RestartSec=2

# Страховочный предел, а не рабочий: определение платформы в internal/platform
# читает заголовок фиксированного размера и в память не упирается вовсе. Эти
# строки нужны на случай будущей ошибки — промах любого нового ограничения
# должен стоить рестарта сервиса, а не свопа и OOM-killer'а, который выбирает
# жертву на всей машине. GOMEMLIMIT ниже MemoryMax, чтобы сборщик мусора
# начинал стараться раньше, чем в дело вступает ядро. Заливки идут потоком на
# диск и в RSS не превращаются, так что гигабайта хватает с запасом;
# поднимаешь один — поднимай и второй.
MemoryMax=1G
Environment=GOMEMLIMIT=768MiB

# Hardening
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
# Обязан совпадать с APPREPO_DATA_DIR из EnvironmentFile: переменные окружения
# systemd в своих директивах не раскрывает, а ProtectSystem=strict делает ФС
# read-only вне этого списка. Меняешь data-dir — правь обе строки + daemon-reload.
# Каталог APPREPO_DB, если БД вынесена из data-dir, тоже обязан быть здесь:
# несколько путей пишутся через пробел — ReadWritePaths=/data/repo/apps /data/repo/db
ReadWritePaths=/opt/apps

[Install]
WantedBy=multi-user.target

Предел памяти: MemoryMax=1G и GOMEMLIMIT=768MiB. Это страховка, а не рабочее ограничение: сервис читает у залитого файла только заголовок, около сотни байт независимо от его размера, и в память тут упираться нечему. Строки стоят на случай будущей ошибки — если какое-нибудь новое ограничение промахнётся, цена должна быть рестарт сервиса, а не своп и OOM-killer, который выбирает жертву на всей машине. GOMEMLIMIT намеренно ниже MemoryMax, чтобы сборщик мусора начинал стараться раньше, чем в дело вступает ядро.

Гигабайта хватает с запасом: заливки идут потоком на диск и в память целиком не попадают, поэтому от -max-upload этот предел не зависит — двухгигабайтный архив кладётся в него спокойно. Если предел всё же понадобилось поднять, поднимайте оба значения и сохраняйте зазор между ними; после правки — daemon-reload и рестарт. Срабатывание видно как status=137 / oom-kill в systemctl status apprepo.

Путь data-dir задан в двух местах и обязан совпадать: ReadWritePaths= в юните и APPREPO_DATA_DIR= в /etc/apprepo/apprepo.env. systemd строит mount-неймспейс до запуска процесса и переменные из EnvironmentFile в свои директивы не подставляет, а ProtectSystem=strict запрещает запись всюду, кроме перечисленного в ReadWritePaths=. Если пути разошлись (или каталог из ReadWritePaths= не создан), сервис не стартует:

apprepository.service: Failed to set up mount namespacing: /opt/apps: No such file or directory
Failed at step NAMESPACE spawning /usr/local/bin/apprepo: No such file or directory
Main process exited, code=exited, status=226/NAMESPACE

APPREPO_DB — второй независимый путь. По умолчанию БД лежит внутри data-dir и отдельной строки в юните не требует, но если её вынесли (например APPREPO_DATA_DIR=/data/repo/apps и APPREPO_DB=/data/repo/db/apps.db), то каталог БД тоже обязан быть в ReadWritePaths= — пути перечисляются через пробел: ReadWritePaths=/data/repo/apps /data/repo/db. Иначе неймспейс построится (data-dir на месте), сервис стартует и упадёт уже на открытии БД:

apprepo[20278]: open database /data/repo/db/apps.db: apply schema: sqlite3: unable to open database file (check directory permissions; under systemd also ReadWritePaths=)
Main process exited, code=exited, status=1/FAILURE

Симптом обманчив: каталог и файл БД существуют и принадлежат apprepo, ls -l показывает корректные права — но под ProtectSystem=strict каталог примонтирован read-only, и записать в него не может даже владелец.

Что реально применилось — systemctl show apprepo -p ReadWritePaths; после правки юнита нужен systemctl daemon-reload. Если свой путь не нужен, вместо ReadWritePaths= можно поставить StateDirectory=apprepo — systemd сам создаст /var/lib/apprepo с нужными правами, а в env-файле указать APPREPO_DATA_DIR=/var/lib/apprepo.

Обновление существующей установки

Обновление — это замена бинарника и рестарт сервиса. Отдельной команды миграции нет и запускать вручную нечего.

Ломающее изменение: ?filename= при заливке версии стал обязательным. Раньше параметр можно было не передавать, и имя собиралось как {имя}-{версия} — без расширения, из-за чего залитый архив скачивался файлом, который система не опознаёт. Теперь запрос без параметра отвечает 400 filename_required и версию не создаёт.

  • Сломается всё, что заливало версии без ?filename=: шаги CI, скрипты деплоя, прямые curl из старых инструкций. Падение шумное — 400 вместо 201, на диске и в БД не остаётся ничего.
  • Чинится добавлением одного параметра, тело и заголовки менять не нужно: ?filename=myapp-1.2.3.tar.gz. Текст ошибки самодостаточен — он называет прежнее умолчательное имя и содержит готовую строку curl, которую можно скопировать из лога CI прямо в скрипт. Если расширение не нужно и прежнее имя устраивало — передайте его явно: ?filename=myapp-1.2.3.
  • Уже залитые версии не затронуты. Они сохраняют имена, с которыми были залиты (включая прежние умолчательные {имя}-{версия}), и скачиваются по-прежнему. Перезаливать, переименовывать и мигрировать ничего не нужно.
  • Загрузка через веб-интерфейс не затронута: там имя всегда бралось из выбранного файла.

Пройдитесь по пайплайнам до обновления — проще добавить параметр заранее, чем ловить красный деплой. Подробнее: docs/api.md, раздел «Совместимость».

Ломающее изменение: архив теперь нельзя залить без ?platform=. У версии появилась платформа (см. «Платформа версии»), и сервис не заводит версию, про которую не знает, под что она собрана. У голого бинарника он читает платформу из самого файла — такие пайплайны не меняются вовсе. У архива читать нечего, и PUT без параметра отвечает 400 platform_required.

  • Сломается всё, что заливает по API не бинарники: архивы .tar.gz/.zip, jar'ы, наборы скриптов. Падение шумное — 400 вместо 201; на диске и в БД не остаётся ничего.
  • Чинится одной строкой в URL, тело и заголовки менять не нужно: добавьте &platform=linux/amd64 (пара, под которую собрано содержимое) или &platform=any, если оно от ОС и архитектуры не зависит.
  • Текст ошибки самодостаточен: он перечисляет допустимые значения и содержит готовую строку curl, которую можно скопировать из лога CI прямо в скрипт.
  • Заливка через веб-интерфейс не ломается молча: там появились выпадающие списки ОС и архитектуры, и на архиве без выбранной платформы форма отвечает тем же отказом на странице.
  • Уже залитые версии не затронуты. Их платформа считается неизвестной (в интерфейсе — прочерк), скачивание и Content-Disposition не меняются.

Схема БД мигрирует автоматически при старте. store.Open (а значит и serve, и любая подкоманда user …) досоздаёт недостающие колонки: role и disabled в таблице users, platform в таблице versions. Операция идемпотентна: на уже обновлённой базе выполняется одна проверка и ничего не меняется, повторные запуски безопасны. Новая колонка platform у всех существующих версий заполняется пустой строкой — «неизвестно».

Платформа старых версий подбирается при старте (backfill). Сразу после миграции serve проходит по версиям с пустой платформой и пробует определить её по файлу на диске. Отдельной команды нет, запускать вручную нечего, повторный прогон безопасен — берутся только версии без платформы.

  • Подберутся голые бинарники: ELF, Mach-O, PE. Архивы, jar'ы и скрипты останутся пустыми навсегда — из файла их платформу взять неоткуда, проставьте её руками (кнопка Platform в интерфейсе или PATCH в API).
  • У прохода есть потолок в 5 секунд. Старт сервиса не должен зависеть от содержимого data-dir: на репозитории в десятки тысяч версий (или там, где часть файлов не опознаётся никогда и перечитывается каждый рестарт) проход обрывается по времени, а недоделанное достаётся следующему запуску или проставляется руками. Ничего при этом не теряется и не ломается.
  • В журнал уходит одна итоговая строка на старт, а не строка на версию: app: platform backfill: checked 11 version(s), set 6, skipped 5. Если проход оборвался по времени, в строке это сказано прямо. На пустой базе не пишется ничего.
  • Битый, отсутствующий или враждебный файл — это пропуск, а не отказ старта.

Все существующие учётки становятся admin. До появления ролей пользователи были равны и могли всё, и миграция сохраняет ровно это поведение: никто не теряет доступ в момент обновления. Обратная сторона — установка, где раньше было пять человек, после обновления получает пять администраторов. Сразу после обновления пройдитесь по списку и раздайте настоящие роли:

apprepo user list -data-dir /opt/apps
apprepo user role ci deployer   -data-dir /opt/apps
apprepo user role bob developer -data-dir /opt/apps

Учётки со старыми именами придётся разобрать вручную. Имена проверяются (^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$, пробелы по краям обрезаются) только с волны 6, поэтому в базе, пережившей обновление, могут лежать имена, которые сегодня не завести: с пробелами, кириллицей, двоеточием. Такая учётка из API и CLI по имени больше не адресуется — управлять ею можно только в веб-интерфейсе, где учётки адресуются числовым id.

Насколько она при этом жива, зависит от имени, и разница существенная:

  • Имя с пробелом или не-ASCII (admin , аlice) — учётка рабочая: вход в веб-интерфейс проходит (форма входа сверяет имя как есть), Basic Auth к API и скачиванию тоже работает. Неудобство ровно одно — ею нельзя управлять по имени из API и CLI.
  • Имя с двоеточием (ci:bot) — учётка не может войти по Basic Auth вообще: HTTP Basic кодирует пару как user:password, так что сервер прочитает такое имя как ci с паролем bot:… и ответит 401. В веб-интерфейс она войдёт, но если её роль — deployer, в интерфейс её не пускают, и учётка мертва полностью: ни API, ни скачивания, ни UI. Такие разбирайте первыми — за ними, скорее всего, стоит уже сломанный пайплайн.

Смены имени учётки в продукте нет (переименовать можно только приложение), и автоматической тоже нет намеренно: она молча сломала бы Basic-креды, уже прописанные в чужих пайплайнах. Единственный путь — удалить учётку и завести заново под нормальным именем; у новой будут новые Basic-креды, их придётся переставить везде, где они прописаны (CI). Такие имена перечисляет apprepo user list и журнал при старте, разделяя те же два случая (фраза про двоеточие появляется, только если такие учётки есть):

warning: 2 account(s) have legacy names ("admin ", "аlice"): they still work and
can sign in, but are not addressable by name from the API or CLI. 1 account(s)
have a colon in the name ("ci:bot"): HTTP Basic encodes user:password, so these
cannot sign in over Basic at all — with the deployer role, which has no web UI
either, such an account is dead; fix these first. There is no way to rename an
account, so the only fix is to delete such an account on the /users page of the
web UI (where accounts are addressed by numeric id) and create it again under a
valid name; the new account gets new Basic credentials, so update them wherever
they are used (CI)

На странице /users такие учётки помечены значком с именем в кавычках — без кавычек admin не отличить от admin. Значки разные: рабочее старое имя — legacy name, имя с двоеточием — broken name (красный, как у блокировки).

Ищется учётка всегда по нормализованному имени, поэтому и отказ CLI называет его: apprepo user role "admin " developer отвечает user "admin" not found — строка "admin " из user list по своему имени недостижима.

Перед обновлением снимите копию файла БД. Останавливаете сервис, копируете {data-dir}/apprepo.db (по умолчанию; путь может быть переопределён APPREPO_DB) вместе с файлами -wal и -shm, если они есть, — и только потом подменяете бинарник:

sudo systemctl stop apprepo
sudo cp -a /opt/apps/apprepo.db /opt/apps/apprepo.db.bak-$(date +%F)
sudo cp -a /opt/apps/apprepo.db-wal /opt/apps/apprepo.db-shm /root/ 2>/dev/null || true
sudo install -m 755 apprepo /usr/local/bin/apprepo
sudo systemctl start apprepo
sudo journalctl -u apprepo -n 20        # убедиться, что стартовал
apprepo user list -data-dir /opt/apps   # роли на месте

Копия нужна потому, что автоматической миграции «назад» нет: команды, которая сняла бы колонки ролей, в сервисе не предусмотрено. Восстановление — это возврат файла БД из копии при остановленном сервисе. Бинарники приложений в data-dir миграция не трогает вовсе, так что копировать их не нужно.

Разработка

make test                # go test ./...
bash scripts/smoke.sh    # сквозной сценарий на временном каталоге

Релизные сборки

make dist                       # версия из `git describe`
make dist VERSION=v1.0.0        # версия задана явно

Собирает пять платформ (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64) в dist/: .tar.gz для linux и macOS, .zip для windows, внутри — бинарник, README.md и LICENSE, если он есть. Рядом кладётся SHA256SUMS. Версия и коммит зашиваются через -ldflags -X и печатаются по apprepo -version; обычная make build собирается без них и показывает dev. Сам dist/ в репозиторий не едет. Нужен zip в системе — всё остальное делает Go.

About

Свой репозиторий бинарников приложений: веб-интерфейс и JSON API для загрузки версий (semver, SHA-256), стабильные ссылки на скачивание и latest, роли для людей и CI. Один бинарник на Go, метаданные в SQLite, файлы на диске — без Docker, внешней базы и CGO.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages