Files
admin/.wiki/concepts/registry-oci-image-index-gc.md

93 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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». books закрыли предохранителем (`62d812e`: protectRe `^buildcache$``^buildcache$|^master$|^latest$`). Подозрение на keep/drop digest-коллизию — **снято**: `keep=1` у books-api это buildcache (protected, он и остался); группировка по digest через Map делает keep/drop взаимоисключающими, kept-манифест не удаляется.
**Восстановление снесённого named-тега — re-push локального образа** (не rebuild): контейнер крутится с локального образа на хосте → `docker tag $(docker inspect -f '{{.Image}}' <ctr>) registry/<repo>:master && docker push` (под books-ci с самого VDS, не из дома). Возвращает **плоский single-platform манифест с .config** (pullable, и будущий GC его датирует). Применено 2026-06-18 к books-api + books-ops-mcp → :master снова 200. **Гоча:** `books-api` вне CI-build-матрицы (вынесен под embed-api-into-web) — для него re-push единственный способ вернуть pullable master, rebuild'ом не выйдет.
## Правила для любого 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).
## Обновление 2026-08-23: 2.8.3 vs OCI-index — docker pull «manifest unknown» + фикс docker-форматом
**Симптом:** `docker pull registry.kzntsv.site/books-tool-create-picking-list-pdf:master``manifest unknown` (и «no basic auth credentials» до починки config.json — см. [[books-vds-no-gitea-actions]]).
**Диагностика (curl к /v2/<repo>/manifests/<tag>):**
- Accept docker-only (`manifest.v2+json`, `manifest.list.v2+json`) → **404**
- Accept c `application/vnd.oci.image.index.v1+json` → 200 (Content-Type: OCI index)
- child-манифест из index по digest → **404** (удалён прежним host-GC — dangling, как описано выше)
→ registry 2.8.3 отдаёт OCI-index тег только на точный OCI-Accept; docker daemon 26.1.4 получает «manifest unknown». Запушенные buildx/curl-флоу теги = OCI index с **отсутствующим child** = сломанный pull.
**Фикс (проверен): перепуш образа как docker schema2-манифест** — те же блобы (content-addressed), обёртка docker: mediaType `application/vnd.docker.distribution.manifest.v2+json`, config `application/vnd.docker.container.image.v1+json` (НЕ `distribution.image.config.v2+json` — docker его не знает, «(unknown)» на pull), layers `application/vnd.docker.image.rootfs.diff.tar.gzip`. Пути: `docker save` → OCI tar → amd64 child → трансформ mediaTypes → `PUT /v2/<repo>/manifests/<tag>` с docker Content-Type. Pull снова работает.
**Вывод для оператора:** на registry:2.8.3 избегать пуш OCI-формата (buildx по умолчанию), host-GC рвать children нельзя; 2026-08-23 dry-run GC: 67 «eligible», реально существующих файлов ~2.5МБ (остальное — уже удалённые ссылки) → host-GC на живом реестре нецелесообразен, риск > выгоды. Аккуратная чистка buildcache/старых тегов — index-aware инструментом books (`registryGc` в task-runner, v2 DELETE).