Билдеры с помощью Podman и Go

Post Thumbnail

Для моего пет-проекта потребовалось запускать сборку документации с помощью Hugo. Использовать бинарник показалось не самым удобным вариантом — хотелось большей универсальности. Здесь на помощь приходят контейнеры.

Контейнеры можно применять как универсальные модули, расширяющие функциональность системы. Данные можно передать в контейнер через подключенные каталоги или файлы, а результат работы получить в других файлах. Именно поэтому контейнеры так популярны в пайплайнах сборки практически любого современного CI/CD.

Общая схема работы проста как две копейки:

  1. Берём готовый контейнер для задачи или публикуем собственный.
  2. Пишем код, который запускает контейнер и обрабатывает результаты его работы.

В этой статье я расскажу о втором пункте — как запускать контейнеры из Go-кода. Есть два основных способа:

  • Использовать классический подход с exec.Command:
    cmd := exec.Command("docker", "run", "chainguard/hugo", "arg1", "arg2")
    
  • Воспользоваться API для работы с контейнерами.

Запуск Docker через API

Начнём с запуска контейнеров через Docker API. Хотя стоит отметить, что контейнеры можно запускать и без самого Docker (но об этом позже).

Для работы потребуется пакет:

"github.com/docker/docker/client"

Создаём экземпляр клиента:

import (
    "github.com/docker/docker/client"
    "github.com/docker/docker/api/types/container"
)

// ...

cli, err := client.NewClientWithOpts(
    client.WithVersion("1.41"), // Явно указываем версию API
    client.WithHost("unix:///var/run/docker.sock"),
    client.FromEnv,
)
if err != nil {
    return fmt.Errorf("error creating docker client: %s", err)
}

Готовим конфигурацию контейнера. Нужно указать используемый образ и подключить необходимые директории:

config := &container.Config{
    Image: "klakegg/hugo", // Образ для запуска
    Cmd:   []string{},     // Команда (если нужна)
    Tty:   true,
}

hostConfig := &container.HostConfig{
    Binds: []string{
        "/tmp/example:/src", // Только чтение
    },
    AutoRemove: true, // Автоудаление после остановки (--rm)
}

Затем создаём и запускаем контейнер:

// Создаём контейнер
resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, "")
if err != nil {
    return fmt.Errorf("error creating container: %s", err)
}

// Запускаем контейнер
if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
    return fmt.Errorf("error starting container: %s", err)
}

return nil

Docker — это просто и понятно, и в сети много материалов на эту тему. Поэтому перейдём к чему-то более интересному.

Запуск Podman через API

Docker уже не так актуален. Для своих пет-проектов я постепенно перехожу на Podman. Он предоставляет схожую среду для запуска контейнеров и поддерживает большинство команд Docker.

В отличие от Docker, Podman не требует отдельного демона, что делает его легче и безопаснее. Также он лучше поддерживает запуск контейнеров без прав root.

Хотя Podman менее распространён, он активно развивается Red Hat с упором на open-source и совместимость с Kubernetes. Правда, сообщество пока меньше.

Для работы с Podman из Go потребуется пакет:

"github.com/containers/podman/v5/pkg/bindings"

Подготовим соединение и загрузим образ:

"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/bindings/containers"
"github.com/containers/podman/v5/pkg/bindings/images"
"github.com/containers/podman/v5/pkg/specgen"
spec "github.com/opencontainers/runtime-spec/specs-go"

// ...

conn, err := bindings.NewConnection(context.Background(), "unix:///run/podman/podman.sock")
if err != nil {
    return fmt.Errorf("error connecting to podman: %w", err)
}

_, err = images.Pull(conn, "mirror.gcr.io/chainguard/hugo", nil)
if err != nil {
    return fmt.Errorf("error pulling image: %w", err)
}

Создаём контейнер и настраиваем подключение директории:

s := specgen.NewSpecGenerator("mirror.gcr.io/chainguard/hugo", false)
s.Mounts = []spec.Mount{
    {
        Source:      "/tmp/example",
        Destination: "/hugo",
        Type:        "bind",
    },
}
s.Command = []string{}
s.Remove = &remove // (--rm)

create, err := containers.CreateWithSpec(conn, s, nil)
if err != nil {
    return fmt.Errorf("error creating container: %w", err)
}

Запускаем контейнер:

if err := containers.Start(conn, create.ID, nil); err != nil {
    return fmt.Errorf("error starting container: %w", err)
}

Готово! Самый большой недостаток работы с Podman через Go — это отсутствие внятной документации. Кроме README в репозитории и старой статьи на официальном сайте, полезных материалов почти нет.

Ссылки