mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 01:14:58 +03:00
Установка раз в шесть часов и по кнопке проверяет страницу релизов GitHub. Администратор установки видит баннер о новой версии и ставит её одной кнопкой; карточка «Обновления» — в «Настройки → Платформа». Установку выполняет отдельный сервис updater с Docker-сокетом: backend-app общается с ним только файлами в томе chatballs-updates (heartbeat, request.json, status.json). Updater принимает лишь релизы своего репозитория с образами по digest и применяет compose.yaml в одноразовом контейнере-помощнике, после чего кладёт файл в каталог установки. Запрос установки попадает в аудит. Релизный workflow собирает образ updater и пришпиливает его в compose.yaml; версия бэкенда зашивается в образ (CHATBALLS_VERSION). Миграции updates.0001 и tenancy.0034 (гранты на таблицу состояния). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
4.4 KiB
Python
103 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Готовит релизную копию compose.yaml: ссылки на образы закреплены по digest.
|
|
|
|
Установка на чистый хост — это один файл: человек скачивает compose.yaml со
|
|
страницы релиза и делает `docker compose up -d --wait`. Значит в этом файле не
|
|
должно остаться ни одной подстановки, которая молча возьмёт `:dev` или плавающий
|
|
тег, если переменной в окружении нет.
|
|
|
|
Скрипт переписывает ровно значения по умолчанию внутри ``${VAR:-...}`` для
|
|
известных ключей образов и падает, если хоть один ключ не найден или пришёл без
|
|
digest. Переопределение переменной окружения остаётся возможным — это нужно
|
|
staging и облаку, которые ведут конфигурацию сами.
|
|
|
|
Использование:
|
|
pin-release-compose.py --source compose.yaml --output dist/compose.yaml \\
|
|
--version 1.0.0 --pin CHATBALLS_BACKEND_IMAGE=ghcr.io/...@sha256:... ...
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
IMAGE_KEYS = (
|
|
"CHATBALLS_BACKEND_IMAGE",
|
|
"CHATBALLS_FRONTEND_IMAGE",
|
|
"CHATBALLS_POSTGRES_IMAGE",
|
|
"CHATBALLS_REDIS_IMAGE",
|
|
"CHATBALLS_GATEWAY_IMAGE",
|
|
"CHATBALLS_COTURN_IMAGE",
|
|
"CHATBALLS_UPDATER_IMAGE",
|
|
)
|
|
|
|
DIGEST_RE = re.compile(r"@sha256:[0-9a-fA-F]{64}$")
|
|
|
|
HEADER = """# Chatballs {version} — релизная копия compose.yaml.
|
|
#
|
|
# Установка на чистый хост с одним докером:
|
|
#
|
|
# docker compose up -d --wait
|
|
#
|
|
# Больше рядом ничего не нужно: Caddyfile, init-скрипты базы и генератор
|
|
# секретов лежат внутри образов. Файл сгенерирован автоматически из
|
|
# compose.yaml релиза {version}; править его руками не нужно — обновление
|
|
# сводится к тому, чтобы скачать этот файл новой версии и повторить команду.
|
|
#
|
|
"""
|
|
|
|
|
|
def pin(source: str, pins: dict[str, str], version: str) -> str:
|
|
text = source
|
|
for key, ref in pins.items():
|
|
pattern = re.compile(r"\$\{" + re.escape(key) + r":-[^}]*\}")
|
|
replaced, count = pattern.subn(ref, text)
|
|
if count == 0:
|
|
raise SystemExit(f"{key}: подстановка ${{{key}:-...}} не найдена в compose.yaml")
|
|
text = replaced
|
|
leftover = [key for key in IMAGE_KEYS if f"${{{key}" in text]
|
|
if leftover:
|
|
raise SystemExit("не закреплены ссылки на образы: " + ", ".join(leftover))
|
|
return HEADER.format(version=version) + text
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--source", default="compose.yaml")
|
|
parser.add_argument("--output", required=True)
|
|
parser.add_argument("--version", required=True)
|
|
parser.add_argument(
|
|
"--pin",
|
|
action="append",
|
|
default=[],
|
|
metavar="KEY=REF",
|
|
help="ссылка на образ по digest, по одной на каждый ключ",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
pins: dict[str, str] = {}
|
|
for item in args.pin:
|
|
key, _, ref = item.partition("=")
|
|
if key not in IMAGE_KEYS:
|
|
raise SystemExit(f"неизвестный ключ образа: {key}")
|
|
if not DIGEST_RE.search(ref):
|
|
raise SystemExit(f"{key}: ссылка обязана быть закреплена по @sha256")
|
|
pins[key] = ref
|
|
|
|
missing = [key for key in IMAGE_KEYS if key not in pins]
|
|
if missing:
|
|
raise SystemExit("не переданы ссылки на образы: " + ", ".join(missing))
|
|
|
|
source = Path(args.source).read_text(encoding="utf-8")
|
|
output = Path(args.output)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(pin(source, pins, args.version), encoding="utf-8", newline="\n")
|
|
print(f"{output}: закреплено ссылок — {len(pins)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|