Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Mieliśmy 2 worki trawy, 75 tabletek meskaliny, środowisko uniksowe, repozytorium dokerów i zadanie implementacji poleceń docker pull i docker push bez klienta dockera.

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

UPD:
Pytanie: Po co to wszystko?
Odpowiedź: Testowanie obciążeniowe produktu (NIE przy użyciu basha, skrypty służą celom edukacyjnym). Zdecydowano się nie używać klienta dokowanego do redukcji dodatkowych warstw (w rozsądnych granicach) i odpowiednio emulować większe obciążenie. W efekcie usunięto wszystkie opóźnienia systemowe klienta Docker. Otrzymaliśmy stosunkowo czysty ładunek bezpośrednio na produkt.
W artykule wykorzystano wersje narzędzi GNU.

Najpierw zastanówmy się, co robią te polecenia.

Do czego więc służy docker pull? Według dokumentacja:

„Wyciągnij obraz lub repozytorium z rejestru”.

Znajdujemy tam również link do zrozumieć obrazy, kontenery i sterowniki pamięci masowej.

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Stąd możemy zrozumieć, że obraz okna dokowanego to zestaw pewnych warstw, które zawierają informacje o najnowszych zmianach w obrazie, a to jest oczywiście to, czego potrzebujemy. Dalej patrzymy API rejestru.

Jest napisane co następuje:

„„ Obraz ” to połączenie manifestu JSON i plików poszczególnych warstw. Proces wyciągania > obrazu koncentruje się na pobraniu tych dwóch komponentów.

Zatem pierwszym krokiem zgodnie z dokumentacją jest „Wyciąganie manifestu obrazu".

Oczywiście nie będziemy go kręcić, ale potrzebujemy z niego danych. Poniżej znajduje się przykładowe żądanie: GET /v2/{name}/manifests/{reference}

„Nazwa i parametry odniesienia identyfikują obraz i są wymagane. Odniesienie może zawierać znacznik lub skrót.”

Nasze repozytorium dokerów jest wdrożone lokalnie, spróbujmy wykonać żądanie:

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

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

W odpowiedzi otrzymujemy json, z którego aktualnie interesują nas tylko linie życia, a raczej ich skróty. Po ich otrzymaniu możemy przejrzeć każdy z nich i wykonać następujące żądanie: „GET /v2/{name}/blobs/{digest}”

„Dostęp do warstwy będzie ograniczony nazwą repozytorium, ale będzie jednoznacznie identyfikowany w rejestrze poprzez skrót”.

Digest w tym przypadku jest skrótem, który otrzymaliśmy.

Spróbujmy

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

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Zobaczmy, jaki plik w końcu otrzymaliśmy jako pierwsze ratunek.

file firstLayer

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

te. szyny to archiwa tar, rozpakowując je w odpowiedniej kolejności otrzymamy zawartość obrazu.

Napiszmy mały skrypt basha, aby wszystko to można było zautomatyzować

#!/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

Teraz możemy uruchomić go z żądanymi parametrami i uzyskać zawartość wymaganego obrazu

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

Część 2 – wypychanie dokera

To będzie trochę bardziej skomplikowane.

Zacznijmy od nowa dokumentacja. Musimy więc pobrać każdego lidera, zebrać odpowiedni manifest i również go pobrać. Wydaje się to proste.

Po przestudiowaniu dokumentacji proces pobierania możemy podzielić na kilka etapów:

  • Inicjalizacja procesu — „POST /v2/{nazwa repo}/blobs/uploads/”
  • Przesyłanie liny ratunkowej (użyjemy przesyłania monolitycznego, czyli wysyłamy każdą linę ratunkową w całości) - „PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Długość zawartości: {rozmiar warstwy}
    Typ treści: aplikacja/strumień oktetowy
    Warstwa danych binarnych”.
  • Ładowanie manifestu - „PUT /v2/{nazwa repo}/manifests/{reference}”.

Ale w dokumentacji brakuje jednego kroku, bez którego nic nie będzie działać. W przypadku ładowania monolitycznego, jak również częściowego (pofragmentowanego), przed załadowaniem szyny należy wykonać żądanie PATCH:

„PATCH /v2/{nazwa repo}/blobs/uploads/{uuid}
Długość zawartości: {rozmiar fragmentu}
Typ treści: aplikacja/strumień oktetowy
{Dane binarne fragmentu warstwy}”.

W przeciwnym razie nie będziesz mógł przejść poza pierwszy punkt, ponieważ... Zamiast oczekiwanego kodu odpowiedzi 202 otrzymasz 4xx.

Teraz algorytm wygląda następująco:

  • Inicjalizacja
  • Szyna łatkowa
  • Ładowanie poręczy
  • Ładowanie manifestu
    Odpowiednio punkty 2 i 3 zostaną powtórzone tyle razy, ile wierszy należy załadować.

Po pierwsze potrzebujemy dowolnego obrazu. Użyję archlinux:latest

docker pull archlinux

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Teraz zapiszmy to lokalnie do dalszej analizy

docker save c24fe13d37b9 -o savedArch

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Rozpakuj powstałe archiwum do bieżącego katalogu

tar xvf savedArch

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Jak widać, każda linia życia znajduje się w osobnym folderze. Przyjrzyjmy się teraz strukturze otrzymanego manifestu

cat manifest.json | json_pp

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Niewiele. Zobaczmy, jaki manifest jest potrzebny do załadowania, zgodnie z dokumentacja.

Implementowanie poleceń docker pull i docker push bez klienta docker przy użyciu żądań HTTP

Oczywiście istniejący manifest nam nie odpowiada, więc stworzymy własny, wykorzystując blackjacka i kurtyzany, koła ratunkowe i konfiguracje.

Zawsze będziemy mieć co najmniej jeden plik konfiguracyjny i tablicę lin ratunkowych. Schemat w wersji 2 (aktualny w momencie pisania), mediaType pozostawimy bez zmian:

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

Po utworzeniu podstawowego manifestu należy wypełnić go poprawnymi danymi. W tym celu używamy szablonu json obiektu Rail:

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

Dodamy go do manifestu dla każdej szyny.

Następnie musimy sprawdzić rozmiar pliku konfiguracyjnego i zastąpić fragmenty w manifeście prawdziwymi danymi

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

Teraz możesz rozpocząć proces pobierania i zapisać identyfikator UUID, który powinien towarzyszyć wszystkim kolejnym żądaniom.

Kompletny skrypt wygląda mniej więcej tak:

#!/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 "nt]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

możemy skorzystać z gotowego skryptu:

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

UPD:
Co w rezultacie otrzymaliśmy?
Po pierwsze, prawdziwe dane do analizy, ponieważ testy są uruchamiane w blazemetrze, a dane dotyczące żądań klientów dokowanych nie dostarczają zbyt wielu informacji, w przeciwieństwie do czystych żądań HTTP.

Po drugie, przejście pozwoliło nam zwiększyć liczbę wirtualnych użytkowników do przesyłania plików dockera o około 150% i uzyskać średni czas reakcji o 20-25% krótszy. W przypadku pobierania dockera udało nam się zwiększyć liczbę użytkowników o 500%, przy jednoczesnym skróceniu średniego czasu odpowiedzi o około 60%.

Dziękuję za uwagę.

Źródło: www.habr.com

Dodaj komentarz