mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
Раньше compose монтировал с хоста Caddyfile, init-скрипты базы и генератор секретов. Из-за этого установка требовала рядом распакованный репозиторий, а файл, забытый при сборке релиза, Docker молча подменял пустым каталогом — и стек падал на первом старте у человека. Плюс production-образ backend вообще не собирался: COPY content ссылался на каталог, которого в репозитории нет, так что релиза не существовало ни на GitHub, ни на GitLab. Теперь весь дистрибутив — один compose.yaml со страницы релиза: curl -fsSL .../compose.yaml -o compose.yaml docker compose up -d --wait - Caddyfile переехал в свой образ шлюза (caddy validate — в сборке), init-скрипты базы — в свой образ postgres, генератор секретов — в backend-образ. Bind-mount'ов в production-манифесте не осталось. - Состояние установки — именованные тома вместо каталогов рабочего каталога. Заодно чинит загрузку файлов на Linux: том наследует владельца из образа (hub), тогда как bind-mount доставался контейнеру как root:root. - scripts/pin-release-compose.py закрепляет ссылки на образы по digest и падает, если хоть один ключ остался подстановкой. - GitHub-workflow собирает четыре образа и прикладывает к релизу compose.yaml (основной путь) и release.env (для `chatballs deploy`). - tests/cli/test_release_compose.py держит свойство: манифест без bind-mount'ов и полностью закрепляем по digest. - Из окружения шлюза убраны CHATBALLS_APP_DOMAIN и CHATBALLS_ACME_EMAIL — Caddyfile их не читает. Dev-контур не меняется по смыслу: compose.dev.yaml по-прежнему собирает всё из исходников и держит состояние в ./data. AUDIT-TODO.md — временный список остального из аудита; удаляется целиком, когда закрыт последний пункт. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
4.3 KiB
Python
102 lines
4.3 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",
|
|
)
|
|
|
|
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:]))
|