refactor: 集中显式注入并归位凭据领域规则
Verify / test (pull_request) Successful in 13m29s
Verify / lint (pull_request) Successful in 14m13s
Verify / database-integration (pull_request) Successful in 15m48s

This commit is contained in:
2026-09-27 19:16:48 +00:00
parent 72ce3eda40
commit 7834cab97f
34 changed files with 612 additions and 259 deletions
@@ -3,22 +3,24 @@ package openbao
import (
"context"
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
)
var _ application.CredentialStore = (*Credentials)(nil)
func (c *Credentials) ProvisionLocation(uid string) (application.CredentialLocation, error) {
func (c *Credentials) ProvisionLocation(uid string) (credentialdomain.Location, error) {
path, err := c.ProvisionPath(uid)
if err != nil {
return application.CredentialLocation{}, err
return credentialdomain.Location{}, err
}
return application.CredentialLocation{Mount: c.mount, Path: path}, nil
return credentialdomain.Location{Mount: c.mount, Path: path}, nil
}
func (c *Credentials) ReadCredential(ctx context.Context, location application.CredentialLocation, version int64) (application.ApplicationCredential, error) {
func (c *Credentials) ReadCredential(ctx context.Context, location credentialdomain.Location, version int64) (credentialdomain.ApplicationCredential, error) {
if location.Mount != c.mount {
return application.ApplicationCredential{}, ErrInvalidLocation
return credentialdomain.ApplicationCredential{}, ErrInvalidLocation
}
if version == 0 {
return c.Read(ctx, location.Path)
@@ -26,7 +28,7 @@ func (c *Credentials) ReadCredential(ctx context.Context, location application.C
return c.ReadConfirmed(ctx, location.Path, version)
}
func (c *Credentials) CreateCredential(ctx context.Context, location application.CredentialLocation, credential application.ApplicationCredential) (int64, error) {
func (c *Credentials) CreateCredential(ctx context.Context, location credentialdomain.Location, credential credentialdomain.ApplicationCredential) (int64, error) {
if location.Mount != c.mount {
return 0, ErrInvalidLocation
}
@@ -26,6 +26,8 @@ import (
"slices"
"strings"
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
bao "github.com/openbao/openbao/api/v2"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
@@ -80,33 +82,33 @@ func (c *Credentials) accepts(path string) bool {
}
// Read 只读取调用方已确认关联的路径;成功读取不构成对既有凭据的自动认领。
func (c *Credentials) Read(ctx context.Context, path string) (application.ApplicationCredential, error) {
func (c *Credentials) Read(ctx context.Context, path string) (credentialdomain.ApplicationCredential, error) {
secret, err := c.read(ctx, path)
if err != nil {
return application.ApplicationCredential{}, err
return credentialdomain.ApplicationCredential{}, err
}
return application.ParseApplicationCredential(secret.Data)
return credentialdomain.ParseApplicationCredential(secret.Data)
}
// ReadConfirmed 读取最新值并核对已持久化的确认版本,不回退读取历史版本。
// 确认后的删除或改写需要人工处理,不能因此重新生成密码。
func (c *Credentials) ReadConfirmed(ctx context.Context, path string, version int64) (application.ApplicationCredential, error) {
func (c *Credentials) ReadConfirmed(ctx context.Context, path string, version int64) (credentialdomain.ApplicationCredential, error) {
if version < 1 {
return application.ApplicationCredential{}, ErrConflict
return credentialdomain.ApplicationCredential{}, ErrConflict
}
secret, err := c.read(ctx, path)
if errors.Is(err, ErrNotFound) {
return application.ApplicationCredential{}, ErrConflict
return credentialdomain.ApplicationCredential{}, ErrConflict
}
if err != nil {
return application.ApplicationCredential{}, err
return credentialdomain.ApplicationCredential{}, err
}
if secret.VersionMetadata == nil || int64(secret.VersionMetadata.Version) != version {
return application.ApplicationCredential{}, ErrConflict
return credentialdomain.ApplicationCredential{}, ErrConflict
}
credential, err := application.ParseApplicationCredential(secret.Data)
credential, err := credentialdomain.ParseApplicationCredential(secret.Data)
if err != nil {
return application.ApplicationCredential{}, ErrConflict
return credentialdomain.ApplicationCredential{}, ErrConflict
}
return credential, nil
}
@@ -130,7 +132,7 @@ func (c *Credentials) read(ctx context.Context, path string) (*bao.KVSecret, err
// Create 只创建从未存在过的路径,并验证回读七键与提交值完全一致。
// 任何不确定写入都不返回凭据;上层必须停止供应并持久化冲突,不能重新生成密码。
func (c *Credentials) Create(ctx context.Context, path string, credential application.ApplicationCredential) error {
func (c *Credentials) Create(ctx context.Context, path string, credential credentialdomain.ApplicationCredential) error {
if !c.accepts(path) {
return ErrInvalidLocation
}
@@ -25,10 +25,11 @@ import (
"sync/atomic"
"testing"
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
bao "github.com/openbao/openbao/api/v2"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
)
const (
@@ -39,9 +40,9 @@ const (
kvVersionKey = "version"
)
func fixtureCredential(t *testing.T) application.ApplicationCredential {
func fixtureCredential(t *testing.T) credentialdomain.ApplicationCredential {
t.Helper()
credential, err := application.ParseApplicationCredential(map[string]any{
credential, err := credentialdomain.ParseApplicationCredential(map[string]any{
"username": "app_owner", "password": fixturePassword, "database": "app",
"host": "postgres.example", "hostaddr": "192.0.2.1", "port": "5432", "sslmode": "verify-full",
})
@@ -54,7 +54,7 @@ func testPreparationConcurrency(t *testing.T, f *preparationFixture) {
if succeeded != 1 || conflicted != 1 {
t.Fatal("同一快照只能有一个用例成功固定位置并继续创建")
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
stored, err := f.bao.KVv2("secret").Get(t.Context(), database.Status.CredentialRef.Path)
if err != nil || stored.VersionMetadata.Version != 1 {
t.Fatal("并发准备用例只能产生一个凭据版本")
@@ -80,7 +80,8 @@ func (f *preparationFixture) bound(t *testing.T, name string) (*databasev1alpha1
if err := f.api.Create(t.Context(), tenant); err != nil {
t.Fatal(err)
}
binder := &databasecontroller.BindingReconciler{Client: f.api, Reader: f.api}
resources := &kubernetes.BindingResources{Client: f.api, Reader: f.api}
binder := databasecontroller.NewBindingReconciler(f.api, &application.BindingService{Resources: resources}, resources)
if _, err := binder.Reconcile(t.Context(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tenant)}); err != nil {
t.Fatal(err)
}
@@ -106,7 +107,7 @@ func (f *preparationFixture) status(t *testing.T, database *databasev1alpha1.Pos
if err := f.api.Get(t.Context(), client.ObjectKeyFromObject(database), database); err != nil {
t.Fatal(err)
}
condition := meta.FindStatusCondition(database.Status.Conditions, application.CredentialsReady)
condition := meta.FindStatusCondition(database.Status.Conditions, "CredentialsReady")
if database.Status.CredentialVersion != version || condition == nil || condition.Reason != reason {
t.Fatalf("凭据版本或条件不符:version=%d,期望 reason=%s", database.Status.CredentialVersion, reason)
}
@@ -8,6 +8,8 @@ import (
"maps"
"testing"
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
bao "github.com/openbao/openbao/api/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -36,7 +38,7 @@ func testPreparationRestart(t *testing.T, f *preparationFixture) {
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
path := database.Status.CredentialRef.Path
before, err := f.bao.KVv2("secret").Get(t.Context(), path)
if err != nil {
@@ -51,12 +53,12 @@ func testPreparationRestart(t *testing.T, f *preparationFixture) {
t.Fatal(err)
}
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
revision := database.ResourceVersion
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
if database.ResourceVersion != revision {
t.Fatal("幂等重试不应改写 status")
}
@@ -135,7 +137,7 @@ func testPreparationLostConfirmation(t *testing.T, f *preparationFixture) {
if err := service.Reconcile(t.Context(), database.Name); err == nil {
t.Fatal("确认写入失败应返回 API 错误")
}
f.status(t, database, 0, application.CredentialCreationStarted)
f.status(t, database, 0, "CreationStarted")
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
@@ -151,7 +153,7 @@ type afterCreateStore struct {
after func() error
}
func (s afterCreateStore) CreateCredential(ctx context.Context, location application.CredentialLocation, credential application.ApplicationCredential) (int64, error) {
func (s afterCreateStore) CreateCredential(ctx context.Context, location credentialdomain.Location, credential credentialdomain.ApplicationCredential) (int64, error) {
version, err := s.CredentialStore.CreateCredential(ctx, location, credential)
if err != nil {
return 0, err
@@ -185,7 +187,7 @@ func testPreparationChangedBinding(t *testing.T, f *preparationFixture) {
if err := service.Reconcile(t.Context(), database.Name); err == nil {
t.Fatal("中途删除 Tenant 后不得确认凭据")
}
f.status(t, database, 0, application.CredentialCreationStarted)
f.status(t, database, 0, "CreationStarted")
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
@@ -205,7 +207,7 @@ func testPreparationDependencies(t *testing.T, f *preparationFixture) {
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
if err := service.Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
@@ -213,7 +215,7 @@ func testPreparationDependencies(t *testing.T, f *preparationFixture) {
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
moved, err := openbao.NewCredentials(f.bao, "other", "elsewhere")
if err != nil {
t.Fatal(err)
@@ -12,7 +12,6 @@ import (
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
)
// 在 API 边界模拟旧 schema 裁剪位置字段;剩余写入仍由真实 API server 处理。
@@ -48,5 +47,5 @@ func testPreparationPruning(t *testing.T, f *preparationFixture) {
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
t.Fatal(err)
}
f.status(t, database, 1, application.CredentialPrepared)
f.status(t, database, 1, "CredentialPrepared")
}
@@ -7,6 +7,9 @@ import (
"testing"
"time"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
@@ -15,7 +18,6 @@ import (
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
databasecontroller "git.ddupan.top/panxiao81/ayatori/internal/database/controller"
)
@@ -26,7 +28,7 @@ func testPreparationWatch(t *testing.T, f *preparationFixture) {
return
}
if err := f.api.Get(context.Background(), client.ObjectKeyFromObject(database), database); err == nil {
if condition := meta.FindStatusCondition(database.Status.Conditions, application.CredentialsReady); condition != nil {
if condition := meta.FindStatusCondition(database.Status.Conditions, "CredentialsReady"); condition != nil {
t.Logf("失败时凭据条件: %s: %s", condition.Reason, condition.Message)
}
}
@@ -50,7 +52,7 @@ func testPreparationWatch(t *testing.T, f *preparationFixture) {
if err := f.api.Get(t.Context(), client.ObjectKeyFromObject(database), database); err != nil {
return false
}
condition := meta.FindStatusCondition(database.Status.Conditions, application.CredentialsReady)
condition := meta.FindStatusCondition(database.Status.Conditions, "CredentialsReady")
return condition != nil && condition.Reason == "DependencyUnavailable"
})
if database.Status.CredentialRef != nil {
@@ -69,7 +71,7 @@ func testPreparationWatch(t *testing.T, f *preparationFixture) {
if err := f.api.Get(t.Context(), client.ObjectKeyFromObject(database), database); err != nil {
return false
}
condition := meta.FindStatusCondition(database.Status.Conditions, application.CredentialsReady)
condition := meta.FindStatusCondition(database.Status.Conditions, "CredentialsReady")
return condition != nil && condition.Status == metav1.ConditionFalse && condition.Reason == "DependencyUnavailable"
})
setReady(metav1.ConditionTrue)
@@ -77,7 +79,7 @@ func testPreparationWatch(t *testing.T, f *preparationFixture) {
if err := f.api.Get(t.Context(), client.ObjectKeyFromObject(database), database); err != nil {
return false
}
condition := meta.FindStatusCondition(database.Status.Conditions, application.CredentialsReady)
condition := meta.FindStatusCondition(database.Status.Conditions, "CredentialsReady")
return condition != nil && condition.Status == metav1.ConditionTrue && database.Status.CredentialVersion == 1
})
stored, err := f.bao.KVv2("secret").Get(t.Context(), database.Status.CredentialRef.Path)
@@ -96,10 +98,14 @@ func startPreparationManager(t *testing.T, f *preparationFixture) func() {
if err != nil {
t.Fatal(err)
}
if err := (&databasecontroller.BindingReconciler{}).SetupWithManager(t.Context(), manager); err != nil {
bindingResources := &kubernetes.BindingResources{Client: manager.GetClient(), Reader: manager.GetAPIReader()}
binder := databasecontroller.NewBindingReconciler(manager.GetClient(), &application.BindingService{Resources: bindingResources}, bindingResources)
if err := binder.SetupWithManager(t.Context(), manager); err != nil {
t.Fatal(err)
}
if err := (&databasecontroller.CredentialReconciler{Store: fixtureStore(t, f.bao)}).SetupWithManager(manager); err != nil {
credentialResources := &kubernetes.CredentialResources{Client: manager.GetClient(), Reader: manager.GetAPIReader()}
preparation := &application.CredentialPreparation{Resources: credentialResources, Store: fixtureStore(t, f.bao)}
if err := databasecontroller.NewCredentialReconciler(manager.GetClient(), preparation).SetupWithManager(manager); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())