Реализация команд docker pull и docker push без docker клиента посредством HTTP запросов

от автора

У нас было 2 мешка травы, 75 таблеток мескалина unix environment, docker репозиторий и задача реализовать команды docker pull и docker push без докер клиента.

Для начала разберемся что делают эти команды
Итак для чего используется docker pull? Согласно документации:

"Pull an image or a repository from a registry".

Там же находим ссылку на understand images, containers, and storage drivers.

Отсюда мы можем понять что docker image это набор неких layers, которые содержат в себе информацию о последних изменениях в имедже, которые очевидно нам и нужны. Дальше смотрим в registry API.
Здесь говорится следующее:

"An “image” is a combination of a JSON manifest and individual layer files. The process of pulling an > image centers around retrieving these two components."

Итак первый шаг согласно документации это “Pulling an Image Manifest
Пулить мы его конечно не будем, но данные из него нам нужны. Дальше приводится пример запроса: GET /v2/{name}/manifests/{reference}

"The name and reference parameter identify the image and are required. The reference may include a tag or digest."

Наш докер репозиторий развернут локально, попробуем выполнить запрос:

curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/manifests/1.1.1" -H "header_if_needed"

В ответ получаем json из которого нам интересны на данный момент только лееры, точнее их хэши. Получив их, можем по каждому пройтись и выполнить следующий запрос: "GET /v2/{name}/blobs/{digest}"

“Access to a layer will be gated by the name of the repository but is identified uniquely in the registry by digest.”

digest в данном случае и есть хэш, который мы получили.

Пробуем

curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/blobs/sha256:f972d139738dfcd1519fd2461815651336ee25a8b54c358834c50af094bb262f" -H "header_if_needed" --output firstLayer

посмотрим что за файл мы в итоге получили в качестве первого леера.

file firstLayer

т.е. лееры представляют из себя tar архивы, распаковав которые в соответствующем порядке мы получим содержимое имеджа.

Напишем небольшой баш скрипт чтобы всё это можно было автоматизировать

#!/bin/bash -eu  downloadDir=$1 # url as http://localhost:8081/link/to/docker/registry url=$2 imageName=$3 tag=$4  # array of layers layers=($(curl -s -X GET "$url/v2/$imageName/manifests/$tag" | grep -oP '(?<=blobSum" : ").+(?=")'))  # download each layer from array for layer in "${layers[@]}"; do     echo "Downloading ${layer}"     curl -v -X GET "$url/v2/$imageName/blobs/$layer" --output "$downloadDir/$layer.tar" done  # find all layers, untar them and remove source .tar files cd "$downloadDir" && find . -name "sha256:*" -exec tar xvf {} \; rm sha256:*.tar exit 0

Теперь можем запустить его с желаемыми параметрами и получить содержимое необходимого имеджа

./script.sh dirName “http://localhost:8081/link/to/docker/registry” myAwesomeImage 1.0

Часть 2 — docker push

Тут будет чуть посложнее.
Начнем опять с документации. Итак нам надо загрузить каждый леер, собрать соответствующий манифест и загрузить его тоже. Вроде звучит просто.

Изучив документацию можем разделить процесс загрузки на несколько шагов:

  • Инициализация процесса — "POST /v2/{repoName}/blobs/uploads/"
  • Загрузка леера (мы будем использовать монолитную загрузку, т.е. каждый леер отправляем целиком) — "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Content-Length: {size of layer}
    Content-Type: application/octet-stream
    Layer Binary Data".
  • Загрузка манифеста — "PUT /v2/{repoName}/manifests/{reference}".

Но в документации упущен один шаг, без которого ничего не получится. Для монолитной загрузки так же как и для частичной (chunked) перед тем как грузить леер необходимо выполнить PATCH запрос:
"PATCH /v2/{repoName}/blobs/uploads/{uuid}
Content-Length: {size of chunk}
Content-Type: application/octet-stream
{Layer Chunk Binary Data}".

В противном случае вы не сможете продвинуться дальше первого пункта, т.к. вместо ожидаемого кода ответа 202 будете получать 4хх.
Теперь алгоритм выглядит как:

  • Инициализация
  • Патч леера
  • Загрузка леера
  • Загрузка манифеста
    Пункты 2 и 3 соответственно будут повторяться столько раз, сколько лееров необходимо загрузить.

Для начала нам понадобиться любой имедж. Я буду использовать archlinux:latest

docker pull archlinux

Теперь сохраним его себе локально для дальнейшего разбора

docker save c24fe13d37b9 -o savedArch

Распакуем полученный архив в текущую директорию

tar xvf savedArch

Как видим каждый леер лежит в отдельной папке. Теперь посмотрим на струткуру манифеста, который мы получили

cat manifest.json | json_pp

Не густо. Посмотрим какой манифест нужен для загрузки, согласно документации.

