feat: 接通 PostgreSQL 角色与数据库创建闭环
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
//go:build integration
|
||||
|
||||
package postgresql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
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"
|
||||
databasecontroller "git.ddupan.top/panxiao81/ayatori/internal/database/controller"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/provisioning"
|
||||
bao "github.com/openbao/openbao/api/v2"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
)
|
||||
|
||||
type provisioningFixture struct {
|
||||
*credentialFixture
|
||||
api client.Client
|
||||
scheme *runtime.Scheme
|
||||
store *openbao.Credentials
|
||||
resources *kubernetes.ProvisioningResources
|
||||
usecase application.DatabaseProvisioning
|
||||
}
|
||||
|
||||
func newProvisioningFixture(t *testing.T) *provisioningFixture {
|
||||
t.Helper()
|
||||
f := newCredentialFixture(t)
|
||||
useNativeManager(t, f)
|
||||
if _, err := envtest.InstallCRDs(f.config, envtest.CRDInstallOptions{Paths: []string{"../../../../config/crd/bases"}, ErrorIfPathMissing: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scheme := runtime.NewScheme()
|
||||
if err := databasev1alpha1.AddToScheme(scheme); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
api, err := client.New(f.config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
object := &databasev1alpha1.PostgreSQLInstance{Name: "supply-instance", Spec: databasev1alpha1.PostgreSQLInstanceSpec{
|
||||
Endpoint: databasev1alpha1.PostgreSQLEndpoint{Host: fixtureHost, HostAddr: fixtureAddress, Port: int32(f.port), SSLMode: "disable"},
|
||||
AdminCredentialRef: databasev1alpha1.AdminCredentialReference{Name: secretName, UsernameKey: managementUsernameKey, PasswordKey: managementPasswordKey},
|
||||
}}
|
||||
if err := api.Create(f.ctx, object); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
instanceResources := &kubernetes.InstanceResources{Client: api, Reader: api}
|
||||
instanceService := &application.InstanceReconciliation{Resources: instanceResources, Observer: f.service}
|
||||
result, err := instanceService.Reconcile(f.ctx, object.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instanceResources.PresentInstance(f.ctx, result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := api.Get(f.ctx, client.ObjectKeyFromObject(object), object); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !meta.IsStatusConditionTrue(object.Status.Conditions, "Ready") {
|
||||
t.Fatal("实际管理账号未就绪")
|
||||
}
|
||||
store := provisioningBao(t)
|
||||
resources := &kubernetes.ProvisioningResources{Client: api, Reader: api}
|
||||
return &provisioningFixture{credentialFixture: f, api: api, scheme: scheme, store: store, resources: resources,
|
||||
usecase: application.DatabaseProvisioning{Resources: resources, Credentials: store, Backend: f.service}}
|
||||
}
|
||||
|
||||
// 三后端组合验收只启动自己的 dev Bao;不读取环境 token 或外部地址。
|
||||
func provisioningBao(t *testing.T) *openbao.Credentials {
|
||||
t.Helper()
|
||||
const image = "openbao/openbao@sha256:5b2486ab0fb90bbc788cc345b0a08616dfb375873ee8be5df3a2fd4d378a67e0"
|
||||
const token = "AYATORI-TEST-ONLY-supply-token"
|
||||
prepare, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
if exec.CommandContext(prepare, "docker", "image", "inspect", image).Run() != nil {
|
||||
if output, err := exec.CommandContext(prepare, "docker", "pull", image).CombinedOutput(); err != nil {
|
||||
t.Fatalf("隔离 Bao 镜像准备失败:%s", output)
|
||||
}
|
||||
}
|
||||
ctx, stop := context.WithTimeout(t.Context(), time.Minute)
|
||||
defer stop()
|
||||
output, err := exec.CommandContext(ctx, "docker", "run", "--pull=never", "--rm", "-d", "-p", "127.0.0.1::8200", image,
|
||||
"server", "-dev", "-dev-root-token-id="+token, "-dev-listen-address=0.0.0.0:8200").Output()
|
||||
if err != nil {
|
||||
t.Fatal("隔离 Bao 启动失败")
|
||||
}
|
||||
id := strings.TrimSpace(string(output))
|
||||
if !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(id) {
|
||||
t.Fatal("无效容器 ID")
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if exec.CommandContext(cleanup, "docker", "rm", "-f", id).Run() != nil {
|
||||
t.Error("隔离 Bao 清理失败")
|
||||
}
|
||||
})
|
||||
output, err = exec.CommandContext(ctx, "docker", "inspect", "--format", `{{(index (index .NetworkSettings.Ports "8200/tcp") 0).HostPort}}`, id).Output()
|
||||
if err != nil {
|
||||
t.Fatal("隔离 Bao 端口不可读")
|
||||
}
|
||||
backend, err := bao.NewClient(&bao.Config{Address: "http://127.0.0.1:" + strings.TrimSpace(string(output)), Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
t.Fatal("隔离 Bao client 构造失败")
|
||||
}
|
||||
backend.SetToken(token)
|
||||
for {
|
||||
if _, err := backend.Sys().HealthWithContext(ctx); err == nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("隔离 Bao 健康检查超时")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
store, err := openbao.NewCredentials(backend, "secret", "applications")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func (f *provisioningFixture) bound(t *testing.T, name string) *databasev1alpha1.PostgreSQLDatabase {
|
||||
t.Helper()
|
||||
tenant := &databasev1alpha1.PostgreSQLTenant{Name: strings.ReplaceAll(name, "_", "-"), Namespace: controllerNamespace,
|
||||
Spec: databasev1alpha1.PostgreSQLTenantSpec{Provision: &databasev1alpha1.DatabaseProvisionRequest{
|
||||
InstanceRef: databasev1alpha1.InstanceReference{Name: "supply-instance"},
|
||||
Database: databasev1alpha1.PostgreSQLIdentifier(name), LoginRole: databasev1alpha1.PostgreSQLIdentifier(name),
|
||||
}}}
|
||||
if err := f.api.Create(f.ctx, tenant); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resources := &kubernetes.BindingResources{Client: f.api, Reader: f.api}
|
||||
binder := databasecontroller.NewBindingReconciler(f.api, &application.BindingService{Resources: resources}, resources)
|
||||
if _, err := binder.Reconcile(f.ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tenant)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.api.Get(f.ctx, client.ObjectKeyFromObject(tenant), tenant); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
database := &databasev1alpha1.PostgreSQLDatabase{}
|
||||
if err := f.api.Get(f.ctx, client.ObjectKey{Name: string(tenant.Status.DatabaseRef.Name)}, database); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prepare := application.CredentialPreparation{Resources: &kubernetes.CredentialResources{Client: f.api, Reader: f.api}, Store: f.store}
|
||||
if err := prepare.Reconcile(f.ctx, database.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return database
|
||||
}
|
||||
|
||||
func (f *provisioningFixture) reconcile(t *testing.T, database *databasev1alpha1.PostgreSQLDatabase, phase provisioning.Phase) {
|
||||
t.Helper()
|
||||
if err := f.usecase.Reconcile(f.ctx, database.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.status(t, database, phase)
|
||||
}
|
||||
|
||||
func (f *provisioningFixture) status(t *testing.T, database *databasev1alpha1.PostgreSQLDatabase, phase provisioning.Phase) {
|
||||
t.Helper()
|
||||
if err := f.api.Get(f.ctx, client.ObjectKeyFromObject(database), database); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
condition := meta.FindStatusCondition(database.Status.Conditions, "ResourcesReady")
|
||||
if condition == nil || condition.Reason != string(phase) {
|
||||
t.Fatalf("期望资源阶段 %s,实际 %v", phase, condition)
|
||||
}
|
||||
if meta.IsStatusConditionTrue(database.Status.Conditions, "Ready") {
|
||||
t.Fatal("角色建库不代表扩展与交付完成")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user