Первый боевой registryGc dryRun:false (books master-289c660, под добро юзера): 63 DELETE, 0 ошибок, 0x405 -> REGISTRY_STORAGE_DELETE_ENABLED=true подтверждён живьём; 61 dangling + 2 datable снесены, реестр почищен. Урок: master у books-api/books-ops-mcp был dangling (не пересобирались ~3нед) -> логика drop-dangling снесла named-тег -> :master 404. Outage нет (контейнеры на локальных образах), но redeploy упрётся. Политика: НЕ удалять named-теги master/latest даже dangling (protectRe -> +master/latest); проверить keep/drop на digest-коллизию (books-api keep=1 но 0 не-buildcache осталось). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
8.9 KiB
Markdown
76 lines
8.9 KiB
Markdown
---
|
||
title: registry.kzntsv.site — books-* образы это OCI image-index (multi-manifest) → дата живёт в sub-manifest, GC должен спускаться
|
||
type: concept
|
||
tags: [registry, vds-kzntsv, oci, buildx, docker, gc, manifest, gotcha]
|
||
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
|
||
related: [registry-kzntsv-auth-model, registry-gc-mount-and-modify-flag, bindmount-config-edit-preserve-mode]
|
||
updated: 2026-06-18
|
||
---
|
||
|
||
# books-* образы в registry = OCI image-index, не плоский манифест
|
||
|
||
books-* образы (собраны `docker buildx`) пушатся в [`registry.kzntsv.site`](registry-kzntsv-auth-model.md) как **OCI image-index** (`application/vnd.oci.image.index.v1+json`), а не как одиночный image-manifest. Это меняет всё, что трогает дату/размер/удаление образа.
|
||
|
||
## Структура (факт, books-web:master, 2026-06-18)
|
||
|
||
```
|
||
tag master → OCI image-index (mediaType: ...image.index.v1+json)
|
||
.config.digest = null ← у index НЕТ top-level config
|
||
.manifests = [
|
||
{ mediaType: ...image.manifest.v1+json, platform: {amd64, linux} }, ← реальный образ
|
||
{ mediaType: ...image.manifest.v1+json, platform: {unknown, unknown}, ← attestation, скипать
|
||
annotations["vnd.docker.reference.type"] = "attestation-manifest" }
|
||
]
|
||
```
|
||
|
||
Дата сборки (`.created`) **не на верхнем уровне** — она в config-blob платформенного sub-manifest:
|
||
```
|
||
index → .manifests[] где platform.os/arch != "unknown" → <sub>.config.digest → GET /v2/<repo>/blobs/<digest> → .created
|
||
```
|
||
Подтверждено: `books-web` amd64-sub `.config.digest=sha256:45249a6…`, blob → `"created":"2026-06-18T08:56:51Z"`. Дата валидная — просто на уровень глубже.
|
||
|
||
## Почему это ломает наивный registry-GC (no-op-баг)
|
||
|
||
Если GC берёт `.created` из top-level манифеста тега — у image-index его НЕТ (`config.digest=null`), `getCreated` возвращает `null` **для всех** тегов. Если планировщик удаления защищает группы с `created==null` (разумный fail-safe «не удаляю то, что не датировал») — **drop пуст всегда**, `keepLastN` не применяется, реестр не чистится. Симптом в отчёте: по каждой репе `keep = tags − 1, drop = 0` (−1 = `buildcache`-тег, отваливается в errors при резолве).
|
||
|
||
Зафиксировано на books `registryGc` dryRun 2026-06-18 (`packages/task-runner/lib/registryV2.js`: `getCreated`→null, `planDeletions` ставит `protected` любой null-dated группе). Auth при этом исправен (books-ci, 401 нет) — баг чисто в обходе manifest-дерева, не в доступе.
|
||
|
||
## Dangling image-индексы — реестр уже засорён ими (2026-06-18)
|
||
|
||
Бóльшая часть тегов books-* — **dangling index**: тег указывает на живой OCI image-index, но его платформенный sub-manifest при фетче отдаёт `MANIFEST_UNKNOWN`:
|
||
```json
|
||
{ "errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown",
|
||
"detail":{"Name":"books-web","Revision":"sha256:8d2b447a…"}}] }
|
||
```
|
||
Sub-manifest (и его слои) физически удалены, а **тегированный родитель-index остался**. Замер books-web 2026-06-18: **19 не-buildcache тегов → 3 датируемых (живых), 16 dangling**.
|
||
|
||
**Происхождение:** классическое последствие host-side `registry garbage-collect`, который для multi-arch НЕ следует ссылкам index→child → удаляет детей-манифесты и блобы, оставляя тег-надгробие на верхнем index'е. (См. [`registry-gc-mount-and-modify-flag`](registry-gc-mount-and-modify-flag.md).)
|
||
|
||
**Ловушка для date-based GC:** `getCreated` на dangling вернёт `null` (sub недоступен) → планировщик защитит группу как «недатированную» → dangling **никогда не удаляется**, хотя это лучший кандидат на чистку. Логика задом наперёд, если не различать:
|
||
- **transient resolve error** (5xx/таймаут) → protect, safe;
|
||
- **`MANIFEST_UNKNOWN` на sub** (dangling) → **eligible for delete** (не protect); даты нет — в хвост сортировки или дроп безусловно, образ уже сломан (`docker pull` по нему падает).
|
||
|
||
**freedBytes-нюанс:** удаление dangling-индекса освобождает ~0 байт (слои уже вычищены прежним GC) — это гигиена tag-list + удаление битых pull-целей, не disk-reclaim. Реальное место по keepLastN экономится только на живых датируемых образах.
|
||
|
||
## Real-run 2026-06-18: чистка прошла, но dangling-master снесён у 2 репо
|
||
|
||
Первый боевой `registryGc {dryRun:false}` (books `master-289c660`, под добро юзера): **63 DELETE, 0 ошибок, ни одного 405** → `REGISTRY_STORAGE_DELETE_ENABLED=true` подтверждён живьём. 61 dangling + 2 живых datable за keepLastN. Теги: web 20→4, task-runner/job-scheduler 17→5, api 8→1, ops-mcp 7→1.
|
||
|
||
**Побочка — урок:** у `books-api` и `books-ops-mcp` сам тег `master` указывал на **dangling**-индекс (образа не пересобирались ~3 нед, sub-манифесты вычищены прежним GC). Логика «dangling → drop безусловно» снесла и named-тег → `…/manifests/master` → **404**. Pull был сломан и до прогона (MANIFEST_UNKNOWN), но теперь тег исчез совсем. **Outage'а нет** (контейнеры books-api/bookva-api/books-ops-mcp крутятся с локальных образов), но любое **пересоздание/redeploy** упрётся в 404 до CI-rebuild.
|
||
|
||
**Вывод для GC-политики:** **не удалять named-теги (`master`/`latest`) даже dangling** — это deploy-указатель; лучше логировать «нужен rebuild». protectRe для registryGc стоит расширить с `^buildcache$` до `^buildcache$|^master$|^latest$`. Также проверить keep/drop на коллизию digest (у books-api `keep=1`, но в реестре не-buildcache не осталось — kept-образ, похоже, делил index-digest с dropped-dangling и ушёл вместе с ним по общему DELETE).
|
||
|
||
## Правила для любого GC/cleanup над этим реестром
|
||
|
||
1. **Accept на manifest-fetch** обязан включать и index, и manifest-list, и oci.image.manifest, и docker.manifest.v2 — иначе registry отдаёт не тот тип / 404 (та же media-type-гоча, что для HEAD тега в [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md)).
|
||
2. **Дату/размер тянуть из платформенного sub-manifest**, спустившись через index. Выбор записи: `platform.os != "unknown"` и `vnd.docker.reference.type != "attestation-manifest"`.
|
||
3. **DELETE — по digest INDEX'а** (digest, на который указывает тег; берётся из `Docker-Content-Digest` HEAD-заголовка тега), НЕ по sub-manifest. Удаление sub осиротит index. Два разных digest — index-digest для DELETE, sub.config для `.created`.
|
||
4. **Одна «версия» = один index** (включает и платформенный manifest, и attestation). Группировать на удаление по index-digest; attestation удаляется вместе с index. keepLastN считать по образам, не по записям `.manifests`.
|
||
5. Disk-reclaim после mark-delete — отдельный host-cron `registry garbage-collect`, см. [`registry-gc-mount-and-modify-flag`](registry-gc-mount-and-modify-flag.md). v2 DELETE сам место не возвращает.
|
||
|
||
## Связь
|
||
|
||
- [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md) — auth/htpasswd, юзер books-ci, GC через v2 DELETE.
|
||
- [`bindmount-config-edit-preserve-mode`](bindmount-config-edit-preserve-mode.md) — как клали кред books-ci в task-runner config (shadow + mode гочи).
|
||
- Хост: [`books-vds`](../entities/books-vds.md) (task-runner), реестр на [`vds-kzntsv`](../entities/vds-kzntsv.md).
|