239 lines
8.9 KiB
Go
239 lines
8.9 KiB
Go
//go:build integration
|
|
|
|
package openbao_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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"
|
|
|
|
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/adapter/openbao"
|
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/binding"
|
|
)
|
|
|
|
func TestCredentialPreparationWithRealBackends(t *testing.T) {
|
|
fixture := newPreparationFixture(t)
|
|
t.Run("创建重启和幂等", func(t *testing.T) { testPreparationRestart(t, fixture) })
|
|
t.Run("已有值不认领", func(t *testing.T) { testPreparationExisting(t, fixture) })
|
|
t.Run("确认写入失败", func(t *testing.T) { testPreparationLostConfirmation(t, fixture) })
|
|
t.Run("后端结果不确定", func(t *testing.T) { testPreparationUncertain(t, fixture) })
|
|
t.Run("创建中绑定变化", func(t *testing.T) { testPreparationChangedBinding(t, fixture) })
|
|
t.Run("依赖恢复与固定位置", func(t *testing.T) { testPreparationDependencies(t, fixture) })
|
|
t.Run("并发用例只有一个写入", func(t *testing.T) { testPreparationConcurrency(t, fixture) })
|
|
t.Run("位置被API裁剪时拒绝外部写入", func(t *testing.T) { testPreparationPruning(t, fixture) })
|
|
t.Run("实际manager的watch与重启", func(t *testing.T) { testPreparationWatch(t, fixture) })
|
|
}
|
|
|
|
func testPreparationRestart(t *testing.T, f *preparationFixture) {
|
|
database, tenant := f.bound(t, "restart")
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, "CredentialPrepared")
|
|
path := database.Status.CredentialRef.Path
|
|
before, err := f.bao.KVv2("secret").Get(t.Context(), path)
|
|
if err != nil {
|
|
t.Fatal("无法读取隔离测试凭据")
|
|
}
|
|
if before.Data["username"] != "restart" || before.Data["database"] != "restart" || len(before.Data) != 7 {
|
|
t.Fatal("用例未使用绑定目标生成七键凭据")
|
|
}
|
|
// 两次新用例实例模拟重启,第二轮必须没有无意义状态更新。
|
|
for range 2 {
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
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, "CredentialPrepared")
|
|
if database.ResourceVersion != revision {
|
|
t.Fatal("幂等重试不应改写 status")
|
|
}
|
|
after, err := f.bao.KVv2("secret").Get(t.Context(), path)
|
|
if err != nil || after.VersionMetadata.Version != 1 || !maps.Equal(before.Data, after.Data) {
|
|
t.Fatal("重启后不应生成或覆盖凭据")
|
|
}
|
|
if err := f.api.Delete(t.Context(), tenant); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, "PreparationStopped")
|
|
if err := f.api.Delete(t.Context(), database); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, "PreparationStopped")
|
|
if len(database.Finalizers) == 0 {
|
|
t.Fatal("本切片不得解除删除保护")
|
|
}
|
|
if _, err := f.bao.KVv2("secret").Get(t.Context(), path); err != nil {
|
|
t.Fatal("删除流程不得在本切片清理凭据")
|
|
}
|
|
}
|
|
|
|
func testPreparationExisting(t *testing.T, f *preparationFixture) {
|
|
database, _ := f.bound(t, "existing")
|
|
store := fixtureStore(t, f.bao)
|
|
path, err := store.ProvisionPath(string(database.UID))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.Create(t.Context(), path, fixtureCredential(t)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 0, binding.Conflict)
|
|
// 即使管理员删除了后端值,未解除的不确定状态也不能自动创建。
|
|
if err := f.bao.KVv2("secret").DeleteMetadata(t.Context(), path); err != nil {
|
|
t.Fatal("无法清理隔离测试 key")
|
|
}
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := f.bao.KVv2("secret").Get(t.Context(), path); !errors.Is(err, bao.ErrSecretNotFound) {
|
|
t.Fatal("Conflict 重入不应生成替代凭据")
|
|
}
|
|
}
|
|
|
|
// 故障只注入确认保存边界;此前写入使用真实 API server 和 Bao。
|
|
type failedConfirmationClient struct{ client.Client }
|
|
|
|
func (c failedConfirmationClient) Status() client.SubResourceWriter {
|
|
return failedConfirmationWriter{SubResourceWriter: c.Client.Status()}
|
|
}
|
|
|
|
type failedConfirmationWriter struct{ client.SubResourceWriter }
|
|
|
|
func (w failedConfirmationWriter) Update(ctx context.Context, object client.Object, options ...client.SubResourceUpdateOption) error {
|
|
if database, ok := object.(*databasev1alpha1.PostgreSQLDatabase); ok && database.Status.CredentialVersion > 0 {
|
|
return errors.New("fixture refuses confirmation update")
|
|
}
|
|
return w.SubResourceWriter.Update(ctx, object, options...)
|
|
}
|
|
|
|
func testPreparationLostConfirmation(t *testing.T, f *preparationFixture) {
|
|
database, _ := f.bound(t, "confirmation")
|
|
service := f.service(t)
|
|
service.Resources = &kubernetes.CredentialResources{Client: failedConfirmationClient{f.api}, Reader: f.api}
|
|
if err := service.Reconcile(t.Context(), database.Name); err == nil {
|
|
t.Fatal("确认写入失败应返回 API 错误")
|
|
}
|
|
f.status(t, database, 0, "CreationStarted")
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 0, binding.Conflict)
|
|
stored, err := f.bao.KVv2("secret").Get(t.Context(), database.Status.CredentialRef.Path)
|
|
if err != nil || stored.VersionMetadata.Version != 1 {
|
|
t.Fatal("写入结果必须保留且不能被重启认领")
|
|
}
|
|
}
|
|
|
|
type afterCreateStore struct {
|
|
application.CredentialStore
|
|
after func() 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
|
|
}
|
|
if err := s.after(); err != nil {
|
|
return 0, err
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
func testPreparationUncertain(t *testing.T, f *preparationFixture) {
|
|
database, _ := f.bound(t, "uncertain")
|
|
service := f.service(t)
|
|
service.Store = afterCreateStore{CredentialStore: service.Store, after: func() error { return application.ErrCredentialUncertain }}
|
|
if err := service.Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 0, binding.Conflict)
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 0, binding.Conflict)
|
|
}
|
|
|
|
func testPreparationChangedBinding(t *testing.T, f *preparationFixture) {
|
|
database, tenant := f.bound(t, "changed")
|
|
service := f.service(t)
|
|
service.Store = afterCreateStore{CredentialStore: service.Store, after: func() error {
|
|
return f.api.Delete(t.Context(), tenant)
|
|
}}
|
|
if err := service.Reconcile(t.Context(), database.Name); err == nil {
|
|
t.Fatal("中途删除 Tenant 后不得确认凭据")
|
|
}
|
|
f.status(t, database, 0, "CreationStarted")
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 0, binding.Conflict)
|
|
}
|
|
|
|
func testPreparationDependencies(t *testing.T, f *preparationFixture) {
|
|
database, _ := f.bound(t, "dependency")
|
|
denied := fixtureClient(t, f.bao.Address())
|
|
denied.ClearToken()
|
|
service := f.service(t)
|
|
service.Store = fixtureStore(t, denied)
|
|
if err := service.Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 0, binding.DependencyUnavailable)
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, "CredentialPrepared")
|
|
if err := service.Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, binding.DependencyUnavailable)
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, "CredentialPrepared")
|
|
moved, err := openbao.NewCredentials(f.bao, "other", "elsewhere")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service.Store = moved
|
|
if err := service.Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, binding.DependencyUnavailable)
|
|
if database.Status.CredentialRef.Mount != "secret" {
|
|
t.Fatal("配置变化不得迁移固定位置")
|
|
}
|
|
if err := f.bao.KVv2("secret").Delete(t.Context(), database.Status.CredentialRef.Path); err != nil {
|
|
t.Fatal("无法准备测试软删除")
|
|
}
|
|
if err := f.service(t).Reconcile(t.Context(), database.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.status(t, database, 1, binding.Conflict)
|
|
}
|