0063. Реестр образов OCI поверх артефактов
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-03. Расширяет публичный HTTP-контракт вторым протоколом рядом с /api/v1 (ADR 0045); хранение и права — прежние.
Контекст
Команды, которые держат в Arkvory пакеты и файлы, собирают и контейнерные образы. Отдельный реестр (Harbor, registry:2) означает второе хранилище, свои ключи, квоты и резервные копии. Docker, containerd, Podman, Buildx, Helm (OCI) и ORAS говорят на одном протоколе — OCI Distribution. Если Arkvory говорит на нём сам, образы получают те же права, квоты, проверку SHA-256 и резервные копии, что и остальные артефакты.
Решение
Протокол. Подмножество OCI Distribution 1.1 под /v2/ на том же шлюзе, что и /api/v1:
- pull:
GET|HEAD /v2/<name>/manifests/<tag|digest>,GET|HEAD /v2/<name>/blobs/<digest>с Range,GET /v2/<name>/tags/list?n=&last=сLinkна следующую страницу; - push:
POST /v2/<name>/blobs/uploads/(сессия либо весь blob сразу с?digest=),PATCHчастями сContent-Range,PUT …?digest=с последней частью или без неё,GETсостояния,DELETEсессии;PUT /v2/<name>/manifests/<ref>; - удаление:
DELETE /v2/<name>/manifests/<digest>удаляет манифест и все его теги,DELETE …/manifests/<tag>— только тег;DELETEblob отвечает 405; GET /v2/— проверка версии; ответы несутDocker-Distribution-API-Version: registry/2.0, ошибки — конверт{errors:[{code,message,detail:{requestId}}]}с кодами спецификации.
Имена. <name> = <repository>/<image>: первый сегмент — репозиторий Arkvory (граница доступа), остальное — имя образа из одного или нескольких компонентов. Пример: docker push arkvory.example.com/releases/team/web:1.4. Дайджесты — только sha256: Arkvory проверяет каждый объект по SHA-256. Реестру нужен корень хоста: Docker не поддерживает префикс пути, поэтому /v2 проксируется как есть.
Аутентификация. Сервис токенов не нужен: docker login получает вызов WWW-Authenticate: Basic realm="Arkvory" и присылает ключ Arkvory как пароль Basic, имя пользователя не проверяется. Подходят сервисный ключ, персональный токен и файловый ключ; Bearer тоже принимается. Отозванный или истёкший ключ — 401 UNAUTHORIZED, нехватка прав — 403 DENIED. Только на маршрутах /v2 учётные данные принимаются в Basic; /api/v1 по-прежнему принимает только Bearer.
Права. Те же действия, что у артефактов:
- pull —
content.read, список тегов —artifact.list(файловые ключи и группы —read); - push —
upload.create,upload.write,upload.complete(write); токен только для чтения и зеркало (ADR 0058) push не принимают; - удаление манифеста или тега —
artifact.delete, как у retention: только сервисный ключ с этим действием.
Хранение. Blob и манифест — обычные артефакты: имя — дайджест, метка oci, metadata oci.kind (blob/manifest), у манифеста ещё oci.image и oci.mediaType. Квоты, ёмкость, проверка SHA-256, маршрутизация хранилища и резервные копии — общие. Индекс реестра в БД (миграция 30, только расширение схемы):
arkvory_oci_blobs— дайджест → артефакт, общий для образов одного репозитория. Повторный push того же слоя ничего не хранит дважды. Монтирования между репозиториями нет: запрос mount получает обычную сессию загрузки, как разрешает спецификация.arkvory_oci_manifests,arkvory_oci_tags— манифесты и теги образа;arkvory_oci_references— что манифест ссылается на blob или дочерний манифест;arkvory_oci_uploads— незавершённые загрузки (в резервные копии не входят).
Загрузка. Части пишутся в <data>/oci-uploads/<id> на томе хранилища:
- Запись идёт потоком с ограничением полосы и сроками загрузки, как у
/api/v1; запас свободного места проверяется каждые 64 MiB; каждая часть синхронизируется на диск до того, как строка БД продвинется. - Часть должна начинаться с уже принятого размера, иначе
416. Байты, оставленные прерванным запросом, отрезаются при следующей части. - Одновременные запросы к одной сессии отклоняются: шлюз записи один, сериализация в процессе.
PUT ?digest=копирует принятые байты в хранилище артефактов, которое само сверяет размер и SHA-256. При несовпадении артефакт отменяется и возвращаетсяDIGEST_INVALID. Повтор после сбоя находит тот же артефакт по ключу сессии.- Временно нужно до двух объёмов слоя.
- Сессия без активности сутки забывается вместе с файлом. Это делает обслуживание шлюза записи раз в минуту по часам БД; файлы без сессии удаляются по времени изменения.
- Шлюз чтения (ADR 0009) обслуживает только
GET|HEAD.
Манифесты. Принимаются OCI image manifest и image index, Docker schema 2 manifest и manifest list:
- Ограничения: не больше 4 MiB,
schemaVersion: 2. Тип берётся изContent-Typeили поляmediaType, и они не должны расходиться. Schema 1 не поддерживается. - Дайджест считает сервер;
PUTпо дайджесту должен с ним совпасть. - Всё, на что ссылается манифест (config и слои, у индекса — платформенные манифесты того же образа), должно быть в репозитории, иначе
MANIFEST_BLOB_UNKNOWN. Проверка повторяется при фиксации под той же блокировкой изменений каталога, под которой удаляет retention. - После фиксации манифест и всё, на что он ссылается, защищены.
Retention. Артефакт манифеста и blob, на который ссылается хранимый манифест, получают уже существующую причину защиты reference. Новых значений в контракте retention нет.
- Тег можно удалить, а образ всё равно будет доступен по дайджесту и защищён.
- После
DELETEманифеста по дайджесту защита снимается. Его артефакт и неиспользуемые слои удаляет retention или администратор. Если удалённый слой понадобится снова, реестр сообщит, что его нет, и примет push заново. - Blob, загруженный без манифеста, не защищён.
Ошибки. Отказы Arkvory сохраняют HTTP-статус и текст, код переводится: unauthorized → UNAUTHORIZED (с вызовом Basic), forbidden, conflict, capacity_exceeded → DENIED, not_found → NAME_UNKNOWN, busy, rate_limited → TOOMANYREQUESTS (с Retry-After), unavailable → UNAVAILABLE, integrity_mismatch → DIGEST_INVALID, прочие ошибки ввода → UNSUPPORTED. Метрики и журналы видят исходный код Arkvory.
Последствия
- Маршруты
/v2следуют спецификации OCI, а не OpenAPI Arkvory: они исключены из инвентаря/api/v1явно, SDK и CLI их не дублируют, клиент — любой OCI-совместимый. - Перед Arkvory не нужен второй сервис; прокси пропускает
/v2без буферизации тела (client_max_body_size 0, как в примере nginx). - Не сделано: Referrers API (404, клиенты переходят на схему тегов),
/_catalog, монтирование между репозиториями, сервис токенов Bearer, pull-through cache, перенос образов в режиме импорта, автоматическая сборка манифестов без тегов, раздел консоли для образов. - Зеркала переносят образы: каждое изменение индекса пишет запись ленты (
oci.blob,oci.manifest,oci.tag,oci.manifest.delete,oci.tag.delete) в своей транзакции (дополнение ADR 0058).
Проверки
tests/oci-registry.test.mjs:- разбор путей, включая вложенные имена и слова маршрута в имени;
- отказы по именам, ссылкам и алгоритмам;
- разбор четырёх типов манифестов и их ошибок;
- порядок частей, чужая сессия, сериализация запросов, отмена артефакта при несовпадении дайджеста.
tests/integration/oci-registry.test.mjs:- вызов
docker loginи Basic; - push частями и целиком, pull по тегу и дайджесту, Range;
- права чтения и записи, повторный push без новых артефактов, страницы тегов;
- неизвестные blob и манифест, неверный дайджест, пропуск в частях, Referrers и прочие маршруты;
- защита retention до удаления манифеста и повторный push после неё;
- индекс с платформенными манифестами, истечение брошенных загрузок.
- вызов
tests/integration/backup-schema.test.mjsсверяет реестр таблиц резервной копии с миграцией 30.
Дополнение: проверка перед 0.4.0
- Байты незавершённых сессий загрузки учитываются в квоте репозитория и ёмкости установки до записи каждого куска. Иначе брошенные сессии заполняли том мимо любой квоты. Отказ — 507
DENIED(storage_quota). - Тело блоба не разбирается ни при каком Content-Type: клиент может объявить
application/json. - Записи индекса с типом манифеста (или без типа) — манифесты образа, остальные — блобы репозитория. Так кэш BuildKit (
--cache-to type=registry) хранит слои и конфигурацию кэша. Обе группы попадают в ссылки, и retention их не удаляет. - Страницы тегов сравнивают курсор в порядке байтов, как и сортируют: при сортировке базы en_US страницы пропускали теги.