Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

Aveam 2 saci de iarbă, 75 de tablete de mescalină mediu Unix, un depozit docker și sarcina de a implementa comenzile docker pull și docker push fără un client docker.

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

UPD:
Întrebare: Pentru ce sunt toate acestea?
Răspuns: Testarea de încărcare a produsului (NU se utilizează bash, scripturile sunt furnizate în scopuri educaționale). S-a decis să nu se folosească clientul docker pentru a reduce straturi suplimentare (în limite rezonabile) și, în consecință, pentru a emula o încărcare mai mare. Ca urmare, toate întârzierile de sistem ale clientului Docker au fost eliminate. Am primit o sarcină relativ curată direct pe produs.
Articolul a folosit versiuni GNU ale instrumentelor.

Mai întâi, să ne dăm seama ce fac aceste comenzi.

Deci, pentru ce este folosit docker pull? Conform documentație:

„Trageți o imagine sau un depozit dintr-un registru”.

Acolo găsim și un link către înțelegeți imaginile, containerele și driverele de stocare.

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

De aici putem înțelege că o imagine docker este un set de anumite straturi care conțin informații despre cele mai recente modificări ale imaginii, care este, evident, ceea ce avem nevoie. În continuare ne uităm la API-ul de registru.

Se spune următoarele:

„O „imagine” este o combinație între un manifest JSON și fișiere de straturi individuale. Procesul de extragere > a unei imagini se concentrează pe recuperarea acestor două componente.”

Deci primul pas conform documentației este „Tragerea unui manifest de imagine".

Desigur, nu o vom fotografia, dar avem nevoie de datele din el. Următorul este un exemplu de solicitare: GET /v2/{name}/manifests/{reference}

„Numele și parametrul de referință identifică imaginea și sunt obligatorii. Referința poate include o etichetă sau un rezumat.”

Depozitul nostru docker este implementat local, să încercăm să execută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ă un client docker folosind cereri HTTP

Ca răspuns, primim json de la care momentan ne interesează doar liniile de salvare, sau mai degrabă hashurile lor. După ce le-am primit, putem să le parcurgem pe fiecare și să executăm următoarea solicitare: „GET /v2/{name}/blobs/{digest}”

„Accesul la un strat va fi blocat de numele depozitului, dar este identificat în mod unic în registru prin rezumat.”

digest în acest caz este hașul pe care l-am primit.

Încercând

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ă un client docker folosind cereri HTTP

Să vedem ce fel de dosar am primit în cele din urmă ca prim colac de salvare.

file firstLayer

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

acestea. șinele sunt arhive gudron, despachetându-le în ordinea corespunzătoare vom obține conținutul imaginii.

Să scriem un mic script bash, astfel încât toate acestea să poată fi automatizate

#!/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 îl putem rula cu parametrii doriti și obținem conținutul imaginii necesare

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

Partea 2 - docker push

Acest lucru va fi puțin mai complicat.

Să începem din nou cu documentație. Așa că trebuie să descarcăm fiecare lider, să colectăm manifestul corespunzător și să îl descărcam și pe acesta. Pare simplu.

După studierea documentației, putem împărți procesul de descărcare în mai mulți pași:

  • Inițializarea procesului - „POST /v2/{repoName}/blobs/uploads/”
  • Încărcarea unei linii de salvare (vom folosi o încărcare monolitică, adică trimitem fiecare linie de salvare în întregime) - „PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Lungimea conținutului: {dimensiunea stratului}
    Tip de conținut: aplicație/flux-octet
    Strat de date binare”.
  • Se încarcă manifestul - „PUT /v2/{repoName}/manifests/{reference}”.

Dar documentația ratează un pas, fără de care nimic nu va funcționa. Pentru încărcarea monolitică, precum și pentru încărcarea parțială (în bucăți), înainte de a încărca șina, trebuie să efectuați o solicitare PATCH:

„PATCH /v2/{repoName}/blobs/uploads/{uuid}
Lungimea conținutului: {size of chunk}
Tip de conținut: aplicație/flux-octet
{Layer Chunk Binary Data}”.

Altfel, nu vei putea trece dincolo de primul punct, pentru că... În loc de codul de răspuns așteptat 202, veți primi 4xx.

Acum algoritmul arată astfel:

  • Inițializare
  • șină de petice
  • Încărcarea balustradei
  • Se încarcă manifestul
    Punctele 2 și 3, respectiv, se vor repeta de câte ori trebuie încărcat numărul de linii.

În primul rând, avem nevoie de orice imagine. Voi folosi archlinux:latest

docker pull archlinux

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

Acum să-l salvăm local pentru analize suplimentare

docker save c24fe13d37b9 -o savedArch

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

Despachetați arhiva rezultată în directorul curent

tar xvf savedArch

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

După cum puteți vedea, fiecare linie de viață este într-un folder separat. Acum să ne uităm la structura manifestului pe care l-am primit

cat manifest.json | json_pp

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

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

Implementarea comenzilor docker pull și docker push fără un client docker folosind cereri HTTP

Evident, manifestul existent nu ni se potrivește, așa că ne vom face al nostru cu blackjack și curtezane, linii de salvare și configurații.

Vom avea întotdeauna cel puțin un fișier de configurare și o serie de linii de salvare. Versiunea 2 a schemei (actuală la momentul scrierii), 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ă crearea manifestului de bază, trebuie să îl completați cu date valide. Pentru a face acest lucru, folosim șablonul json al obiectului șină:

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

O vom adăuga în manifest pentru fiecare șină.

Apoi, trebuie să aflăm dimensiunea fișierului de configurare și să înlocuim stub-urile din manifest cu date reale

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

Acum puteți iniția procesul de descărcare și puteți salva un uuid, care ar trebui 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 făcut:

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

UPD:
Ce am obținut ca rezultat?
În primul rând, date reale pentru analiză, deoarece testele sunt rulate în blazemeter, iar datele despre solicitările clientului docker nu sunt foarte informative, spre deosebire de cererile pur HTTP.

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

Vă mulțumesc pentru atenție.

Sursa: www.habr.com

Adauga un comentariu