ADR-011: статика игры вынесена из куба на nginx (LXC 152 game-web), версии каталогами

- корень smart-updown.binom.pw -> nginx на Proxmox (LXC 152, 192.168.76.152)
- /api,/ws -> куб (router PathPrefix на внешнем Traefik VDS), куб не менялся
- версионирование: /opt/game/releases/<N>/, активная = симлинк /opt/game/current
- tools/proxmox-nginx: unpack-release.sh, publish-release.sh, game.nginx.conf, smart-updown.traefik.yaml, README
- приёмка живьём: корень 200 nginx/1.24.0 (Ubuntu), /27/ /28/ 200, /api/state 200 JSON, /api/ws цена, autoclose-check 9/9
- питфоллы: OCI-manifest в skopeo dir-дампе, слэш в конце версии, /solana-rpc мимо PathPrefix /api
This commit is contained in:
Porfiry
2026-09-15 17:07:30 +03:00
parent 21a3a37a66
commit f105c1adff
8 changed files with 405 additions and 2 deletions
+4 -2
View File
@@ -80,8 +80,10 @@ binance-market — это **не часть игры**, а **общий инфр
| **k3s-кластер** | VM `192.168.76.120`, ns `game` | — | | **k3s-кластер** | VM `192.168.76.120`, ns `game` | — |
| **Игровой бэк (боевой)** | TS-шлюз `updown-relayer`, сервис `testfront-gateway-smart-updown-relayer:8895` | оба домена: `/api`, `/ws` | | **Игровой бэк (боевой)** | TS-шлюз `updown-relayer`, сервис `testfront-gateway-smart-updown-relayer:8895` | оба домена: `/api`, `/ws` |
| **Домены** | `smart-updown.binom.pw` (Unity-игра), `testfront.binom.pw` (тех-дашборд) | один и тот же бэк | | **Домены** | `smart-updown.binom.pw` (Unity-игра), `testfront.binom.pw` (тех-дашборд) | один и тот же бэк |
| **Фронт игры** | deployment `fun-game-front2` (nginx + Unity WebGL) | образ `images.binom.pw/fun/game-front2:<тег>` | | **Статика игры (корень `smart-updown.binom.pw`)** | **nginx на Proxmox, LXC 152 `game-web` (192.168.76.152)** | `/opt/game/releases/<N>/`, активная — `/opt/game/current` (ADR-011) |
| **Ingress-пути** | **`smart-updown-api`** — `/api`,`/ws` (ВНЕ helm); `fun-game-front2-…` — только `/` | ADR-010 | | **Фронт игры (сборка)** | репозиторий `fun-game-front2` → образ `images.binom.pw/fun/game-front2:<тег>` | из образа извлекается статика и кладётся на nginx (`tools/proxmox-nginx/`) |
| **Ingress-пути (куб)** | **`smart-updown-api`** — `/api`,`/ws` (ВНЕ helm); `fun-game-front2-…` — только `/` (уже не обслуживает домен) | ADR-010 |
| **Маршрутизация корня** | внешний Traefik VDS: `/api`,`/ws` → куб `192.168.76.120`; всё остальное → nginx `192.168.76.152` | ADR-011 |
| **Solana localnet** | LXC 151 на Proxmox `192.168.76.66` | RPC `192.168.76.181:8899` | | **Solana localnet** | LXC 151 на Proxmox `192.168.76.66` | RPC `192.168.76.181:8899` |
| **Контракт (в цепи)** | `9ALs…S1fCpf` (фантики, 4 инструкции) | прод-игра работает тут | | **Контракт (в цепи)** | `9ALs…S1fCpf` (фантики, 4 инструкции) | прод-игра работает тут |
| **Реестр образов** | `images.binom.pw` | — | | **Реестр образов** | `images.binom.pw` | — |
+37
View File
@@ -156,3 +156,40 @@ helm upgrade fun-game-front2 binom/fun-game-front2 -n game \
## Ссылки (дополнено 2026-09-13) ## Ссылки (дополнено 2026-09-13)
- Цена: `docs/PRICE-NATS.md`, диаграмма `docs/diagrams/price-flow.puml` - Цена: `docs/PRICE-NATS.md`, диаграмма `docs/diagrams/price-flow.puml`
- **Безопасность: `docs/SECURITY.md` — чек-лист `SEC-1…SEC-15` (аудит живого стенда 2026-09-13)** - **Безопасность: `docs/SECURITY.md` — чек-лист `SEC-1…SEC-15` (аудит живого стенда 2026-09-13)**
## ⭐ 2026-09-15 (2) — статика игры ушла из куба на nginx (LXC 152), версии каталогами
**Где мы:** корень `smart-updown.binom.pw` раздаёт **nginx на Proxmox (LXC 152 `game-web`,
192.168.76.152)** со версионированием по каталогам; `/api` и `/ws` по-прежнему идут в куб
на TS-шлюз отдельным router'ом внешнего Traefik. Решение и разбор — **ADR-011** в `docs/DECISIONS.md`.
**Как выпустить новую сборку игры (памятка — вместо helm upgrade фронта):**
```bash
# 1. CI репозитория fun-game-front2 собирает Unity WebGL и пушит образ
# images.binom.pw/fun/game-front2:<N> (тег = номер релиза, как раньше)
# 2. раскладываем на nginx и включаем как активную:
ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh <N> --activate"
# 3. проверяем ДОМЕН (не helm):
curl -sI https://smart-updown.binom.pw/ | grep -iE 'server|last-modified' # nginx/1.24.0 (Ubuntu)
curl -s -o /dev/null -w '%{http_code}\n' https://smart-updown.binom.pw/<N>/Build/WebGL.framework.js
```
Без `--activate` сборка просто ложится в `/opt/game/releases/<N>/` и доступна по `/<N>/` —
корень не меняется. Откат версии = `publish-release.sh <старый N> --activate` (каталоги не удаляются).
**Инструменты:** `tools/proxmox-nginx/` — `unpack-release.sh` (распаковать dir-дамп образа),
`publish-release.sh` (тянуть образ + разложить + переключить), `game.nginx.conf`,
`smart-updown.traefik.yaml` (боевой конфиг внешнего Traefik). Скопированы в LXC 152
в `/opt/game/tools/` и `/etc/nginx/sites-available/game`.
**Проверено живьём:** `/` → 200 `nginx/1.24.0 (Ubuntu)`; `/27/`,`/28/` → 200, ассеты версий
различаются; `/api/state` → 200 JSON; `/api/ws` → цена идёт; сквозной `autoclose-check.cjs` — **9/9**.
**Задачи на будущее по этой схеме:**
- [ ] **TODO** `[pump.meta]` Ссылки на версии — **только со слэшем** (`/28/`): голый `/28` даёт 301.
Если понадобится бесслэшевый вид — добавить в nginx `return 301 /$build/`.
- [ ] **TODO** `[pump.meta]` Чистка старых сборок на 152: каталоги `releases/<N>` не удаляются сами
(~68 МБ на сборку). Политика ретеншена (сколько версий держим) — за владельцем.
- [ ] **TODO** `[pump.meta]` Автоматизировать шаг публикации в CI `fun-game-front2`: после push образа
дёргать `publish-release.sh` на 152 (сейчас — ручной вызов одной командой).
- [ ] **TODO** `[pump.meta]` Уточнить, нужен ли под `fun-game-front2` в кубе вообще (домен он больше
не обслуживает) — либо оставить как «предыдущая схема на откат», либо выключить.
+55
View File
@@ -203,3 +203,58 @@
(отдать фронтендеру, см. `TASKS.md`). (отдать фронтендеру, см. `TASKS.md`).
--- ---
## ADR-011 — Статика игры вынесена из куба на nginx (LXC 152), версии — каталогами (2026-09-15)
**Контекст.** До этого и корень, и `/api`,`/ws` жили в кубе: внешний Traefik (VDS) вёл весь домен
`smart-updown.binom.pw` на k3s `192.168.76.120`, статику отдавал под `fun-game-front2`
(nginx внутри образа), API — отдельный ingress `smart-updown-api` → TS-шлюз `:8895`.
Владелец решил **разделить плоскости**: бэк честно деплоится в куб, а сама игра (статика)
раздаётся с nginx, развёрнутого на Proxmox — со **версионированием по каталогам** (`/<номер>/`).
**Решение:**
1. **nginx на Proxmox** — новый LXC **152** (`game-web`, 192.168.76.152, Ubuntu 24.04, 2 ядра,
1 ГБ RAM, диск 8 GB на `local-lvm`, `onboot=1`, unprivileged). Отдельный LXC потому, что
статика игры — «10 строк конфига и каталог», держать её в кубе рядом с бэком смысла нет.
2. **Версионирование каталогами:**
- `/opt/game/releases/<N>/` — файлы сборки N (`Build/`, `TemplateData/`, `index.html`);
- `/opt/game/current` — симлинк на активную сборку;
- `https://smart-updown.binom.pw/` → активная сборка, `/28/…` → конкретная сборка.
3. **Маршрутизация на внешнем Traefik (VDS)** — два router'а на один host, вместо одного:
- `smart-updown-api-router`: `Host(...) && (PathPrefix('/api') || PathPrefix('/ws'))` → `192.168.76.120` (куб);
- `smart-updown-router`: `Host(...)` → `192.168.76.152` (nginx на проксмоксе).
PathPrefix-роутер специфичнее Host-роутера, поэтому `/api`,`/ws` **не перехватываются статикой**.
IP LXC 152 достижим с VDS через wg-туннель (192.168.76.0/22 dev wg0).
4. **В кубе ничего не менялось**: `ingress smart-updown-api` (`/api`,`/ws` → шлюз) как был, вне helm.
Под `fun-game-front2` остался жив, но домен он больше не обслуживает (статика ушла на nginx) —
в куб его релизить больше не обязательно; релизы — только доставка статики на nginx.
5. **Инструменты** (`tools/proxmox-nginx/`): `unpack-release.sh` (распаковка dir-дампа образа в
`releases/<N>`), `publish-release.sh` (тянет образ из реестра, раскладывает, по `--activate`
переключает `current`), `game.nginx.conf`, `smart-updown.traefik.yaml`.
**Питфоллы, пойманные живьём:**
- `skopeo copy docker://… dir:` кладёт слои по **sha256-digest**, а `manifest.json` — это **OCI-манифест**
(словарь `layers[].digest`, не список строк как в docker-archiv). Парсить `json['layers'][i]['digest']`,
а не печатать элемент целиком (иначе `tar: Cannot connect to {'mediaType': ...}`).
- Голый шаблон `/28` (без слэша) отдаёт **301** — это `try_files ... /$build$subpath/`, т.е. редирект
на `/28/`. Для относительных путей Unity (`src="Build/WebGL.loader.js"`) важно, чтобы **браузерный
URL заканчивался слэшем**; иначе `Build/...` уедет в `/Build/...` и вернётся 404. Ссылаться на
версии только со слэшем (`/28/`), либо ставить редирект в nginx (`return 301 /$build/`).
- `/solana-rpc/` **не** перехватывается PathPrefix `/api` — это отдельный path, и на VDS он уходит
в корневой router (nginx). Поэтому страховочный `location /solana-rpc/` в nginx нужен: старые сборки
игры стучатся именно туда (проверено: `getHealth` → `ok` через домен).
- Имена ассетов Unity внутри сборки не меняются (`WebGL.data`/`.wasm`/`.framework.js`), но теперь
URL содержит номер сборки → `immutable`-кэш больше не «прилипает» между релизами (закрыт остаточный
риск из ADR-010). Внутри одной версии ассеты неизменяемы, т.к. каталог версии не перезаписывается.
**Приёмка (живой домен, 2026-09-15):** корень `/` → 200 от `nginx/1.24.0 (Ubuntu)` (LXC 152);
`/27/`,`/28/` → 200, ассеты версий различаются (md5 `WebGL.data`: `76b6f3…` vs `47453d…`);
`/api/state` → 200 JSON (куб), `/api/ws` → `[open]` + `{"type":"price"}`; сквозной
`tools/autoclose-check.cjs` на `BASE=https://smart-updown.binom.pw/api` — **9/9**; `testfront` не задет.
Откат: вернуть `/opt/traefik/static/smart-updown.yaml` из бэкапа VDS
(`/root/bak-smart-updown-nginx-2026-09-15/`) — домен снова целиком уйдёт в куб.
**Альтернативы:** (а) оставить статику в кубе и версионировать каталогами внутри образа — тянет
пересборку образа на каждый билд игры и не решает вопрос кэша; (б) поднять в LXC не nginx, а
podman-контейнер с nginx — лишний слой на 10 строках конфига; (в) отдавать статику прямо с k3s-Traefik
по PVC — не то, о чём просил владелец (он хотел nginx на проксмоксе).
+56
View File
@@ -0,0 +1,56 @@
# nginx на Proxmox — раздача статики игры (LXC 152)
Боевой веб-слой игры «Бинарная ракета»: **корень `smart-updown.binom.pw` раздаётся nginx'ом
на LXC 152 с версионированием по каталогам**. Бэк (API/WS) остаётся в кубе — см. ADR-011.
## Что где живёт
| Что | Где |
|---|---|
| LXC | **152 `game-web`**, 192.168.76.152 (Server3, `pct exec 152` через `root@192.168.76.66`) |
| nginx-конфиг | `/etc/nginx/sites-available/game` (симлинк в `sites-enabled/`) |
| Сборки | `/opt/game/releases/<N>/` — `Build/`, `TemplateData/`, `index.html`, `ServiceWorker.js` |
| Активная сборка | `/opt/game/current` → симлинк на `releases/<N>` |
| Инструменты | `/opt/game/tools/` (`unpack-release.sh`, `publish-release.sh`) |
| Логи | `/var/log/nginx/game-access.log`, `game-error.log` |
## Адреса
- `https://smart-updown.binom.pw/` — активная сборка;
- `https://smart-updown.binom.pw/<N>/` — конкретная сборка (**обязательно со слэшем**);
- `https://smart-updown.binom.pw/<N>/Build/WebGL.data` — ассет конкретной сборки;
- `/api`, `/ws` — **не nginx**: их забирает внешний Traefik (VDS) → куб (ingress `smart-updown-api`).
## Публикация сборки
```bash
# CI fun-game-front2 пушит образ images.binom.pw/fun/game-front2:<N>
# затем на хосте Proxmox:
ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh <N> --activate"
```
Без `--activate` сборка ляжет в `/opt/game/releases/<N>/` и будет доступна по `/<N>/`,
но корень останется прежним. Откат = `publish-release.sh <старый N> --activate`.
## Проверка
```bash
curl -sI https://smart-updown.binom.pw/ | grep -iE 'server|last-modified' # nginx/1.24.0 (Ubuntu)
curl -s -o /dev/null -w '%{http_code}\n' https://smart-updown.binom.pw/<N>/
curl -s https://smart-updown.binom.pw/api/state | head -c 80 # JSON от шлюза в кубе
# сквозная живая приёмка (ставит реальную ставку):
cd ~/WORK/games/pump-game-ops && BASE=https://smart-updown.binom.pw/api \
WS_URL=wss://smart-updown.binom.pw/api/ws node tools/autoclose-check.cjs # ждём 9/9
```
## Питфоллы
- **Слэш в конце обязателен** для версии: `/28` → 301 (редирект на `/28/`). Unity грузит ассеты
относительным путём (`Build/WebGL.loader.js`), поэтому без слэша `Build/...` уедет в корень → 404.
- **`skopeo ... dir:` пишет OCI-манифест**, где `layers` — словари. `unpack-release.sh` берёт
`digest` каждого слоя и распаковывает по имени файла `<hex>` (не `sha256:...`).
- **Кэш больше не прилипает между релизами**: URL содержит номер сборки → `immutable` корректен.
Внутри версии каталог не перезаписывается, поэтому ассеты действительно неизменяемы.
- **Сборки копятся** (~68 МБ на версию) — ретеншен пока ручной, см. `TASKS.md`.
- **Откат схемы целиком**: вернуть `/opt/traefik/static/smart-updown.yaml` из
`/root/bak-smart-updown-nginx-2026-09-15/` на VDS — домен снова пойдёт целиком в куб.
+98
View File
@@ -0,0 +1,98 @@
# nginx для раздачи Unity-сборок «Бинарной ракеты» — LXC 152 (game-web, 192.168.76.152)
# Файл: /etc/nginx/sites-available/game
#
# Модель версионирования:
# /opt/game/releases/<N>/ — файлы сборки N (Build/, TemplateData/, index.html)
# /opt/game/current -> releases/<N> — активная сборка (симлинк, переключает publish-release.sh)
#
# https://smart-updown.binom.pw/ -> активная сборка (current)
# https://smart-updown.binom.pw/28/ -> конкретная сборка 28
# https://smart-updown.binom.pw/28/Build/WebGL.data -> ассет сборки 28
#
# API и WebSocket nginx НЕ обслуживает: их забирает Traefik на VDS
# (router smart-updown-api: PathPrefix /api,/ws -> куб 192.168.76.120).
# Локальные location /api, /ws, /solana-rpc — страховка для запросов,
# которые всё же дойдут до nginx (прямой заход по IP, старые сборки).
# Ассеты Unity внутри каталога версии неизменяемы -> immutable.
# index.html и всё остальное -> no-store.
map $uri $game_cache_control {
default "no-store";
~^/[0-9]+/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?)$ "public, max-age=604800, immutable";
~^/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?)$ "public, max-age=604800, immutable";
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /opt/game/current;
index index.html;
access_log /var/log/nginx/game-access.log;
error_log /var/log/nginx/game-error.log warn;
gzip on;
gzip_min_length 512;
gzip_types application/javascript application/wasm application/json text/css application/octet-stream;
add_header Cache-Control $game_cache_control;
# --- версионированные сборки: /<число>/... -------------------------------
# /28/index.html -> /opt/game/releases/28/index.html
location ~ ^/(?<build>[0-9]+)(?<subpath>/.*)?$ {
root /opt/game/releases;
try_files /$build$subpath /$build$subpath/ =404;
}
# --- ассеты активной сборки ----------------------------------------------
location ~* \.(js|wasm|data|mem|symbols|json|css|png|jpg|jpeg|gif|svg|woff2?)$ {
root /opt/game/current;
try_files $uri =404;
}
# --- страховка: API -------------------------------------------------------
# Нормальный /api идёт мимо nginx (Traefik VDS -> куб). Если запрос всё же
# пришёл сюда — уходим в куб на k3s-Traefik с правильным Host, дальше
# ingress smart-updown-api ведёт в шлюз :8895.
location /api {
proxy_pass http://192.168.76.120;
proxy_http_version 1.1;
proxy_set_header Host smart-updown.binom.pw;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 10s;
proxy_read_timeout 90s;
}
# страховка: WebSocket шлюза
location /ws {
proxy_pass http://192.168.76.120;
proxy_http_version 1.1;
proxy_set_header Host smart-updown.binom.pw;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
# страховка: тонкий CORS-прокси до Solana RPC (старые сборки игры)
location /solana-rpc/ {
proxy_pass http://192.168.76.181:8899/;
proxy_http_version 1.1;
proxy_set_header Host 192.168.76.181:8899;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Headers "content-type" always;
add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
if ($request_method = OPTIONS) { return 204; }
}
# --- корень = активная сборка --------------------------------------------
location / {
try_files $uri $uri/ /index.html;
}
}
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# Публикация сборки игры на nginx (LXC 152, game-web).
#
# publish-release.sh <тег> [--activate]
#
# Что делает:
# 1. тянет образ images.binom.pw/fun/game-front2:<тег> из реестра (skopeo copy в dir);
# 2. распаковывает статику в /opt/game/releases/<тег>/;
# 3. без --activate оставляет активной текущую сборку (корень не меняется);
# с --activate переключает симлинк /opt/game/current на новую версию;
# 4. печатает проверку.
#
# Итог: https://smart-updown.binom.pw/ -> активная сборка
# https://smart-updown.binom.pw/<тег>/ -> конкретная сборка
set -euo pipefail
TAG="${1:?использование: publish-release.sh <тег> [--activate]}"
ACTIVATE="${2:-}"
REG="images.binom.pw/fun/game-front2"
CREDS="subochev:Drovosek319"
IMG="/tmp/gameimg-${TAG}"
echo "=== 1/4 тяну образ ${REG}:${TAG} ==="
rm -rf "$IMG"
skopeo copy --src-creds "$CREDS" "docker://${REG}:${TAG}" "dir:${IMG}"
# креды не нужны при распаковке
chmod -R a+rX "$IMG"
echo "=== 2/4 распаковываю статику в /opt/game/releases/${TAG} ==="
/opt/game/tools/unpack-release.sh "$TAG" "$IMG"
if [ "$ACTIVATE" = "--activate" ]; then
echo "=== 3/4 переключаю активную сборку на ${TAG} ==="
PREV="$(readlink -f /opt/game/current || echo none)"
rm -f /opt/game/current
ln -s "/opt/game/releases/${TAG}" /opt/game/current
echo "было: $PREV"
echo "стало: $(readlink -f /opt/game/current)"
else
echo "=== 3/4 активную сборку НЕ трогаю (нет --activate) ==="
echo "активна: $(readlink -f /opt/game/current)"
fi
echo "=== 4/4 проверка ==="
systemctl reload nginx
for u in "/" "/${TAG}/" "/${TAG}/index.html" "/${TAG}/Build/WebGL.framework.js"; do
printf ' %-42s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1${u}")"
done
echo "готово: сборка ${TAG}"
rm -rf "$IMG"
@@ -0,0 +1,62 @@
http:
# ── middlewares ─────────────────────────────────────────────────────────
middlewares:
# Подмена Host на входе в куб — k3s-Traefik роутит именно по Host,
# а ingress `smart-updown-api` объявлен на smart-updown.binom.pw.
smart-updown-host:
headers:
customRequestHeaders:
Host: "smart-updown.binom.pw"
smart-updown-redirect-https:
redirectScheme:
scheme: https
permanent: true
# ── routers ─────────────────────────────────────────────────────────────
routers:
# API и WebSocket игры: уходят в k3s (192.168.76.120) на ingress
# `smart-updown-api` (ns game), дальше в TS-шлюз :8895.
# Отдельный router потому, что PathPrefix специфичнее Host:
# так /api и /ws не перехватываются статикой nginx.
smart-updown-api-router:
rule: "Host(`smart-updown.binom.pw`) && (PathPrefix(`/api`) || PathPrefix(`/ws`))"
service: smart-updown-api
entryPoints:
- websecure
tls:
certResolver: mycloudflare
# Корень: статика игры. Раздаёт nginx на LXC 152 (game-web, 192.168.76.152)
# в домашней сети через wg-туннель. Версии — каталогами (/28/, /29/, ...),
# корень — активная сборка (симлинк /opt/game/current).
smart-updown-router:
rule: Host(`smart-updown.binom.pw`)
service: smart-updown
entryPoints:
- websecure
tls:
certResolver: mycloudflare
# http -> https
smart-updown-http-redirect:
rule: Host(`smart-updown.binom.pw`)
entryPoints:
- web
service: noop
middlewares:
- smart-updown-redirect-https
# ── services ────────────────────────────────────────────────────────────
services:
smart-updown-api:
loadBalancer:
servers:
- url: "http://192.168.76.120"
smart-updown:
loadBalancer:
servers:
- url: "http://192.168.76.152"
noop:
loadBalancer:
servers:
- url: "http://127.0.0.1:9"
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Распаковка образа игры из dir-дампа skopeo в версионированный каталог nginx.
# Использование: unpack-release.sh <тег> <dir-дамп образа>
set -euo pipefail
TAG="$1"
IMG="$2"
DEST="/opt/game/releases/${TAG}"
[ -d "$IMG" ] || { echo "нет дампа $IMG"; exit 1; }
rm -rf /tmp/rootfs-${TAG}
mkdir -p /tmp/rootfs-${TAG}
cd "$IMG"
# слои из manifest.json в правильном порядке
/usr/bin/python3 - <<'PY' > /tmp/layers-${TAG}.txt
import json
m = json.load(open('manifest.json'))
for l in m['layers']:
print(l['digest'].split(':', 1)[1])
PY
while read -r layer; do
[ -z "$layer" ] && continue
case "$layer" in
*tar.gz|*tgz) tar -xzf "$layer" -C /tmp/rootfs-${TAG} ;;
*tar.zst) tar --zstd -xf "$layer" -C /tmp/rootfs-${TAG} ;;
*) tar -xf "$layer" -C /tmp/rootfs-${TAG} ;;
esac
done < /tmp/layers-${TAG}.txt
SRC="/tmp/rootfs-${TAG}/usr/share/nginx/html"
[ -d "$SRC" ] || { echo "нет $SRC в образе"; ls /tmp/rootfs-${TAG}/usr/share/nginx 2>/dev/null; exit 1; }
mkdir -p "$DEST"
cp -a "$SRC/." "$DEST/"
chmod -R a+rX /opt/game/releases
rm -rf /tmp/rootfs-${TAG} /tmp/layers-${TAG}.txt
echo "=== релиз ${TAG} разложен в ${DEST} ==="
ls -la "$DEST"
du -sh "$DEST"