Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

Avevamo 2 sacchi di erba, 75 compresse di mescalina in ambiente unix, un repository docker e l'obiettivo di implementare i comandi docker pull e docker push senza il client docker.

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

UPD:
Domanda: A cosa serve tutto ciò?
Risposta: Test di carico del prodotto (NON tramite bash, gli script sono forniti a scopo educativo). È stata presa la decisione di non utilizzare il client docker per ridurre le ulteriori layer (in limiti ragionevoli) e quindi emulare un carico maggiore. Di conseguenza, abbiamo rimosso tutti i ritardi sistemici del client docker. Abbiamo ottenuto un carico relativamente pulito direttamente sul prodotto.
Nell'articolo sono stati utilizzati strumenti GNU.

Iniziamo a capire cosa fanno questi comandi.

Dunque, a cosa serve docker pull? Secondo documentazione:

"Scarica un'immagine o un repository da un registry".

Là troviamo il link su capire immagini, contenitori e driver di archiviazione.

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

Da qui possiamo capire che un'immagine docker è un insieme di alcuni layer che contengono informazioni sulle ultime modifiche nell'immagine, che evidentemente ci servono. Proseguiamo con API del registry.

Qui si dice quanto segue:

"Un'immagine è una combinazione di un manifesto JSON e file di layer individuali. Il processo di scaricamento di un'immagine si concentra sul recupero di questi due componenti."

Quindi il primo passo secondo la documentazione è "Scaricare un manifesto dell'immagine”.

Non lo scaricheremo ovviamente, ma ci servono i dati. Qui viene fornito un esempio di richiesta: GET /v2/{name}/manifests/{reference}

"Il parametro name e reference identificano l'immagine e sono richiesti. Il riferimento può includere un tag o un digest."

Il nostro repository docker è distribuito localmente, proviamo a eseguire la richiesta:

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

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

In risposta otteniamo un json da cui ci interessano al momento solo i layer, precisamente i loro hash. Ottenendoli, possiamo procedere con la seguente richiesta: "GET /v2/{name}/blobs/{digest}"

“L'accesso a un layer sarà limitato dal nome del repository ma è identificato univocamente nel registry dal digest.”

Il digest in questo caso è l'hash che abbiamo ottenuto.

Proviamo

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

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

vediamo che file abbiamo ottenuto come primo layer.

file firstLayer

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

ossia, i layer sono archivi tar, scompattandoli nell'ordine corretto otteniamo il contenuto dell'immagine.

Scriviamo un piccolo script bash per automatizzare tutto ciò.

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

Ora possiamo avviarlo con i parametri desiderati e ottenere il contenuto dell'immagine necessaria

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

Parte 2 — docker push

Qui sarà un po' più complicato.

Iniziamo di nuovo con documentazione. Quindi dobbiamo caricare ogni layer, assemblare il manifesto corrispondente e caricarlo anche. Sembra semplice.

Dopo aver studiato la documentazione possiamo suddividere il processo di caricamento in diversi passaggi:

  • Inizializzazione del processo — "POST \/v2\/{repoName}\/blobs\/uploads\/"
  • Caricamento del layer (useremo il caricamento monolitico, ovvero ogni layer viene inviato per intero) — "PUT \/v2\/{repoName}\/blobs\/uploads\/{uuid}?digest=\{digest}
    Content-Length: {size of layer}
    Content-Type: application\/octet-stream
    Layer Binary Data".
  • Caricamento del manifesto — "PUT \/v2\/{repoName}\/manifests\/{reference}".

Ma nella documentazione è stato omesso un passaggio, senza il quale non si può andare avanti. Per il caricamento monolitico, così come per quello parziale (chunked), prima di caricare il layer è necessario eseguire una richiesta PATCH:

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

Altrimenti non riuscirai a procedere oltre il primo punto, poiché invece del codice di risposta 202 previsto riceverai 4xx.

Ora l'algoritmo appare come:

  • Inizializzazione
  • Patch del layer
  • Caricamento del layer
  • Caricamento del manifesto
    I punti 2 e 3 si ripeteranno rispettivamente tante volte quanti sono i layer da caricare.

Per iniziare, abbiamo bisogno di qualsiasi immagine. Utilizzerò archlinux:latest

docker pull archlinux

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

Ora salviamolo localmente per ulteriori analisi

docker save c24fe13d37b9 -o savedArch

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

Estrarremo l'archivio ottenuto nella directory corrente

tar xvf savedArch

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

Come vediamo, ogni layer si trova in una cartella separata. Ora diamo un'occhiata alla struttura del manifesto che abbiamo ottenuto

cat manifest.json | json_pp

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

Non c'è molto. Vediamo quale manifesto è necessario caricare, secondo documentazione.

Implementazione dei comandi docker pull e docker push senza client docker tramite richieste HTTP

È ovvio che il manifesto esistente non è adatto, quindi creeremo il nostro con layer e configurazioni di blackjack e prostitute.

Avremo sempre almeno un file di config e un array di layer. La versione dello schema è 2 (attuale al momento della scrittura dell'articolo), lasciamo il mediaType invariato:

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

Dopo aver creato il manifesto di base, è necessario riempirlo con dati validi. Per fare ciò, utilizziamo il modello JSON dell'oggetto layer:

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

lo aggiungeremo nel manifesto per ogni layer.

Dobbiamo innanzitutto scoprire la dimensione del file di configurazione e sostituire i segnaposto nel manifesto con dati reali.

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

Ora possiamo avviare il processo di caricamento e salvare il uuid che dovrà accompagnare tutte le richieste successive.

Lo script completo appare più o meno così:

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

Possiamo utilizzare uno script già pronto:

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

UPD:
Cosa abbiamo ottenuto in risultato?
In primo luogo, dati reali per l'analisi, poiché i test vengono eseguiti in blazemeter e i dati delle richieste del client Docker sono piuttosto poco informativi rispetto alle pure richieste HTTP.

In secondo luogo, il passaggio ci ha permesso di aumentare il numero di utenti virtuali per il caricamento di Docker di circa il 150% ottenendo nel contempo un tempo medio di risposta più veloce del 20-25%. Per il download di Docker, siamo riusciti ad aumentare il numero di utenti del 500%, mentre il tempo medio di risposta è diminuito di circa il 60%.

Grazie per l'attenzione.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster