Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

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

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

UPD:
Domanda: a cosa serve tutto questo?
Risposta: test di caricamento del prodotto (NON utilizzando bash, gli script sono forniti per scopi didattici). Si è deciso di non utilizzare il client docker per ridurre i livelli aggiuntivi (entro limiti ragionevoli) e, di conseguenza, emulare un carico maggiore. Di conseguenza, tutti i ritardi di sistema del client Docker sono stati rimossi. Abbiamo ricevuto un carico relativamente pulito direttamente sul prodotto.
L'articolo utilizzava versioni GNU degli strumenti.

Per prima cosa, scopriamo cosa fanno questi comandi.

Allora a cosa serve il docker pull? Secondo documentazione:

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

Lì troviamo anche un collegamento a comprendere immagini, contenitori e driver di archiviazione.

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

Da qui possiamo capire che un'immagine docker è un insieme di determinati livelli che contengono informazioni sulle ultime modifiche apportate all'immagine, che è ovviamente ciò di cui abbiamo bisogno. Successivamente guardiamo API del registro.

Dice quanto segue:

"Un'"immagine" è una combinazione di un manifest JSON e di singoli file di livello. Il processo di estrazione di un'immagine è incentrato sul recupero di questi due componenti."

Quindi il primo passo secondo la documentazione è “Estrazione di un manifesto di immagine".

Ovviamente non lo gireremo, ma ci servono i dati. Di seguito è riportato un esempio di richiesta: GET /v2/{name}/manifests/{reference}

"Il nome e il parametro di riferimento identificano l'immagine e sono obbligatori. 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 un client docker utilizzando richieste HTTP

In risposta riceviamo json da cui al momento ci interessano solo le lifeline, o meglio i loro hash. Dopo averli ricevuti, possiamo esaminarli ciascuno ed eseguire la seguente richiesta: "GET /v2/{name}/blobs/{digest}"

"L'accesso a un livello sarà controllato dal nome del repository ma sarà identificato in modo univoco nel registro dal digest."

digest in questo caso è l'hash che abbiamo ricevuto.

Provare

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 un client docker utilizzando richieste HTTP

Vediamo che tipo di file abbiamo finalmente ricevuto come prima ancora di salvezza.

file firstLayer

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

quelli. i rail sono archivi tar, decomprimendoli nell'ordine appropriato otterremo il contenuto dell'immagine.

Scriviamo un piccolo script bash in modo che tutto ciò possa essere automatizzato

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

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

Parte 2: push della finestra mobile

Questo sarà un po' più complicato.

Ricominciamo con documentazione. Quindi dobbiamo scaricare ciascun leader, raccogliere il manifest corrispondente e scaricarlo anche lui. Sembra semplice.

Dopo aver studiato la documentazione, possiamo dividere il processo di download in più passaggi:

  • Inizializzazione del processo: "POST /v2/{repoName}/blobs/uploads/"
  • Caricamento di una linea di vita (utilizzeremo un caricamento monolitico, ovvero invieremo ciascuna linea di vita nella sua interezza) - "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Lunghezza contenuto: {dimensione del livello}
    Tipo di contenuto: application / octet-stream
    Dati binari a strati".
  • Caricamento del manifest - "PUT /v2/{repoName}/manifests/{reference}".

Ma alla documentazione manca un passaggio, senza il quale nulla funzionerebbe. Per il caricamento monolitico, così come per quello parziale (chunked), prima di caricare la rotaia, è necessario eseguire una richiesta PATCH:

"PATCH /v2/{repoName}/blobs/uploads/{uuid}
Lunghezza contenuto: {dimensione del pezzo}
Tipo di contenuto: application / octet-stream
{Dati binari del blocco layer}".

Altrimenti non potrai andare oltre il primo punto, perché... Invece del codice di risposta previsto 202, riceverai 4xx.

Ora l'algoritmo è simile a:

  • inizializzazione
  • Binario patch
  • Caricamento del corrimano
  • Caricamento del manifesto
    I punti 2 e 3, rispettivamente, verranno ripetuti tante volte quante sono le righe da caricare.

Innanzitutto, abbiamo bisogno di un'immagine. Utilizzerò archlinux:latest

docker pull archlinux

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

Ora salviamolo localmente per ulteriori analisi

docker save c24fe13d37b9 -o savedArch

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

Decomprimi l'archivio risultante nella directory corrente

tar xvf savedArch

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

Come puoi vedere, ogni ancora di salvezza si trova in una cartella separata. Ora diamo un'occhiata alla struttura del manifest che abbiamo ricevuto

cat manifest.json | json_pp

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

Non tanto. Vediamo quale manifest è necessario caricare, secondo documentazione.

Implementazione dei comandi docker pull e docker push senza un client docker utilizzando richieste HTTP

Ovviamente, il manifesto esistente non ci soddisfa, quindi ne creeremo uno nostro con blackjack e cortigiane, ancora di salvezza e configurazioni.

Avremo sempre almeno un file di configurazione e una serie di linee di vita. Schema versione 2 (attuale al momento in cui scrivo), 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 manifest di base, è necessario riempirlo con dati validi. Per fare ciò, utilizziamo il template json dell'oggetto rail:

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

Lo aggiungeremo al manifest per ogni binario.

Successivamente, dobbiamo scoprire la dimensione del file di configurazione e sostituire gli stub nel manifest con dati reali

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

Ora puoi avviare il processo di download e salvare un uuid, che dovrebbe accompagnare tutte le richieste successive.

Lo script completo è simile al seguente:

#!/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 usare uno script già 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 sulle richieste del client docker non sono molto informativi, a differenza delle richieste HTTP pure.

In secondo luogo, la transizione ci ha consentito di aumentare il numero di utenti virtuali per il caricamento su docker di circa il 150% e di ottenere tempi di risposta medi più rapidi del 20-25%. Per il download della finestra mobile, siamo riusciti ad aumentare il numero di utenti del 500%, mentre il tempo di risposta medio è diminuito di circa il 60%.

Grazie per la vostra attenzione.

Fonte: habr.com

Aggiungi un commento