feat: 接通 Database 凭据准备闭环
Verify / test (pull_request) Successful in 11m23s
Verify / lint (pull_request) Successful in 12m18s
Verify / database-integration (pull_request) Successful in 13m49s

This commit is contained in:
2026-09-27 18:35:13 +00:00
parent 371248659b
commit 35ada6d7eb
24 changed files with 1385 additions and 37 deletions
+28 -3
View File
@@ -6,7 +6,10 @@ import (
"fmt"
"os"
bao "github.com/openbao/openbao/api/v2"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
databasebao "git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao"
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/postgresql"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
databasecontroller "git.ddupan.top/panxiao81/ayatori/internal/database/controller"
@@ -14,14 +17,18 @@ import (
)
type databaseOptions struct {
secretNamespace string
rootCert string
secretNamespace string
rootCert string
credentialMount string
credentialPrefix string
}
func (o *databaseOptions) bindFlags(flags *flag.FlagSet) {
flags.StringVar(&o.secretNamespace, "database-secret-namespace", os.Getenv("POD_NAMESPACE"),
"固定管理 Secret namespace;为空时不启用 Instance 观测")
flags.StringVar(&o.rootCert, "database-root-cert", "", "PostgreSQL 管理连接信任的公开 CA bundle 路径")
flags.StringVar(&o.credentialMount, "database-credential-mount", "", "应用凭据 KV v2 mount;为空时不启用凭据准备")
flags.StringVar(&o.credentialPrefix, "database-credential-prefix", "applications", "应用凭据路径前缀;已有固定位置不随配置变化迁移")
}
func (o databaseOptions) configureManager(options *ctrl.Options) {
@@ -31,7 +38,7 @@ func (o databaseOptions) configureManager(options *ctrl.Options) {
}
// setupDatabase 封装 Database 的内部装配,并返回在 manager 停止后执行的清理。
func setupDatabase(ctx context.Context, manager ctrl.Manager, options databaseOptions) (func(), error) {
func setupDatabase(ctx context.Context, manager ctrl.Manager, options databaseOptions, baoClient *bao.Client) (func(), error) {
cleanup := func() {}
if options.secretNamespace != "" {
service, err := setupInstanceObservation(manager, options.secretNamespace, options.rootCert)
@@ -44,9 +51,27 @@ func setupDatabase(ctx context.Context, manager ctrl.Manager, options databaseOp
cleanup()
return nil, fmt.Errorf("set up Database binding controller: %w", err)
}
if err := setupCredentialPreparation(manager, options, baoClient); err != nil {
cleanup()
return nil, fmt.Errorf("set up Database credential preparation: %w", err)
}
return cleanup, nil
}
func setupCredentialPreparation(manager ctrl.Manager, options databaseOptions, baoClient *bao.Client) error {
if options.credentialMount == "" {
return nil
}
if baoClient == nil {
return fmt.Errorf("database credential preparation requires OpenBao authentication configuration")
}
store, err := databasebao.NewCredentials(baoClient, options.credentialMount, options.credentialPrefix)
if err != nil {
return err
}
return (&databasecontroller.CredentialReconciler{Store: store}).SetupWithManager(manager)
}
func setupInstanceObservation(manager ctrl.Manager, namespace, rootCert string) (*application.InstanceService, error) {
credentials, err := kubernetes.NewSecretCredentials(manager.GetAPIReader(), namespace)
if err != nil {
+16 -2
View File
@@ -9,6 +9,8 @@ import (
"testing"
"time"
bao "github.com/openbao/openbao/api/v2"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
@@ -46,16 +48,28 @@ func TestBootstrapWithRealAPIServer(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := setupOpenBaoAuthentication(manager, options.openBao); err != nil {
baoClient, err := setupOpenBaoAuthentication(manager, options.openBao)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
cleanup, err := setupDatabase(ctx, manager, options.database)
cleanup, err := setupDatabase(ctx, manager, options.database, baoClient)
if err != nil {
t.Fatal(err)
}
defer cleanup()
// 空 API 中没有供应目标;此处验证启用路径确实注册 controller,不访问外部 Bao。
fixtureConfig := bao.NewConfig()
fixtureConfig.Address = "http://127.0.0.1:1"
fixtureClient, err := bao.NewClient(fixtureConfig)
if err != nil {
t.Fatal(err)
}
options.database.credentialMount = "secret"
if err := setupCredentialPreparation(manager, options.database, fixtureClient); err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- manager.Start(ctx) }()
defer func() {
+12 -6
View File
@@ -21,6 +21,8 @@ import (
"flag"
"net/http"
bao "github.com/openbao/openbao/api/v2"
ctrl "sigs.k8s.io/controller-runtime"
"git.ddupan.top/panxiao81/ayatori/internal/infra/openbao"
@@ -45,28 +47,32 @@ func (o *openBaoOptions) bindFlags(flags *flag.FlagSet) {
flags.StringVar(&o.identity.Audience, "openbao-token-audience", "openbao", "SA JWT audience,须匹配 OpenBao role")
}
func setupOpenBaoAuthentication(manager ctrl.Manager, options openBaoOptions) error {
func setupOpenBaoAuthentication(manager ctrl.Manager, options openBaoOptions) (*bao.Client, error) {
if options.address == "" {
return nil
return nil, nil
}
client, err := openbao.NewClient(options.address, options.caCert)
if err != nil {
return err
return nil, err
}
// 复用 manager 已装配的 Kubernetes client,不重复加载配置或创建客户端。
session, err := openbao.NewKubernetesSession(
client, manager.GetClient(), options.mount, options.role, options.identity,
)
if err != nil {
return err
return nil, err
}
if err := manager.Add(session); err != nil {
return err
return nil, err
}
return manager.AddReadyzCheck("openbao-auth", func(_ *http.Request) error {
err = manager.AddReadyzCheck("openbao-auth", func(_ *http.Request) error {
if !session.Ready() {
return errors.New("OpenBao Kubernetes authentication unavailable")
}
return nil
})
if err != nil {
return nil, err
}
return client, nil
}
+14 -1
View File
@@ -38,7 +38,7 @@ func TestDefaultConfiguration(t *testing.T) {
if options.manager.webhookOptions().Port != 9443 || !options.logging.Development {
t.Fatal("webhook or logging defaults changed")
}
if options.database.secretNamespace != "" || options.openBao.address != "" {
if options.database.secretNamespace != "" || options.database.credentialMount != "" || options.openBao.address != "" {
t.Fatal("optional backends enabled by default")
}
if options.openBao.mount != "kubernetes" || options.openBao.identity.Audience != "openbao" {
@@ -58,6 +58,19 @@ func TestDefaultConfiguration(t *testing.T) {
}
}
func TestCredentialPreparationOptions(t *testing.T) {
options := parseTestOptions(t, "--database-credential-mount=applications-kv", "--database-credential-prefix=database")
if options.database.credentialMount != "applications-kv" || options.database.credentialPrefix != "database" {
t.Fatal("凭据准备参数未传入领域装配")
}
if err := setupCredentialPreparation(nil, options.database, nil); err == nil {
t.Fatal("启用凭据准备必须有显式配置的认证 client")
}
if err := setupCredentialPreparation(nil, databaseOptions{}, nil); err != nil {
t.Fatal("默认停用凭据准备不应要求后端")
}
}
func TestManagerFlagOverrides(t *testing.T) {
options := parseTestOptions(t,
"--metrics-bind-address=:9090", "--metrics-secure=false", "--health-probe-bind-address=:9091",
+3 -2
View File
@@ -23,10 +23,11 @@ func Run() error {
if err != nil {
return err
}
if err := setupOpenBaoAuthentication(manager, options.openBao); err != nil {
baoClient, err := setupOpenBaoAuthentication(manager, options.openBao)
if err != nil {
return fmt.Errorf("set up OpenBao authentication: %w", err)
}
cleanup, err := setupDatabase(context.Background(), manager, options.database)
cleanup, err := setupDatabase(context.Background(), manager, options.database, baoClient)
if err != nil {
return err
}