Pull to refresh

Путешествие к центру… docker image. Или как скачать образ из registry без docker

Reading time 9 min
Views 11K

За 3 дня до нового года появилась задача, передать клиенту наше ПО через менеджера, на флешке. ПО – это микросервисная платформа в несколько десятков docker-образов с множеством настроек и “километровым” helm-чартом. Что мы имели:


  • Менеджер в Москве (я не оттуда)
  • Windows
  • Прямого взаимодействия нет (а если бы и было, то не особо помогло)
  • docker-а нет

Пфф, подумал я! Возьму Golang, напишу программку, скомпилирую под Windows.
… и 5 часов спустя осознал поспешность своих выводов. В тот момент в первый раз вспомнился смех Нельсона. ХА-ХА! Который преследовал меня все то время, что я потратил на изучение вопроса.


Большинство найденных мной примеров требуют наличие dockerd. Два скрипта, не использующие dockerd, которые нашлись после часа гугления, раз и два. Первый вариант помог мне разобраться с процессом получения всех слоев образа и файлов конфигураций, но использовать его с Windows невозможно. А второй вариант указал, что не просто так на экране мелькают разнообразные хэши, конкретно этот FIXME. Можно было бы, конечно, на этом и остановится, работает же! Перенести на go особого труда не составит. Но как проверить, что у менеджера образы оказались именно в том виде, что и в нашем registry? А никак! Поэтому просто выложил в шаренное хранилище, скаченные с помощью команды docker save, образы и поделился ссылкой. И на этом успокоился.


На четвертый день праздников, изрядно от них устав, идея скачать и собрать правильно docker-образ настигла меня опять, и я погрузился в код moby на пару часов.


Что у меня было в этот раз:


  • Понимание как получить все слои

Взяв Python "в руки" и за основу этот скрипт, решил его подправить. На второй день всё таки решил написать скрипт с нуля. Вспомнив про свои поделки для oauth-авторизации, просто скопировал часть кода оттуда, плюс сделанные уже мной наработки при правке скрипта. С авторизацией и скачиванием данных проблем не было, но появились вопросы:


  • что это за хэши которые отображаются при выполнении команды docker pull?
  • что это за хэши которые используются для именования директорий внутри tar-архива образа?
  • Как собрать tar-архив, чтобы чек-сумма совпадала с оригинальным образом?

Для изучения я выбрал образ ubuntu:18.04
sha256sum образа сохраненного через 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

Содержимое tar-архива образа


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

Image config


{
  ...
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:2dc9f76fb25b31e0ae9d36adce713364c682ba0d2fa70756486e5cedfaf40012",
      "sha256:9f3bfcc4a1a8a676da07287a1aa6f2dcc8e869ea6f054c337593481a5bb1345e",
      "sha256:27dd43ea46a831c39d224e7426794145fba953cd7309feccf4d5ea628072f6a2",
      "sha256:918efb8f161b4cbfa560e00e8e0efb737d7a8b00bf91bb77976257cd0014b765"
    ]
  }
  ...
}

С первым вопросом удалось разобраться достаточно быстро, помогла документация https://github.com/opencontainers/image-spec/blob/master/config.md. Хэши появляющиеся при выполнении команды docker pull, это chainID, высчитываемые из списка diff_ids манифеста image-config, где первый chainID всегда равен первому из списка diff_ids, а последующие это хэш-суммы от строки (chain_id[i-1] + " " + diff_id[i]). Код для построения цепочки 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

Добавление префикса с названием алгоритма, в данном случае "sha256:", обязательно и входит в требования стандарта opencontainers, т.е. строка должна быть вида "algorithm:hash".


На вопрос по именованию директорий потратил два вечера. Достаточно длительное время я просматривал исходники docker-daemon и О! чудо! Удалось найти код генерации вот здесь и здесь. Для генерации имени директории надо вычислить хэш из json-а конфигурации слоя. У docker есть несколько версий конфигураций и до версии docker engine 1.9 использовались конфигурации версии v1. Сказано-сделано! И вот опять возникает силуэт Нельсона. После непродолжительного дебага понял, что проблема скрывалась в генерации json-а. В Python, порядок данных в словаре может отличаться от порядка данных в json генерируемого из этого словаря. Порядок данных в json будет отличаться, соответственно будет отличаться и его хэш. Пришлось перейти на OrderedDict, заранее прописать нужный порядок данных в них. Это увеличило размер кода в полтора раза.


