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
+10 -10
View File
@@ -37,25 +37,25 @@ func (o databaseOptions) configureManager(options *ctrl.Options) {
}
}
// setupDatabase 封装 Database 的内部装配,并返回在 manager 停止后执行的清理。
func setupDatabase(ctx context.Context, manager ctrl.Manager, options databaseOptions, baoClient *bao.Client) (func(), error) {
cleanup := func() {}
// registerDatabaseControllers 封装 Database 的内部装配,并返回在 manager 停止后执行的清理。
func registerDatabaseControllers(ctx context.Context, manager ctrl.Manager, options databaseOptions, baoClient *bao.Client) (func(), error) {
closeDatabaseConnections := func() {}
if options.secretNamespace != "" {
service, err := setupInstanceObservation(manager, options.secretNamespace, options.rootCert)
if err != nil {
return nil, fmt.Errorf("set up Instance observation: %w", err)
}
cleanup = service.Close
closeDatabaseConnections = service.Close
}
if err := (&databasecontroller.BindingReconciler{}).SetupWithManager(ctx, manager); err != nil {
cleanup()
if err := wireBindingController(manager.GetClient(), manager.GetAPIReader()).SetupWithManager(ctx, manager); err != nil {
closeDatabaseConnections()
return nil, fmt.Errorf("set up Database binding controller: %w", err)
}
if err := setupCredentialPreparation(manager, options, baoClient); err != nil {
cleanup()
closeDatabaseConnections()
return nil, fmt.Errorf("set up Database credential preparation: %w", err)
}
return cleanup, nil
return closeDatabaseConnections, nil
}
func setupCredentialPreparation(manager ctrl.Manager, options databaseOptions, baoClient *bao.Client) error {
@@ -69,7 +69,7 @@ func setupCredentialPreparation(manager ctrl.Manager, options databaseOptions, b
if err != nil {
return err
}
return (&databasecontroller.CredentialReconciler{Store: store}).SetupWithManager(manager)
return wireCredentialController(manager.GetClient(), manager.GetAPIReader(), store).SetupWithManager(manager)
}
func setupInstanceObservation(manager ctrl.Manager, namespace, rootCert string) (*application.InstanceService, error) {
@@ -81,7 +81,7 @@ func setupInstanceObservation(manager ctrl.Manager, namespace, rootCert string)
if err != nil {
return nil, err
}
reconciler := &databasecontroller.InstanceReconciler{Observer: service, SecretNamespace: namespace}
reconciler := wireInstanceController(manager.GetClient(), manager.GetAPIReader(), service, namespace)
if err := reconciler.SetupWithManager(manager); err != nil {
service.Close()
return nil, err
+28
View File
@@ -0,0 +1,28 @@
package bootstrap
import (
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
databasecontroller "git.ddupan.top/panxiao81/ayatori/internal/database/controller"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// 本文件是 Database 的显式依赖图:adapter → 用例 → controller。
// 构造只在启动时执行;用例和 controller 都不持有容器或动态查找依赖。
func wireBindingController(writer client.Client, reader client.Reader) *databasecontroller.BindingReconciler {
resources := &kubernetes.BindingResources{Client: writer, Reader: reader}
service := &application.BindingService{Resources: resources}
return databasecontroller.NewBindingReconciler(writer, service, resources)
}
func wireCredentialController(writer client.Client, reader client.Reader, store application.CredentialStore) *databasecontroller.CredentialReconciler {
resources := &kubernetes.CredentialResources{Client: writer, Reader: reader}
service := &application.CredentialPreparation{Resources: resources, Store: store}
return databasecontroller.NewCredentialReconciler(writer, service)
}
func wireInstanceController(writer client.Client, reader client.Reader, observer application.InstanceObserver, namespace string) *databasecontroller.InstanceReconciler {
resources := &kubernetes.InstanceResources{Client: writer, Reader: reader}
service := &application.InstanceReconciliation{Resources: resources, Observer: observer}
return databasecontroller.NewInstanceReconciler(writer, service, resources, namespace)
}
@@ -0,0 +1,74 @@
package bootstrap
import (
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/postgresql"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const controllerLayer = "controller"
func TestDatabaseExplicitWiring(t *testing.T) {
writer := fake.NewClientBuilder().Build()
directReader := fake.NewClientBuilder().Build()
binder := wireBindingController(writer, directReader)
bindingResources, ok := binder.Service.Resources.(*kubernetes.BindingResources)
if !ok || binder.Client != writer || bindingResources.Reader != directReader || bindingResources.Client != writer || binder.Presenter != bindingResources {
t.Fatal("绑定用例没有共享显式注入的 writer、直连 reader 与 presenter")
}
credentials, err := kubernetes.NewSecretCredentials(directReader, "wiring-tests")
if err != nil {
t.Fatal(err)
}
observer, err := application.NewInstanceService(credentials, postgresql.Connector{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(observer.Close)
reconciler := wireInstanceController(writer, directReader, observer, "wiring-tests")
resources, ok := reconciler.Service.Resources.(*kubernetes.InstanceResources)
if !ok || resources.Reader != directReader || resources.Client != writer || reconciler.Service.Observer != observer || reconciler.Presenter != resources {
t.Fatal("Instance 的服务或读取边界未按依赖图注入")
}
}
// 防止领域重新依赖用例/存储,也防止 controller 再次私自构造具体 adapter。
func TestDatabaseLayerImports(t *testing.T) {
for _, layer := range []string{"domain", controllerLayer} {
err := filepath.WalkDir(filepath.Join("../database", layer), func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
return err
}
for _, dependency := range file.Imports {
name, err := strconv.Unquote(dependency.Path.Value)
if err != nil {
return err
}
if strings.Contains(name, "/database/adapter/") || (layer == "domain" &&
(strings.Contains(name, "/database/application") || strings.Contains(name, "k8s.io/") || strings.Contains(name, "/internal/infra/"))) {
t.Errorf("%s 不得导入 %s", path, name)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
}
@@ -54,11 +54,11 @@ func TestBootstrapWithRealAPIServer(t *testing.T) {
}
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
cleanup, err := setupDatabase(ctx, manager, options.database, baoClient)
closeDatabaseConnections, err := registerDatabaseControllers(ctx, manager, options.database, baoClient)
if err != nil {
t.Fatal(err)
}
defer cleanup()
defer closeDatabaseConnections()
// 空 API 中没有供应目标;此处验证启用路径确实注册 controller,不访问外部 Bao。
fixtureConfig := bao.NewConfig()
fixtureConfig.Address = "http://127.0.0.1:1"
@@ -80,7 +80,7 @@ func TestBootstrapWithRealAPIServer(t *testing.T) {
t.Error(err)
}
case <-time.After(20 * time.Second):
t.Error("manager did not stop before component cleanup")
t.Error("manager did not stop before component closeDatabaseConnections")
}
}()
if !manager.GetCache().WaitForCacheSync(ctx) {
+2 -2
View File
@@ -27,12 +27,12 @@ func Run() error {
if err != nil {
return fmt.Errorf("set up OpenBao authentication: %w", err)
}
cleanup, err := setupDatabase(context.Background(), manager, options.database, baoClient)
closeDatabaseConnections, err := registerDatabaseControllers(context.Background(), manager, options.database, baoClient)
if err != nil {
return err
}
// manager 的 worker 完全停止后才释放组件持有的资源。
defer cleanup()
defer closeDatabaseConnections()
setupLog.Info("Starting manager")
if err := manager.Start(ctrl.SetupSignalHandler()); err != nil {