Трендовые github проекты в нашем телеграм канале. Подпишись → Управляемое окружение для проверки Kubernetes-контроллера
Оператор Kubernetes работает с системой связанных ресурсов и выполняет изменения асинхронно. Один Custom Resource может создавать Deployment, Pod, Service и другие Custom Resource. Поэтому тест контроллера должен учитывать состояние зависимостей, события API и задержки reconciliation loop.
Unit-тесты хорошо покрывают чистую бизнес-логику: преобразование спецификаций, расчёт параметров, обработку ошибок. Интеграционный слой проверяет другое: как контроллер взаимодействует с Kubernetes API, получает события и приводит объекты к ожидаемому состоянию. Для этой задачи удобно использовать EnvTest из экосистемы controller-runtime.
Что требуется проверить у оператора
У Custom Resource обычно есть иерархия ресурсов. Например, объект, описывающий экземпляр базы данных, может управлять Secret, StatefulSet, Service и собственными CR. Тесту важно создать нужную комбинацию объектов и убедиться, что контроллер корректно реагирует на неё.
В production это происходит в полноценном кластере. Поднимать его для каждого интеграционного теста затратно: такие проверки медленнее, требуют больше ресурсов и зависят от работы сторонних контроллеров. Простая эмуляция через fake-клиент тоже имеет ограничения: она не даёт поведение настоящего API-сервера и не покрывает механизм watch.
EnvTest занимает промежуточное место. Он запускает локальные etcd и kube-apiserver, но без controller-manager, scheduler и cloud-controller-manager. Тест получает настоящий Kubernetes API в ограниченном и полностью контролируемом окружении.
Возможности EnvTest
Главное свойство EnvTest — прямое управление состоянием кластера. В тесте можно создать объект, изменить его status, удалить зависимость или подготовить ошибочный сценарий. Внешние контроллеры не вмешиваются в эксперимент, поэтому исходные условия остаются воспроизводимыми.
Локальный API-сервер поддерживает watch. Это важно для контроллеров: именно события создания, обновления и удаления ресурсов запускают reconciliation. Тест способен проверить цепочку целиком — от записи объекта через Kubernetes-клиент до реакции запущенного контроллера.
EnvTest также умеет устанавливать CRD перед началом набора тестов. Пользовательские типы после этого создаются и валидируются через API так же, как стандартные Pod или Service. Это даёт возможность проверить манифесты CRD и взаимодействие контроллера с собственными ресурсами.
Отдельный полезный сценарий — admission webhooks. Mutating webhook меняет объект до сохранения, а Validating webhook отклоняет недопустимую конфигурацию. EnvTest позволяет настроить webhook и отправить запрос настоящему API-серверу. В результате тест видит итоговый объект либо ошибку валидации, не разворачивая полный кластер.
Базовая структура окружения
Окружение запускают один раз перед набором тестов и останавливают после него. В Go это удобно описать в BeforeSuite и AfterSuite:
var testEnv *envtest.Environment
var cfg *rest.Config
var _ = BeforeSuite(func() {
testEnv = &envtest.Environment{
BinaryAssetsDirectory: "../bin/k8s/linux-amd64",
CRDDirectoryPaths: []string{"../config/crd/bases"},
}
var err error
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
})
var _ = AfterSuite(func() {
Expect(testEnv.Stop()).To(Succeed())
})
Из cfg создают обычный Kubernetes-клиент и запускают тестируемый manager или контроллер. Путь к бинарным файлам API-сервера и etcd следует закрепить в CI: это исключит расхождения между локальным запуском и pipeline. CRD загружаются до создания объектов, иначе API-сервер не распознает пользовательский тип.
Поскольку EnvTest не запускает стандартные контроллеры Kubernetes, их эффекты нужно моделировать явно. Если логика зависит от готового Pod, заполненного status или связанного объекта, тест создаёт это состояние сам. Такой подход делает проверяемые предпосылки видимыми в коде.
Сценарий проверки reconciliation
Полезный тест описывает наблюдаемое поведение. Например, cloud provider после появления Node должен заполнить spec.providerID, добавить внутренний IP-адрес и снять taint node.cloudprovider.kubernetes.io/uninitialized. Последовательность проверки выглядит так:
- Подготовить EnvTest и запустить компоненты, вызывающие тестируемую логику.
- Создать Node с taint, который запрещает планирование.
- Дождаться обработки контроллером.
- Проверить
providerID, адреса и отсутствие taint. - Очистить временные объекты или namespace.
Контроллер нельзя проверять мгновенным Get сразу после Create: reconciliation выполняется отдельно от запроса API. Фиксированная пауза вроде time.Sleep замедляет тесты и делает их нестабильными. Гораздо точнее ожидать именно требуемое состояние.
Ginkgo и Gomega для асинхронных проверок
Ginkgo помогает организовать сценарии в дерево Describe, Context, When и It. Блоки BeforeEach и AfterEach создают изолированный namespace для каждого кейса и удаляют его после завершения. Благодаря этому тесты читаются как описание поведения контроллера, а данные одного сценария не влияют на другой.
Gomega дополняет эту структуру матчерами и polling-проверками. Eventually повторяет функцию до выполнения условия или истечения таймаута, а Consistently подтверждает, что условие сохраняется в течение заданного периода. Проверку обработки Node можно выразить так:
Eventually(func(g Gomega) {
node := &corev1.Node{}
err := client.Get(ctx, client.ObjectKey{Name: "test-node"}, node)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(node.Spec.ProviderID).NotTo(BeEmpty())
g.Expect(node.Status.Addresses).NotTo(BeEmpty())
}, 30*time.Second, 500*time.Millisecond).Should(Succeed())
После завершения ожидания стоит выполнять точные assertions: сравнить значение providerID, тип и адрес Node, проверить отсутствие конкретного taint. Для больших Custom Resource полезны частичные сравнения структур через MatchFields и проверки вложенных полей через HaveField. Коллекции taint, labels и условий удобно проверять матчерами ContainElement и HaveEach.
Практические границы теста
EnvTest не заменяет end-to-end-проверки. В нём отсутствуют scheduler и обычные контроллеры, поэтому сценарии с реальным размещением Pod, сетевой интеграцией или поведением облачной инфраструктуры требуют отдельного тестового кластера. Зато EnvTest быстро обнаруживает ошибки в reconciliation, CRD, watch и webhook ещё до запуска тяжёлых проверок.
Устойчивый набор тестов строится слоями: unit-тесты проверяют локальную логику, EnvTest покрывает контракт контроллера с Kubernetes API, а небольшое количество e2e-сценариев подтверждает интеграцию компонентов в полноценном окружении. Такое разделение ускоряет обратную связь и сохраняет контроль над сложностью Kubernetes-оператора.