Python package registry в GitLab

Kate

Administrator
Команда форума
О чем статья: при разработке проектов, и, особенно, распределенных приложений, возникает необходимость использования некоторых частей приложения в качестве отдельных модулей. Например скомпилированные классы для gRPC, модули для работы с БД, и многое другое, могут применяться в неизменном виде в кодовой базе десятка микросервисов. Оставив за скобками копипасту, как "хорошую" плохую практику. Можно рассмотреть git submodules, однако, такое решение не очень удобно тем, что, во-первых, нужно предоставлять разработчикам доступ к конкретным репозиториям с кодовой базой, во-вторых, нужно понимать, какой коммит надо забрать себе, и в-третьих установка зависимостей для кода, включенного в проект как субмодуль, остается на совести разработчика. Менеджеры пакетов (pip, или, лучше, poetry), умеют разрешать зависимости из коробки, без лишних действий, и, в целом, использование менеджера пакетов значительно проще, чем работа с субмодулем. В статье рассмотрим, как организовать реестр пакетов в GitLab, а также различные подводные камни, поджидающие на пути к удобной работе с ним.

Для кого: статья будет полезна разработчикам, столкнувшимся с необходимостью организации приватного реестра пакетов, в качестве руководства по организации такого реестра в GitLab.

О реестрах пакетов в целом, и почему именно GitLab​

Шаги по организации частного реестра пакетов отражены в документации. По сути, в описанном варианте размещения, реестр представляет собой директорию, раздаваемую по HTTP, и содержащую .tar.gz и\или .whl пакеты, распределенные по папкам, соответствующим именам пакетов. Автоматическая загрузка пакетов в репозиторий в таком варианте является задачей "на подумать". В случае с GitLab становится возможным организовать работу таким образом, что кодовая база пакетов и реестр пакетов хранятся в одном пространстве, что дает следующие возможности:

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

Подготовка реестра​

Мы будем использовать отдельный проект, как реестр пакетов, создаваемых нами. Достаточно просто создать новый проект, и, опционально, обозвать его так, чтобы было понятно, что это реестр пакетов.

Список загруженных пакетов и дополнительную информацию о них можно посмотреть, перейдя в пункт Packages & Registries > Package Registry.

26aa697eafc3b048c41f0c3ecc2c57f3.png

К слову, GitLab может быть реестром не только PyPI-пакетов, но и npm, NuGet, и т.д.

Учетные данные

Для загрузки пакетов в реестр, и их установки в проекте, нам учетные данные. В документации указано, что можно использовать следующие виды токенов:

  • Personal access token - аутентифицирует владельца токена в соответствии с правами на репозиторий. Данный вид токенов лучше использовать для выдачи доступа конкретных пользователей к реестру. В этом случае пользователю необходимо назначить права на чтение из реестра, а access token генерируется ими самостоятельно.
  • Project access token - подойдет в случае, если необходимо дать доступ к реестру пакетов большому числу пользователей. Если использовать этот вид токена для передачи пользователям - возникает опасность “утечки” токена. Да и банально отобрать права у конкретного пользователя (в случае использования одного токена) не получится. Либо все, либо ничего.
  • Group access token - позволяет получать доступ ко всем пакетам в группе проектов.
В моем случае используется следующая схема:

  • Для загрузки пакетов в реестр используется Project access token с правами write_registry. Данный токен используется исключительно при автоматической сборке пакетов. Прописан в Variables для репозиториев, содержащих исходный код модулей;
  • Пользователи получают доступ с применением Personal access token.

Создание пакета​

Создадим новый проект, в котором будет храниться код модуля. Для сборки пакета будем использовать poetry.

Для начала установим poetry.

pip install poetry
Далее, если репозиторий для модуля уже создан, и там есть код, можно воспользоваться командой:

poetry init
В противном случае можно создать новый проект (вместе со структурой каталогов) командой:

poetry new
И в том и в другом случае в проекте появится файл pyproject.toml, который используется poetry для хранения информации о проекте (наименование, описание, зависимости).

Если у вас уже есть файл requirements.txt и вы не хотите руками переносить все, что наросло за время разработки, можно перенести зависимости в poetry одной командой:

cat requirements.txt | xargs poetry add
Теперь давайте заглянем в pyproject.toml:

[tool.poetry]
name = "hello-world-package"
version = "0.1.3"
description = ""
authors = ["Dmitry <dmitry8912@gmail.com>"]
readme = "README.md"
packages = [{include = "hello_world_package"}]

[tool.poetry.dependencies]
python = "^3.10"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Здесь нужно обратить внимание на первую секцию файла - [tool.poetry], в ней описывается общая информация: имя пакета, версия, описание, и поддиректории, в которых непосредственно находится код модуля (за это отвечает строчка packages = [{include = "hello_world_package"}]). Если файлы с кодом находятся в корне проекта - перенесите их в отдельную поддиректорию, и укажите ее имя в строчке packages.

Также создайте README.md с детальным описанием пакета.

Этих настроек достаточно для построения файлов пакета.

Команда poetry build создаст в корне проекта папку dist, в которой будут размещены .tar.gz и .whl файлы пакетов (в соответствии с версиями).

Версионирование​

Пакет, как правило, не живет по принципу “выстрелил и забыл”. В процессе разработки в исходный код могут вноситься изменения, и возникает необходимость отличать одну версию от другой. Версия, хранимая в pyproject.toml, является текущей, и при пересборке пакета без обновления версии содержимое папки dist (для конкретной версии, разумеется) будет просто перезаписано. Поэтому, перед построением новой версии, нужно изменить номер в pyproject.toml. Руками оно, понятное дело, веселее, но предлагаю отойти от “обновления версий курильщика”. В poetry доступна команда poetry version с опциональными аргументами (patch, major, minor…) инкрементирующими версию в соответствии с правилом для каждого типа инкремента.

Например, для выпуска новой версии, содержащей незначительные багфиксы, и не ломающей совместимость, можно использовать poetry version patch.

При выходе новой версии, содержащей значительные изменения - poetry version major.

Забегая вперед замечу, что GitLab не примет пакет, если такая версия уже есть в его реестре, поэтому “поднимать” версию перед загрузкой пакета - необходимо.

Загрузка пакетов в реестр​

Загрузка пакета в реестр возможна с помощью команды poetry publish, однако на своем опыте я неизменно получал 422-ю ошибку при попытке отправить изменения. Поэтому расскажу о том, как загрузить пакет в реестр, используя пакет twine. Сначала, конечно же, установим нужный пакет:

pip install twine
Нам понадобится создать файл .pypirc для хранения информации о реестре (адрес, токен для доступа):

[distutils]
index-servers =
gitlab

[gitlab]
repository = https://<gitlab_address>/api/v4/projects/<project_id>/packages/pypi
username = <token_name>
password = <token>
Подставив в адрес реестра, логин и пароль нужные значения, можем загрузить собранный ранее пакет.

