Três dias antes do ano novo, havia uma tarefa de transferir nosso software para o cliente através do gerente, em uma unidade flash. O software é uma plataforma de microsserviço com várias dezenas de imagens de docker com muitas configurações e um gráfico de leme com um quilômetro de comprimento. O que tínhamos:
- Gerente em Moscou (não sou de lá)
- Windows
- Não há interação direta (e, se fosse, realmente não ajudava)
- estivador não
Pff, pensei! Vou pegar Golang, escrever um programa, compilar para Windows.
... e cinco horas depois eu percebi a pressa das minhas conclusões. Nesse momento, o riso de Nelson se lembrou pela primeira vez. HA HA O que me assombrava o tempo todo que passava estudando a questão.
A maioria dos exemplos que encontrei requer dockerd. Dois scripts que não usam dockerd, encontrados após uma hora de pesquisa no Google, um e dois . A primeira opção me ajudou a entender o processo de obtenção de todas as camadas de imagem e arquivos de configuração, mas é impossível usá-lo no Windows. E a segunda opção indicava que não eram apenas vários hashes piscando na tela, especificamente este FIXME . Pode-se, é claro, parar por aí, funciona! Transferir para ir não é difícil. Mas como verificar se as imagens do gerente estavam na mesma forma que em nosso registro? Mas de jeito nenhum! Portanto, enviei apenas imagens para o armazenamento compartilhado, baixei usando o comando docker save e compartilhei o link. E se acalmou com isso.
No quarto dia de férias, bastante cansada deles, a idéia de baixar e montar a imagem do docker me tomou novamente, e eu mergulhei no código moby por algumas horas.
O que eu tive desta vez:
- Entendendo como obter todas as camadas
Tomando o Python "na mão" e com base nesse script , decidi corrigi-lo. No segundo dia, decidi escrever um roteiro do zero. Lembrando-me dos meus trabalhos para autorização automática, simplesmente copiei parte do código de lá, mais os desenvolvimentos já feitos por mim ao editar o script. Não houve problemas com a autorização e o download desses dados, mas surgiram questões:
- quais são os hashes exibidos quando o comando pull do docker é executado?
- quais são os hashes usados para nomear diretórios dentro do tarball da imagem?
- Como construir um arquivo tar para que a soma da verificação corresponda à imagem original?
Para estudar, escolhi uma imagem do ubuntu: 18.04
imagem sha256sum salva por meio do docker save
- 257cab9137419a53359d0ed76f680fe926ed3645238357bdcdb84070a8f26cd0.
> docker pull ubuntu:18.04 18.04: Pulling from library/ubuntu 2746a4a261c9: Downloading [==============> ] 6.909MB/26.69MB 4c1d20cdee96: Download complete 0d3160e1d0de: Download complete c8e37668deea: Download complete Digest: sha256:250cc6f3f3ffc5cdaa9d8f4946ac79821aafb4d3afc93928f0de9336eba21aa4 Status: Downloaded newer image for ubuntu:18.04 docker.io/library/ubuntu:18.04
Conteúdo do tarball da imagem
tar tvf ubuntu.tar drwxr-xr-x 0 root root 0 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/ -rw-r--r-- 0 root root 3 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/VERSION -rw-r--r-- 0 root root 477 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/json -rw-r--r-- 0 root root 991232 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/layer.tar drwxr-xr-x 0 root root 0 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/ -rw-r--r-- 0 root root 3 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/VERSION -rw-r--r-- 0 root root 477 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/json -rw-r--r-- 0 root root 15872 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/layer.tar -rw-r--r-- 0 root root 3411 Dec 19 11:21 549b9b86cb8d75a2b668c21c50ee092716d070f129fd1493f95ab7e43767eab8.json drwxr-xr-x 0 root root 0 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/ -rw-r--r-- 0 root root 3 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/VERSION -rw-r--r-- 0 root root 1264 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/json -rw-r--r-- 0 root root 3072 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/layer.tar drwxr-xr-x 0 root root 0 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/ -rw-r--r-- 0 root root 3 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/VERSION -rw-r--r-- 0 root root 401 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/json -rw-r--r-- 0 root root 65571328 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/layer.tar -rw-r--r-- 0 root root 432 Jan 1 1970 manifest.json -rw-r--r-- 0 root root 88 Jan 1 1970 repositories
Configuração da imagem
{ ... "rootfs": { "type": "layers", "diff_ids": [ "sha256:2dc9f76fb25b31e0ae9d36adce713364c682ba0d2fa70756486e5cedfaf40012", "sha256:9f3bfcc4a1a8a676da07287a1aa6f2dcc8e869ea6f054c337593481a5bb1345e", "sha256:27dd43ea46a831c39d224e7426794145fba953cd7309feccf4d5ea628072f6a2", "sha256:918efb8f161b4cbfa560e00e8e0efb737d7a8b00bf91bb77976257cd0014b765" ] } ... }
A primeira pergunta foi resolvida com bastante rapidez, com a ajuda da documentação https://github.com/opencontainers/image-spec/blob/master/config.md . Os hashes que aparecem quando você executa o comando docker pull são chainIDs calculados na lista diff_ids do manifesto image-config, onde o primeiro chainID é sempre o primeiro da lista diff_ids e os subseqüentes são somas de hash da cadeia de caracteres (chain_id[i-1] + " " + diff_id[i])
Código para criar chainID chainID:
def chain_ids(ids: list) -> list: chain = list() chain.append(ids[0]) if len(ids) < 2: return ids nxt = list() nxt.append("sha256:" + hashlib.sha256(f'{ids[0]} {ids[1]}'.encode()).hexdigest()) nxt.extend(ids[2:]) chain.extend(chain_ids(nxt)) return chain
A adição de um prefixo com o nome do algoritmo, neste caso "sha256:", é obrigatória e está incluída nos requisitos do padrão opencontainers, ou seja, a string deve estar no formato "algoritmo: hash".
Ele passou duas noites em uma pergunta sobre como nomear um diretório. Por um longo tempo, observei o código-fonte do docker-daemon e o Oh! um milagre! Consegui encontrar o código de geração aqui e aqui . Para gerar o nome do diretório, é necessário calcular o hash na configuração da camada json. O Docker possui várias versões de configuração e, antes do docker engine 1.9, as configurações da versão v1 eram usadas. Mal disse o que fez! E novamente, a silhueta de Nelson surge. Após uma pequena depuração, percebi que o problema estava oculto na geração de json. No Python, a ordem dos dados em um dicionário pode diferir da ordem dos dados em json gerados a partir deste dicionário. A ordem dos dados em json será diferente e, portanto, seu hash também será diferente. Eu tive que ir para OrderedDict, pré-registrar a ordem de dados desejada neles. Isso aumentou o tamanho do código em uma vez e meia.
Parece que tudo está corrigido, eu corro o script e ... em algum lugar lá no fundo, o notório HA-HA aparece! O último hash não corresponde. Mais uma vez, estudo o código e vejo , e essa é outra v1-config que contém todas as informações sobre a imagem, que podem ser vistas usando o docker inspect
. Adicione outro OrderDict especificamente para ele, complemente o código e ... HA HA!
Já eram cinco da manhã e minha cabeça realmente não pensava; então, depois de dormir, voltei a visualizar o código. Observar repetidamente o código de geração encontrou uma linha . Como fiquei feliz em ver isso. Antes de vê-la, havia pensamentos de construir minha janela de encaixe com Jack preto e registro de dados. docker save
o debug, executo o docker save
e ... não é nada engraçado, no docker-desktop para mac os há um limite no comprimento da linha no log de 947 caracteres e a configuração gerada é interrompida para \"
. Após executar todas essas etapas no Linux, consegui obter a configuração camada da primeira versão, com base na qual escrevi o código, e consegui obter o hash desejado da última camada. Os hashes para todos os arquivos são iguais, os diretórios são nomeados da mesma maneira que a imagem original. É hora de coletar o arquivo tar ... HAHA!
O tamanho do arquivo não corresponde, leia https://github.com/opencontainers/image-spec/blob/master/layer.md e o formato de arquivo tar . O valor padrão é 10240 bytes, e o tamanho do arquivo morto que eu coletei é 9216 bytes maior. Inicialmente, pensei que era necessário reduzir o tamanho do bloco para 1024 bytes, o que resultou incorreto e, como resultado, o tamanho do bloco de 512 bytes igualou o tamanho dos arquivos.
tarfile.RECORDSIZE = 512
A primeira linha do arquivo recém-criado contém a pasta raiz "/". Esta opção não é adequada, portanto, complemento o código, verificando o conteúdo da pasta e adicionando-o individualmente ao arquivo, depois de o ter classificado anteriormente.
Finalmente, conseguimos obter o mesmo tamanho de arquivo, uma aparência uniforme de diretórios, mas isso não é tudo. Arquivos e diretórios, com exceção de manifest.json e respoitories, devem ter atributos st_atime no archive, st_mtime igual a st_ctime. Para arquivos manifest.json e respoitories, os atributos st_atime, st_mtime e st_ctime devem estar datados do início da era 1970-01-01 00:00
. Todas as datas devem ser definidas de acordo com o fuso horário, respectivamente. Desde que realizei todo o trabalho no Mac OS, notei uma diferença. Ao salvar a imagem no Linux, a lista de arquivos no arquivo ficou assim:
tar tvf ubuntu.tar drwxr-xr-x 0/0 0 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/ -rw-r--r-- 0/0 3 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/VERSION -rw-r--r-- 0/0 477 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/json -rw-r--r-- 0/0 991232 Dec 19 11:21 07adecfcb06a1142a69c3e769cb38f2d4ef9d772726ce1e65bc6dbd4448cccc9/layer.tar drwxr-xr-x 0/0 0 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/ -rw-r--r-- 0/0 3 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/VERSION -rw-r--r-- 0/0 477 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/json -rw-r--r-- 0/0 15872 Dec 19 11:21 3e1d90747aa9d2a7ec6e9693bdd490dff8528b9aec4a2fac2300824e4ba3a60e/layer.tar -rw-r--r-- 0/0 3411 Dec 19 11:21 549b9b86cb8d75a2b668c21c50ee092716d070f129fd1493f95ab7e43767eab8.json drwxr-xr-x 0/0 0 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/ -rw-r--r-- 0/0 3 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/VERSION -rw-r--r-- 0/0 1264 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/json -rw-r--r-- 0/0 3072 Dec 19 11:21 b0474230e27ddbba2f46397aac85d4d2fd748064ed9c0ff1e57fec4f063fcf6b/layer.tar drwxr-xr-x 0/0 0 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/ -rw-r--r-- 0/0 3 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/VERSION -rw-r--r-- 0/0 401 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/json -rw-r--r-- 0/0 65571328 Dec 19 11:21 c8ba25f7db9f70220ac92449b238a0697f9eb580ef4f905225a333fc0a5e8719/layer.tar -rw-r--r-- 0/0 432 Jan 1 1970 manifest.json -rw-r--r-- 0/0 88 Jan 1 1970 repositories
Diferentemente da lista no início do artigo, no Linux, o arquivo é salvo com o sinalizador somente numérico. Existem duas variáveis no objeto tarinfo responsáveis por isso, tarinfo.uname e tarinfo.gname. E o segundo problema com o mac os é a falta de um grupo raiz, ele é corrigido usando a variável tarinfo.gid no mesmo objeto tarinfo. Bem, tudo parece estar criando um arquivo ...

