git:
- add: <absolute path in git repository>
  to: <absolute path inside image>
  owner: <owner>
  group: <group>
  includePaths:
  - <path or glob relative to path in add>
  excludePaths:
  - <path or glob relative to path in add>
  stageDependencies:
    install:
    - <path or glob relative to path in add>
    beforeSetup:
    - <path or glob relative to path in add>
    setup:
    - <path or glob relative to path in add>
git:
- url: <git repo url>
  branch: <branch name>
  commit: <commit>
  tag: <tag>
  add: <absolute path in git repository>
  to: <absolute path inside image>
  owner: <owner>
  group: <group>
  includePaths:
  - <path or glob relative to path in add>
  excludePaths:
  - <path or glob relative to path in add>
  stageDependencies:
    install:
    - <path or glob relative to path in add>
    beforeSetup:
    - <path or glob relative to path in add>
    setup:
    - <path or glob relative to path in add>

Что такое git mapping?

Git mapping определяет, какой файл или папка из git-репозитория должны быть добавлены в конкретное место образа. Git-репозиторий может быть как локальным репозиторием, в котором находится файл конфигурации сборки (werf.yaml), так и удаленным репозиторием. В этом случае указывается адрес репозитория и версия кода — ветка, тег или конкретный коммит.

werf добавляет файлы из git-репозитория в образ, копируя их с помощью git archive, либо накладывая git-патч. При повторных сборках и появлении изменений в git-репозитории werf добавляет patch к собранному ранее образу, чтобы в конечном образе отразить изменения файлов и папок. Более подробно механизм переноса файлов и накладывания патчей рассматриваются в следующей секции.

Конфигурация git mapping поддерживает фильтры, что позволяет сформировать практически любую файловую структуру в образе, используя произвольное количество git mappings. Также вы можете указать группу и владельца конечных файлов в образе, что освобождает от необходимости делать это отдельной командой (chown).

В werf реализована поддержка сабмодулей git (git submodules) и если werf определяет, что какая-то часть git mapping является сабмодулем, то принимаются соответствующие меры, чтобы обрабатывать изменения в сабмодулях корректно.

Все git-сабмодули проекта связаны с конкретным коммитом, поэтому все разработчики, работающие с репозиторием с сабмодулями, получают одинаковое содержимое werf не инициализирует и не обновляет сабмодули, а использует соответствующие связанные коммиты

Пример добавления файлов из папки /src локального git-репозитория в папку /app собираемого образа и добавления кода PhantomJS из удаленного репозитория в папку /src/phantomjs собираемого образа:

git:
- add: /src
  to: /app
- url: https://github.com/ariya/phantomjs
  add: /
  to: /src/phantomjs

Зачем использовать git mapping?

Основная идея использования git mapping — добавление истории git к сборочному процессу.

Наложение патчей вместо копирования

Большинство коммитов в репозитории реального приложения относятся к обновлению кода самого приложения. В этом случае, если компиляция приложения не требуется, то для получения нового образа достаточно применить изменения к файлам предыдущего образа.

Удаленные репозитории

Сборка конечного образа может зависеть от файлов в других репозитория. werf позволяет добавлять файлы из удаленных репозиториев, а также отслеживать изменения в них.

Синтаксис

Для добавления кода из локального git-репозитория используется следующий синтаксис:

  • add — (не обязательный параметр) путь к директории или файлу, содержимое которого (которой) нужно добавить в образ. Указывается абсолютный путь относительно корня репозитория, т.е. он должен начинаться с /. По умолчанию копируется все содержимое репозитория, отсутствие параметра add равносильно указанию add: /;
  • to — путь внутри образа, куда будет скопировано соответствующее содержимое;
  • owner — имя или id пользователя-владельца файлов в образе;
  • group — имя или id группы-владельца файлов в образе;
  • excludePaths — список исключений (маска) при рекурсивном копировании файлов и папок. Указывается относительно пути, указанного в add;
  • includePaths — список масок файлов и папок для рекурсивного копирования. Указывается относительно пути, указанного в add;
  • stageDependencies — список масок файлов и папок для указания зависимости пересборки стадии от их изменений. Позволяет указать, при изменении каких файлов и папок необходимо принудительно пересобирать конкретную пользовательскую стадию. Более подробно рассматривается здесь.

При использовании удаленных репозиториев дополнительно используются следующие параметры:

  • url — адрес удаленного репозитория;
  • branch, tag, commit — имя ветки, тега или коммита соответственно. По умолчанию — ветка master.

Использование git mapping

Копирование директорий

Параметр add определяет источник, путь в git-репозитории, откуда файлы рекурсивно копируются в образ и помещаются по адресу, указанному в параметре to. Если параметр не определен, то по умолчанию используется значение /, т.е. копируется весь репозиторий. Пример простейшей конфигурации, добавляющей содержимое всего локального git-репозитория в образ в папку /app.

git:
- add: /
  to: /app
git repository files tree
image files tree

Также можно указать несколько git mappings:

git:
- add: /src
  to: /app/src
- add: /assets
  to: /static
git repository files tree
image files tree

Следует отметить, что конфигурация git mapping не похожа, например, на копирование типа cp -r /src /app. Параметр add указывает на содержимое каталога, которое будет рекурсивно копироваться из репозитория. Поэтому, если папка /assets со всем содержимым из репозитория должна быть скопирована в папку /app/assets образа, то имя assets вы должны указать два раза. Либо, как вариант, вы можете использовать фильтр (например, параметр includePaths).

Примеры обоих вариантов, которые вы можете использовать для достижения одинакового результата:

git:
- add: /assets
  to: /app/assets

либо

git:
- add: /
  to: /app
  includePaths: assets

В werf нет ограничения или соглашения насчет использования / в конце, как, например, в rsync. Т.о. add: /src и add: /src/ — одно и тоже

Изменение владельца

При добавлении файла из git-репозитория вы можете указать имя и/или группу владельца файлов в образе. Добавляемым файлам и папкам в образе при копировании будут установлены соответствующие права. Пользователь и группа могут быть указаны как именем, так и числовым id (userid, groupid).

Пример использования:

git:
- add: /src/index.php
  to: /app/index.php
  owner: www-data

Если указан только параметр owner, как в приведенном примере, то группой-владельцем устанавливается основная группа указанного пользователя в системе.

В результате в папку /app образа будет добавлен файл index.php и ему будут установлены следующие права:

index.php owned by www-data user and group

Если значения параметра owner или group не числовые id, а текстовые (т.е. названия соответственно пользователя и группы), то соответствующие пользователь и группа должны существовать в системе. Их нужно добавить заранее при необходимости (к примеру, на стадии beforeInstall), иначе при сборке возникнет ошибка.

git:
- add: /src/index.php
  to: /app/index.php
  owner: wwwdata

Использование фильтров

Парамеры фильтров, includePaths и excludePaths, используются при составлении списка файлов для добавления. Эти параметры содержат набор путей или масок, применяемых соответственно для включения и исключения файлов и папок при добавлении в образ.

Фильтр excludePaths работает следующим образом: каждая маска списка применяется к каждому файлу, найденному по пути add. Если файл удовлетворяет хотя бы одной маске, то он исключается из списка файлов на добавление. Если файл не удовлетворяет ни одной маске, то он добавляется в образ.

Фильтр includePaths работает наоборот — если файл удовлетворяет хотя бы одной маске, то он добавляется в образ.

Конфигурация git mapping может содержать оба фильтра. В этом случае файл добавляется в образ, если его путь удовлетворяет хотя бы одной маске includePaths и не удовлетворяет ни одной маске excludePaths.

Пример:

git:
- add: /src
  to: /app
  includePaths:
  - '**/*.php'
  - '**/*.js'
  excludePaths:
  - '**/*-dev.*'
  - '**/*-test.*'

В приведенном примере добавляются .php и .js файлы из папки /src исключая файлы с суффиксом -dev. или -test. в имени файла.

При определении соответствия файла маске применяется следующий алгоритм:

  • Определяется абсолютный путь к очередному файлу в репозитории.
  • Путь сравнивается с масками, определенными в includePaths и excludePaths, либо с конкретным указанным путем:
    • путь в параметре add объединяется с маской или указанным путем из параметров includePaths и excludePaths;
    • оба варианта проверяются с учетом правил применения глобальных шаблонов: если файл удовлетворяет маске, то он включается, в случае includePaths, либо исключается, в случае excludePaths.
  • Путь сравнивается с масками, определенными в includePaths и excludePaths, либо с конкретным указанным путем с учетом дополнительных условий:
    • путь в параметре add объединяется с маской или указанным путем из параметров includePaths и excludePaths и объединяется с суффиксом **/* к шаблону;
    • оба варианта проверяются с учетом правил применения глобальных шаблонов: если файл удовлетворяет маске, то он включается, в случае includePaths, либо исключается, в случае excludePaths.

Последний шаг в алгоритме, с добавлением суффикса**/* сделан для удобства — вам достаточно указать название папки в параметрах git mapping, чтобы все ее содержимое удовлетворяло шаблону параметра

Маска может содержать следующие шаблоны:

  • * — Удовлетворяет любому файлу. Шаблон включает . и исключает /.
  • ** — Удовлетворяет директории со всем ее содержимым, рекурсивно.
  • ? — Удовлетворяет любому одному символу в имени файла (аналогично regexp-шаблону /.{1}/).
  • [set] — Удовлетворяет любому символу из указанного набора символов. Аналогично использованию в regexp-шаблонах, включая указание диапазонов типа [^a-z].
  • \ — Экранирует следующий символ.

Маска, которая начинается с шаблона * или **, должна быть взята в одинарные или двойные кавычки в werf.yaml:

  • "*.rb" — двойные кавычки
  • '**/*' — одинарные кавычки

Примеры фильтров:

add: /src
to: /app
includePaths:
# удовлетворяет всем php файлам, расположенным конкретно в папке /src
- '*.php'

# удовлетворяет всем phph файлам рекурсивно, начиная с папки /src
# (также удовлетворяет файлам *.php, т.к. '.' включается шаблон **)
- '**/*.php'

# удовлетворяет всем файлам в папке /src/module1 рекурсивно
- module1

Фильтр includePaths может применяться для копирования одного файла без изменения имени. Пример:

git:
- add: /src
  to: /app
  includePaths: index.php

Наложение путей копирования

Если вы определяете несколько git mappings, вы должны учитывать, что при наложении путей в образе в параметре to вы можете столкнуться с невозможностью добавления файлов. Пример:

git:
- add: /src
  to: /app
- add: /assets
  to: /app/assets

Чтобы избежать ошибок сборки, werf определяет возможные наложения касающиеся фильтров includePaths и excludePaths, и если такое наложение присутствует, то werf пытается разрешить самые простые конфликты, неявно добавляя соответствующий параметр excludePaths в git mapping. Однако, такое поведение может все-таки привести к неожиданным результатам, поэтому лучше всего избегать наложения путей при определении git mappings.

В примере выше, werf в итоге неявно добавит параметр excludePaths и итоговая конфигурация будет следующей:

git:
- add: /src
  to: /app
  excludePaths:  # werf добавил этот фильтр, чтобы исключить конфликт наложения результирующих путей
  - assets       # между /src/assets и /assets
- add: /assets
  to: /app/assets

Работа с удаленными репозиториями

werf может использовать удаленные репозитории в качестве источника файлов. Для указания адреса внешнего репозитория используется параметр url. werf поддерживает работу с удаленными репозиториями по протоколам https и git+ssh.

https

Синтаксис для работы по протоколу https:

git:
- url: https://[USERNAME[:PASSWORD]@]repo_host/repo_path[.git/]

Указание логина и пароля при доступе по https опционально.

Пример доступа к репозиторию из pipeline GitLab CI с использованием переменных окружения:

git:
- url: https://{{ env "CI_REGISTRY_USER" }}:{{ env "CI_JOB_TOKEN" }}@registry.gitlab.company.name/common/helper-utils.git

В приведенном примере используется метод env библиотеки Sprig для доступа к переменным окружения.

git, ssh

Доступ к удаленному репозиторию с помощью протокола git защищается с использованием доступа поверх ssh. Это распространенная практика, используемая в частности GitHub, Bitbucket, GitLab, Gogs, Gitolite и т.д. Обычно адрес репозитория выглядит следующим образом:

git:
- url: git@gitlab.company.name:project_group/project.git

Для работы с удаленными репозиториями по ssh необходимо понимать, как werf находит ssh-ключи (читай далее подробнее).

Работа с ssh-ключами

Ssh-ключи для доступа предоставляются через ssh-agent. Ssh-agent — это демон, который работает через файловый сокет, путь к которому хранится в переменной окружения SSH_AUTH_SOCK. werf монтирует этот файловый сокет во все сборочные контейнеры и устанавливает переменную окружения SSH_AUTH_SOCK. Т.о. соединение с удаленным git-репозиторием устанавливается с использованием ключей, зарегистрированных в запущенном ssh-агенте.

werf использует следующий алгоритм для определения запущенного ssh-агента:

  • werf запущен с ключом --ssh-key (одним или несколькими):
    • Запускается временный ssh-агент, в который добавляются указанные при запуске werf ключи. Эти ключи используются при всех операциях с удаленными репозиториями.
    • Уже запущенный ssh-агент игнорируется.
  • werf запущен без указания ключа --ssh-key и есть запущенный ssh-агент:
    • Используется переменная окружения SSH_AUTH_SOCK, ключи добавляются в соответствующий ssh-агент и используются далее при всех операциях работы с удаленными репозиториями.
  • werf запущен без указания ключа --ssh-key и нет запущенного ssh-агента:
    • Если существует файл ~/.ssh/id_rsa, запускается временный ssh-агент, в который добавляется ключ из файла ~/.ssh/id_rsa.
  • Если ни один из вариантов не применим, то ssh-агент не запускается и при операциях с внешними git-репозиториями не используются никакие ssh-ключи. Сборка образа, с объявленными удаленными репозиториями в git mapping, завершится с ошибкой.

Подробнее про gitArchive, gitCache, gitLatestPatch

Далее будет более подробно рассмотрен процесс добавления файлов в конечный образ. Как упоминалось ранее, Docker-образ состоит из набора слоёв. Чтобы понимать, какие слои создает werf, представим последовательную сборку трех коммитов: 1, 2 и 3:

  • Сборка коммита 1. Исходя из конфигурации git mapping, все соответствующие файлы добавляются в один слой. Сам процесс добавления выполняется с помощью git archive. Получившийся слой соответствует стадии gitArchive.
  • Сборка коммита 2. Накладывается патч с изменениями файлов, в результате чего получается еще один слой. Получившийся слой соответствует стадии gitLatestPatch.
  • Сборка коммита 3. Файлы уже добавлены, и werf накладывает патч, обновляя слой gitLatestPatch.

Сборки для этих коммитов можно представить ​​следующим образом:

  gitArchive gitLatestPatch
Сделан коммит 1, сборка в 10:00 файлы согласно коммита 1 -
Сделан коммит 2, сборка в 10:05 файлы согласно коммита 1 файлы согласно коммита 2
Сделан коммит 3, сборка в 10:15 файлы согласно коммита 1 файлы согласно коммита 3

Пустая колонка между стадиями gitArchive и gitLatestPatch не случайна. С увеличением числа коммитов размер патча между первым и текущим коммитом будет расти и может стать довольно большим, что увеличит размер последнего слоя и общий размер всех стадий. Чтобы избежать роста размера стадии gitLatestPatch, werf использует стадию gitCache.

Чтобы проиллюстрировать, как werf работает с этими тремя стадиями и как изменится алгоритм по сравнению с приведенным ранее, возьмем пример работы с семью коммитами. Алгоритм будет следующий:

  • Сборка коммита 1. Файлы также добавляются в единый слой исходя из конфигурации git mapping. Процесс добавления выполняется с помощью git archive. Получившийся слой соответствует стадии gitArchive.
  • Сборка коммита 2. Размер патча между коммитами 1 и 2 не превышает 1 MiB, поэтому создается один слой стадии gitLatestPatch, содержащий изменения между коммитами 1 и 2.
  • Сборка коммита 3. Размер патча между коммитами 1 и 3 не превышает 1 MiB, поэтому слой, соответствующий стадии gitLatestPatch заменяется слоем, содержащим изменения между коммитами 1 и 3.
  • Сборка коммита 4. Размер патча между коммитами 1 и 4 превышает 1 MiB. Добавляется слой стадии gitCache, содержащий разность между коммитами 1 и 4.
  • Сборка коммита 5. Размер патча между коммитами 4 и 5 не превышает 1 MiB, поэтому создается один слой gitLatestPatch, содержащий разность между коммитами 4 и 5.

Описанный алгоритм иллюстрирует, что как только размер патча между gitArchive (gitCache, если существует) и gitLatestPatch превышает 1 MiB собирается или пересобирается стадия gitCache. Таким образом, накопленные изменения сохраняются в стадии gitCache, а размер стадии gitLatestPatch никогда не превышает 1 MiB. Такая логика позволяет сократить общий размер стадий в stages storage.

  gitArchive gitCache gitLatestPatch
Сделан коммит 1, сборка в 12:00 1 - -
Сделан коммит 2, сборка в 12:19 1 - 2
Сделан коммит 3, сборка в 12:25 1 - 3
Сделан коммит 4, сборка в 12:45 1 *4 -
Сделан коммит 5, сборка в 12:57 1 4 5

* — размер патча для коммита 4 превышает 1 MiB, поэтому на основе патча создается слой для стадии gitCache.

Сброс стадии gitArchive

В некоторых случая вам может потребоваться сбросить стадию gitArchive. Например, чтобы уменьшить общий размер стадии и размер конечного образа.

Чтобы показать, когда проблема нежелательного роста образа может стать актуальной, рассмотрим, хоть и довольно редкий, случай добавления файла размером 2GiB в git-репозиторий. При первой сборке произойдет добавление файла на стадии gitArchive. Допустим, затем, в результате оптимизаций, размер файла уменьшился до 1.6GiB. Новый коммит с такими изменениями попадёт в стадию gitCache. Размер образа станет 3.6GiB, 2GiB из которых — старая версия файла в кэше. Принудительная пересборка стадии gitArchive позволит уменьшить размер образа до 1.6GiB. Приведенный пример хоть и редкий, но позволяет наглядно продемонстрировать логику слоёв Docker образа.

Для сброса стадии gitArchive необходимо указать в сообщении к коммиту специальную подстроку [werf reset] или [reset werf].

Предположим, что в приведенном выше примере был сделан коммит 6 содержащий в сообщении подстроку [werf reset]. Тогда историю сборки можно представить примерно так:

  gitArchive gitCache gitLatestPatch
Сделан коммит 1, сборка в 12:00 1 - -
Сделан коммит 2, сборка в 12:19 1 - 2
Сделан коммит 3, сборка в 12:25 1 - 3
Сделан коммит 4, сборка в 12:45 1 *4 -
Сделан коммит 5, сборка в 12:57 1 4 5
Сделан коммит 6, сборка в 13:22 *6 - -

* — коммит 6 содержит строку [werf reset] в сообщении, что приводит к пересборке стадии gitArchive.

Rebase и git-стадии

Каждая git-стадия хранит служебные лейблы с SHA коммитами, которые использовались при сборки стадии. Эти коммиты будут использоваться при сборке следующей git-стадии при создании патчей (по сути это git diff COMMIT_FROM_PREVIOUS_GIT_STAGE LATEST_COMMIT для каждого git-mapping).

Если в стадии сохранён коммит, который отсутствует в git-репозитории (например, после выполнения rebase), werf пересоберёт эту стадию, используя актуальный коммит.