Files
ayatori/internal/database/adapter/kubernetes/binding_resources.go
T
panxiao81 7e9e8e828b
Verify / test (pull_request) Successful in 11m39s
Verify / lint (pull_request) Successful in 12m51s
Verify / database-integration (pull_request) Successful in 13m12s
feat: 接入 Database 三资源 API 与分层绑定协调
确定单库单账号、集群级 Database、资源侧先写绑定和凭据定位合同。领域层承载纯规则,service 协調流程,Kubernetes adapter 负责资源呈现与版本保护。

验证:全量 make test、三轮 race、真实 API server 并发与重启补写、最小 RBAC/watch、lint 和文档检查通过。供应、凭据交付及删除清理尚未实现,保留 DeletionPending/finalizer 边界。
2026-09-25 04:09:43 +00:00

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
}