Para todos os arquivos, o hash converge, os nomes dos diretórios e arquivos são os mesmos, os atributos st_atime, st_mtime e st_ctime convergem para o original, os direitos para os arquivos são exatamente os mesmos. A abertura dos dois arquivos em um editor hexadecimal viu uma pequena diferença:

A janela superior é o original, o arquivo inferior que eu coletei.
Compreendendo o formato tar. Depois do nome do diretório está o valor dos direitos para o arquivo (retângulo laranja). A diferença é que as informações sobre o objeto que está sendo adicionado ao arquivo morto não são adicionadas aos direitos do diretório. Um valor 40755 indicado nos direitos indica que este é um diretório com permissões de 755 e 100644 é um arquivo com direitos de 644. O retângulo vermelho é uma sequência mágica e, a julgar pelo código do arquivo ustar\000
sequência mágica ustar\000
, é usado apenas nos formatos PAX e USTAR. O formato PAX não se encaixa, ele usa um tipo especial de cabeçalho. O retângulo azul é uma soma de verificação e difere devido ao uso de diferentes formatos para direitos de gravação em um arquivo e cabeçalho mágico.
Mudo o formato de arquivo para USTAR, mas não está claro o que fazer com as permissões de gravação de arquivos. Aqui e aqui , a mágica está acontecendo para mim, nunca trabalhei com o sistema octal e não entendo por que o e comercial é necessário aqui (alguém pode compartilhar seu conhecimento nos comentários). Eu tive que adicionar várias impressões para ver quais dados chegam com argumentos e quais dados são usados para formar o bloco de arquivamento. Tomando um número inteiro da segunda posição do bloco de dados, era 16877 e levando-o ao cálculo octal, descobriu-se que esse valor é 0o40755
, que é 0o40755
que eu preciso. Substituindo as funções get_info e _create_header, excluindo & 0o7777
delas (nada mais me ocorreu), consegui coletar um arquivo tar com um hash sha256 que coincidia com o original.
PS Enquanto escrevia um artigo, a imagem do ubuntu foi atualizada: 18.04 em hub.docker.com. Então eu tive que baixar a imagem no Digest. Os valores de hash não correspondem mais ao original devido ao fato de o Digest ter sido gravado em vez da tag, caso contrário, eram imagens idênticas.
A segunda descoberta para mim foi a ausência de um arquivo de repositórios no archive ao salvar uma imagem com uma tag ausente usando o docker save
.
Código de funcionamento completo aqui: https://github.com/myback/docker_pull
Imagem de Nelson Manz, bem como sua risada "HA-HA" é propriedade da FOX :)