Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

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

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

UPD:
Pergunta: Para que tudo isso?
Resposta: Teste de carga do produto (NÃO usando bash, os scripts são fornecidos para fins educacionais). Decidiu-se não usar o cliente docker para reduzir camadas adicionais (dentro de limites razoáveis) e, consequentemente, emular uma carga mais alta. Assim, eliminamos todas as latências do cliente docker. Obtivemos uma carga relativamente limpa diretamente no produto.
Na artigos foram utilizados as ferramentas GNU versions.

Para começar, vamos entender o que esses comandos fazem.

Então, para que serve o docker pull? Segundo documentação:

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

Lá encontramos um link para compreender imagens, contêineres e drivers de armazenamento.

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

Daqui, podemos entender que a imagem docker é um conjunto de certas camadas, que contêm informações sobre as últimas modificações na imagem, que são obviamente o que precisamos. Depois, olhamos no API de registro.

Aqui diz o seguinte:

"Uma 'imagem' é uma combinação de um manifesto JSON e arquivos de camada individuais. O processo de puxar uma > imagem gira em torno da recuperação desses dois componentes."

Então, o primeiro passo segundo a documentação é "Puxando um Manifesto de Imagem”.

Nós não vamos realmente puxá-lo, mas precisamos dos dados dele. Abaixo está um exemplo de requisição: GET /v2/{name}/manifests/{reference}

"Os parâmetros nome e referência identificam a imagem e são obrigatórios. A referência pode incluir uma tag ou digest."

Nosso repositório docker está instalado localmente, vamos tentar fazer a requisição:

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

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

Na resposta, recebemos um json do qual, por enquanto, estamos interessados apenas nas camadas, mais especificamente seus hashes. Obtendo-os, podemos passar por cada um e fazer a próxima requisição: "GET /v2/{name}/blobs/{digest}"

"O acesso a uma camada será controlado pelo nome do repositório, mas é identificado de forma única no registro pelo digest."

digest, neste caso, é o hash que obtivemos.

Vamos tentar

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

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

vamos ver que arquivo obtivemos como a primeira camada.

file firstLayer

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

ou seja, as camadas são arquivos tar, extraindo-os na ordem correspondente, obteremos o conteúdo da imagem.

Escreveremos um pequeno script de 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

Aqui será um pouco mais complicado.

Vamos começar novamente com documentação. Portanto, precisamos carregar cada camada, reunir o manifesto correspondente e carregá-lo também. Parece simples.

Estudando a documentação, podemos dividir o processo de upload em várias etapas:

  • Inicialização do processo — "POST \/v2\/{repoName}\/blobs\/uploads\/"
  • Carregamento da camada (vamos usar upload monolítico, ou seja, cada camada será enviada inteira) — "PUT \/v2\/{repoName}\/blobs\/uploads\/{uuid}?digest={digest}
    Content-Length: {tamanho da camada}
    Content-Type: application\/octet-stream
    Dados Binários da Camada".
  • Carregamento do manifesto — "PUT \/v2\/{repoName}\/manifests\/{reference}".

Mas a documentação omitiu uma etapa sem a qual nada funcionará. Para o upload monolítico, assim como para o parcial (chunked), antes de carregar a camada, é necessário realizar uma requisição PATCH:

"PATCH \/v2\/{repoName}\/blobs\/uploads\/{uuid}
Content-Length: {tamanho do chunk}
Content-Type: application\/octet-stream
{Dados Binários do Chunk da Camada}".

Caso contrário, você não conseguirá avançar além do primeiro ponto, pois em vez do código de resposta 202 esperado, você receberá 4xx.

Agora, o algoritmo se parece com:

  • Inicialização
  • Patch da camada
  • Upload da camada
  • Upload do manifesto
    Os pontos 2 e 3, respectivamente, serão repetidos tantas vezes quantas camadas forem necessárias para carregar.

Para começar, precisaremos de qualquer imagem. Eu vou usar archlinux:latest

docker pull archlinux

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

Agora o salvaremos localmente para análise posterior.

docker save c24fe13d37b9 -o savedArch

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

Vamos descompactar o arquivo obtido no diretório atual.

tar xvf savedArch

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

Como podemos ver, cada camada está em uma pasta separada. Agora vamos olhar a estrutura do manifesto que obtivemos.

cat manifest.json | json_pp

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

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

Implementação dos comandos docker pull e docker push sem o cliente docker por meio de requisições HTTP

Obviamente, o manifesto existente não nos serve, então vamos fazer o nosso, com camadas e configurações de blackjack e cortesan.

Sempre teremos ao menos um arquivo de configuração e um array de camadas. A versão do esquema é 2 (válida no momento da redação deste artigo), o 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 a criação do manifesto básico, é necessário preenchê-lo com dados válidos. Para isso, utilizamos o modelo do objeto JSON da camada:

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

este iremos adicionar ao manifesto para cada camada.

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

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

Agora podemos iniciar o processo de upload e salvar o uuid, que deve 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 conseguimos como resultado?
Primeiro, dados reais para análise, uma vez que os testes são executados no blazemeter e os dados de solicitações do cliente docker são bastante informativos, ao contrário das solicitações HTTP limpas.

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, ao mesmo tempo, obter um tempo médio de resposta 20-25% mais rápido. Para o download do docker, foi possível aumentar o número de usuários em 500%, enquanto o tempo médio de resposta caiu cerca de 60%.

Obrigado pela atenção.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster