3 días antes del año nuevo hubo una tarea para transferir nuestro software al cliente a través del administrador, en una unidad flash. El software es una plataforma de microservicio con docenas de imágenes acoplables con muchas configuraciones y un gráfico de timón de un kilómetro de largo. Lo que tuvimos
- Gerente en Moscú (no soy de allí)
- Ventanas
- No hay interacción directa (y si lo fuera, realmente no ayudó)
- acoplador no
Pff, pensé! Tomaré Golang, escribiré un programa, compilaré para Windows.
... y 5 horas después me di cuenta de la rapidez de mis conclusiones. En ese momento, la risa de Nelson se acordó por primera vez. HA HA Lo que me persiguió todo el tiempo que pasé estudiando el tema.
La mayoría de los ejemplos que encontré requieren dockerd. Dos scripts que no usan dockerd, que se encontraron después de una hora de búsqueda en Google, uno y dos . La primera opción me ayudó a comprender el proceso de obtención de todas las capas de imágenes y archivos de configuración, pero es imposible usarlo con Windows. Y la segunda opción indicó que no era solo que varios hashes parpadeaban en la pantalla, específicamente este FIXME . Uno podría, por supuesto, detenerse allí, ¡funciona! Transferir para ir no es difícil. Pero, ¿cómo verificar que las imágenes del administrador resultaron estar en la misma forma que en nuestro registro? Pero de ninguna manera! Por lo tanto, acabo de subir imágenes al almacenamiento compartido, las descargué con el comando Docker save y compartí el enlace. Y se calmó con esto.
El cuarto día de las vacaciones, bastante cansado de ellos, la idea de descargar y armar la imagen de la ventana acoplable me superó nuevamente, y me sumergí en el código Moby durante un par de horas.
Lo que tuve esta vez:
- Comprender cómo obtener todas las capas
Tomando Python "en la mano" y en base a este script , decidí arreglarlo. El segundo día, decidí escribir un guión desde cero. Recordando mis manualidades para la autorización oauth, simplemente copié parte del código desde allí, más los desarrollos que ya hice cuando edité el script. No hubo problemas con la autorización y la descarga de estos datos, pero surgieron preguntas:
- ¿Cuáles son los hashes que se muestran cuando se ejecuta el comando de extracción de Docker?
- ¿Cuáles son los hashes que se usan para nombrar directorios dentro del tarball de la imagen?
- ¿Cómo construir un archivo tar para que la suma de verificación coincida con la imagen original?
Para estudiar, elegí una imagen de ubuntu: 18.04
sha256sum imagen guardada a través de la ventana 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
Contenido de tarball de imagen
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
Config de imagen
{ ... "rootfs": { "type": "layers", "diff_ids": [ "sha256:2dc9f76fb25b31e0ae9d36adce713364c682ba0d2fa70756486e5cedfaf40012", "sha256:9f3bfcc4a1a8a676da07287a1aa6f2dcc8e869ea6f054c337593481a5bb1345e", "sha256:27dd43ea46a831c39d224e7426794145fba953cd7309feccf4d5ea628072f6a2", "sha256:918efb8f161b4cbfa560e00e8e0efb737d7a8b00bf91bb77976257cd0014b765" ] } ... }
La primera pregunta se resolvió lo suficientemente rápido, la documentación https://github.com/opencontainers/image-spec/blob/master/config.md ayudó. Los hash que aparecen cuando ejecuta el comando docker pull son chainIDs calculados a partir de la lista diff_ids del manifiesto de configuración de imagen, donde el primer chainID siempre es el primero de la lista diff_ids, y los siguientes son sumas hash de la cadena (chain_id[i-1] + " " + diff_id[i])
. Código para construir 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
Agregar un prefijo con el nombre del algoritmo, en este caso "sha256:", es obligatorio y está incluido en los requisitos del estándar de contenedores abiertos, es decir la cadena debe tener la forma "algoritmo: hash".
Pasó dos tardes en una pregunta sobre cómo nombrar un directorio. Durante bastante tiempo, miré el código fuente de docker-daemon y ¡Oh! un milagro! Logré encontrar el código de generación aquí y aquí . Para generar el nombre del directorio, debe calcular el hash a partir de la configuración de la capa json. Docker tiene varias versiones de configuración y antes de Docker Engine 1.9, se usaban las configuraciones de la versión v1. Apenas dicho que hecho! Y de nuevo, surge la silueta de Nelson. Después de una breve depuración me di cuenta de que el problema se estaba ocultando en la generación de json. En Python, el orden de los datos en un diccionario puede diferir del orden de los datos en json generados a partir de este diccionario. El orden de los datos en json será diferente y, en consecuencia, su hash también será diferente. Tuve que ir a OrderedDict, registrar previamente el orden de datos deseado en ellos. Esto aumentó el tamaño del código una vez y media.
Parece que todo está corregido, ejecuto el script y ... en algún lugar profundo, ¡aparece el notorio HA-HA! El último hash no coincide. Una vez más estudio el código y veo , y esta es otra configuración v1 que contiene toda la información sobre la imagen, que se puede ver usando el docker inspect
. Agregue otro OrderDict específicamente para él, complemente el código y ... ¡JA, JA!
Ya eran las 5 de la mañana y mi cabeza realmente no pensaba, así que después de dormir volví a ver el código. Mirando repetidamente el código de generación se encontró con una línea . Qué contento estaba de ver eso. Antes de verla, había pensamientos de construir mi docker con Gato negro y registro de datos. Enciendo depuración, ejecuto el docker save
y ... no es del todo ridículo, en docker-desktop para mac os hay un límite en la longitud de línea en el registro de 947 caracteres y la configuración generada se rompe a \"
. Después de realizar todos estos pasos en Linux, logré obtener la configuración capa de la primera versión, sobre la base de la cual escribí el código, y logré obtener el hash deseado de la última capa. Los hashes para todos los archivos son los mismos, los directorios se nombran de la misma manera que la imagen original. Es hora de recopilar el archivo tar ... ¡JAJA!
El tamaño del archivo no coincide, lea https://github.com/opencontainers/image-spec/blob/master/layer.md y el formato de archivo tar . El valor predeterminado es 10240 bytes, y el tamaño del archivo que he recopilado es 9216 bytes más grande. Al principio pensé que era necesario reducir el tamaño del bloque a 1024 bytes, lo que resultó ser incorrecto y, como resultado, el tamaño del bloque de 512 bytes igualó el tamaño de los archivos.
tarfile.RECORDSIZE = 512
La primera línea del archivo recién creado contiene la carpeta raíz "/". Esta opción no es adecuada, por lo tanto, complemento el código escaneando el contenido de la carpeta y lo agrego individualmente al archivo, habiéndolo ordenado previamente.
Finalmente, logramos lograr el mismo tamaño de archivo, una apariencia uniforme de directorios, pero esto no es todo. Los archivos y directorios, con la excepción de manifest.json y los repositorios, en el archivo deben tener atributos st_atime, st_mtime igual a st_ctime. Para los archivos manifest.json y respoitories, los atributos st_atime, st_mtime y st_ctime deben estar fechados al comienzo de la era 1970-01-01 00:00
. Todas las fechas deben establecerse según la zona horaria, respectivamente. Como realicé todo el trabajo en mac os, noté una diferencia. Al guardar la imagen en Linux, la lista de archivos en el archivo se veía así:
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
A diferencia de la lista al principio del artículo, en Linux, el archivo se guarda con el indicador de solo numérico. Hay dos variables en el objeto tarinfo responsables de esto, tarinfo.uname y tarinfo.gname. Y el segundo problema con mac os es la falta de un grupo raíz, se soluciona utilizando la variable tarinfo.gid en el mismo objeto tarinfo. Bueno, todo parece estar creando un archivo ...

Para todos los archivos, el hash converge, los nombres de directorios y archivos son los mismos, los atributos st_atime, st_mtime y st_ctime convergen con el original, los derechos de los archivos son exactamente los mismos. Abrir ambos archivos en un editor hexadecimal vio una ligera diferencia:

La ventana superior es la original, el archivo inferior que he recopilado.
Comprender el formato tar. Después del nombre del directorio se encuentra el valor de los derechos del archivo (rectángulo naranja). La diferencia es que la información sobre el objeto que se agrega al archivo no se agrega a los derechos del directorio. Un valor de 40755 indicado en los derechos indica que este es un directorio con permisos de 755, y 100644 es un archivo con permisos de 644. El rectángulo rojo es magic-string y, a juzgar por el código ustar\000
, magic-string ustar\000
, solo se usa en formatos PAX y USTAR. El formato PAX no se ajusta en absoluto, utiliza un tipo especial de encabezado. El rectángulo azul es una suma de verificación y difiere debido al uso de diferentes formatos para escribir derechos en un archivo y encabezado mágico.
Cambio el formato de archivo a USTAR, pero no está claro qué hacer con los permisos de grabación de archivos. Aquí y aquí está ocurriendo la magia para mí, nunca he trabajado con el sistema octal y no entiendo por qué se necesita el ampersand aquí (tal vez alguien en los comentarios compartirá conocimientos). Tuve que agregar varias impresiones para ver qué datos llegan con argumentos y qué datos se utilizan para formar el bloque de archivo. Tomando un número entero de la segunda posición del bloque de datos, era 16877, y llevándolo al cálculo octal, resultó que este valor es 0o40755
, que es 0o40755
lo que necesito. Simplemente anulando las funciones get_info y _create_header, eliminando & 0o7777
de ellas (no se me ocurrió nada más), logré recopilar un archivo tar con un hash sha256 que coincidía con el original.
PD Mientras escribía un artículo, la imagen de ubuntu se actualizó: 18.04 en hub.docker.com. Así que tuve que descargar la imagen en Digest. Las cantidades de hash ya no coinciden con el original debido al hecho de que Digest se escribió en lugar de la etiqueta, de lo contrario, eran imágenes idénticas.
El segundo descubrimiento para mí fue la ausencia de un archivo de repositorios en el archivo al guardar una imagen con una etiqueta faltante usando el docker save
.
Código de trabajo completo aquí: https://github.com/myback/docker_pull
Imagen de Nelson Manz, así como su risa "HA-HA" es propiedad de FOX :)