feat: 接通 Database 凭据准备闭环
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
const (
|
||||
TenantFinalizer = "database.ayatori.ddupan.top/tenant-protection"
|
||||
DatabaseFinalizer = "database.ayatori.ddupan.top/database-protection"
|
||||
readyCondition = "Ready"
|
||||
)
|
||||
|
||||
// BindingResources 读取领域所需事实,并把用例结果呈现为 CR、finalizer 与 Conditions。
|
||||
@@ -148,7 +149,7 @@ func (r *BindingResources) presentStatus(ctx context.Context, object *databasev1
|
||||
}
|
||||
}
|
||||
meta.SetStatusCondition(&object.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionFalse, Reason: status.Reason, Message: status.Message,
|
||||
Type: readyCondition, Status: metav1.ConditionFalse, Reason: status.Reason, Message: status.Message,
|
||||
ObservedGeneration: object.Generation,
|
||||
})
|
||||
if equality.Semantic.DeepEqual(*previous, object.Status) {
|
||||
@@ -186,6 +187,6 @@ func bindingVersionConflict(resource, name string) error {
|
||||
}
|
||||
|
||||
func currentReady(generation int64, conditions []metav1.Condition) bool {
|
||||
condition := meta.FindStatusCondition(conditions, "Ready")
|
||||
condition := meta.FindStatusCondition(conditions, readyCondition)
|
||||
return condition != nil && condition.Status == metav1.ConditionTrue && condition.ObservedGeneration == generation
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// CredentialResources 只映射资源事实与状态写入,供应资格和执行顺序由用例决定。
|
||||
type CredentialResources struct {
|
||||
Client client.Client
|
||||
Reader client.Reader
|
||||
}
|
||||
|
||||
var _ application.CredentialResources = (*CredentialResources)(nil)
|
||||
|
||||
func (r *CredentialResources) Load(ctx context.Context, name string) (*application.CredentialRecord, error) {
|
||||
database := &databasev1alpha1.PostgreSQLDatabase{}
|
||||
if err := r.Reader.Get(ctx, types.NamespacedName{Name: name}, database); err != nil {
|
||||
return nil, client.IgnoreNotFound(err)
|
||||
}
|
||||
record := &application.CredentialRecord{
|
||||
Database: *bindingDatabase(database), DatabaseProtected: controllerutil.ContainsFinalizer(database, DatabaseFinalizer),
|
||||
Status: credentialStatus(database),
|
||||
}
|
||||
if database.Spec.Source != "Provision" {
|
||||
return record, nil
|
||||
}
|
||||
if ref := database.Spec.TenantRef; ref != nil {
|
||||
tenant := &databasev1alpha1.PostgreSQLTenant{}
|
||||
err := r.Reader.Get(ctx, types.NamespacedName{Namespace: ref.Namespace, Name: string(ref.Name)}, tenant)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil {
|
||||
record.Tenant = bindingTenant(tenant)
|
||||
record.TenantProtected = controllerutil.ContainsFinalizer(tenant, TenantFinalizer)
|
||||
}
|
||||
}
|
||||
instance := &databasev1alpha1.PostgreSQLInstance{}
|
||||
err := r.Reader.Get(ctx, types.NamespacedName{Name: string(database.Spec.InstanceRef.Name)}, instance)
|
||||
if err != nil {
|
||||
return record, client.IgnoreNotFound(err)
|
||||
}
|
||||
observed, err := instanceRecord(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record.Instance = &application.CredentialInstance{
|
||||
Identity: binding.Identity{Name: instance.Name, UID: string(instance.UID)},
|
||||
Deleting: !instance.DeletionTimestamp.IsZero(), Ready: currentReady(instance.Generation, instance.Status.Conditions),
|
||||
Generation: instance.Generation, Endpoint: observed.Target.Definition().Endpoint(),
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func credentialStatus(database *databasev1alpha1.PostgreSQLDatabase) application.CredentialStatus {
|
||||
status := application.CredentialStatus{Version: database.Status.CredentialVersion}
|
||||
if ref := database.Status.CredentialRef; ref != nil {
|
||||
status.Location = &application.CredentialLocation{Mount: ref.Mount, Path: ref.Path}
|
||||
}
|
||||
if condition := meta.FindStatusCondition(database.Status.Conditions, application.CredentialsReady); condition != nil {
|
||||
status.Ready = condition.Status == metav1.ConditionTrue && condition.ObservedGeneration == database.Generation
|
||||
status.Reason, status.Message = condition.Reason, condition.Message
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (r *CredentialResources) Save(ctx context.Context, record *application.CredentialRecord, status application.CredentialStatus) (*application.CredentialRecord, error) {
|
||||
bindingResources := &BindingResources{Client: r.Client, Reader: r.Reader}
|
||||
database, err := bindingResources.databaseAtVersion(ctx, &record.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previous := database.Status.DeepCopy()
|
||||
if status.Location != nil {
|
||||
database.Status.CredentialRef = &databasev1alpha1.CredentialReference{Mount: status.Location.Mount, Path: status.Location.Path}
|
||||
}
|
||||
database.Status.CredentialVersion = status.Version
|
||||
conditionStatus := metav1.ConditionFalse
|
||||
if status.Ready {
|
||||
conditionStatus = metav1.ConditionTrue
|
||||
}
|
||||
meta.SetStatusCondition(&database.Status.Conditions, metav1.Condition{
|
||||
Type: application.CredentialsReady, Status: conditionStatus, Reason: status.Reason, Message: status.Message,
|
||||
ObservedGeneration: database.Generation,
|
||||
})
|
||||
meta.SetStatusCondition(&database.Status.Conditions, metav1.Condition{
|
||||
Type: readyCondition, Status: metav1.ConditionFalse, Reason: "ProvisioningIncomplete",
|
||||
Message: "凭据准备不代表 PostgreSQL 供应、应用登录或 Tenant 交付已经完成", ObservedGeneration: database.Generation,
|
||||
})
|
||||
database.Status.ObservedGeneration = database.Generation
|
||||
if !equality.Semantic.DeepEqual(*previous, database.Status) {
|
||||
if err := r.Client.Status().Update(ctx, database); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
updated := *record
|
||||
updated.Database = *bindingDatabase(database)
|
||||
updated.Status = credentialStatus(database)
|
||||
// 旧 CRD 会裁剪未知 status 字段。不能把 HTTP 成功当作位置/版本已保存后继续写后端。
|
||||
if !equality.Semantic.DeepEqual(updated.Status.Location, status.Location) || updated.Status.Version != status.Version {
|
||||
return nil, fmt.Errorf("凭据位置或版本未被 API 保留;请先升级 Database CRD,未继续外部操作")
|
||||
}
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
func (r *CredentialResources) CheckCurrent(ctx context.Context, record *application.CredentialRecord) error {
|
||||
current, err := r.Load(ctx, record.Database.Identity.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current == nil || current.Database.Identity != record.Database.Identity || current.Database.Revision != record.Database.Revision ||
|
||||
!sameCredentialDependencies(current, record) {
|
||||
return apierrors.NewConflict(databasev1alpha1.GroupVersion.WithResource("postgresqldatabases").GroupResource(), record.Database.Identity.Name,
|
||||
fmt.Errorf("凭据准备的资源快照已变化;停止本轮操作并重新观察"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameCredentialDependencies(current, previous *application.CredentialRecord) bool {
|
||||
if current.Tenant == nil || previous.Tenant == nil || current.Instance == nil || previous.Instance == nil {
|
||||
return false
|
||||
}
|
||||
// Tenant Ready 的诊断变化、Instance 对同一 generation 的观测刷新不改变写入目标。
|
||||
// 仍检查申请 spec generation、完整绑定身份、删除状态、保护和当前 Instance Ready。
|
||||
return current.Tenant.Generation == previous.Tenant.Generation &&
|
||||
equality.Semantic.DeepEqual(current.Tenant.Tenant, previous.Tenant.Tenant) &&
|
||||
current.TenantProtected == previous.TenantProtected && current.DatabaseProtected == previous.DatabaseProtected &&
|
||||
current.Instance.Instance == previous.Instance.Instance && current.Instance.Generation == previous.Instance.Generation &&
|
||||
current.Instance.Endpoint == previous.Instance.Endpoint
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (r *InstanceResources) PresentInstance(ctx context.Context, result applicat
|
||||
ready = metav1.ConditionTrue
|
||||
}
|
||||
meta.SetStatusCondition(&object.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: ready, ObservedGeneration: object.Generation,
|
||||
Type: readyCondition, Status: ready, ObservedGeneration: object.Generation,
|
||||
Reason: result.Reason, Message: result.Message,
|
||||
})
|
||||
if !equality.Semantic.DeepEqual(*previous, object.Status) {
|
||||
|
||||
Reference in New Issue
Block a user