Вроде всё поправил, запускаю скрипт и … где-то глубоко внутри всплывает пресловутый ХА-ХА! Последний хэш не совпадает. Ещё раз изучаю код и вижу, а это другой v1-конфиг содержащий в себе всю информацию об образе, которую можно увидеть с помощью команды docker inspect. Добавляю ещё один OrderDict специально для него, дополняю код и … ХА-ХА!
Было уже 5 утра и голова не особо думала, так что после сна я вернулся к просмотру кода. Повторно просматривая код генерации наткнулся на строку. Как же я был рад видеть это. До того, как её увидел, были мысли собрать свой docker с Блек-Джеком и логированием данных. Включаю debug, выполняю команду docker save и … вот прям совсем не смешно, в docker-desktop для mac os ограничение на длину строки в логе 947 символов и сгенерированный конфиг обрывается на \". После выполнения всех этих действий в Linux, мне удалось получить конфиг слоя первой версии, на основании которого написал код и мне удалось получить нужный хэш последнего слоя. Хэши для всех файлов совпадают, директории называются по аналогии с оригинальным образом. Настало время собрать tar-архив … ХА-ХА!


Не совпадает размер файла, читаю https://github.com/opencontainers/image-spec/blob/master/layer.md и формат tar-архива. Дефолтное значение 10240 байт, а размер мною собранного архива больше на 9216 байт. Сначала я подумал, что надо уменьшить размер блока до 1024 байта, что оказалось неверным и в итоге размер блока 512 байт уравнял размеры архивов.


tarfile.RECORDSIZE = 512

Первой строкой только что в созданном архиве фигурирует корневая папка "/". Такой вариант не подходит, поэтому дополняю код сканированием содержимого папки и добавляю по отдельности в архив, предварительно отсортировав.


Наконец удалось добиться одинакового размера файлов, единообразного вида каталогов, но и это ещё не всё. Файлы и каталоги, за исключением manifest.json и respoitories, в архиве должны иметь атрибуты st_atime, st_mtime равный st_ctime. Для файлов manifest.json и respoitories атрибуты st_atime, st_mtime и st_ctime должны быть датированы началом эпохи 1970-01-01 00:00. Все даты должны быть установлены с учетом часового пояса, соответственно. Так как все работы я проводил в mac os, то заметил одно отличие. При сохранении образа в Linux, список файлов в архиве выглядел так:


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

В отличие от списка приведенного в начале статьи, в Linux, архив сохраняется с флагом numeric-only. В tarinfo-объекте есть две переменные отвечающие за это, tarinfo.uname и tarinfo.gname. И вторая проблема с mac os, это отсутствие группы root, она исправляется с помощью переменной tarinfo.gid в том же tarinfo-объекте. Ну вроде бы всё, создаю архив …



Для всех файлов хэш сходится, имена директорий и файлов одинаковые, атрибуты st_atime, st_mtime и st_ctime сходятся с оригиналом, права на файлы абсолютно такие же. Открыв оба архива в hex-редакторе увидел небольшое отличие:



Верхнее окно оригинал, нижнее мною собранный архив.


Разбираюсь с tar-форматом. После имени директории идет значение прав на файл (оранжевый прямоугольник). Отличие в том, что в права директории не добавляется информация о добавляемом в архив объекте. Указанные в правах значение 40755 указывает, что это директория с правами 755, а 100644 это файл с правами 644. Красный прямоугольник это magic-string и судя по коду tarfile, magic-string ustar\000, используется только в форматах PAX и USTAR. PAX-формат совсем не подходит, у него используется особого вида заголовок. Синий же прямоугольник, это чек-сумма и она отличается из-за использования разных форматов записи прав на файл и magic-header.


Переключаю формат архива на USTAR, а вот с записью прав файлов непонятно что делать. Вот здесь и здесь для меня творится магия, я никогда не работал с восьмеричной системой и не понимаю для чего здесь нужен амперсанд (может кто в комментах поделится знаниями). Пришлось добавить несколько print-ов чтобы увидеть какие данные прилетают аргументами и какие данные используются для формирования блока архива. Взяв целое число из второй позиции блока данных, оно было 16877, и приведя его к восьмеричному исчислению оказалось, что это значение 0o40755, собственно, что мне и нужно. Просто переопределив функции get_info и _create_header, удалив из них & 0o7777 (ничего другого в голову не пришло), мне удалось собрать tar-архив c sha256 хэш-суммой которая совпала с оригинальной.


P.S. Пока писал статью обновился образ ubuntu:18.04 на hub.docker.com. Так что пришлось качать образ по Digest. Хэш-суммы уже не сошлись с оригиналом из-за того, что вместо тэга был записан Digest, во всё остальном это были идентичные образы.


Второе открытие для меня было отсутствие файла repositories в архиве при сохранении образа с отсутствующим тэгом с помощью команды docker save.


Полностью рабочий код тут: https://github.com/myback/docker_pull


UPD 06.21 теперь есть и на Go: https://github.com/myback/go-docker-pull


Изображение Нельсона Манца, а так же его смех “ХА-ХА” является собственностью компании FOX :)

Tags:
Hubs:
+21
Comments 13
Comments Comments 13

Articles