Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

Am avut 2 saci de iarbă, 75 de tablete de mescalină, un mediu UNIX, un repository Docker și sarcina de a implementa comenzile docker pull și docker push fără client Docker.

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

UPD:
Întrebare: De ce toate acestea?
Răspuns: Testarea de încărcare a produsului (NU prin intermediul bashului, scripturile sunt furnizate în scopuri educaționale). S-a decis să nu se folosească clientul Docker pentru a reduce straturile suplimentare (în limite rezonabile) și, în consecință, pentru a emula o sarcină mai mare. Drept urmare, s-au eliminat toate întârzierile sistemice ale clientului Docker. Am obținut o sarcină comparativ curată direct pe produs.
În articol s-au utilizat uneltele GNU.

Pentru început, să înțelegem ce fac aceste comenzi.

Așadar, pentru ce este folosit docker pull? Conform documentation:

"Trage o imagine sau un repository dintr-un registry".

Acolo găsim și un link către înțelegerea imaginilor, containerelor și driverelor de stocare.

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

Din asta putem înțelege că o imagine Docker este un set de layere, care conțin informații despre ultimele modificări în imagine, care evident ne sunt necesare. Continuăm să ne uităm în API-ul registry-ului.

Aici se spune următoarele:

"O ‘imagine’ este o combinație între un manifest JSON și fișiere de layer individuale. Procesul de tragere a unei imagini se axează pe recuperarea acestor două componente."

Așadar, primul pas conform documentației este “Tragerea unui Manifest de Imagine”.

Nu o vom trage, dar avem nevoie de datele din acesta. În continuare, se oferă un exemplu de cerere: GET /v2/{name}/manifests/{reference}

"Parametrii name și reference identifică imaginea și sunt necesari. Referința poate include un tag sau un digest."

Repository-ul nostru Docker este desfășurat local, să încercăm să efectuăm cererea:

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

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

În răspuns obținem un json din care ne interesează deocamdată doar layerele, mai precis hash-urile lor. După ce le-am obținut, putem parcurge fiecare și efectua următoarea cerere: "GET /v2/{name}/blobs/{digest}"

“Accesul la un layer va fi reglementat de numele repository-ului dar este identificat unic în registry prin digest.”

digest în acest caz este hash-ul pe care l-am obținut.

Să încercăm

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

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

să vedem ce fișier am obținut în final ca primul layer.

file firstLayer

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

adică, layerele reprezintă arhive tar, dezarhivându-le în ordinea corespunzătoare vom obține conținutul imaginii.

Vom scrie un mic script bash pentru a automatiza tot acest proces.

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

Acum putem să-l lansăm cu parametrii doriti și să obținem conținutul imaginii necesare

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

Partea 2 — docker push

Aici va fi un pic mai complicat.

Să începem din nou cu documentation. Așadar, trebuie să încărcăm fiecare layer, să construim manifestul corespunzător și să-l încărcăm și pe acesta. Pare simplu.

Studiind documentația, putem împărți procesul de încărcare în mai mulți pași:

  • Inițializarea procesului — "POST /v2/{repoName}/blobs/uploads/"
  • Încărcarea layer-ului (vom folosi încărcarea monolitică, adică fiecare layer este trimis în întregime) — "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}"
    Content-Length: {size of layer}
    Content-Type: application/octet-stream
    Layer Binary Data".
  • Încărcarea manifestului — "PUT /v2/{repoName}/manifests/{reference}".

Dar în documentație a fost omis un pas, fără care nimic nu va funcționa. Pentru încărcarea monolitică, la fel ca și pentru încărcarea parțială (chunked), înainte de a încărca layer-ul, este necesar să efectuezi o cerere PATCH:

"PATCH /v2/{repoName}/blobs/uploads/{uuid}"
Content-Length: {size of chunk}
Content-Type: application/octet-stream
{Layer Chunk Binary Data}".

În caz contrar, nu vei putea avansa mai departe de primul punct, deoarece în loc de codul de răspuns așteptat 202, vei primi un 4xx.

Acum algoritmul arată astfel:

  • Inițializare
  • Patch layer-ului
  • Încărcarea layer-ului
  • Încărcarea manifestului
    Punctele 2 și 3 vor fi repetate de câte ori este necesar să încarci layer-e.

Pentru început, ne va trebui orice imagine. Eu voi folosi archlinux:latest

docker pull archlinux

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

Acum să-l salvăm local pentru o analiză ulterioară

docker save c24fe13d37b9 -o savedArch

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

Să despachetăm arhiva obținută în directorul curent

tar xvf savedArch

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

După cum vedem, fiecare layer se află în folderul său. Acum să vedem structura manifestului pe care l-am obținut

cat manifest.json | json_pp

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

Nu este prea mult. Să vedem ce manifest este necesar pentru încărcare, conform documentation.

Implementarea comenzilor docker pull și docker push fără clientul Docker prin intermediul cererilor HTTP

Este evident că manifestul existent nu ne este potrivit, așa că vom crea unul cu blacjack și layer-e și configurații.

În mod constant, vom avea cel puțin un fișier de configurare și un array de layer-e. Versiunea schemei 2 (valabilă la scrierea acestui articol), mediaType va rămâne neschimbat:

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

După ce am creat manifestul de bază, trebuie să-l umplem cu date valide. Pentru aceasta, vom folosi un șablon de obiect JSON pentru layer:

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

pe acesta îl vom adăuga în manifest pentru fiecare layer.

În continuare, trebuie să aflăm dimensiunea fișierului de configurare și să înlocuim placeholder-urile din manifest cu date reale.

sed -i "s/config_size/[38;2;138;143;151m$configSize/g; s/config_hash/[38;2;138;143;151m$configName/g" $manifestFile

Acum putem iniția procesul de încărcare și ne putem salva uuid-ul, care trebuie să însoțească toate solicitările ulterioare.

Scriptul complet arată cam așa:

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

putem folosi un script gata:

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

UPD:
Ce am obținut în urma acestui proces?
În primul rând, datele reale pentru analiză, deoarece testele sunt rulate în blazemeter, iar datele din solicitările clientului Docker sunt destul de neinformative în comparație cu solicitările HTTP clare.

În al doilea rând, tranziția ne-a permis să creștem numărul utilizatorilor virtuali pentru încărcarea Docker cu aproximativ 150% și să obținem un timp mediu de răspuns cu 20-25% mai rapid. La descărcarea Docker am reușit să creștem numărul utilizatorilor cu 500%, iar timpul mediu de răspuns a scăzut cu aproximativ 60%.

Vă mulțumesc pentru atenție.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster