Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Wir hatten 2 Säcke Gras, 75 Meskalin-Tabletten in einer Unix-Umgebung, ein Docker-Repository und die Aufgabe, die Befehle Docker Pull und Docker Push ohne Docker-Client zu implementieren.

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

UPD:
Frage: Wozu dient das alles?
Antwort: Lasttest des Produkts (NICHT mit Bash, die Skripte werden zu Bildungszwecken bereitgestellt). Es wurde beschlossen, den Docker-Client nicht zu verwenden, um zusätzliche Schichten (innerhalb angemessener Grenzen) zu reduzieren und dementsprechend eine höhere Last zu emulieren. Dadurch wurden alle Systemverzögerungen des Docker-Clients entfernt. Wir haben eine relativ saubere Ladung direkt auf das Produkt erhalten.
Der Artikel verwendete GNU-Versionen von Tools.

Lassen Sie uns zunächst herausfinden, was diese Befehle bewirken.

Wofür wird Docker Pull verwendet? Entsprechend Dokumentation:

„Ein Image oder ein Repository aus einer Registrierung abrufen“.

Dort finden wir auch einen Link zu Verstehen Sie Bilder, Container und Speichertreiber.

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Von hier aus können wir verstehen, dass ein Docker-Image eine Reihe bestimmter Ebenen ist, die Informationen über die letzten Änderungen im Image enthalten, was offensichtlich das ist, was wir brauchen. Als nächstes schauen wir uns an Registrierungs-API.

Darin heißt es:

„Ein „Bild“ ist eine Kombination aus einem JSON-Manifest und einzelnen Ebenendateien. Der Prozess des Ziehens eines > Bildes konzentriert sich auf das Abrufen dieser beiden Komponenten.“

Der erste Schritt laut Dokumentation ist also „Ein Bildmanifest abrufen".

Natürlich werden wir es nicht drehen, aber wir brauchen die Daten davon. Das Folgende ist eine Beispielanfrage: GET /v2/{name}/manifests/{reference}

„Der Name und der Referenzparameter identifizieren das Bild und sind erforderlich. Die Referenz kann ein Tag oder einen Digest enthalten.“

Unser Docker-Repository wird lokal bereitgestellt. Versuchen wir, die Anfrage auszuführen:

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

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Als Antwort erhalten wir JSON, von dem uns derzeit nur die Lebenslinien bzw. deren Hashes interessieren. Nachdem wir sie erhalten haben, können wir sie durchgehen und die folgende Anfrage ausführen: „GET /v2/{name}/blobs/{digest}“

„Der Zugriff auf eine Ebene wird durch den Namen des Repositorys gesteuert, wird aber in der Registrierung durch Digest eindeutig identifiziert.“

Digest ist in diesem Fall der Hash, den wir erhalten haben.

Lass es uns versuchen

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

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Mal sehen, was für eine Datei wir schließlich als erste Lebensader erhalten haben.

file firstLayer

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

diese. Schienen sind Tar-Archive. Wenn wir sie in der richtigen Reihenfolge entpacken, erhalten wir den Inhalt des Bildes.

Schreiben wir ein kleines Bash-Skript, damit das alles automatisiert werden kann

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

Jetzt können wir es mit den gewünschten Parametern ausführen und den Inhalt des erforderlichen Bildes erhalten

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

Teil 2 – Docker-Push

Das wird etwas komplizierter.

Beginnen wir noch einmal mit Dokumentation. Wir müssen also jeden Anführer herunterladen, das entsprechende Manifest sammeln und es ebenfalls herunterladen. Es scheint einfach.

Nach dem Studium der Dokumentation können wir den Download-Prozess in mehrere Schritte unterteilen:

  • Prozessinitialisierung – „POST /v2/{repoName}/blobs/uploads/“
  • Hochladen einer Lebenslinie (wir verwenden einen monolithischen Upload, d. h. wir senden jede Lebenslinie in ihrer Gesamtheit) – „PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}“
    Inhaltslänge: {Größe der Ebene}
    Inhaltstyp: Anwendung/Oktett-Stream
    Layer-Binärdaten".
  • Laden des Manifests – „PUT /v2/{repoName}/manifests/{reference}“.

Doch in der Dokumentation fehlt ein Schritt, ohne den nichts geht. Für monolithisches Laden sowie für teilweises (chunkiertes) Laden müssen Sie vor dem Laden der Schiene eine PATCH-Anfrage durchführen:

„PATCH /v2/{repoName}/blobs/uploads/{uuid}
Inhaltslänge: {Größe des Blocks}
Inhaltstyp: Anwendung/Oktett-Stream
{Layer Chunk Binary Data}".

Andernfalls kommen Sie nicht über den ersten Punkt hinaus, denn... Anstelle des erwarteten Antwortcodes 202 erhalten Sie 4xx.

Nun sieht der Algorithmus so aus:

  • Initialisierung
  • Patchschiene
  • Laden des Handlaufs
  • Laden des Manifests
    Die Punkte 2 und 3 werden jeweils so oft wiederholt, wie Zeilen geladen werden müssen.

Zuerst brauchen wir ein beliebiges Bild. Ich werde archlinux:latest verwenden

docker pull archlinux

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Speichern wir es nun lokal zur weiteren Analyse

docker save c24fe13d37b9 -o savedArch

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Entpacken Sie das resultierende Archiv in das aktuelle Verzeichnis

tar xvf savedArch

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Wie Sie sehen, befindet sich jede Lebenslinie in einem separaten Ordner. Schauen wir uns nun die Struktur des Manifests an, das wir erhalten haben

cat manifest.json | json_pp

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Nicht viel. Mal sehen, welches Manifest zum Laden benötigt wird Dokumentation.

Implementieren von Docker-Pull- und Docker-Push-Befehlen ohne Docker-Client mithilfe von HTTP-Anfragen

Offensichtlich passt das bestehende Manifest nicht zu uns, also machen wir unser eigenes mit Blackjack und Kurtisanen, Lebenslinien und Konfigurationen.

Wir werden immer mindestens eine Konfigurationsdatei und eine Reihe von Lebenslinien haben. Schemaversion 2 (aktuell zum Zeitpunkt des Schreibens), mediaType bleibt unverändert:

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

Nachdem Sie das Basismanifest erstellt haben, müssen Sie es mit gültigen Daten füllen. Dazu verwenden wir das JSON-Template des Rail-Objekts:

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

Wir werden es dem Manifest für jede Schiene hinzufügen.

Als nächstes müssen wir die Größe der Konfigurationsdatei ermitteln und die Stubs im Manifest durch echte Daten ersetzen

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

Jetzt können Sie den Downloadvorgang starten und sich eine UUID speichern, die allen nachfolgenden Anfragen beiliegen sollte.

Das komplette Skript sieht in etwa so aus:

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

Wir können ein vorgefertigtes Skript verwenden:

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

UPD:
Was haben wir als Ergebnis erhalten?
Erstens echte Daten zur Analyse, da die Tests in Blazemeter ausgeführt werden und die Daten zu Docker-Client-Anfragen im Gegensatz zu reinen HTTP-Anfragen nicht sehr aussagekräftig sind.

Zweitens konnten wir durch die Umstellung die Anzahl der virtuellen Benutzer für den Docker-Upload um etwa 150 % erhöhen und die durchschnittliche Antwortzeit um 20–25 % verkürzen. Beim Docker-Download ist es uns gelungen, die Anzahl der Benutzer um 500 % zu steigern, während die durchschnittliche Antwortzeit um etwa 60 % sank.

Vielen Dank für Ihre Aufmerksamkeit.

Source: habr.com

Kommentar hinzufügen