Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

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

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

UPD:
Domanda: A cosa serve tutto questo?
Risposta: Test di carico del prodotto (NON con strumenti bash, gli script sono forniti a scopo educativo). È stata presa la decisione di non utilizzare il client docker per ridurre ulteriori strati (nei limiti ragionevoli) e quindi simulare un carico più elevato. Di conseguenza, abbiamo eliminato tutti i ritardi sistemici del client docker, ottenendo un carico relativamente pulito direttamente sul prodotto.
Nell'articolo sono stati usati strumenti GNU versioni.

Per iniziare, chiariremo cosa fanno questi comandi.

Allora, a cosa serve docker pull? Secondo documentazione:

"Tira un'immagine o un repository da un registro".

Lì troviamo anche il link a comprendere le immagini, i contenitori e i driver di archiviazione.

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Da qui possiamo capire che l'immagine docker è un insieme di strati che contengono informazioni sulle ultime modifiche all'immagine, che ovviamente ci servono. Proseguiamo quindi con API del registry.

Qui si dice quanto segue:

"Un'«immagine» è una combinazione di un manifesto JSON e singoli file di layer. Il processo per recuperare un'immagine si concentra sull'ottenere questi due componenti."

Quindi il primo passo secondo la documentazione è “Recuperare un manifesto dell'immagine”.

Non lo recupereremo, ma abbiamo bisogno dei dati contenuti. Di seguito è riportato un esempio di richiesta: GET /v2/{name}/manifests/{reference}

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

Il nostro repository Docker è stato installato 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"

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

In risposta otteniamo un JSON da cui ci interessano al momento solo i layer, più precisamente i loro hash. Ottenendoli, possiamo eseguire la seguente richiesta per ciascuno: "GET /v2/{name}/blobs/{digest}"

“L'accesso a un layer sarà controllato dal nome del repository ma è identificato in modo univoco nel registro 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

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Vediamo che tipo di file abbiamo ottenuto come primo layer.

file firstLayer

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Quindi, i layer sono archivi tar; estraendoli nell'ordine corretto otterremo il contenuto dell'immagine.

Scriviamo un piccolo script bash per automatizzare tutto questo.

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

Esaminando la documentazione possiamo suddividere il processo di caricamento in più passaggi:

  • Inizializzazione del processo — "POST /v2/{repoName}/blobs/uploads/"
  • Caricamento del layer (utilizzeremo il caricamento monolitico, ovvero inviamo ogni layer per intero) — "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}"
    Content-Length: {dimensione del layer}
    Content-Type: application/octet-stream
    Dati binari del layer".
  • Caricamento del manifesto — "PUT /v2/{repoName}/manifests/{reference}".

Ma la documentazione omette un passaggio, senza il quale nulla funzionerà. 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: {dimensione del chunk}
Content-Type: application/octet-stream
{Dati binari del chunk del layer}".

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

Ora l'algoritmo appare come segue:

  • Inizializzazione
  • Layer patch
  • Caricamento del layer
  • Caricamento del manifesto
    I punti 2 e 3 verranno ripetuti tante volte quante sono le layer da caricare.

Per iniziare avremo bisogno di un'immagine qualsiasi. Userò archlinux:latest.

docker pull archlinux

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Ora salviamo questa immagine localmente per ulteriori analisi.

docker save c24fe13d37b9 -o savedArch

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Estraiamo l'archivio ottenuto nella directory corrente.

tar xvf savedArch

Esecuzione dei comandi docker pull e docker push senza il 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

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Non è molto. Controlliamo quale manifesto è necessario per il caricamento, secondo. documentazione.

Esecuzione dei comandi docker pull e docker push senza il client docker tramite richieste HTTP

Ovviamente, il manifesto esistente non è adatto, quindi ne creeremo uno nostro con layer, configurazioni e… blackjack.

Avremo sempre almeno un file di configurazione e un array di layer. La versione dello schema è 2 (valida al momento della scrittura di questo articolo), mentre il mediaType rimarrà 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 compilarlo con dati validi. Per fare ciò, utilizziamo il modello dell'oggetto JSON del layer:

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

questo sarà aggiunto al manifesto per ogni layer.

In seguito, dobbiamo conoscere la dimensione del file di configurazione e sostituire i segnaposto nel manifesto con i dati reali

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

Ora possiamo avviare il processo di caricamento e salvare l'UUID, che deve 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 pronto:

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

UPD:
Cosa abbiamo ottenuto come 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 a richieste HTTP pulite.

In secondo luogo, la transizione ci ha permesso di aumentare il numero di utenti virtuali per il caricamento di Docker di circa il 150% e, nel contempo, di ottenere un tempo di risposta medio più veloce del 20-25%. Per il download di Docker, siamo riusciti ad aumentare il numero di utenti del 500%, riducendo il tempo di risposta medio 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