使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

我們有 2 袋草、75 片麥斯卡林片劑、unix 環境、一個 docker 儲存庫,以及在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 指令的任務。

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

UPD:
Q:這一切是為了什麼?
答案:產品的負載測試(不使用 bash,提供的腳本用於教育目的)。 決定不使用 docker 用戶端來減少額外的層(在合理的限制內),並相應地模擬更高的負載。 這樣一來,Docker 客戶端的所有系統延遲都被消除了。 我們直接在產品上收到了相對乾淨的負載。
本文使用了 GNU 版本的工具。

首先,讓我們弄清楚這些命令的作用。

那麼 docker pull 是用來做什麼的呢? 根據 文件:

「從登錄中提取映像或儲存庫」。

在那裡我們還找到了一個鏈接 了解鏡像、容器和儲存驅動程式.

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

從這裡我們可以理解,docker鏡像是一組某些層,其中包含鏡像最新變化的訊息,這顯然是我們所需要的。 接下來我們來看看 註冊表API.

它說如下:

“‘鏡像’是 JSON 清單和各個層文件的組合。拉取 > 鏡像的過程主要圍繞檢索這兩個組件。”

因此,根據文檔,第一步是“拉取鏡像清單“。

當然,我們不會拍攝它,但我們需要其中的數據。 以下是請求範例: GET /v2/{name}/manifests/{reference}

“名稱和參考參數可識別圖像,並且是必需的。參考可以包括標籤或摘要。”

我們的docker儲存庫部署在本地,讓我們嘗試執行請求:

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

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

作為回應,我們收到 json,我們目前只對生命線感興趣,或者更確切地說對它們的雜湊值感興趣。 收到它們後,我們可以檢查每一個並執行以下請求:“GET /v2/{name}/blobs/{digest}”

“對層的存取將通過存儲庫的名稱進行控制,但在註冊表中通過摘要進行唯一標識。”

在這種情況下,摘要是我們收到的雜湊值。

咱們試試吧

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

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

讓我們看看我們最終收到什麼樣的文件作為第一條生命線。

file firstLayer

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

那些。 Rails 是 tar 檔案,按照適當的順序解壓縮它們,我們將獲得圖像的內容。

讓我們編寫一個小型的 bash 腳本,以便所有這些都可以自動化

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

現在我們可以使用所需的參數來運行它並獲取所需圖像的內容

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

第 2 部分 - docker 推送

這會稍微複雜一些。

讓我們重新開始 文件。 所以我們需要下載每個leader,收集對應的manifest並下載。 看起來很簡單。

研究完文件後,我們可以將下載過程分為幾個步驟:

  • 進程初始化 - “POST /v2/{repoName}/blob/uploads/”
  • 上傳生命線(我們將使用整體上傳,即我們完整地發送每個生命線)-“PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    內容長度:{層的大小}
    內容類型:應用程式/八位元組流
    層二進位資料」。
  • 載入清單 - “PUT /v2/{repoName}/manifests/{reference}”。

但該文檔遺漏了一步,沒有這一步驟,一切都將不起作用。 對於整體加載以及部分(分塊)加載,在加載導軌之前,您必須執行 PATCH 請求:

「補丁/v2/{repoName}/blob/uploads/{uuid}
內容長度:{區塊的大小}
內容類型:應用程式/八位元組流
{層塊二進位資料}」。

否則,你將無法超越第一點,因為… 您將收到 202xx,而不是預期的回應代碼 4。

現在演算法看起來像:

  • 初始化
  • 補軌
  • 裝載扶手
  • 載入清單
    第 2 點和第 3 點將分別根據需要載入的行數重複多次。

首先,我們需要任何圖像。 我將使用 archlinux:latest

docker pull archlinux

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

現在我們將其保存在本地以供進一步分析

docker save c24fe13d37b9 -o savedArch

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

將產生的存檔解壓縮到目前目錄

tar xvf savedArch

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

如您所見,每條生命線都位於單獨的資料夾中。 現在讓我們來看看我們收到的清單的結構

cat manifest.json | json_pp

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

不多。 讓我們看看需要加載什麼清單,根據 文件.

使用 HTTP 請求在沒有 docker 用戶端的情況下實作 docker pull 和 docker push 命令

顯然,現有的宣言不適合我們,所以我們將用二十一點和妓女、生命線和配置來製作我們自己的宣言。

我們將始終擁有至少一個設定檔和一組生命線。 方案版本 2(撰寫本文時為目前版本),mediaType 將保持不變:

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

建立基本清單後,您需要填入有效資料。 為此,我們使用 Rail 物件的 json 模板:

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

我們會將其添加到每條鐵路的清單中。

接下來,我們需要找出設定檔的大小,並將清單中的存根替換為真實數據

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

現在您可以啟動下載過程並為自己保存一個 uuid,該 uuid 應該伴隨所有後續請求。

完整的腳本看起來像這樣:

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

我們可以使用現成的腳本:

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

UPD:
結果我們得到了什麼?
首先,用於分析的真實數據,因為測試是在 blazemeter 中運行的,並且與純 HTTP 請求不同,docker 客戶端請求的數據資訊量不大。

其次,這項轉變使我們能夠將 docker upload 的虛擬用戶數量增加約 150%,並將平均回應時間加快 20-25%。 對於 docker download,我們成功地將使用者數量增加了 500%,而平均回應時間減少了約 60%。

感謝您的關注。

來源: www.habr.com

添加評論