确定单库单账号、集群级 Database、资源侧先写绑定和凭据定位合同。领域层承载纯规则,service 协調流程,Kubernetes adapter 负责资源呈现与版本保护。 验证:全量 make test、三轮 race、真实 API server 并发与重启补写、最小 RBAC/watch、lint 和文档检查通过。供应、凭据交付及删除清理尚未实现,保留 DeletionPending/finalizer 边界。
192 lines
7.2 KiB
Go
192 lines
7.2 KiB
Go
package kubernetes
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
|
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/binding"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
)
|
|
|
|
const (
|
|
TenantFinalizer = "database.ayatori.ddupan.top/tenant-protection"
|
|
DatabaseFinalizer = "database.ayatori.ddupan.top/database-protection"
|
|
)
|
|
|
|
// BindingResources 读取领域所需事实,并把用例结果呈现为 CR、finalizer 与 Conditions。
|
|
// 重新读取后校验快照版本,保留不属于本用例的字段;不决定绑定资格或恢复顺序。
|
|
type BindingResources struct {
|
|
Client client.Client
|
|
Reader client.Reader
|
|
}
|
|
|
|
var _ application.BindingResources = (*BindingResources)(nil)
|
|
|
|
func (r *BindingResources) Tenant(ctx context.Context, namespace, name string) (*application.BindingTenant, error) {
|
|
object := &databasev1alpha1.PostgreSQLTenant{}
|
|
if err := r.Reader.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, object); err != nil {
|
|
return nil, client.IgnoreNotFound(err)
|
|
}
|
|
return bindingTenant(object), nil
|
|
}
|
|
|
|
func (r *BindingResources) Database(ctx context.Context, name string) (*application.BindingDatabase, error) {
|
|
object := &databasev1alpha1.PostgreSQLDatabase{}
|
|
if err := r.Reader.Get(ctx, types.NamespacedName{Name: name}, object); err != nil {
|
|
return nil, client.IgnoreNotFound(err)
|
|
}
|
|
return bindingDatabase(object), nil
|
|
}
|
|
|
|
func (r *BindingResources) Instance(ctx context.Context, name string) (*binding.Instance, error) {
|
|
object := &databasev1alpha1.PostgreSQLInstance{}
|
|
if err := r.Reader.Get(ctx, types.NamespacedName{Name: name}, object); err != nil {
|
|
return nil, client.IgnoreNotFound(err)
|
|
}
|
|
return &binding.Instance{
|
|
Identity: binding.Identity{Name: object.Name, UID: string(object.UID)},
|
|
Deleting: !object.DeletionTimestamp.IsZero(), Ready: currentReady(object.Generation, object.Status.Conditions),
|
|
}, nil
|
|
}
|
|
|
|
func (r *BindingResources) BeginBinding(ctx context.Context, tenant *application.BindingTenant,
|
|
checkpoint *application.BindingStatus) (*application.BindingTenant, error) {
|
|
object, err := r.tenantAtVersion(ctx, tenant)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if controllerutil.AddFinalizer(object, TenantFinalizer) {
|
|
if err := r.Client.Update(ctx, object); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if checkpoint != nil {
|
|
if err := r.presentStatus(ctx, object, *checkpoint); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return bindingTenant(object), nil
|
|
}
|
|
|
|
func (r *BindingResources) CreateDatabase(ctx context.Context, target binding.Target,
|
|
tenant binding.TenantIdentity) (*application.BindingDatabase, error) {
|
|
object := &databasev1alpha1.PostgreSQLDatabase{}
|
|
object.Name = target.Name
|
|
object.Spec = databasev1alpha1.PostgreSQLDatabaseSpec{
|
|
InstanceRef: databasev1alpha1.InstanceReference{Name: databasev1alpha1.ObjectName(target.Provision.Instance)},
|
|
Database: databasev1alpha1.PostgreSQLIdentifier(target.Provision.Database),
|
|
LoginRole: databasev1alpha1.PostgreSQLIdentifier(target.Provision.LoginRole),
|
|
Source: "Provision", ReclaimPolicy: databasev1alpha1.ReclaimRetain, TenantRef: tenantReference(tenant),
|
|
}
|
|
controllerutil.AddFinalizer(object, DatabaseFinalizer)
|
|
if err := r.Client.Create(ctx, object); err != nil {
|
|
return nil, err
|
|
}
|
|
return bindingDatabase(object), nil
|
|
}
|
|
|
|
func (r *BindingResources) RecordInstance(ctx context.Context, database *application.BindingDatabase,
|
|
instanceUID string) (*application.BindingDatabase, error) {
|
|
object, err := r.databaseAtVersion(ctx, database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
object.Status.InstanceUID = types.UID(instanceUID)
|
|
if err := r.Client.Status().Update(ctx, object); err != nil {
|
|
return nil, err
|
|
}
|
|
return bindingDatabase(object), nil
|
|
}
|
|
|
|
func (r *BindingResources) BindDatabase(ctx context.Context, database *application.BindingDatabase,
|
|
tenant binding.TenantIdentity) (*application.BindingDatabase, error) {
|
|
object, err := r.databaseAtVersion(ctx, database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wanted := tenantReference(tenant)
|
|
changed := controllerutil.AddFinalizer(object, DatabaseFinalizer)
|
|
if object.Spec.TenantRef == nil || *object.Spec.TenantRef != *wanted {
|
|
object.Spec.TenantRef = wanted
|
|
changed = true
|
|
}
|
|
if changed {
|
|
if err := r.Client.Update(ctx, object); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return bindingDatabase(object), nil
|
|
}
|
|
|
|
func (r *BindingResources) Present(ctx context.Context, result application.BindingResult) error {
|
|
if result.Tenant == nil {
|
|
return nil
|
|
}
|
|
object, err := r.tenantAtVersion(ctx, result.Tenant)
|
|
if err != nil {
|
|
return client.IgnoreNotFound(err)
|
|
}
|
|
return r.presentStatus(ctx, object, result.Status)
|
|
}
|
|
|
|
func (r *BindingResources) presentStatus(ctx context.Context, object *databasev1alpha1.PostgreSQLTenant,
|
|
status application.BindingStatus) error {
|
|
previous := object.Status.DeepCopy()
|
|
object.Status.Phase = status.Phase
|
|
object.Status.ObservedGeneration = object.Generation
|
|
if status.Database != nil {
|
|
object.Status.DatabaseRef = &databasev1alpha1.BoundDatabaseReference{
|
|
Name: databasev1alpha1.ObjectName(status.Database.Name), UID: types.UID(status.Database.UID),
|
|
}
|
|
}
|
|
meta.SetStatusCondition(&object.Status.Conditions, metav1.Condition{
|
|
Type: "Ready", Status: metav1.ConditionFalse, Reason: status.Reason, Message: status.Message,
|
|
ObservedGeneration: object.Generation,
|
|
})
|
|
if equality.Semantic.DeepEqual(*previous, object.Status) {
|
|
return nil
|
|
}
|
|
return r.Client.Status().Update(ctx, object)
|
|
}
|
|
|
|
func (r *BindingResources) tenantAtVersion(ctx context.Context, tenant *application.BindingTenant) (*databasev1alpha1.PostgreSQLTenant, error) {
|
|
object := &databasev1alpha1.PostgreSQLTenant{}
|
|
key := types.NamespacedName{Namespace: tenant.Identity.Namespace, Name: tenant.Identity.Name}
|
|
if err := r.Reader.Get(ctx, key, object); err != nil {
|
|
return nil, err
|
|
}
|
|
if string(object.UID) != tenant.Identity.UID || object.ResourceVersion != tenant.Revision {
|
|
return nil, bindingVersionConflict("postgresqltenants", object.Name)
|
|
}
|
|
return object, nil
|
|
}
|
|
|
|
func (r *BindingResources) databaseAtVersion(ctx context.Context, database *application.BindingDatabase) (*databasev1alpha1.PostgreSQLDatabase, error) {
|
|
object := &databasev1alpha1.PostgreSQLDatabase{}
|
|
if err := r.Reader.Get(ctx, types.NamespacedName{Name: database.Identity.Name}, object); err != nil {
|
|
return nil, err
|
|
}
|
|
if string(object.UID) != database.Identity.UID || object.ResourceVersion != database.Revision {
|
|
return nil, bindingVersionConflict("postgresqldatabases", object.Name)
|
|
}
|
|
return object, nil
|
|
}
|
|
|
|
func bindingVersionConflict(resource, name string) error {
|
|
return apierrors.NewConflict(databasev1alpha1.GroupVersion.WithResource(resource).GroupResource(), name,
|
|
fmt.Errorf("绑定快照已过期,请重新读取后判断"))
|
|
}
|
|
|
|
func currentReady(generation int64, conditions []metav1.Condition) bool {
|
|
condition := meta.FindStatusCondition(conditions, "Ready")
|
|
return condition != nil && condition.Status == metav1.ConditionTrue && condition.ObservedGeneration == generation
|
|
}
|