feat: 接入 Database 三资源 API 与分层绑定协调
Verify / test (pull_request) Successful in 11m39s
Verify / lint (pull_request) Successful in 12m51s
Verify / database-integration (pull_request) Successful in 13m12s

确定单库单账号、集群级 Database、资源侧先写绑定和凭据定位合同。领域层承载纯规则,service 协調流程,Kubernetes adapter 负责资源呈现与版本保护。

验证:全量 make test、三轮 race、真实 API server 并发与重启补写、最小 RBAC/watch、lint 和文档检查通过。供应、凭据交付及删除清理尚未实现,保留 DeletionPending/finalizer 边界。
This commit is contained in:
2026-09-25 04:09:43 +00:00
parent 347a667c0c
commit 7e9e8e828b
33 changed files with 3516 additions and 45 deletions
+67
View File
@@ -0,0 +1,67 @@
package v1alpha1
import "k8s.io/apimachinery/pkg/types"
// ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$`
type ObjectName string
// PostgreSQLIdentifier 是第一版受管 database 与 login role 使用的名称。
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z][a-z0-9_]{0,62}$`
type PostgreSQLIdentifier string
// InstanceReference 仅引用同 API group 的集群级 PostgreSQLInstance。
type InstanceReference struct {
Name ObjectName `json:"name"`
}
// DatabaseReference 是 Tenant 对已有集群级 PostgreSQLDatabase 的选择。
type DatabaseReference struct {
Name ObjectName `json:"name"`
}
// BoundDatabaseReference 记录已经参与绑定的对象身份,而非仅记录可复用的名称。
type BoundDatabaseReference struct {
Name ObjectName `json:"name"`
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=128
UID types.UID `json:"uid"`
}
// TenantReference 是 Database 的当前绑定记录,不是允许绑定名单。
type TenantReference struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Namespace string `json:"namespace"`
Name ObjectName `json:"name"`
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=128
UID types.UID `json:"uid"`
}
// ReclaimPolicy 控制资源释放后的处置,只有资源管理者可以修改。
// +kubebuilder:validation:Enum=Retain;Delete
type ReclaimPolicy string
const (
ReclaimRetain ReclaimPolicy = "Retain"
ReclaimDelete ReclaimPolicy = "Delete"
)
// CredentialReference 定位已有 OpenBao KV v2 凭据,不包含任何秘密值。
// 只由资源管理员在导入时填写;controller 必须检查部署允许的 mount/path 范围。
type CredentialReference struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Mount string `json:"mount"`
// Path 是 mount 内的逻辑路径,不含 KV v2 的 data/ API 前缀。
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=1024
Path string `json:"path"`
}
@@ -0,0 +1,25 @@
// Package v1alpha1 定义 Database 领域的 Kubernetes API。
// +kubebuilder:object:generate=true
// +groupName=database.ayatori.ddupan.top
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
SchemeGroupVersion = schema.GroupVersion{Group: "database.ayatori.ddupan.top", Version: "v1alpha1"}
GroupVersion = SchemeGroupVersion
SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&PostgreSQLInstance{}, &PostgreSQLInstanceList{},
&PostgreSQLDatabase{}, &PostgreSQLDatabaseList{},
&PostgreSQLTenant{}, &PostgreSQLTenantList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
})
AddToScheme = SchemeBuilder.AddToScheme
)
@@ -0,0 +1,62 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// PostgreSQLDatabaseSpec 是一库、一个 login owner 及凭据的独立资源声明。
// +kubebuilder:validation:XValidation:rule="(self.source == 'Import') == has(self.credentialRef)",message="only imported databases require an existing credentialRef"
type PostgreSQLDatabaseSpec struct {
InstanceRef InstanceReference `json:"instanceRef"`
Database PostgreSQLIdentifier `json:"database"`
LoginRole PostgreSQLIdentifier `json:"loginRole"`
// Source 明确区分创建与只读导入,不从后端同名对象推断。
// +kubebuilder:validation:Enum=Provision;Import
Source string `json:"source"`
// +optional
CredentialRef *CredentialReference `json:"credentialRef,omitempty"`
// +kubebuilder:default=Retain
// +optional
ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"`
// TenantRef 由 controller 先写入;Released 时仍保留旧身份。
// +optional
TenantRef *TenantReference `json:"tenantRef,omitempty"`
}
type PostgreSQLDatabaseStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// InstanceUID 记录观察时的实例身份,不把同名新实例视为原目标。
// +optional
InstanceUID types.UID `json:"instanceUID,omitempty"`
// Phase 暂不冻结供应子阶段枚举;它不是操作授权或绑定的替代记录。
// +optional
Phase string `json:"phase,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.spec.tenantRef) || (has(oldSelf.status) && has(oldSelf.status.instanceUID))) || (self.spec.instanceRef == oldSelf.spec.instanceRef && self.spec.database == oldSelf.spec.database && self.spec.loginRole == oldSelf.spec.loginRole && self.spec.source == oldSelf.spec.source && has(self.spec.credentialRef) == has(oldSelf.spec.credentialRef) && (!has(oldSelf.spec.credentialRef) || self.spec.credentialRef == oldSelf.spec.credentialRef))",message="managed database target cannot change after observation or binding starts"
// +kubebuilder:printcolumn:name="Instance",type=string,JSONPath=`.spec.instanceRef.name`
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.spec.database`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
type PostgreSQLDatabase struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec PostgreSQLDatabaseSpec `json:"spec"`
// +optional
Status PostgreSQLDatabaseStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type PostgreSQLDatabaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []PostgreSQLDatabase `json:"items"`
}
@@ -0,0 +1,80 @@
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// PostgreSQLEndpoint 显式区分证书主机名与实际连接 IP,不进行 DNS 推导。
type PostgreSQLEndpoint struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Host string `json:"host"`
// +kubebuilder:validation:MaxLength=45
// +kubebuilder:validation:XValidation:rule="isIP(self)",message="hostaddr must be a single IPv4 or IPv6 address"
HostAddr string `json:"hostaddr"`
// +kubebuilder:default=5432
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
// +optional
Port int32 `json:"port,omitempty"`
// +kubebuilder:default=postgres
// +optional
Database PostgreSQLIdentifier `json:"database,omitempty"`
// +kubebuilder:default=verify-full
// +kubebuilder:validation:Enum=disable;require;verify-ca;verify-full
// +optional
SSLMode string `json:"sslMode,omitempty"`
}
// AdminCredentialReference 只能读取 controller namespace 的 Secret。
type AdminCredentialReference struct {
Name ObjectName `json:"name"`
// +kubebuilder:default=username
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[-._a-zA-Z0-9]+$`
// +optional
UsernameKey string `json:"usernameKey,omitempty"`
// +kubebuilder:default=password
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[-._a-zA-Z0-9]+$`
// +optional
PasswordKey string `json:"passwordKey,omitempty"`
}
type PostgreSQLInstanceSpec struct {
Endpoint PostgreSQLEndpoint `json:"endpoint"`
AdminCredentialRef AdminCredentialReference `json:"adminCredentialRef"`
}
type PostgreSQLInstanceStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +kubebuilder:validation:Enum=Pending;Validating;Ready;Deleting
// +optional
Phase string `json:"phase,omitempty"`
// +optional
PostgreSQLVersion string `json:"postgresqlVersion,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
type PostgreSQLInstance struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec PostgreSQLInstanceSpec `json:"spec"`
// +optional
Status PostgreSQLInstanceStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type PostgreSQLInstanceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []PostgreSQLInstance `json:"items"`
}
@@ -0,0 +1,70 @@
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// DatabaseProvisionRequest 仅用于动态申请,省略名称时由 controller 按 Tenant 名称解析。
type DatabaseProvisionRequest struct {
InstanceRef InstanceReference `json:"instanceRef"`
// +optional
Database PostgreSQLIdentifier `json:"database,omitempty"`
// +optional
LoginRole PostgreSQLIdentifier `json:"loginRole,omitempty"`
}
// PostgreSQLTenantSpec 显式选择动态申请或已有 Database,不重复声明来源。
// +kubebuilder:validation:XValidation:rule="has(self.provision) != has(self.databaseRef)",message="exactly one of provision and databaseRef is required"
type PostgreSQLTenantSpec struct {
// +optional
Provision *DatabaseProvisionRequest `json:"provision,omitempty"`
// +optional
DatabaseRef *DatabaseReference `json:"databaseRef,omitempty"`
// Extensions 保留后端扩展名称的原样拼写,不按 SQL identifier 限制。
// +listType=set
// +optional
Extensions []string `json:"extensions,omitempty"`
// SecretName 指定 Tenant namespace 内的投射目标,省略时使用合同约定的默认名称。
// +optional
SecretName ObjectName `json:"secretName,omitempty"`
}
type PostgreSQLTenantStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// DatabaseRef 只有在资源侧确认绑定后才写入。
// +optional
DatabaseRef *BoundDatabaseReference `json:"databaseRef,omitempty"`
// +optional
Phase string `json:"phase,omitempty"`
// SecretName 是已观察到的同 namespace 投射目标,不包含凭据值。
// +optional
SecretName ObjectName `json:"secretName,omitempty"`
// CredentialURL 只含 OpenBao API 位置,禁止嵌入认证信息。
// +optional
CredentialURL string `json:"credentialURL,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase in ['Binding', 'Bound', 'Deleting']) || ((has(self.spec.provision) == has(oldSelf.spec.provision)) && (!has(oldSelf.spec.provision) || self.spec.provision == oldSelf.spec.provision) && (has(self.spec.databaseRef) == has(oldSelf.spec.databaseRef)) && (!has(oldSelf.spec.databaseRef) || self.spec.databaseRef == oldSelf.spec.databaseRef))",message="binding target cannot change after binding starts"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase in ['Binding', 'Bound', 'Deleting']) || (has(self.status) && has(self.status.phase) && self.status.phase in ['Binding', 'Bound', 'Deleting'])",message="binding progress cannot return to an unbound state"
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.status.databaseRef.name`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
type PostgreSQLTenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec PostgreSQLTenantSpec `json:"spec"`
// +optional
Status PostgreSQLTenantStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type PostgreSQLTenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []PostgreSQLTenant `json:"items"`
}
+312
View File
@@ -0,0 +1,312 @@
package v1alpha1_test
import (
"context"
"io"
"os"
"path/filepath"
"testing"
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/yaml"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
const (
testNamespace = "database-api"
testInstanceName = "shared-postgres"
readyPhase = "Ready"
)
func TestDatabaseAPI(t *testing.T) {
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
t.Skip("KUBEBUILDER_ASSETS 未设置;运行 make test 执行真实 API server 测试")
}
scheme := runtime.NewScheme()
if err := databasev1alpha1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
crdPath, err := filepath.Abs("../../../config/crd/bases")
if err != nil {
t.Fatal(err)
}
environment := &envtest.Environment{CRDDirectoryPaths: []string{crdPath}, ErrorIfCRDPathMissing: true}
config, err := environment.Start()
if err != nil {
t.Fatalf("启动 envtest: %v", err)
}
t.Cleanup(func() {
if err := environment.Stop(); err != nil {
t.Errorf("停止 envtest: %v", err)
}
})
client, err := ctrlclient.New(config, ctrlclient.Options{Scheme: scheme})
if err != nil {
t.Fatal(err)
}
namespace := &corev1.Namespace{}
namespace.Name = testNamespace
if err := client.Create(t.Context(), namespace); err != nil {
t.Fatal(err)
}
t.Run("作用域和默认值", func(t *testing.T) { testDefaults(t, client) })
t.Run("拒绝非法声明", func(t *testing.T) { testInvalidDeclarations(t, client) })
t.Run("status隔离和绑定并发", func(t *testing.T) { testBindingWrites(t, client) })
t.Run("仓库示例", func(t *testing.T) { testSamples(t, client, scheme) })
}
func testSamples(t *testing.T, client ctrlclient.Client, scheme *runtime.Scheme) {
paths := []string{
"database_v1alpha1_postgresqlinstance.yaml",
"database_v1alpha1_postgresqldatabase.yaml",
"database_v1alpha1_postgresqltenant.yaml",
}
for _, name := range paths {
t.Run(name, func(t *testing.T) {
file, err := os.Open(filepath.Join("../../../config/samples", name))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := file.Close(); err != nil {
t.Error(err)
}
})
decoder := yaml.NewYAMLOrJSONDecoder(file, 4096)
for {
var raw runtime.RawExtension
if err := decoder.Decode(&raw); err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
object, _, err := serializer.NewCodecFactory(scheme).UniversalDeserializer().Decode(raw.Raw, nil, nil)
if err != nil {
t.Fatal(err)
}
resource, ok := object.(ctrlclient.Object)
if !ok {
t.Fatalf("示例不是资源对象: %T", object)
}
if resource.GetNamespace() != "" {
resource.SetNamespace(testNamespace)
}
if err := client.Create(t.Context(), resource); err != nil {
t.Fatalf("示例未通过 API 校验: %v", err)
}
}
})
}
}
func testDefaults(t *testing.T, client ctrlclient.Client) {
instance := validInstance("defaults")
if err := client.Create(t.Context(), instance); err != nil {
t.Fatal(err)
}
endpoint := instance.Spec.Endpoint
if endpoint.Port != 5432 || endpoint.Database != "postgres" || endpoint.SSLMode != "verify-full" {
t.Fatalf("连接默认值不符: %+v", endpoint)
}
credentials := instance.Spec.AdminCredentialRef
if credentials.UsernameKey != "username" || credentials.PasswordKey != "password" {
t.Fatal("管理 Secret 字段默认值不符")
}
database := validDatabase("defaults")
if err := client.Create(t.Context(), database); err != nil {
t.Fatal(err)
}
if database.Spec.ReclaimPolicy != databasev1alpha1.ReclaimRetain {
t.Fatalf("默认回收策略 = %q", database.Spec.ReclaimPolicy)
}
// 进入删除流程前允许双向修改策略,不要求第二次审批字段。
for _, policy := range []databasev1alpha1.ReclaimPolicy{databasev1alpha1.ReclaimDelete, databasev1alpha1.ReclaimRetain} {
database.Spec.ReclaimPolicy = policy
if err := client.Update(t.Context(), database); err != nil {
t.Fatalf("修改回收策略: %v", err)
}
}
objects := []struct {
object ctrlclient.Object
namespaced bool
}{
{instance, false}, {database, false}, {validTenant("scope"), true},
}
for _, item := range objects {
namespaced, err := client.IsObjectNamespaced(item.object)
if err != nil || namespaced != item.namespaced {
t.Fatalf("%T 作用域 = %v, error = %v", item.object, namespaced, err)
}
}
// 导入不要求 Tenant 或 Instance 对象已经存在,跨对象就绪由 controller 判断。
imported := validDatabase("imported")
imported.Spec.Source = "Import"
imported.Spec.CredentialRef = &databasev1alpha1.CredentialReference{Mount: "secret", Path: "existing/app"}
if err := client.Create(t.Context(), imported); err != nil {
t.Fatal(err)
}
for _, name := range []string{"first", "second"} {
tenant := validTenant(name)
tenant.Spec.Provision = nil
tenant.Spec.DatabaseRef = &databasev1alpha1.DatabaseReference{Name: "imported"}
if err := client.Create(t.Context(), tenant); err != nil {
t.Fatalf("声明已有资源申请: %v", err)
}
}
// 两个申请都可被 API 接受,不代表二者都已绑定或获得凭据。
}
func testInvalidDeclarations(t *testing.T, client ctrlclient.Client) {
instanceCases := []struct {
name string
mutate func(*databasev1alpha1.PostgreSQLInstance)
}{
{"port", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.Port = -1 }},
{"address", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.HostAddr = "localhost" }},
{"scoped-address", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.HostAddr = "fe80::1%eth0" }},
{"tls", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.SSLMode = "prefer" }},
{"identifier", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.Database = "bad-name" }},
{"secret-key", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.AdminCredentialRef.PasswordKey = "bad/key" }},
}
for _, tc := range instanceCases {
t.Run(tc.name, func(t *testing.T) {
object := validInstance(tc.name)
tc.mutate(object)
requireInvalidCreate(t, client, object)
})
}
databaseCases := []struct {
name string
mutate func(*databasev1alpha1.PostgreSQLDatabase)
}{
{"missing-instance", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.InstanceRef.Name = "" }},
{"missing-role", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.LoginRole = "" }},
{"unknown-source", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.Source = "Adopt" }},
{"missing-credentials", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.Source = "Import" }},
{"provision-credentials", func(d *databasev1alpha1.PostgreSQLDatabase) {
d.Spec.CredentialRef = &databasev1alpha1.CredentialReference{Mount: "secret", Path: "existing"}
}},
{"unknown-policy", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.ReclaimPolicy = "Recycle" }},
{"binding-without-uid", func(d *databasev1alpha1.PostgreSQLDatabase) {
d.Spec.TenantRef = &databasev1alpha1.TenantReference{Namespace: testNamespace, Name: "tenant"}
}},
}
for _, tc := range databaseCases {
t.Run(tc.name, func(t *testing.T) {
object := validDatabase(tc.name)
tc.mutate(object)
requireInvalidCreate(t, client, object)
})
}
t.Run("互斥申请入口", func(t *testing.T) {
tenant := validTenant("ambiguous")
tenant.Spec.DatabaseRef = &databasev1alpha1.DatabaseReference{Name: "existing"}
requireInvalidCreate(t, client, tenant)
tenant.Spec.Provision = nil
tenant.Spec.DatabaseRef = nil
requireInvalidCreate(t, client, tenant)
})
}
// 本测试验证 API 写入语义,不模拟或宣称已经实现 controller 的恢复循环。
func testBindingWrites(t *testing.T, client ctrlclient.Client) {
ctx := t.Context()
tenant := validTenant("binding")
tenant.Status.Phase = readyPhase
if err := client.Create(ctx, tenant); err != nil {
t.Fatal(err)
}
if tenant.Status.Phase != "" {
t.Fatal("普通 Create 不应写入 status")
}
database := validDatabase("binding")
if err := client.Create(ctx, database); err != nil {
t.Fatal(err)
}
stale := database.DeepCopy()
database.Spec.TenantRef = &databasev1alpha1.TenantReference{
Namespace: tenant.Namespace, Name: databasev1alpha1.ObjectName(tenant.Name), UID: tenant.UID,
}
if err := client.Update(ctx, database); err != nil {
t.Fatal(err)
}
stale.Spec.TenantRef = &databasev1alpha1.TenantReference{Namespace: tenant.Namespace, Name: "other", UID: "other-uid"}
if err := client.Update(ctx, stale); !apierrors.IsConflict(err) {
t.Fatalf("过期并发写入 = %v, want Conflict", err)
}
// 换用 API 回读的对象补第二步,证明恢复所需记录不依赖先前内存。
observedDatabase := &databasev1alpha1.PostgreSQLDatabase{}
if err := client.Get(ctx, ctrlclient.ObjectKeyFromObject(database), observedDatabase); err != nil {
t.Fatal(err)
}
if observedDatabase.Spec.TenantRef.UID != tenant.UID {
t.Fatal("资源侧绑定被竞争写入覆盖")
}
beforeGeneration := tenant.Generation
tenant.Status.DatabaseRef = &databasev1alpha1.BoundDatabaseReference{
Name: databasev1alpha1.ObjectName(database.Name), UID: database.UID,
}
if err := client.Status().Update(ctx, tenant); err != nil {
t.Fatal(err)
}
if tenant.Generation != beforeGeneration || tenant.Status.DatabaseRef.UID != database.UID {
t.Fatal("status 更新错误地影响 generation 或绑定身份")
}
tenant.Status.Phase = readyPhase
if err := client.Update(ctx, tenant); err != nil {
t.Fatal(err)
}
if tenant.Status.Phase != "" {
t.Fatal("普通 Update 不应修改 status")
}
condition := metav1.Condition{Type: readyPhase, Status: metav1.ConditionFalse,
Reason: "Pending", Message: "尚未验证后端", LastTransitionTime: metav1.Now()}
tenant.Status.Conditions = []metav1.Condition{condition, condition}
if err := client.Status().Update(ctx, tenant); !apierrors.IsInvalid(err) {
t.Fatalf("重复 Condition = %v, want Invalid", err)
}
}
func requireInvalidCreate(t *testing.T, client ctrlclient.Client, object ctrlclient.Object) {
t.Helper()
if err := client.Create(context.Background(), object); !apierrors.IsInvalid(err) {
t.Fatalf("Create %T = %v, want Invalid", object, err)
}
}
func validInstance(name string) *databasev1alpha1.PostgreSQLInstance {
object := &databasev1alpha1.PostgreSQLInstance{}
object.Name = name
object.Spec.Endpoint = databasev1alpha1.PostgreSQLEndpoint{Host: "postgres.example.test", HostAddr: "127.0.0.1"}
object.Spec.AdminCredentialRef.Name = "postgres-admin"
return object
}
func validDatabase(name string) *databasev1alpha1.PostgreSQLDatabase {
object := &databasev1alpha1.PostgreSQLDatabase{}
object.Name = name
object.Spec = databasev1alpha1.PostgreSQLDatabaseSpec{
InstanceRef: databasev1alpha1.InstanceReference{Name: testInstanceName},
Database: "app", LoginRole: "app", Source: "Provision",
}
return object
}
func validTenant(name string) *databasev1alpha1.PostgreSQLTenant {
object := &databasev1alpha1.PostgreSQLTenant{}
object.Name = name
object.Namespace = testNamespace
object.Spec.Provision = &databasev1alpha1.DatabaseProvisionRequest{
InstanceRef: databasev1alpha1.InstanceReference{Name: testInstanceName},
}
return object
}
@@ -0,0 +1,452 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdminCredentialReference) DeepCopyInto(out *AdminCredentialReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminCredentialReference.
func (in *AdminCredentialReference) DeepCopy() *AdminCredentialReference {
if in == nil {
return nil
}
out := new(AdminCredentialReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BoundDatabaseReference) DeepCopyInto(out *BoundDatabaseReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundDatabaseReference.
func (in *BoundDatabaseReference) DeepCopy() *BoundDatabaseReference {
if in == nil {
return nil
}
out := new(BoundDatabaseReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CredentialReference) DeepCopyInto(out *CredentialReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialReference.
func (in *CredentialReference) DeepCopy() *CredentialReference {
if in == nil {
return nil
}
out := new(CredentialReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DatabaseProvisionRequest) DeepCopyInto(out *DatabaseProvisionRequest) {
*out = *in
out.InstanceRef = in.InstanceRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseProvisionRequest.
func (in *DatabaseProvisionRequest) DeepCopy() *DatabaseProvisionRequest {
if in == nil {
return nil
}
out := new(DatabaseProvisionRequest)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DatabaseReference) DeepCopyInto(out *DatabaseReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseReference.
func (in *DatabaseReference) DeepCopy() *DatabaseReference {
if in == nil {
return nil
}
out := new(DatabaseReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InstanceReference) DeepCopyInto(out *InstanceReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceReference.
func (in *InstanceReference) DeepCopy() *InstanceReference {
if in == nil {
return nil
}
out := new(InstanceReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabase) DeepCopyInto(out *PostgreSQLDatabase) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabase.
func (in *PostgreSQLDatabase) DeepCopy() *PostgreSQLDatabase {
if in == nil {
return nil
}
out := new(PostgreSQLDatabase)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLDatabase) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabaseList) DeepCopyInto(out *PostgreSQLDatabaseList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PostgreSQLDatabase, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabaseList.
func (in *PostgreSQLDatabaseList) DeepCopy() *PostgreSQLDatabaseList {
if in == nil {
return nil
}
out := new(PostgreSQLDatabaseList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLDatabaseList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabaseSpec) DeepCopyInto(out *PostgreSQLDatabaseSpec) {
*out = *in
out.InstanceRef = in.InstanceRef
if in.CredentialRef != nil {
in, out := &in.CredentialRef, &out.CredentialRef
*out = new(CredentialReference)
**out = **in
}
if in.TenantRef != nil {
in, out := &in.TenantRef, &out.TenantRef
*out = new(TenantReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabaseSpec.
func (in *PostgreSQLDatabaseSpec) DeepCopy() *PostgreSQLDatabaseSpec {
if in == nil {
return nil
}
out := new(PostgreSQLDatabaseSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabaseStatus) DeepCopyInto(out *PostgreSQLDatabaseStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabaseStatus.
func (in *PostgreSQLDatabaseStatus) DeepCopy() *PostgreSQLDatabaseStatus {
if in == nil {
return nil
}
out := new(PostgreSQLDatabaseStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLEndpoint) DeepCopyInto(out *PostgreSQLEndpoint) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLEndpoint.
func (in *PostgreSQLEndpoint) DeepCopy() *PostgreSQLEndpoint {
if in == nil {
return nil
}
out := new(PostgreSQLEndpoint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstance) DeepCopyInto(out *PostgreSQLInstance) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstance.
func (in *PostgreSQLInstance) DeepCopy() *PostgreSQLInstance {
if in == nil {
return nil
}
out := new(PostgreSQLInstance)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLInstance) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstanceList) DeepCopyInto(out *PostgreSQLInstanceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PostgreSQLInstance, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceList.
func (in *PostgreSQLInstanceList) DeepCopy() *PostgreSQLInstanceList {
if in == nil {
return nil
}
out := new(PostgreSQLInstanceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLInstanceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstanceSpec) DeepCopyInto(out *PostgreSQLInstanceSpec) {
*out = *in
out.Endpoint = in.Endpoint
out.AdminCredentialRef = in.AdminCredentialRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceSpec.
func (in *PostgreSQLInstanceSpec) DeepCopy() *PostgreSQLInstanceSpec {
if in == nil {
return nil
}
out := new(PostgreSQLInstanceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstanceStatus) DeepCopyInto(out *PostgreSQLInstanceStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceStatus.
func (in *PostgreSQLInstanceStatus) DeepCopy() *PostgreSQLInstanceStatus {
if in == nil {
return nil
}
out := new(PostgreSQLInstanceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenant) DeepCopyInto(out *PostgreSQLTenant) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenant.
func (in *PostgreSQLTenant) DeepCopy() *PostgreSQLTenant {
if in == nil {
return nil
}
out := new(PostgreSQLTenant)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLTenant) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenantList) DeepCopyInto(out *PostgreSQLTenantList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PostgreSQLTenant, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantList.
func (in *PostgreSQLTenantList) DeepCopy() *PostgreSQLTenantList {
if in == nil {
return nil
}
out := new(PostgreSQLTenantList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLTenantList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenantSpec) DeepCopyInto(out *PostgreSQLTenantSpec) {
*out = *in
if in.Provision != nil {
in, out := &in.Provision, &out.Provision
*out = new(DatabaseProvisionRequest)
**out = **in
}
if in.DatabaseRef != nil {
in, out := &in.DatabaseRef, &out.DatabaseRef
*out = new(DatabaseReference)
**out = **in
}
if in.Extensions != nil {
in, out := &in.Extensions, &out.Extensions
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantSpec.
func (in *PostgreSQLTenantSpec) DeepCopy() *PostgreSQLTenantSpec {
if in == nil {
return nil
}
out := new(PostgreSQLTenantSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenantStatus) DeepCopyInto(out *PostgreSQLTenantStatus) {
*out = *in
if in.DatabaseRef != nil {
in, out := &in.DatabaseRef, &out.DatabaseRef
*out = new(BoundDatabaseReference)
**out = **in
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantStatus.
func (in *PostgreSQLTenantStatus) DeepCopy() *PostgreSQLTenantStatus {
if in == nil {
return nil
}
out := new(PostgreSQLTenantStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantReference) DeepCopyInto(out *TenantReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantReference.
func (in *TenantReference) DeepCopy() *TenantReference {
if in == nil {
return nil
}
out := new(TenantReference)
in.DeepCopyInto(out)
return out
}