Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Tínhamos 2 sacos de grama, 75 comprimidos de mescalina em ambiente unix, um repositório docker e a tarefa de implementar os comandos docker pull e docker push sem um cliente docker.

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

UPD:
Pergunta: Para que serve tudo isso?
Resposta: Teste de carga do produto (NÃO usando bash, os scripts são fornecidos para fins educacionais). Foi decidido não usar o cliente docker para reduzir camadas adicionais (dentro de limites razoáveis) e, consequentemente, emular uma carga maior. Como resultado, todos os atrasos do sistema do cliente Docker foram removidos. Recebemos uma carga relativamente limpa diretamente no produto.
O artigo usou versões GNU de ferramentas.

Primeiro, vamos descobrir o que esses comandos fazem.

Então, para que é usado o docker pull? De acordo com documentação:

"Extrair uma imagem ou repositório de um registro".

Lá também encontramos um link para entender imagens, contêineres e drivers de armazenamento.

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

A partir daqui podemos entender que uma imagem docker é um conjunto de certas camadas que contêm informações sobre as últimas alterações na imagem, que é obviamente o que precisamos. A seguir olhamos API de registro.

Diz o seguinte:

"Uma “imagem” é uma combinação de um manifesto JSON e arquivos de camadas individuais. O processo de extração de uma imagem gira em torno da recuperação desses dois componentes."

Portanto, o primeiro passo de acordo com a documentação é “Extraindo um manifesto de imagem".

Claro, não vamos filmar, mas precisamos dos dados dele. A seguir está um exemplo de solicitação: GET /v2/{name}/manifests/{reference}

"O nome e o parâmetro de referência identificam a imagem e são obrigatórios. A referência pode incluir uma tag ou resumo."

Nosso repositório docker é implantado localmente, vamos tentar executar a solicitação:

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

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Em resposta, recebemos json do qual atualmente estamos interessados ​​apenas nas linhas de vida, ou melhor, em seus hashes. Depois de recebê-los, podemos percorrer cada um deles e executar a seguinte solicitação: "GET /v2/{name}/blobs/{digest}"

“O acesso a uma camada será controlado pelo nome do repositório, mas será identificado exclusivamente no registro por resumo.”

digest, neste caso, é o hash que recebemos.

Tentando

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

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Vamos ver que tipo de arquivo finalmente recebemos como primeira tábua de salvação.

file firstLayer

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

aqueles. rails são arquivos tar, descompactando-os na ordem apropriada obteremos o conteúdo da imagem.

Vamos escrever um pequeno script bash para que tudo isso possa ser automatizado

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

Agora podemos executá-lo com os parâmetros desejados e obter o conteúdo da imagem necessária

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

Parte 2 - docker push

Isso será um pouco mais complicado.

Vamos começar de novo com documentação. Então precisamos baixar cada líder, coletar o manifesto correspondente e baixá-lo também. Parece simples.

Depois de estudar a documentação, podemos dividir o processo de download em várias etapas:

  • Inicialização do processo - "POST /v2/{repoName}/blobs/uploads/"
  • Fazendo upload de uma linha de vida (usaremos um upload monolítico, ou seja, enviaremos cada linha de vida na íntegra) - "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Comprimento do conteúdo: {tamanho da camada}
    Tipo de conteúdo: aplicativo / fluxo de octeto
    Camada de dados binários".
  • Carregando o manifesto - "PUT /v2/{repoName}/manifests/{reference}".

Mas a documentação perde uma etapa, sem a qual nada funcionará. Para carregamento monolítico, bem como para parcial (em pedaços), antes de carregar o trilho, deve-se realizar uma solicitação PATCH:

"PATCH /v2/{repoName}/blobs/uploads/{uuid}
Comprimento do conteúdo: {tamanho do pedaço}
Tipo de conteúdo: aplicativo / fluxo de octeto
{Dados binários do pedaço de camada}".

Caso contrário, você não conseguirá ir além do primeiro ponto, porque... Em vez do código de resposta esperado 202, você receberá 4xx.

Agora o algoritmo se parece com:

  • Inicialização
  • Trilho de remendo
  • Carregando o corrimão
  • Carregando o manifesto
    Os pontos 2 e 3, respectivamente, serão repetidos quantas vezes for necessário carregar o número de linhas.

Primeiro, precisamos de qualquer imagem. Vou usar archlinux:mais recente

docker pull archlinux

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Agora vamos salvá-lo localmente para análise posterior

docker save c24fe13d37b9 -o savedArch

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Descompacte o arquivo resultante no diretório atual

tar xvf savedArch

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Como você pode ver, cada linha de vida está em uma pasta separada. Agora vamos dar uma olhada na estrutura do manifesto que recebemos

cat manifest.json | json_pp

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Não muito. Vamos ver qual manifesto é necessário carregar, de acordo com documentação.

Implementando comandos docker pull e docker push sem um cliente docker usando solicitações HTTP

Obviamente, o manifesto existente não nos convém, por isso faremos o nosso próprio com blackjack e cortesãs, linhas de vida e configurações.

Sempre teremos pelo menos um arquivo de configuração e uma série de linhas de vida. Versão 2 do esquema (atual no momento da escrita), mediaType permanecerá inalterado:

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

Após criar o manifesto básico, você precisa preenchê-lo com dados válidos. Para fazer isso, usamos o template json do objeto rail:

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

Iremos adicioná-lo ao manifesto de cada trilho.

Em seguida, precisamos descobrir o tamanho do arquivo de configuração e substituir os stubs no manifesto por dados reais

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

Agora você pode iniciar o processo de download e salvar um uuid, que deverá acompanhar todas as solicitações subsequentes.

O script completo é mais ou menos assim:

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

podemos usar um script pronto:

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

UPD:
O que obtivemos como resultado?
Em primeiro lugar, dados reais para análise, já que os testes são executados no blazemeter e os dados das solicitações do cliente docker não são muito informativos, ao contrário das solicitações HTTP puras.

Em segundo lugar, a transição nos permitiu aumentar o número de usuários virtuais para upload do docker em cerca de 150% e obter um tempo médio de resposta 20-25% mais rápido. Para download do docker, conseguimos aumentar o número de usuários em 500%, enquanto o tempo médio de resposta diminuiu cerca de 60%.

Obrigado por sua atenção.

Fonte: habr.com

Adicionar um comentário