[root@srv-dev-core0 hello_world_package]$ python -m twine upload --repository gitlab dist/* --config-file ./.pypirc

Uploading distributions to http://gitlab.local/api/v4/projects/29/packages/pypi
Uploading hello_world_package-0.0.1-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.0/4.0 kB • 00:00 • ?
Uploading hello_world_package-0.0.1.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.8/3.8 kB • 00:00 • ?
Информация о пакете должна отобразиться на странице реестра пакетов.

Установка из реестра​

Собственно, устанавливать пакет я также рекомендую через poetry, поскольку pip не хранит информацию о том, откуда пакет был взят, и pip install -r requirements.txt будет искать пакет не там, где надо. Либо придется делить зависимости на части, хранить в отдельный requirements.txt, и указывать pip`у откуда что нужно брать. Poetry же хранит информацию об источнике пакета в pyproject.toml. При развертывании проекта остается только добавить данные для аутентификации.

В общем случае, порядок действий следующий:

  1. Добавляем новый адрес реестра в список (gitlab в данном случае - просто имя)
poetry source add gitlab https://<gitlab_address>/api/v4/projects/<project_id>/packages/pypi/simple
  1. Добавляем данные для аутентификации
poetry config http-basic.gitlab <token_name> <token>
  1. Указываем poetry откуда поставить пакет
poetry add --source gitlab <your_package_name>
В pyproject.toml, в информации о пакете, появятся данные об удаленном реестре, и связи пакета с конкретным реестром.

your_package = {version = "^0.1.2", source = "gitlab"}

...

[[tool.poetry.source]]
name = "gitlab"
url = "https://<gitlab_address>/api/v4/projects/<project_id>/packages/pypi/simple"
default = false
secondary = false
Теперь, для установки пакета другими разработчиками им нужно сообщить:

  • Адрес реестра;
  • Имя пакета;
  • Данные для доступа (либо указать на необходимость самостоятельной генерации).

CI\CD​

Напоследок немного автоматизации. Гораздо удобнее, когда в каждом репозитории с кодом модулей настроена автоматическая сборка и загрузка в реестр прямо в CI\CD-пайплайне.

Для этого можно использовать следующий скрипт:

#!/bin/sh

# Скрипт оперирует следующими env-переменными
# TOKEN_VALUE - токен для доступа к реестру на запись (привилегия write_registry)
# BUMP_VERSION - patch|minor|major|..., или как именно инкриментировать версию

echo 'Preparing .pypirc'
# В .pypirc для token вписано значение {REGISTRY_TOKEN}, заменяемое через sed "на лету"
sed -i "s/{REGISTRY_TOKEN}/${TOKEN_VALUE}/g" .pypirc
echo "Package building stage"

# Скрипт version.py получает последнюю загруженную версию через GitLab API
VERSION=$(python version.py)
CURRENT_VERSION=$(cat pyproject.toml | grep -n version | cut -d : -f 1)
# Полученная версия прописывается в pyproject.toml
sed -i "${CURRENT_VERSION}s/.*/version = \"${VERSION}\"/" pyproject.toml

# Версия пакета поднимается
echo "Bumping version with rule ${BUMP_VERSION}"
poetry version $BUMP_VERSION

# Пакет собирается и загружается в реестр
echo "Building package"
poetry build

echo "Uploading package to registry"
python -m twine upload --repository gitlab dist/* --config-file ./.pypirc\
Данный скрипт работает внутри docker-контейнера, запускаемого раннером. В контейнер в качестве env-переменных передаются данные для аутентификации. В файл .pypirc заранее внесена информация об адресе реестра, и остается только заменить токен.

Но возникает проблема с обновлением версии. Два разработчика могут установить одну и ту же версию в pyproject.toml, и, в результате, какие-то изменения могут потеряться, так как GitLab не примет новый пакет с уже существующей в реестре версией. Выход из этой ситуации видится только один - определять последнюю актуальную версию в реестре, записывать ее в pyproject.toml, поднимать версию с помощью poetry version [patch|minor|major|...].

Для получения пакетов из реестра можно воспользоваться скриптом:

import json
import os
import requests

if __name__ == '__main__':
token = os.getenv('TOKEN_VALUE')
package_name = os.getenv('CI_PROJECT_NAME')
result = None

try:
result = requests.get(f'https://<gitlab_address>/api/v4/projects/<proejct_id>/packages?sort=desc&package_name={package_name}',
headers={'Authorization': f"Bearer {token}"})
except Exception as e:
print(e)
exit(1)

if result.status_code == 200:
data = json.loads(result.content)
print(data[0]['version'])
exit(0)
Скрипт, используя GitLab API, получит список пакетов в json, отсортированный по убыванию (самый последний загруженный пакет будет первым). Остается только подменить версию в pyproject.toml, поднять ее, и загрузить пакет в реестр.

Заключение​

Управление PyPI-пакетами в GitLab достаточно простое. Буквально за пару часов можно заменить копипастнутые\загруженные через git submodules части приложения на более изящное решение, которое, к тому же, обновляет версии при каждом пуше в репозиторий.


 
Сверху