За 3 дня до нового года появилась задача, передать клиенту наше ПО через менеджера, на флешке. ПО – это микросервисная платформа в несколько десятков docker-образов с множеством настроек и “километровым” helm-чартом. Что мы имели: Пфф, подумал я! Возьму Golang, напишу программку, скомпилирую под Windows.
… и 5 часов спустя осознал поспешность своих выводов. В тот момент в первый раз вспомнился смех Нельсона. ХА-ХА! Который преследовал меня все то время, что я потратил на изучение вопроса. Большинство найденных мной примеров требуют наличие dockerd. Два скрипта, не использующие dockerd, которые нашлись после часа гугления, раз и два. Первый вариант помог мне разобраться с процессом получения всех слоев образа и файлов конфигураций, но использовать его с Windows невозможно. А второй вариант указал, что не просто так на экране мелькают разнообразные хэши, конкретно этот FIXME. Можно было бы, конечно, на этом и остановится, работает же! Перенести на go особого труда не составит. Но как проверить, что у менеджера образы оказались именно в том виде, что и в нашем registry? А никак! Поэтому просто выложил в шаренное хранилище, скаченные с помощью команды docker save, образы и поделился ссылкой. И на этом успокоился. На четвертый день праздников, изрядно от них устав, идея скачать и собрать правильно docker-образ настигла меня опять, и я погрузился в код moby на пару часов. Что у меня было в этот раз: Взяв Python "в руки" и за основу этот скрипт, решил его подправить. На второй день всё таки решил написать скрипт с нуля. Вспомнив про свои поделки для oauth-авторизации, просто скопировал часть кода оттуда, плюс сделанные уже мной наработки при правке скрипта. С авторизацией и скачиванием данных проблем не было, но появились вопросы: Для изучения я выбрал образ ubuntu:18.04
sha256sum образа сохраненного через docker save — 257cab9137419a53359d0ed76f680fe926ed3645238357bdcdb84070a8f26cd0. Содержимое tar-архива образа Image config С первым вопросом удалось разобраться достаточно быстро, помогла документация 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: Добавление префикса с названием алгоритма, в данном случае "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 байт уравнял размеры архивов. Первой строкой только что в созданном архиве фигурирует корневая папка "/". Такой вариант не подходит, поэтому дополняю код сканированием содержимого папки и добавляю по отдельности в архив, предварительно отсортировав. Наконец удалось добиться одинакового размера файлов, единообразного вида каталогов, но и это ещё не всё. Файлы и каталоги, за исключением 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, список файлов в архиве выглядел так: В отличие от списка приведенного в начале статьи, в 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 :) DevOps