В мире существует множество различных систем для хранения кода. Различаются они как протоколом работы: Git, Mercurial, Bazaar, — так и форматом работы (cloud, self-hosted). Но есть и другой важный параметр при их выборе: степень интеграции с сопутствующим инструментарием: issue tracker, CI/CD, wiki и т.д. Так сложилось, что мы в компании предпочитаем GitLab (вариант on-premise) и по умолчанию, если клиент не против, предлагаем ему это решение. В статье я расскажу про миграцию из Gitea c Jenkins в GitLab и о том, с какими сложностями пришлось столкнуться, а заодно поделюсь Python-скриптами, которые пригодились для успеха этого мероприятия. Важно! В статье рассматривается Gitea 1.13.4 и GitLab 13.8. В новых версиях могут быть какие-то улучшения, которые облегчат перенос, но на момент миграции эти версии были актуальными. Gitea — легковесная Open Source-система для управления Git-репозиториями. Это форк другой легковесной системы — Gogs. Она интересна возможностью сочетать в одном инстансе несколько организаций с довольно широким спектром настроек прав доступа и GitHub-подобным API. Проект популярен и имеет более 25 тысяч звёзд на GitHub. Среди спонсоров — DigitalOcean и Equinix, а также поддержать Gitea можно на Open Collective (и проследить, куда пойдут ваши средства).  Из плюсов хочется отметить, что Gitea очень проста как в установке и настройке, так и в бэкапе. Систему можно запустить на любом относительно современном компьютере, и она предложит некоторые встроенные сервисы: wiki-страницы, задачи и проекты, т.е. todo-листы. Но эта простота может иметь и обратную сторону. В приложении нет готового CI/CD, и для реализации этих механизмов приходится использовать стороннее решение. В нашем случае у клиента эту роль играл Jenkins, для которого существует специальный плагин. Однако данный выбор был скорее историческим наследием, чем технической необходимостью. CI/CD с ним был не очень удобен в работе. Для оптимизации процесса деплоя мы сошлись на переходе на GitLab, а это означало и замену самой Gitea, функции которой теряли смысл. Также в процессе работ были найдены мелкие проблемы, которые мешали миграции. Исходное состояние — Gitea 1.13.4 со 165 репозиториями и 94 пользователями. Всё это было разложено по 18 организациями, а некоторые репозитории были личными. Кроме того, клиент не хотел терять историю pull requests, а их было достаточно много: в некоторых репозиториях — более 5 тысяч. Несмотря на то, что самих пользователей, а также организаций и групп было не так уж и много, «накликивать» такое (да ещё и без ошибок) — это очень долго.   Да и вообще, мы же инженеры! Поэтому пошли путем автоматизации и ниже расскажем о проблемах, которые пришлось преодолеть. Попутно мы научимся работе с REST API обоих решений: и Gitea, и GitLab. Посему этот опыт может оказаться полезным не только для непосредственной миграции, но и повседневных задач. Итак, перейдем к самой миграции. Оба проекта имеют развитый API. Выберем клиенты для работы с API обеих систем: Для Gitea я выбрал Python-вариант giteapy. К сожалению, он не так хорош, как аналогичный для GitLab. (В процессе повествования ещё встретятся соответствующие ремарки.) Для GitLab — python-gitlab. Для подключения к GitLab используется единый класс: В giteapy для каждого раздела API есть свой подкласс. Их общее количество — 6, но нам потребуются только 4: AdminApi, OrganizationApi, RepositoryApi, UserApi. Вот как будет выглядеть код (полные итоговые листинги — см. в репозитории flant/examples): И уже тут ожидал первый сюрприз: Вы спросите: что же не так? Очень просто: в выводе API не видно, заблокирован пользователь или нет. Изучение всех возможных методов привело к необходимости запрашивать эту информацию из базы Gitea. Благо, это не такая уж и проблема — она решается простым запросом: Заблокированных пользователей было немного (около 10), и мы перенесли их вручную. Логично предположить, что если стоит задача миграции, нам требуется перенести еще и SSH-ключи пользователей. В библиотеке клиента API Gitea описан метод user_current_get_key. Однако он работает странным образом: если у пользователя много ключей, он возвращает всего один ключ; если ключей нет — он возвращает ошибку 404. При первичном переносе мы это обстоятельство не учли и использовали вызов API как есть. В результате были получены неверные ключи — их не хватало. Поэтому настоятельно советую использовать метод user_list_keys. Однако и с ним нас ждал подвох: в базе отсутствует уникальный индекс по fingerprint. Из-за этого в момент переноса пришлось решать конфликтные ситуации при импорте ключей в GitLab. К счастью, эти ключи были в основном у заблокированных пользователей. Поэтому мы приняли простое решение — удалить все ключи у заблокированных пользователей, которые были перенесены. В GitLab это делается вот так: Следующий шаг — выдача верных прав. Организации Gitea преобразуются в группы GitLab, а команды преобразуются в права доступов. Получив все команды из API, мы согласовали с клиентом матрицу сопоставления прав: Что произойдет после этих манипуляций? Перенос пользователей «породит» массу почтовых сообщений: о создании пользователя, о предоставлении доступа и т.п. Поэтому стоит заранее решить, оставлять эту корреспонденцию или же перенаправить в чёрную дыру. Если вы не хотите рассылать почту и делать временные пароли, которые потом раздадите клиентам, создание пользователя будет выглядеть так: Кроме того, понадобится почтовый сервер, который будет все письма отправлять в /dev/null. Для этого подойдёт следующий конфиг Postfix: В нашем случае клиент сначала попросил сделать вариант с отправкой почты в «черную дыру», а затем подтвердить все почтовые адреса. Если не прописать при создании пользователя skip_confirmation, то в дальнейшем, если потребуется подтвердить пользователей, это надо делать вручную. К сожалению, это известный баг GitLab: для подтверждения придется лезть в консоль Rails. Проблемы, выявленные в Gitea за время этих операций: отсутствие важной информации о свойствах пользователя в API; неочевидные методы API для работы с ключами; дублирующиеся ключи. С другой стороны, в GitLab есть проблема с подтверждением email. Итоговый скрипт миграции пользователей можно найти в нашем репозитории с примерами. Теперь, когда готово дерево пользователей, можно произвести миграцию репозиториев. GitLab умеет импортировать проекты Gitea уже с версии 8.15. Однако всё не так просто, как хотелось бы. Начнём с того, что надо добавить нашего пользователя, которого мы завели в Gitea для миграции, во все репозитории. В этом поможет такой скрипт. Здесь опущено подключение к API, так как пример был уже выше. Можно заметить еще одну проблему клиента к Gitea: в разных местах попеременно используется либо User ID, либо User Login. Местами это неудобно, т.к. требует постоянно сверяться с документацией. Теперь, когда все репозитории видны в API, можно попробовать начать импортировать их в GitLab. Казалось бы, процесс не будет сложным: зайти в создание нового проекта, нажать кнопку импорта… но так ничего не импортируется. В реальности проблемы будут встречаться на каждом шагу. Для начала стоит отметить, что GitLab знает, что API Gitea устроен аналогично API GitHub, и использует Octokit Gem. Однако имплементация API у нас не является полной. Поэтому импорт периодически спотыкается. Есть 2 основных момента: Отсутствие rate_limit в API; Путаница с путями, когда Octokit пытался добавить мусорный префикс в запросы. К счастью, исходный инстанс Gitea был за реверсным прокси на основе nginx, так что удалось дописать его конфигурацию, чтобы обойти эти проблемы. Первым делом разберемся с rate limit. Это встроенный метод, через который Octokit спрашивает, как часто он может отправлять запросы в API. При запросе к корневым методам Gitea отдаёт 404, что клиентом воспринимается как Unimplemented: Однако запрос к RepoApi возвращает 401, из-за чего импорт останавливается: Чтобы обойти это, сделаем такой location в nginx: Все запросы вернут 404 — миграция пойдёт без проблем. Вторая проблемы была интереснее: Octokit делал запросы с мусорным префиксом. Допустим, у нас есть org1 с project1 и есть пользователь i.ivanov с project2 в личном пространстве имён. В логе nginx появятся странные запросы: Легко заметить, что в случае с организацией Octokit добавил префикс /org1/project1, а в случае с пользовательским репозиторием были добавлены 2 префикса: /i. /i.ivanov/project2 В общем, пришлось написать rewrite, который исправлял неверные запросы: После этого проблемные запросы были переписаны и, наконец-то, импорт прошел успешно! Останется последняя проблема: у нас 160 проектов, которые надо импортировать в верные namespaces. К сожалению, импорт через API не позволяет сделать полный импорт и поддерживает только импорт архива, который не даёт загрузить merge requests, issues и другие вспомогательные вещи. Пришлось сделать скрипт, который бы работал с GitLab WebUI и отправлял запросы на импорт. Я предпочел сделать скрипт для Selenium: Затем, используя полученную сессию, запросим страницу импорта: А теперь начнём импорт всех репозиториев. Для этого инициализируем подключение к Gitea и начнём импортировать репозитории: Казалось бы, вот и всё! Но это, к сожалению, не так. После миграции мы временно подключили GitLab к Jenkins и получили ошибку: Оказалось, что при переносе были потеряны Git references. Чтобы это исправить, мы решили во все ветки, которые имеют открытые merge requests, сделать пустые коммиты. Это действие пересоздаст references. Вот реализация такого workaround: После выполнения скрипта всё заработало корректно. Полные листинги Python-скриптов, приведённых в статье, доступны в репозитории flant/examples. Миграция из Gitea в GitLab, несмотря на кажущуюся простоту, оказалась непростой задачей. Чтобы добиться результата, пришлось написать ряд скриптов и пройти через множество неожиданных нюансов из-за неполноты в реализации и совместимости разных API. Тем не менее, это удалось, и надеюсь, что полученный опыт поможет кому-нибудь в миграции и облегчит жизнь. Читайте также в нашем блоге: «GitLab CI: 6 фич из последних релизов, которых мы так ждали»; «Динамическая сборка и деплой Docker-образов с werf на примере сайта версионированной документации»;  «Сборка и деплой однотипных микросервисов с werf и GitLab CI». DevOps-инженер