Очевидно существующий манифест нам не подходит, значит сделаем свой с блэкджеком и куртизанками леерами и конфигами.

У нас всегда будет минмум один config файл и массив лееров. Версия схемы 2 (актуальна на момент написания статьи), mediaType оставим без изменений:

echo ‘{    "schemaVersion": 2,    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",    "config": {       "mediaType": "application/vnd.docker.container.image.v1+json",       "size": config_size,       "digest": "config_hash"    },    "layers": [       ’ > manifest.json

После создания базового манифеста необходимо его наполнить валидными данными. Для этого используем шаблон json объекта леера:

{          "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",          "size": ${layersSizes[$i]},          "digest": \"sha256:${layersNames[$i]}\"       },

его мы будем добавлять в манифест для каждого леера.

Далее нам надо узнать размер конфиг файла и заменить заглушки в манифесте на реальные данные

sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFile

Теперь можно инициировать процесс загрузки и сохранить себе uuid, которым должны сопровождаться все последующие запросы.

Полный скрипт выглядит примерно так:

#!/bin/bash -eux  imageDir=$1 # url as http://localhost:8081/link/to/docker/registry url=$2 repoName=$3 tag=$4 manifestFile=$(readlink -f ${imageDir}/manifestCopy) configFile=$(readlink -f $(find $imageDir -name "*.json" ! -name "manifest.json"))  # calc layers sha 256 sum, rename them accordingly, and add info about each to manifest file function prepareLayersForUpload() {   info_file=$imageDir/info   # lets calculate layers sha256 and use it as layers names further   layersNames=($(find $imageDir -name "layer.tar" -exec shasum -a 256 {} \; | cut -d" " -f1))    # rename layers according to shasums. !!!Set required amount of fields for cut command!!!   # this part definitely can be done easier but i didn't found another way, sry   find $imageDir -name "layer.tar" -exec bash -c 'mv {} "$(echo {} | cut -d"/" -f1,2)/$(shasum -a 256 {} | cut -d" " -f1)"' \;    layersSizes=($(find $imageDir -name "*.tar" -exec ls -l {} \; | awk '{print $5}'))    for i in "${!layersNames[@]}"; do     echo "{          "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",          "size": ${layersSizes[$i]},          "digest": \"sha256:${layersNames[$i]}\"       }," >> $manifestFile   done   # remove last ','   truncate -s-2 $manifestFile   # add closing brakets to keep json consistent   printf "\n\t]\n}" >> $manifestFile }  # calc config sha 256 sum and add info about it to manifest function setConfigProps() {   configSize=$(ls -l $configFile | awk '{print $5}')   configName=$(basename $configFile | cut -d"." -f1)    sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFile }  #prepare manifest file prepareLayersForUpload setConfigProps cat $manifestFile  # initiate upload and get uuid uuid=$(curl -s -X POST -I "$url/v2/$repoName/blobs/uploads/" | grep -oP "(?<=Docker-Upload-Uuid: ).+")  # patch layers # in data-binary we're getting absolute path to layer file for l in "${!layersNames[@]}"; do   pathToLayer=$(find $imageDir -name ${layersNames[$l]} -exec readlink -f {} \;)     curl -v -X PATCH "$url/v2/$repoName/blobs/uploads/$uuid" \   -H "Content-Length: ${layersSizes[$i]}" \   -H "Content-Type: application/octet-stream" \   --data-binary "@$pathToLayer"  # put layer   curl -v -X PUT "$url/v2/$repoName/blobs/uploads/$uuid?digest=sha256:${layersNames[$i]}" \   -H 'Content-Type: application/octet-stream' \   -H "Content-Length: ${layersSizes[$i]}" \   --data-binary "@$pathToLayer" done  # patch and put config after all layers curl -v -X PATCH "$url/v2/$repoName/blobs/uploads/$uuid" \   -H "Content-Length: $configSize" \   -H "Content-Type: application/octet-stream" \   --data-binary "@$configFile"    curl -v -X PUT "$url/v2/$repoName/blobs/uploads/$uuid?digest=sha256:$configName" \   -H 'Content-Type: application/octet-stream' \   -H "Content-Length: $configSize" \   --data-binary "@$configFile"  # put manifest curl -v -X PUT "$url/v2/$repoName/manifests/$tag" \   -H 'Content-Type: application/vnd.docker.distribution.manifest.v2+json' \   --data-binary "@$manifestFile"  exit 0 

можем использовать готовый скрипт:

./uploadImage.sh "~/path/to/saved/image" "http://localhost:8081/link/to/docker/registry" myRepoName 1.0

В статье использовались тулы GNU версий.

В целом статья не открывает Америку, но немного структурирует разрозненные данные и дополняет пробелы в документации. Спасибо, что дочитали.

ссылка на оригинал статьи https://habr.com/ru/post/504314/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *