Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Wir hatten 2 Säcke Gras, 75 Tabletten Mescalin, eine Unix-Umgebung, ein Docker-Repository und die Aufgabe, die Befehle docker pull und docker push ohne den Docker-Client umzusetzen.

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

UPD:
Frage: Wozu das alles?
Antwort: Lasttests des Produkts (NICHT mit Bash, die Skripte dienen Bildungszwecken). Die Entscheidung, den Docker-Client nicht zu verwenden, wurde getroffen, um zusätzliche Schichten (im vertretbaren Rahmen) zu minimieren und entsprechend eine höhere Last zu simulieren. Dadurch wurden alle systembedingten Verzögerungen des Docker-Clients eliminiert. Wir erhielten eine vergleichsweise saubere Last direkt auf das Produkt.
In diesem Artikel wurden Tools von GNU verwendet.

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

Wofür wird also docker pull verwendet? Laut Dokumentation.:

"Ein Bild oder ein Repository aus einem Registry ziehen".

Dort finden wir auch den Link zu Verstehen von Images, Containern und Speichertreibern.

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Von hier aus können wir verstehen, dass ein Docker-Image eine Ansammlung von Layers ist, die Informationen über die letzten Änderungen im Image enthalten, die uns offensichtlich wichtig sind. Weiter schauen wir in die Registry API.

Hier wird Folgendes gesagt:

"Ein 'Image' ist eine Kombination aus einem JSON-Manifest und einzelnen Schichtdateien. Der Prozess des Abrufens eines > Images konzentriert sich auf das Abrufen dieser beiden Komponenten."

Der erste Schritt gemäß der Dokumentation ist "Abrufen eines Image-Manifestes”.

Das werden wir natürlich nicht abziehen, aber die Daten daraus benötigen wir. Im Folgenden wird ein Beispiel für die Anfrage gegeben: GET /v2/{name}/manifests/{reference}

"Der Name und der Referenzparameter identifizieren das Image und sind erforderlich. Die Referenz kann ein Tag oder einen Digest enthalten."

Unser Docker-Repository ist lokal eingerichtet, 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"

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Als Antwort erhalten wir ein JSON, aus dem uns momentan nur die Schichten, genauer gesagt deren Hashes, interessieren. Nachdem wir diese erhalten haben, können wir für jede die folgende Anfrage durchführen: "GET /v2/{name}/blobs/{digest}"

„Der Zugriff auf eine Schicht wird durch den Namen des Repositories gesteuert, aber im Registry wird sie eindeutig durch den Digest identifiziert."

Der 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

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Schauen wir uns an, welche Datei wir letztendlich als erste Schicht erhalten haben.

file firstLayer

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Das heißt, die Schichten stellen TAR-Archive dar, die wir in der entsprechenden Reihenfolge entpacken, um den Inhalt des Images zu erhalten.

Lassen Sie uns ein kleines Bash-Skript schreiben, um alles zu automatisieren.

#!/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 benötigten Images erhalten.

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

Teil 2 — docker push

Das wird etwas komplizierter.

Beginnen wir erneut mit Dokumentation.. Wir müssen jeden Layer hochladen, das entsprechende Manifest erstellen und dieses ebenfalls hochladen. Klingt einfach.

Nachdem wir die Dokumentation durchgesehen haben, können wir den Upload-Prozess in mehrere Schritte unterteilen:

  • Initialisierung des Prozesses — "POST /v2/{repoName}/blobs/uploads/"
  • Hochladen des Layers (wir verwenden den monolithischen Upload, d.h. wir senden jeden Layer komplett) — "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}"
    Content-Length: {size of layer}
    Content-Type: application/octet-stream
    Layer Binärdaten.
  • Hochladen des Manifests — "PUT /v2/{repoName}/manifests/{reference}."

Aber in der Dokumentation fehlt ein Schritt, ohne den nichts funktionieren wird. Vor dem Hochladen des Layers muss auch bei einem monolithischen Upload wie bei einem teilweisen Upload ein PATCH-Request ausgeführt werden:

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

Andernfalls können Sie nicht über den ersten Punkt hinauskommen, da Sie statt des erwarteten Antwortcodes 202 einen 4xx-Code erhalten.

Jetzt sieht der Algorithmus so aus:

  • Initialisierung
  • Layer-Patch
  • Layer-Upload
  • Manifest-Upload
    Die Punkte 2 und 3 werden entsprechend so oft wiederholt, wie es nötig ist, um alle Layer zu laden.

Zunächst benötigen wir ein beliebiges Image. Ich werde archlinux:latest verwenden.

docker pull archlinux

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Jetzt speichern wir es lokal für die weitere Analyse.

docker save c24fe13d37b9 -o savedArch

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Entpacken wir das erhaltene Archiv im aktuellen Verzeichnis.

tar xvf savedArch

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Wie wir sehen, befindet sich jeder Layer in einem eigenen Ordner. Lassen Sie uns die Struktur des erhaltenen Manifests ansehen.

cat manifest.json | json_pp

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Nicht viel. Schauen wir, welches Manifest für den Upload benötigt wird, gemäß Dokumentation..

Ausführung der Docker-Befehle docker pull und docker push ohne Docker-Client über HTTP-Anfragen

Offensichtlich ist das vorhandene Manifest für uns nicht geeignet, also erstellen wir unser eigenes mit Layern und Konfigurationen, mit Blackjack und Nutten.

Wir werden immer mindestens eine Konfigurationsdatei und ein Array von Layers haben. Die Schemaprimärversion ist 2 (zum Zeitpunkt des Schreibens des Artikels aktuell), 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

Nach der Erstellung des Basis-Manifests müssen wir es mit gültigen Daten füllen. Hierzu verwenden wir die JSON-Vorlage des Layers:

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

dies werden wir für jedes Layer in das Manifest einfügen.

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

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

Jetzt können wir den Upload-Prozess initiieren und uns die UUID merken, die alle nachfolgenden Anfragen begleiten sollte.

Das vollständige Skript sieht ungefähr 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 fertiges 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, denn die Tests werden in BlazeMeter durchgeführt und die Daten von Docker-Client-Anfragen sind im Vergleich zu reinen HTTP-Anfragen wenig aussagekräftig.

Zweitens hat der Übergang es uns ermöglicht, die Anzahl der virtuellen Benutzer für den Docker-Upload um etwa 150 % zu erhöhen und gleichzeitig die durchschnittliche Antwortzeit um 20-25 % zu verbessern. Für den Docker-Download konnten wir die Anzahl der Benutzer um 500 % steigern, während sich die durchschnittliche Antwortzeit um etwa 60 % verringerte.

Vielen Dank für Ihre Aufmerksamkeit.

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster