Привет, Хабр!
Сегодня поговорим о том, как тестировать Kubernetes Operators с помощью одного замечательного фреймворка. Функциональное тестирование — это не просто «хорошо бы», это необходимость. А вот как сделать качественное тестирование без боли? Здесь и поможет фреймворк Kubebuilder — инструмент, который упрощает тестирование и разработку операторов.
Немного про Kubebuilder
Kubebuilder построен на базе controller‑runtime и client‑go, двух мощнейших библиотек от самого Kubernetes.
Kubebuilder автоматически генерирует много boilerplate‑кода, конфигурации CRD и все остальное, что необходимо для полноценного оператора. А еще этот инструмент включает в себя тестовый фреймворк, который позволяет тебе не только писать контроллеры, но и тестировать их в изолированной среде. Мы поговорим о тестировании чуть позже, но пока — настроим окружение и запустим Kubebuilder.
Для начала понадобится установить несколько зависимостей. Прежде чем двигаться дальше, нужно будет установить Go, потому что Kubebuilder — это инструмент для Golang.
А сам Kubebuilder можно скачать с официального репозитория, есть команда, которая сделает все за тебя:
curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.4.0/kubebuilder_linux_amd64 -o kubebuilder chmod +x kubebuilder sudo mv kubebuilder /usr/local/bin/
Если ты на MacOS:
brew install kubebuilder
Проверяем установку:
kubebuilder version
Если все прошло успешно, увидишь версию Kubebuilder и то, что все нужные компоненты работают.
Теперь создадим новый проект оператора. Kubebuilder генерирует основу для оператора, начиная с командной строки. Сначала нужно инициализировать проект:
kubebuilder init --domain my.domain --repo github.com/your-username/my-operator
Эта команда создаст минимальную структуру проекта с основными файлами для Go-модуля и зависимостями. Параметр --domain
указывает доменное имя для твоих CRD. Например, если ты разрабатываешь оператора для своей компании, то можешь указать --domain yourcompany.com
.
Далее нужно создать API и контроллер для нашего оператора:
kubebuilder create api --group batch --version v1 --kind Job
Эта команда генерирует необходимые файлы для API и контроллера Kubernetes. Параметр --group
указывает на группу ресурсов (в данном случае это batch
), --version
на версию API, а --kind
на тип ресурса, с которым работает оператор (например, Job).
После этого мы видим новую структуру проекта с файлом API в api/v1/job_types.go
, где определена структура CRD, и файлом контроллера в controllers/job_controller.go
, где прописана логика работы оператора.
Теперь рассмотрим как писать логику для нашего оператора. Возьмем за основу пример с контроллером Job
. В файле job_controller.go
ты найдешь метод Reconcile
, который отвечает за то, как оператор реагирует на изменения в ресурсах. Здесь мы будем писать логику, что делать, когда Kubernetes вносит изменения в объект Job
.
Пример простейшей логики:
func (r *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) // Получаем ресурс var job batchv1.Job if err := r.Get(ctx, req.NamespacedName, &job); err != nil { log.Error(err, "unable to fetch Job") return ctrl.Result{}, client.IgnoreNotFound(err) } // Здесь пишем логику работы с ресурсом, например: // Проверяем, создан ли под для этого Job, если нет — создаем. return ctrl.Result{}, nil }
Здесь мы используем стандартный клиент Kubebuilder для получения объекта Job из кластера. После этого можно написать любую логику, которую ты хочешь внедрить в работу оператора.
Но мы здесь собрались для тестирования. Приступим.
Функциональное тестирование Kubernetes Operators с Kubebuilder
EnvTest — это lightweight-окружение для тестирования контроллеров Kubernetes, которое позволяет запускать тесты без развертывания полноценного кластера.
Первым делом нам нужно подготовить тестовое окружение. Для этого воспользуемся пакетом controller-runtime/pkg/envtest, который уже входит в состав Kubebuilder. Для начала, добавим его в зависимости нашего проекта:
go get sigs.k8s.io/controller-runtime/pkg/envtest
Затем создаем файл main_test.go
, где будет находиться наш тестовый код:
package main_test import ( "testing" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/log/zap" "github.com/onsi/gomega" ) var k8sClient client.Client var testEnv *envtest.Environment func TestMain(m *testing.M) { gomega.RegisterFailHandler(gomega.Fail) testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{"../config/crd/bases"}, } var err error cfg, err := testEnv.Start() if err != nil { panic(err) } k8sClient, err = client.New(cfg, client.Options{}) if err != nil { panic(err) } code := m.Run() testEnv.Stop() os.Exit(code) }
Что тут происходит:
-
envtest.Environment настраивает минимальный Kubernetes API-сервер и etcd для тестирования CRD и контроллеров.
-
client.New создает клиента для взаимодействия с объектами в кластере.
Этот код запускает тестовую среду и инициализирует API-сервер. Теперь можно приступать к написанию тестов.
Тестирование CRD
Начнем с простого теста, который проверяет, создается ли корректно наш CRD.
Допустим, мы работаем с ресурсом Job
. Пример кода для создания CRD и проверки, что оно корректно создано в кластере:
func TestCreateCRD(t *testing.T) { g := gomega.NewWithT(t) // Создаем объект CRD job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "test-job", Namespace: "default", }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox", Command: []string{"sleep", "10"}, }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } // Создаем объект в тестовой среде err := k8sClient.Create(context.Background(), job) g.Expect(err).NotTo(gomega.HaveOccurred()) // Проверяем, что объект действительно создан fetchedJob := &batchv1.Job{} err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "test-job", Namespace: "default"}, fetchedJob) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(fetchedJob.Name).To(gomega.Equal("test-job")) }
Этот тест проверяет, что при создании объекта Job
наш контроллер корректно его обрабатывает и объект появляется в кластере. Используя gomega как фреймворк для утверждений, можно убедиться, что ошибки не возникают, и объект действительно создан.
Взаимодействие с другими объектами в кластере
Теперь усложним задачу и проверим, как оператор взаимодействует с другими объектами Kubernetes. Например, оператор должен автоматически создавать ConfigMap
при создании определенного CRD. Вот как можно протестировать эту логику:
func TestConfigMapCreation(t *testing.T) { g := gomega.NewWithT(t) // Создаем CRD job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "job-with-configmap", Namespace: "default", }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } err := k8sClient.Create(context.Background(), job) g.Expect(err).NotTo(gomega.HaveOccurred()) // Проверяем, что ConfigMap создан configMap := &corev1.ConfigMap{} err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "job-configmap", Namespace: "default"}, configMap) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(configMap.Data["config"]).To(gomega.Equal("some-config-data")) }
Здесь проверяем, что при создании Job
, наш контроллер автоматически создает ConfigMap
, содержащий нужные данные.
Обработка событий и реакция на изменения
Последний важный момент — это проверка, как оператор реагирует на изменения в ресурсах и события. Например, если Job
завершился с ошибкой, оператор должен создавать уведомление или перезапускать Pod
.
Пример теста, который проверяет реакцию на событие:
func TestJobFailureEvent(t *testing.T) { g := gomega.NewWithT(t) // Создаем объект Job с ошибочным подом job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "failing-job", Namespace: "default", }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox", Command: []string{"false"}, // Под завершится с ошибкой }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } err := k8sClient.Create(context.Background(), job) g.Expect(err).NotTo(gomega.HaveOccurred()) // Проверяем, что оператор среагировал на событие и выполнил корректные действия // Например, оператор создает событие с ошибкой events := &corev1.EventList{} err = k8sClient.List(context.Background(), events, client.InNamespace("default")) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(events.Items).NotTo(gomega.BeEmpty()) g.Expect(events.Items[0].Reason).To(gomega.Equal("FailedJob")) }
Этот тест симулирует ошибку в Job
и проверяет, что оператор правильно реагирует на это событие, создавая запись о сбое.
Тестирование обновлений ресурсов
Например, оператор должен корректно обрабатывать изменения в уже созданных Job
. Допустим, при изменении конфигурации Job
наш оператор должен обновлять сопутствующий ConfigMap
. Вот как можно написать тест, который проверяет это:
func TestUpdateJobConfig(t *testing.T) { g := gomega.NewWithT(t) // Создаем исходный объект Job job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "update-job", Namespace: "default", }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } err := k8sClient.Create(context.Background(), job) g.Expect(err).NotTo(gomega.HaveOccurred()) // Изменяем Job job.Spec.Template.Spec.Containers[0].Image = "nginx:latest" err = k8sClient.Update(context.Background(), job) g.Expect(err).NotTo(gomega.HaveOccurred()) // Проверяем, что изменения были приняты и оператор обновил ConfigMap configMap := &corev1.ConfigMap{} err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "update-job-configmap", Namespace: "default"}, configMap) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(configMap.Data["config"]).To(gomega.Equal("updated-config-data")) }
Оператор реагирует на обновление существующего ресурса и выполняет соответствующие действия, как обновление ConfigMap
.
Тестирование зависимостей между ресурсами
Иногда оператор должен управлять несколькими ресурсами одновременно и поддерживать их состояние в синхронизации. Например, если один ресурс зависит от другого, оператор должен следить за тем, чтобы все компоненты оставались в актуальном состоянии. В следующем примере оператор следит за тем, чтобы Deployment
был в актуальном состоянии, когда изменяется связанный Job
:
func TestJobDeploymentSync(t *testing.T) { g := gomega.NewWithT(t) // Создаем Job job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "sync-job", Namespace: "default", }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } err := k8sClient.Create(context.Background(), job) g.Expect(err).NotTo(gomega.HaveOccurred()) // Создаем связанный Deployment deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "sync-deployment", Namespace: "default", }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "nginx"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app": "nginx"}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, } err = k8sClient.Create(context.Background(), deployment) g.Expect(err).NotTo(gomega.HaveOccurred()) // Проверяем, что Deployment синхронизирован с Job fetchedDeployment := &appsv1.Deployment{} err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "sync-deployment", Namespace: "default"}, fetchedDeployment) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(fetchedDeployment.Spec.Template.Spec.Containers[0].Image).To(gomega.Equal("nginx")) }
Этот тест проверяет, что оператор синхронизирует состояние Deployment
с изменениями в Job
.
Заключение
Kubebuilder дает возможность тестировать сложные сценарии в легковесной среде, не поднимая полноценный Kubernetes кластер.
Скоро в рамках онлайн-курса «Инфраструктурная платформа на основе Kubernetes» пройдут открытые уроки:
-
8 октября: «Хранение данных в Kubernetes: Volumes, Storages, Stateful-приложения». Узнать подробнее
-
21 октября: «Service Mesh: Введение в Istio». Узнать подробнее
ссылка на оригинал статьи https://habr.com/ru/articles/848582/
Добавить комментарий