Лабораторная 5.3: Создание мутирующего вебхука
Связанный урок: Урок 5.3: Реализация мутирующих вебхуков
Навигация: ← Предыдущая лабораторная: Валидирующие вебхуки | Обзор модуля | Следующая лабораторная: Развёртывание вебхуков →
Цели
- Добавить мутирующий вебхук к существующему валидирующему
- Реализовать логику установки значений по умолчанию
- Протестировать сценарии мутации
- Обеспечить идемпотентность
Предварительные требования
- Завершение Лабораторной 5.2
- Оператор Database с валидирующим вебхуком
- Понимание паттернов установки значений по умолчанию
Упражнение 1: добавление мутирующего вебхука
Поскольку мы уже создали валидирующий вебхук в Лабораторной 5.2, наш файл вебхука уже существует в internal/webhook/v1/database_webhook.go. Мы добавим мутирующую логику (установку значений по умолчанию) в этот файл.
Примечание: если бы вы начинали с нуля, вы бы запустили:
kubebuilder create webhook --group database --version v1 --kind Database --defaultingНо поскольку у нас уже есть вебхук, мы добавим дефолтер вручную.
Задача 1.1: разберитесь в интерфейсе CustomDefaulter
Новый паттерн kubebuilder использует интерфейс webhook.CustomDefaulter:
type CustomDefaulter interface {
Default(ctx context.Context, obj runtime.Object) error
}
Задача 1.2: добавьте дефолтер в настройку вебхука
Отредактируйте internal/webhook/v1/database_webhook.go, чтобы обновить функцию настройки вебхука:
// SetupDatabaseWebhookWithManager registers the webhook for Database in the manager.
func SetupDatabaseWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&databasev1.Database{}).
WithValidator(&DatabaseCustomValidator{}).
WithDefaulter(&DatabaseCustomDefaulter{}).
Complete()
}
Задача 1.3: добавьте структуру дефолтера и маркер
Добавьте следующее в internal/webhook/v1/database_webhook.go:
// +kubebuilder:webhook:path=/mutate-database-example-com-v1-database,mutating=true,failurePolicy=fail,sideEffects=None,groups=database.example.com,resources=databases,verbs=create;update,versions=v1,name=mdatabase-v1.kb.io,admissionReviewVersions=v1
// DatabaseCustomDefaulter struct is responsible for setting default values on the Database resource.
type DatabaseCustomDefaulter struct {
// Add fields as needed for defaulting
}
var _ webhook.CustomDefaulter = &DatabaseCustomDefaulter{}
Упражнение 2: реализация логики установки значений по умолчанию
Задача 2.1: добавьте метод Default
Добавьте метод Default в internal/webhook/v1/database_webhook.go:
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type Database.
func (d *DatabaseCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
database, ok := obj.(*databasev1.Database)
if !ok {
return fmt.Errorf("expected a Database object but got %T", obj)
}
databaselog.Info("Defaulting for Database", "name", database.GetName())
// Set default image if not specified
if database.Spec.Image == "" {
database.Spec.Image = "postgres:14"
}
// Set default replicas if not specified
if database.Spec.Replicas == nil {
replicas := int32(1)
database.Spec.Replicas = &replicas
}
// Set default storage class if not specified
if database.Spec.Storage.StorageClass == "" {
database.Spec.Storage.StorageClass = "standard"
}
return nil
}
Задача 2.2: добавьте значения по умолчанию с учётом контекста
Дополните метод Default значениями по умолчанию на основе пространства имён:
func (d *DatabaseCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
database, ok := obj.(*databasev1.Database)
if !ok {
return fmt.Errorf("expected a Database object but got %T", obj)
}
databaselog.Info("Defaulting for Database", "name", database.GetName(), "namespace", database.GetNamespace())
// Set defaults based on namespace
if database.Namespace == "production" {
// Production defaults - ensure minimum 3 replicas
// Note: We check < 3 instead of nil because CRD schema defaults may already set replicas=1
if database.Spec.Replicas == nil || *database.Spec.Replicas < 3 {
replicas := int32(3)
database.Spec.Replicas = &replicas
}
}
// For non-production, CRD schema default of 1 replica is fine
// Common defaults
if database.Spec.Storage.StorageClass == "" {
database.Spec.Storage.StorageClass = "standard"
}
// Add labels (idempotent)
if database.Labels == nil {
database.Labels = make(map[string]string)
}
if _, exists := database.Labels["managed-by"]; !exists {
database.Labels["managed-by"] = "database-operator"
}
// Add annotations (idempotent)
if database.Annotations == nil {
database.Annotations = make(map[string]string)
}
if _, exists := database.Annotations["database.example.com/version"]; !exists {
database.Annotations["database.example.com/version"] = "v1"
}
return nil
}
Примечание: мы проверяем
< 3, а неnil, для реплик, потому что значения по умолчанию схемы CRD (через+kubebuilder:default=1) применяются до запуска вебхуков. Это гарантирует, что продакшен-пространства всегда получают минимум 3 реплики.
Упражнение 3: обеспечение идемпотентности
Задача 3.1: разберитесь в идемпотентности
Мутации должны быть идемпотентными — их многократное применение должно давать один и тот же результат:
// Idempotent: Only set if not already set
if database.Spec.Image == "" {
database.Spec.Image = "postgres:14"
}
// If already set, doesn't change
// Idempotent: Check before adding to map
if _, exists := database.Labels["managed-by"]; !exists {
database.Labels["managed-by"] = "database-operator"
}
// If already exists, doesn't add again
Упражнение 4: развёртывание и тестирование мутирующего вебхука
Задача 4.1: включите MutatingWebhookConfiguration в Kustomization
Поскольку мы добавили мутирующий вебхук вручную, нужно раскомментировать замены (replacements) MutatingWebhookConfiguration в config/default/kustomization.yaml, чтобы cert-manager мог внедрить CA bundle:
cd ~/postgres-operator
# Uncomment the MutatingWebhookConfiguration section (around lines 188-217)
# Find the section that says "Uncomment the following block if you have a DefaultingWebhook"
# and uncomment it.
# Verify it's uncommented - should show MutatingWebhookConfiguration without # prefix
grep -A 5 "DefaultingWebhook" config/default/kustomization.yaml
Задача 4.2: сгенерируйте манифесты
# Generate manifests (includes new mutating webhook configuration)
make manifests
# Verify mutating webhook manifest was generated
grep "mutating" config/webhook/manifests.yaml
Задача 4.3: снимите развёртывание и удалите устаревшие вебхуки
Поскольку мы добавили новый вебхук, нужно полностью переразвернуть. Также удалите любые устаревшие конфигурации вебхуков от предыдущих развёртываний:
# Remove existing deployment
make undeploy
# Clean up any stale webhook configurations (from previous deployments without proper prefixes)
kubectl delete validatingwebhookconfiguration validating-webhook-configuration 2>/dev/null || true
kubectl delete mutatingwebhookconfiguration mutating-webhook-configuration 2>/dev/null || true
# Verify cleanup
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations
# Should only show cert-manager and ingress-nginx webhooks, not our old ones
# Wait for resources to be deleted
kubectl get all -n postgres-operator-system
# Should show "No resources found"
Задача 4.4: пересоберите и разверните
# Rebuild the image
# For Docker:
make docker-build IMG=postgres-operator:latest
# For Podman:
make docker-build IMG=postgres-operator:latest CONTAINER_TOOL=podman
# Load image into kind
# For Docker:
kind load docker-image postgres-operator:latest --name k8s-operators-course
# For Podman:
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
# For Docker:
make deploy IMG=postgres-operator:latest
# For Podman:
make deploy IMG=localhost/postgres-operator:latest
Задача 4.5: дождитесь сертификатов
cert-manager нужно время, чтобы сгенерировать сертификаты и внедрить CA bundle:
# Wait for certificate to be ready
kubectl get certificate -n postgres-operator-system -w
# Wait for pod to be ready
kubectl wait --for=condition=Ready pod -l control-plane=controller-manager \
-n postgres-operator-system --timeout=120s
# Check operator logs - should see both webhooks registered
kubectl logs -n postgres-operator-system deployment/postgres-operator-controller-manager | grep -i webhook
Задача 4.6: проверьте, что оба вебхука зарегистрированы
# Check both webhooks are configured
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations
# You should see both:
# - postgres-operator-validating-webhook-configuration
# - postgres-operator-mutating-webhook-configuration
Задача 4.7: протестируйте минимальный ресурс
# Create resource with minimal spec (missing image, replicas)
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: minimal-db
spec:
databaseName: mydb
username: admin
storage:
size: 10Gi
# image and replicas should be defaulted
EOF
# Check defaults were applied
echo "Image:"
kubectl get database minimal-db -o jsonpath='{.spec.image}'
echo
echo "Replicas:"
kubectl get database minimal-db -o jsonpath='{.spec.replicas}'
echo
echo "Storage Class:"
kubectl get database minimal-db -o jsonpath='{.spec.storage.storageClass}'
echo
echo "Managed-by label:"
kubectl get database minimal-db -o jsonpath='{.metadata.labels.managed-by}'
echo
Задача 4.8: протестируйте значения по умолчанию на основе пространства имён
Наш вебхук проверяет replicas < 3 (а не просто nil) для продакшен-пространства, поэтому он работает, даже когда значения по умолчанию схемы CRD уже установили replicas=1.
# Clean up previous test resources
kubectl delete database --all --ignore-not-found
kubectl delete database --all -n production --ignore-not-found
# Create in default namespace (should stay at 1 replica - CRD default)
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: dev-db
spec:
databaseName: mydb
username: admin
storage:
size: 10Gi
EOF
# Create in production namespace (should be bumped to 3 replicas)
kubectl create namespace production --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: prod-db
namespace: production
spec:
databaseName: mydb
username: admin
storage:
size: 10Gi
EOF
# Check results
echo "=== Default namespace (should be 1 replica) ==="
kubectl get database dev-db -o jsonpath='Replicas: {.spec.replicas}'
echo
echo "=== Production namespace (should be 3 replicas) ==="
kubectl get database prod-db -n production -o jsonpath='Replicas: {.spec.replicas}'
echo
Ключевой вывод: значения по умолчанию схемы CRD (
+kubebuilder:default) применяются до вебхуков. Чтобы переопределить их, проверяйте значение по умолчанию (например,< 3), а не просто наличиеnil.
Упражнение 5: тестирование порядка мутаций
Задача 5.1: проверьте, что мутация выполняется до валидации
# Create resource that would fail validation without defaults
# (missing image, but mutating webhook will set it to postgres:14)
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: test-order
spec:
databaseName: mydb
username: admin
storage:
size: 10Gi
EOF
# Should succeed because:
# 1. Mutating webhook sets image to postgres:14
# 2. Validating webhook validates it's a postgres image
kubectl get database test-order -o jsonpath='{.spec.image}'
echo
Очистка
# Delete test resources
kubectl delete databases --all -A
kubectl delete namespace production --ignore-not-found
Итоги лабораторной
В этой лабораторной вы:
- Добавили мутирующий вебхук к существующему валидирующему
- Реализовали логику установки значений по умолчанию с помощью интерфейса
CustomDefaulter - Добавили значения по умолчанию с учётом контекста
- Обеспечили идемпотентность
- Протестировали сценарии мутации
- Проверили порядок мутаций
Ключевые уроки
- Добавляйте мутирующий вебхук в существующий
internal/webhook/v1/database_webhook.go - Используйте интерфейс
webhook.CustomDefaulterс отдельной структурой - Метод
Defaultполучаетcontext.Contextиruntime.Object - Регистрируйте дефолтер через
.WithDefaulter(&DatabaseCustomDefaulter{}) - Значения по умолчанию могут учитывать контекст (пространство имён и т. д.)
- Мутации должны быть идемпотентными
- Мутирующие вебхуки запускаются до валидирующих
Решения
Полные рабочие решения для этой лабораторной доступны в каталоге решений:
- Mutating Webhook — полная реализация мутирующего вебхука с логикой установки значений по умолчанию
Дальнейшие шаги
Теперь давайте изучим управление сертификатами и развёртывание!