feat: 接入 Database 三资源 API 与分层绑定协调
确定单库单账号、集群级 Database、资源侧先写绑定和凭据定位合同。领域层承载纯规则,service 协調流程,Kubernetes adapter 负责资源呈现与版本保护。 验证:全量 make test、三轮 race、真实 API server 并发与重启补写、最小 RBAC/watch、lint 和文档检查通过。供应、凭据交付及删除清理尚未实现,保留 DeletionPending/finalizer 边界。
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
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/types"
|
||||
)
|
||||
|
||||
func bindingTenant(object *databasev1alpha1.PostgreSQLTenant) *application.BindingTenant {
|
||||
tenant := &application.BindingTenant{
|
||||
Revision: object.ResourceVersion, Generation: object.Generation,
|
||||
}
|
||||
tenant.Tenant = binding.Tenant{
|
||||
Identity: binding.TenantIdentity{Namespace: object.Namespace, Name: object.Name, UID: string(object.UID)},
|
||||
Phase: object.Status.Phase, Deleting: !object.DeletionTimestamp.IsZero(),
|
||||
}
|
||||
if request := object.Spec.Provision; request != nil {
|
||||
tenant.Request.Provision = &binding.ProvisionRequest{
|
||||
Instance: string(request.InstanceRef.Name), Database: string(request.Database), LoginRole: string(request.LoginRole),
|
||||
}
|
||||
}
|
||||
if object.Spec.DatabaseRef != nil {
|
||||
tenant.Request.ExistingDatabase = string(object.Spec.DatabaseRef.Name)
|
||||
}
|
||||
if ref := object.Status.DatabaseRef; ref != nil {
|
||||
tenant.Database = &binding.Identity{Name: string(ref.Name), UID: string(ref.UID)}
|
||||
}
|
||||
return tenant
|
||||
}
|
||||
|
||||
func bindingDatabase(object *databasev1alpha1.PostgreSQLDatabase) *application.BindingDatabase {
|
||||
database := &application.BindingDatabase{
|
||||
Revision: object.ResourceVersion,
|
||||
}
|
||||
database.Database = binding.Database{
|
||||
Identity: binding.Identity{Name: object.Name, UID: string(object.UID)},
|
||||
Instance: string(object.Spec.InstanceRef.Name), InstanceUID: string(object.Status.InstanceUID),
|
||||
Name: string(object.Spec.Database), LoginRole: string(object.Spec.LoginRole), Source: object.Spec.Source,
|
||||
Phase: object.Status.Phase, Deleting: !object.DeletionTimestamp.IsZero(),
|
||||
Ready: currentReady(object.Generation, object.Status.Conditions),
|
||||
}
|
||||
if ref := object.Spec.TenantRef; ref != nil {
|
||||
database.Tenant = &binding.TenantIdentity{Namespace: ref.Namespace, Name: string(ref.Name), UID: string(ref.UID)}
|
||||
}
|
||||
return database
|
||||
}
|
||||
|
||||
func tenantReference(tenant binding.TenantIdentity) *databasev1alpha1.TenantReference {
|
||||
return &databasev1alpha1.TenantReference{
|
||||
Namespace: tenant.Namespace, Name: databasev1alpha1.ObjectName(tenant.Name), UID: types.UID(tenant.UID),
|
||||
}
|
||||
}
|
||||
|
||||
// BindingTargetName 供 informer 索引使用;不把无效请求丢出事件映射。
|
||||
func BindingTargetName(tenant *databasev1alpha1.PostgreSQLTenant) string {
|
||||
if tenant.Spec.DatabaseRef != nil {
|
||||
return string(tenant.Spec.DatabaseRef.Name)
|
||||
}
|
||||
return binding.DynamicDatabaseName(string(tenant.UID))
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user