Путешествие к центру… docker image. Или как скачать образ из registry без docker
За 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 хэш-суммой которая совпала с оригинальной.
PS. Пока писал статью обновился образ ubuntu:18.04 на hub.docker.com. Так что пришлось качать образ по Digest. Хэш-суммы уже не сошлись с оригиналом из-за того, что вместо тэга был записан Digest, во всё остальном это были идентичные образы.
Второе открытие для меня было отсутствие файла repositories в архиве при сохранении образа с отсутствующим тэгом с помощью команды docker save
.
Полностью рабочий код тут: https://github.com/myback/docker_pull
Изображение Нельсона Манца, а так же его смех «ХА-ХА» является собственностью компании FOX:).