Лабораторная 3.3: Создание оператора PostgreSQL
Связанный урок: Урок 3.3: Реализация логики согласования
Навигация: ← Предыдущая лабораторная: Проектирование API | Обзор модуля | Следующая лабораторная: Client-Go →
Цели
- Реализовать логику согласования для оператора PostgreSQL
- Обрабатывать создание и обновление ресурсов
- Использовать ссылки-владельцы
- Управлять Secret для учётных данных базы данных
- Протестировать идемпотентность
Предварительные требования
- Завершение Лабораторной 3.2
- Определённый API Database
- Понимание паттернов согласования
Упражнение 1: реализация базового согласования
Задача 1.1: настройте структуру контроллера
Отредактируйте internal/controller/database_controller.go:
package controller
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
databasev1 "github.com/example/postgres-operator/api/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DatabaseReconciler reconciles a Database object
type DatabaseReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=database.example.com,resources=databases,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=database.example.com,resources=databases/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=database.example.com,resources=databases/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Read Database resource
db := &databasev1.Database{}
if err := r.Get(ctx, req.NamespacedName, db); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
logger.Info("Reconciling Database", "name", db.Name)
// Reconcile Secret (must be done before StatefulSet)
if err := r.reconcileSecret(ctx, db); err != nil {
return ctrl.Result{}, err
}
// Reconcile StatefulSet
if err := r.reconcileStatefulSet(ctx, db); err != nil {
return ctrl.Result{}, err
}
// Reconcile Service
if err := r.reconcileService(ctx, db); err != nil {
return ctrl.Result{}, err
}
// Update status
if err := r.updateStatus(ctx, db); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
Упражнение 2: реализация управления Secret
Контроллер автоматически генерирует случайный пароль и хранит его в Secret Kubernetes. Это безопаснее, чем требовать от пользователей указывать пароли в открытом виде.
Задача 2.1: вспомогательные функции
Добавьте вспомогательные функции для управления Secret:
// secretName returns the name of the Secret for this Database
func (r *DatabaseReconciler) secretName(db *databasev1.Database) string {
return fmt.Sprintf("%s-credentials", db.Name)
}
// generatePassword generates a random password
func generatePassword(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes)[:length], nil
}
Задача 2.2: согласование Secret
// reconcileSecret ensures the credentials Secret exists
func (r *DatabaseReconciler) reconcileSecret(ctx context.Context, db *databasev1.Database) error {
logger := log.FromContext(ctx)
secretName := r.secretName(db)
secret := &corev1.Secret{}
err := r.Get(ctx, client.ObjectKey{
Name: secretName,
Namespace: db.Namespace,
}, secret)
if errors.IsNotFound(err) {
// Generate random password
password, err := generatePassword(16)
if err != nil {
return fmt.Errorf("failed to generate password: %w", err)
}
// Create new secret
secret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: db.Namespace,
},
Type: corev1.SecretTypeOpaque,
StringData: map[string]string{
"username": db.Spec.Username,
"password": password,
"database": db.Spec.DatabaseName,
},
}
// Set owner reference
if err := ctrl.SetControllerReference(db, secret, r.Scheme); err != nil {
return err
}
logger.Info("Creating Secret", "name", secretName)
return r.Create(ctx, secret)
} else if err != nil {
return err
}
// Secret already exists, don't update password
return nil
}
Упражнение 3: реализация согласования StatefulSet
Задача 3.1: постройте StatefulSet
Добавьте вспомогательную функцию для построения StatefulSet. Обратите внимание, как мы ссылаемся на пароль из Secret:
func (r *DatabaseReconciler) buildStatefulSet(db *databasev1.Database) *appsv1.StatefulSet {
replicas := int32(1)
if db.Spec.Replicas != nil {
replicas = *db.Spec.Replicas
}
image := db.Spec.Image
if image == "" {
image = "postgres:14"
}
secretName := r.secretName(db)
return &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: db.Name,
Namespace: db.Namespace,
},
Spec: appsv1.StatefulSetSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "database",
"database": db.Name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "database",
"database": db.Name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "postgres",
Image: image,
Env: []corev1.EnvVar{
{
Name: "POSTGRES_DB",
Value: db.Spec.DatabaseName,
},
{
Name: "POSTGRES_USER",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secretName,
},
Key: "username",
},
},
},
{
Name: "POSTGRES_PASSWORD",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secretName,
},
Key: "password",
},
},
},
{
Name: "PGDATA",
Value: "/var/lib/postgresql/data/pgdata",
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "data",
MountPath: "/var/lib/postgresql/data",
},
},
},
},
},
},
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse(db.Spec.Storage.Size),
},
},
},
},
},
},
}
}
Задача 3.2: согласование StatefulSet
func (r *DatabaseReconciler) reconcileStatefulSet(ctx context.Context, db *databasev1.Database) error {
logger := log.FromContext(ctx)
statefulSet := &appsv1.StatefulSet{}
err := r.Get(ctx, client.ObjectKey{
Name: db.Name,
Namespace: db.Namespace,
}, statefulSet)
desiredStatefulSet := r.buildStatefulSet(db)
if errors.IsNotFound(err) {
// Set owner reference
if err := ctrl.SetControllerReference(db, desiredStatefulSet, r.Scheme); err != nil {
return err
}
logger.Info("Creating StatefulSet", "name", desiredStatefulSet.Name)
return r.Create(ctx, desiredStatefulSet)
} else if err != nil {
return err
}
// Update if needed
if statefulSet.Spec.Replicas != desiredStatefulSet.Spec.Replicas ||
statefulSet.Spec.Template.Spec.Containers[0].Image != desiredStatefulSet.Spec.Template.Spec.Containers[0].Image {
statefulSet.Spec = desiredStatefulSet.Spec
logger.Info("Updating StatefulSet", "name", statefulSet.Name)
return r.Update(ctx, statefulSet)
}
return nil
}
Упражнение 4: реализация согласования Service
Задача 4.1: постройте Service
func (r *DatabaseReconciler) buildService(db *databasev1.Database) *corev1.Service {
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: db.Name,
Namespace: db.Namespace,
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": "database",
"database": db.Name,
},
Ports: []corev1.ServicePort{
{
Port: 5432,
Name: "postgres",
},
},
},
}
}
Задача 4.2: согласование Service
func (r *DatabaseReconciler) reconcileService(ctx context.Context, db *databasev1.Database) error {
service := &corev1.Service{}
err := r.Get(ctx, client.ObjectKey{
Name: db.Name,
Namespace: db.Namespace,
}, service)
desiredService := r.buildService(db)
if errors.IsNotFound(err) {
if err := ctrl.SetControllerReference(db, desiredService, r.Scheme); err != nil {
return err
}
return r.Create(ctx, desiredService)
} else if err != nil {
return err
}
// Service updates are less common, but handle if needed
return nil
}
Упражнение 5: обновление статуса
Задача 5.1: реализуйте обновление статуса
Статус включает имя Secret, чтобы пользователи знали, где найти учётные данные:
func (r *DatabaseReconciler) updateStatus(ctx context.Context, db *databasev1.Database) error {
// Set the secret name in status
db.Status.SecretName = r.secretName(db)
// Check StatefulSet status
statefulSet := &appsv1.StatefulSet{}
err := r.Get(ctx, client.ObjectKey{
Name: db.Name,
Namespace: db.Namespace,
}, statefulSet)
if err != nil {
db.Status.Phase = "Pending"
db.Status.Ready = false
} else {
if statefulSet.Status.ReadyReplicas == *statefulSet.Spec.Replicas {
db.Status.Phase = "Ready"
db.Status.Ready = true
db.Status.Endpoint = fmt.Sprintf("%s.%s.svc.cluster.local:5432", db.Name, db.Namespace)
} else {
db.Status.Phase = "Creating"
db.Status.Ready = false
}
}
return r.Status().Update(ctx, db)
}
Упражнение 6: настройка Controller Manager
Чтобы контроллер получал события при изменении подчинённых (owned) ресурсов (например, когда StatefulSet становится готовым), нужно указать менеджеру отслеживать эти ресурсы.
Задача 6.1: настройте отслеживание
Добавьте функцию SetupWithManager в конце вашего контроллера:
// SetupWithManager sets up the controller with the Manager.
func (r *DatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&databasev1.Database{}).
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
Owns(&corev1.Secret{}).
Complete(r)
}
Ключевые моменты:
For(&databasev1.Database{})— отслеживает ресурсы Database (основной ресурс)Owns(&appsv1.StatefulSet{})— отслеживает StatefulSet, принадлежащие Database (через ссылку-владельца)Owns(&corev1.Service{})— отслеживает Service, принадлежащие DatabaseOwns(&corev1.Secret{})— отслеживает Secret, принадлежащие Database
Это гарантирует, что при изменении статуса StatefulSet (поды становятся готовы) контроллер получает уведомление и согласовывает родительский Database, чтобы обновить его статус.
Упражнение 7: тестирование оператора
Задача 7.1: установите и запустите
# Install CRD
make install
# Run operator
make run
Задача 7.2: создайте Database
# Create Database resource (no password needed - it's auto-generated!)
kubectl apply -f - <<EOF
apiVersion: database.example.com/v1
kind: Database
metadata:
name: my-database
spec:
image: postgres:14
replicas: 1
databaseName: mydb
username: admin
storage:
size: 10Gi
EOF
Задача 7.3: наблюдайте за согласованием
# Watch Database status
kubectl get database my-database -w
# Check StatefulSet
kubectl get statefulset my-database
# Check Service
kubectl get service my-database
# Check the auto-generated Secret
kubectl get secret my-database-credentials
# View the generated password (base64 decoded)
kubectl get secret my-database-credentials -o jsonpath='{.data.password}' | base64 -d
# Check operator logs
Упражнение 8: тестирование идемпотентности
Задача 8.1: примените несколько раз
# Apply the same resource multiple times
for i in {1..3}; do
kubectl apply -f database.yaml
sleep 2
done
# Verify only one StatefulSet exists
kubectl get statefulsets | grep my-database
Задача 8.2: протестируйте обновления
# Update replicas
kubectl patch database my-database --type merge -p '{"spec":{"replicas":2}}'
# Verify StatefulSet was updated
kubectl get statefulset my-database -o jsonpath='{.spec.replicas}'
Очистка
# Delete Database (should cascade delete StatefulSet, Service, and Secret)
kubectl delete database my-database
# Verify resources were deleted
kubectl get statefulset my-database
kubectl get service my-database
kubectl get secret my-database-credentials
Итоги лабораторной
В этой лабораторной вы:
- Реализовали полную логику согласования
- Создали Secret с автоматически сгенерированным паролем
- Создали StatefulSet и Service
- Использовали ссылки-владельцы для всех ресурсов
- Настроили отслеживание с помощью
Owns(), чтобы реагировать на изменения подчинённых ресурсов - Обновили статус именем Secret
- Протестировали идемпотентность
- Проверили каскадное удаление
Ключевые уроки
- Согласование следует схеме: чтение, сравнение, создание/обновление, статус
- Ссылки-владельцы обеспечивают каскадное удаление
- Используйте
Owns()для отслеживания подчинённых ресурсов — без этого контроллер не получит уведомление при изменении статуса StatefulSet/Service/Secret - Идемпотентность критически важна
- Secret должны генерироваться автоматически, а не предоставляться пользователем в открытом виде
- Обновления статуса отражают фактическое состояние и предоставляют полезную информацию (например, имя Secret)
- Обработка ошибок важна
- Логирование помогает при отладке
Решения
Полные рабочие решения для этой лабораторной доступны в каталоге решений:
- Database Types — полные определения типов API Database
- Database Controller — полный контроллер с согласованием Secret/StatefulSet/Service
Дальнейшие шаги
Теперь давайте изучим продвинутые операции клиента для более совершенных контроллеров!