Files
admin/.wiki/concepts/registry-oci-image-index-gc.md
vitya e5aeafa914 docs(wiki): :master restored via re-push; digest-collision suspicion cleared
books закрыли named-tag предохранителем (62d812e: protectRe +master|latest).
keep/drop digest-коллизия — снято: keep=1 у books-api это buildcache
(group-by-digest через Map делает keep/drop взаимоисключающими).

Восстановление снесённого named-тега = re-push локального образа с VDS
(docker tag inspect-Image + push под books-ci), не rebuild → плоский
single-platform манифест с .config (pullable, датируется). Применено к
books-api + books-ops-mcp -> :master снова 200. Гоча: books-api вне
CI-матрицы (embed-api-into-web) -> re-push единственный путь.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:32:11 +03:00

9.8 KiB
Raw Blame History

title, type, tags, sources, related, updated
title type tags sources related updated
registry.kzntsv.site — books-* образы это OCI image-index (multi-manifest) → дата живёт в sub-manifest, GC должен спускаться concept
registry
vds-kzntsv
oci
buildx
docker
gc
manifest
gotcha
../sources/vds-kzntsv-bootstrap-2026-05-20.md
registry-kzntsv-auth-model
registry-gc-mount-and-modify-flag
bindmount-config-edit-preserve-mode
2026-06-18

books-* образы в registry = OCI image-index, не плоский манифест

books-* образы (собраны docker buildx) пушатся в registry.kzntsv.site как 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:

{ "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.)

Ловушка для 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 ошибок, ни одного 405REGISTRY_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/master404. 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).
  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. v2 DELETE сам место не возвращает.

Связь