Лабораторная 5.2: Создание валидирующего вебхука
Связанный урок: Урок 5.2: Реализация валидирующих вебхуков
Навигация: ← Предыдущая лабораторная: Контроль допуска | Обзор модуля | Следующая лабораторная: Мутирующие вебхуки →
Цели
- Сгенерировать каркас валидирующего вебхука с помощью kubebuilder
- Реализовать пользовательскую логику валидации
- Протестировать на корректных и некорректных ресурсах
- Предоставить информативные сообщения об ошибках
Предварительные требования
Упражнение 1: генерация каркаса валидирующего вебхука
Задача 1.1: перейдите к вашему оператору
# Navigate to your Database operator
cd ~/postgres-operator
Задача 1.2: создайте валидирующий вебхук
# Create validating webhook
kubebuilder create webhook \
--group database \
--version v1 \
--kind Database \
--programmatic-validation
Обратите внимание:
- Какие файлы были созданы?
- Что было изменено?
Задача 1.3: изучите сгенерированный код
# Check the generated webhook file
cat internal/webhook/v1/database_webhook.go
# Check webhook markers
grep "kubebuilder:webhook" internal/webhook/v1/database_webhook.go
Обратите внимание на структуру:
- Код вебхука находится в каталоге
internal/webhook/v1/ - Использует структуру
DatabaseCustomValidator - Реализует интерфейс
webhook.CustomValidator - Методы принимают
context.Contextпервым параметром
Упражнение 2: реализация логики валидации
Задача 2.1: добавьте ValidateCreate
Отредактируйте internal/webhook/v1/database_webhook.go:
package v1
import (
"context"
"fmt"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
databasev1 "github.com/example/postgres-operator/api/v1"
)
var databaselog = logf.Log.WithName("database-resource")
// SetupDatabaseWebhookWithManager registers the webhook for Database in the manager.
func SetupDatabaseWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&databasev1.Database{}).
WithValidator(&DatabaseCustomValidator{}).
Complete()
}
// +kubebuilder:webhook:path=/validate-database-example-com-v1-database,mutating=false,failurePolicy=fail,sideEffects=None,groups=database.example.com,resources=databases,verbs=create;update,versions=v1,name=vdatabase-v1.kb.io,admissionReviewVersions=v1
// DatabaseCustomValidator struct is responsible for validating the Database resource
// when it is created, updated, or deleted.
type DatabaseCustomValidator struct {
// Add more fields as needed for validation
}
var _ webhook.CustomValidator = &DatabaseCustomValidator{}
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Database.
func (v *DatabaseCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
database, ok := obj.(*databasev1.Database)
if !ok {
return nil, fmt.Errorf("expected a Database object but got %T", obj)
}
databaselog.Info("Validation for Database upon creation", "name", database.GetName())
// Validate image is PostgreSQL
if !strings.Contains(database.Spec.Image, "postgres") {
return nil, fmt.Errorf("spec.image must be a PostgreSQL image, got %s", database.Spec.Image)
}
// Validate replicas and storage relationship
if database.Spec.Replicas != nil && *database.Spec.Replicas > 5 {
if database.Spec.Storage.Size == "10Gi" {
return nil, fmt.Errorf("replicas > 5 requires storage >= 50Gi, got %s", database.Spec.Storage.Size)
}
}
// Validate database name format
if len(database.Spec.DatabaseName) > 63 {
return nil, fmt.Errorf("spec.databaseName must be <= 63 characters, got %d", len(database.Spec.DatabaseName))
}
return nil, nil
}
Задача 2.2: добавьте ValidateUpdate
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Database.
func (v *DatabaseCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
database, ok := newObj.(*databasev1.Database)
if !ok {
return nil, fmt.Errorf("expected a Database object for the newObj but got %T", newObj)
}
oldDB, ok := oldObj.(*databasev1.Database)
if !ok {
return nil, fmt.Errorf("expected a Database object for the oldObj but got %T", oldObj)
}
databaselog.Info("Validation for Database upon update", "name", database.GetName())
// Prevent reducing storage size
oldSize := parseStorageSize(oldDB.Spec.Storage.Size)
newSize := parseStorageSize(database.Spec.Storage.Size)
if newSize < oldSize {
return nil, fmt.Errorf("cannot reduce storage from %s to %s", oldDB.Spec.Storage.Size, database.Spec.Storage.Size)
}
// Prevent changing database name
if oldDB.Spec.DatabaseName != database.Spec.DatabaseName {
return nil, fmt.Errorf("cannot change spec.databaseName from %s to %s", oldDB.Spec.DatabaseName, database.Spec.DatabaseName)
}
return nil, nil
}
// Helper function to parse storage size (e.g., "10Gi" -> 10)
func parseStorageSize(size string) int64 {
if strings.HasSuffix(size, "Gi") {
num := strings.TrimSuffix(size, "Gi")
val, err := strconv.ParseInt(num, 10, 64)
if err != nil {
return 0
}
return val
}
return 0
}
Задача 2.3: добавьте ValidateDelete (опционально)
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Database.
func (v *DatabaseCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
database, ok := obj.(*databasev1.Database)
if !ok {
return nil, fmt.Errorf("expected a Database object but got %T", obj)
}
databaselog.Info("Validation for Database upon deletion", "name", database.GetName())
// Add any deletion validation logic
// For example, prevent deletion if database has important data
return nil, nil
}
Упражнение 3: генерация манифестов
Задача 3.1: сгенерируйте манифесты вебхука
# Generate manifests
make manifests
# Check webhook configuration was generated
ls -la config/webhook/
# Examine webhook configuration
cat config/webhook/manifests.yaml
Задача 3.2: проверьте конфигурацию вебхука
# Check the configuration
cat config/webhook/manifests.yaml | grep -A 20 "ValidatingWebhookConfiguration"
Упражнение 4: тестирование валидирующего вебхука
Понимание тестирования вебхуков
В отличие от логики контроллера, вебхуки нельзя легко протестировать через make run, потому что:
- Вебхукам нужны TLS-сертификаты
- API-серверу Kubernetes (внутри кластера) нужно достучаться до эндпоинта вебхука
- При локальном запуске API-сервер не может обратиться обратно к вашему localhost
Два подхода для разработки:
| Подход | Команда | Работают ли вебхуки? | Когда использовать |
|---|---|---|---|
| Локальная разработка | make install && make run |
❌ Нет | Тестирование логики контроллера/согласования |
| Развёртывание в кластере | make deploy |
✅ Да | Тестирование валидации вебхуком |
Примечание: если вы использовали скрипт курса
scripts/setup-kind-cluster.shдля создания кластера, cert-manager уже установлен. Проверьте командой:kubectl get pods -n cert-manager
Задача 4.1: убедитесь, что Cert-Manager установлен
Если cert-manager не установлен:
# Install cert-manager in your cluster
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# Wait for cert-manager to be ready
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=120s
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
kubectl wait --for=condition=Available deployment/cert-manager-cainjector -n cert-manager --timeout=120s
Задача 4.2: разверните оператор в кластер
Поскольку вебхуки должны работать внутри кластера, нужно собрать и развернуть:
# Build the container image
make docker-build IMG=postgres-operator:latest
# Load image into kind cluster
kind load docker-image postgres-operator:latest --name k8s-operators-course
Перед развёртыванием нужно установить imagePullPolicy: IfNotPresent, чтобы Kubernetes использовал локально загруженный образ вместо попытки скачать его из Docker Hub:
# Edit config/manager/manager.yaml and add imagePullPolicy
# Find the container spec and add: imagePullPolicy: IfNotPresent
Или используйте эту команду, чтобы применить патч:
# Add imagePullPolicy to manager.yaml
sed -i.bak 's/image: controller:latest/image: controller:latest\n imagePullPolicy: IfNotPresent/' config/manager/manager.yaml
Теперь разверните:
# Deploy operator with webhooks to cluster
make deploy IMG=postgres-operator:latest
Используете Podman вместо Docker?
Makefile использует переменную
CONTAINER_TOOL(по умолчаниюdocker). Podman добавляет к образам префиксlocalhost/, поэтому используйте:# Build with podman (note: image will be localhost/postgres-operator:latest) make docker-build IMG=postgres-operator:latest CONTAINER_TOOL=podman # Load image into kind (save to tarball, then load) podman save localhost/postgres-operator:latest -o /tmp/postgres-operator.tar kind load image-archive /tmp/postgres-operator.tar --name k8s-operators-course rm /tmp/postgres-operator.tar # Deploy operator - use localhost/ prefix to match the loaded image make deploy IMG=localhost/postgres-operator:latest
Получаете
ErrImagePullилиImagePullBackOff?Это означает, что Kubernetes пытается скачать образ из Docker Hub вместо использования локального.
- Убедитесь, что в
config/manager/manager.yamlустановленоimagePullPolicy: IfNotPresent: ```yaml containers:
- name: manager image: controller:latest imagePullPolicy: IfNotPresent # Add this line ```
- Пользователи Podman: проверьте фактическое имя образа, загруженного в kind:
podman exec k8s-operators-course-control-plane crictl images | grep postgresЕсли показывается
localhost/postgres-operator, используйте это имя при развёртывании:make deploy IMG=localhost/postgres-operator:latest
Совет: для повседневной разработки контроллера вы всё ещё можете использовать
make install && make run. Разворачивайте в кластер только когда нужно протестировать поведение вебхуков.
Задача 4.3: проверьте, что вебхук зарегистрирован
# Check webhook configuration was created
kubectl get validatingwebhookconfigurations
# Check operator pods are running
kubectl get pods -n postgres-operator-system
# Check logs if needed
kubectl logs -n postgres-operator-system deployment/postgres-operator-controller-manager
Задача 4.4: протестируйте корректный ресурс
# Create valid Database
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: valid-db
spec:
image: postgres:14
replicas: 1
databaseName: mydb
username: admin
storage:
size: 10Gi
EOF
# Should succeed
kubectl get database valid-db
Задача 4.5: протестируйте некорректные ресурсы
# Test invalid image
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: invalid-image
spec:
image: nginx:latest # Not PostgreSQL
replicas: 1
databaseName: mydb
username: admin
storage:
size: 10Gi
EOF
# Should fail with validation error
# Test invalid storage for replicas
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: invalid-storage
spec:
image: postgres:14
replicas: 10 # Too many replicas
databaseName: mydb
username: admin
storage:
size: 10Gi # Too small
EOF
# Should fail with validation error
Задача 4.6: протестируйте валидацию обновления
# Create database
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: update-test
spec:
image: postgres:14
replicas: 1
databaseName: mydb
username: admin
storage:
size: 20Gi
EOF
# Try to reduce storage
kubectl patch database update-test --type merge -p '{"spec":{"storage":{"size":"10Gi"}}}'
# Should fail with validation error
# Try to change database name
kubectl patch database update-test --type merge -p '{"spec":{"databaseName":"newdb"}}'
# Should fail with validation error
Упражнение 5: улучшение сообщений об ошибках
Задача 5.1: добавьте контекст к ошибкам
Улучшите сообщения об ошибках:
func (v *DatabaseCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
database, ok := obj.(*databasev1.Database)
if !ok {
return nil, fmt.Errorf("expected a Database object but got %T", obj)
}
databaselog.Info("Validation for Database upon creation", "name", database.GetName())
var errors []string
// Validate image
if !strings.Contains(database.Spec.Image, "postgres") {
errors = append(errors, fmt.Sprintf("spec.image: must be a PostgreSQL image, got '%s'. Valid examples: postgres:14, postgres:13", database.Spec.Image))
}
// Validate storage
if database.Spec.Replicas != nil && *database.Spec.Replicas > 5 {
if database.Spec.Storage.Size == "10Gi" {
errors = append(errors, fmt.Sprintf("spec.storage.size: when replicas > 5, storage must be >= 50Gi, got '%s'", database.Spec.Storage.Size))
}
}
if len(errors) > 0 {
return nil, fmt.Errorf("validation failed: %s", strings.Join(errors, "; "))
}
return nil, nil
}
Пересоберите и загрузите новый образ, как описано в Задаче 4.2: Разверните оператор в кластер, и перезапустите развёртывание, чтобы оно подхватило новый образ — kubectl rollout restart deploy -n postgres-operator-system postgres-operator-controller-manager.
Теперь проверьте на примере ниже:
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: invalid-image-storage
spec:
image: nginx:latest # Not PostgreSQL
replicas: 10
databaseName: mydb
username: admin
storage:
size: 10Gi # less storage for replicas
EOF
# Should fail and error message should show both the spec.Image and spec.Storage errors
Очистка
# Delete test resources
kubectl delete databases --all
# Stop operator (Ctrl+C)
Итоги лабораторной
В этой лабораторной вы:
- Сгенерировали каркас валидирующего вебхука с помощью kubebuilder
- Реализовали пользовательскую логику валидации
- Протестировали на корректных и некорректных ресурсах
- Улучшили сообщения об ошибках
- Протестировали валидацию обновления
Ключевые уроки
- Kubebuilder легко генерирует каркас вебхуков в
internal/webhook/v1/ - Использует структуру
DatabaseCustomValidator, реализующуюwebhook.CustomValidator - Методы получают
context.Contextпервым параметром ValidateUpdateполучает и старый, и новый объект какruntime.Object- Приводите по типу
runtime.Objectк фактическому типу вашего ресурса - Предоставляйте понятные, применимые сообщения об ошибках
- Тестируйте на корректных и некорректных ресурсах
- Вебхуки запускаются после валидации схемы CRD
- Сообщения об ошибках помогают пользователям исправлять проблемы
Решения
Полные рабочие решения для этой лабораторной доступны в каталоге решений:
- Validating Webhook — полная реализация валидирующего вебхука с пользовательской логикой валидации
Дальнейшие шаги
Теперь давайте создадим мутирующий вебхук для установки значений по умолчанию!