docs(wiki): OCI image-index GC gotcha + bind-mount config-shadow lesson

Следствие закрытия [books-task-runner-registry-auth-cred].

concepts/registry-oci-image-index-gc.md (new): books-* образы в registry =
OCI image-index (buildx), top-level .config=null, дата .created в платформенном
sub-manifest. Наивный GC по top-level дате → null у всех → null-dated группы
защищаются → drop=0 всегда (keepLastN не применяется). Правила: Accept со
всеми media-types, дата из sub, DELETE по index-digest не sub. Зафиксировано
на books registryGc dryRun (deleted=0 при 14 tags) + манифест-dump books-web.

concepts/bindmount-config-edit-preserve-mode.md: дополнен гочей про
bind-mount shadow (config образа затенён целиком → полная секция, не дельта)
+ worked example task-runner (mode не слетел, постмортем сработал).

+index.md (2 строки) +log.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 12:31:41 +03:00
parent 18a1e3ff61
commit 85b13ae6a4
4 changed files with 76 additions and 1 deletions

View File

@@ -0,0 +1,50 @@
---
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-дерева, не в доступе.
## Правила для любого 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).