feat: 固定 Database 凭据位置与确认版本
This commit is contained in:
@@ -54,8 +54,8 @@ const (
|
||||
ReclaimDelete ReclaimPolicy = "Delete"
|
||||
)
|
||||
|
||||
// CredentialReference 定位已有 OpenBao KV v2 凭据,不包含任何秘密值。
|
||||
// 只由资源管理员在导入时填写;controller 必须检查部署允许的 mount/path 范围。
|
||||
// CredentialReference 定位 OpenBao KV v2 凭据,不包含任何秘密值。
|
||||
// 管理员在导入声明中指定,controller 在 status 中固定位置;两者均须检查部署允许的范围。
|
||||
type CredentialReference struct {
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:MaxLength=253
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package v1alpha1_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func testCredentialStatus(t *testing.T, client ctrlclient.Client) {
|
||||
database := validDatabase("credential-status")
|
||||
if err := client.Create(t.Context(), database); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withoutLocation := database.DeepCopy()
|
||||
withoutLocation.Status.CredentialVersion = 1
|
||||
if err := client.Status().Update(t.Context(), withoutLocation); !apierrors.IsInvalid(err) {
|
||||
t.Fatalf("没有位置不能确认版本: %v", err)
|
||||
}
|
||||
database.Status.CredentialRef = &databasev1alpha1.CredentialReference{
|
||||
Mount: "application-secrets", Path: "applications/" + string(database.UID),
|
||||
}
|
||||
if err := client.Status().Update(t.Context(), database); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testCredentialStatusRemoval(t, client, database)
|
||||
changedTarget := database.DeepCopy()
|
||||
changedTarget.Spec.Database = "another_database"
|
||||
if err := client.Update(t.Context(), changedTarget); !apierrors.IsInvalid(err) {
|
||||
t.Fatalf("凭据位置固定后不得更换实际目标: %v", err)
|
||||
}
|
||||
stale := database.DeepCopy()
|
||||
database.Status.CredentialVersion = 1
|
||||
if err := client.Status().Update(t.Context(), database); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stale.Status.Phase = "Binding"
|
||||
if err := client.Status().Update(t.Context(), stale); !apierrors.IsConflict(err) {
|
||||
t.Fatalf("旧 resourceVersion 不得覆盖确认结果: %v", err)
|
||||
}
|
||||
testCredentialStatusRemoval(t, client, database)
|
||||
for _, version := range []int64{0, -1, 2} {
|
||||
changed := database.DeepCopy()
|
||||
changed.Status.CredentialVersion = version
|
||||
if err := client.Status().Update(t.Context(), changed); !apierrors.IsInvalid(err) {
|
||||
t.Fatalf("不可移除或更改确认版本 %d: %v", version, err)
|
||||
}
|
||||
}
|
||||
database.Status.Conditions = []metav1.Condition{{
|
||||
Type: "CredentialsReady", Status: metav1.ConditionFalse,
|
||||
Reason: "DependencyUnavailable", Message: "凭据暂时无法读取",
|
||||
ObservedGeneration: database.Generation, LastTransitionTime: metav1.Now(),
|
||||
}}
|
||||
if err := client.Status().Update(t.Context(), database); err != nil {
|
||||
t.Fatalf("当前不可用不应阻止保留确认记录: %v", err)
|
||||
}
|
||||
reloaded := &databasev1alpha1.PostgreSQLDatabase{}
|
||||
if err := client.Get(t.Context(), ctrlclient.ObjectKeyFromObject(database), reloaded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reloaded.Status.CredentialVersion != 1 || reloaded.Status.CredentialRef == nil {
|
||||
t.Fatal("重读丢失凭据确认记录")
|
||||
}
|
||||
}
|
||||
|
||||
func testCredentialStatusRemoval(t *testing.T, client ctrlclient.Client, database *databasev1alpha1.PostgreSQLDatabase) {
|
||||
t.Helper()
|
||||
for _, change := range []struct {
|
||||
name string
|
||||
edit func(*databasev1alpha1.PostgreSQLDatabase)
|
||||
}{
|
||||
{"移除整个 status", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status = databasev1alpha1.PostgreSQLDatabaseStatus{} }},
|
||||
{"移除位置", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status.CredentialRef = nil }},
|
||||
{"修改 mount", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status.CredentialRef.Mount = "other" }},
|
||||
{"修改 path", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status.CredentialRef.Path = "applications/other" }},
|
||||
} {
|
||||
t.Run(change.name, func(t *testing.T) {
|
||||
changed := database.DeepCopy()
|
||||
change.edit(changed)
|
||||
if err := client.Status().Update(t.Context(), changed); !apierrors.IsInvalid(err) {
|
||||
t.Fatalf("不应接受已固定凭据位置的更改: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@ type PostgreSQLDatabaseStatus struct {
|
||||
// InstanceUID 记录观察时的实例身份,不把同名新实例视为原目标。
|
||||
// +optional
|
||||
InstanceUID types.UID `json:"instanceUID,omitempty"`
|
||||
// CredentialRef 在首次外部写入前固定凭据位置;部署配置变化不迁移此位置。
|
||||
// +optional
|
||||
CredentialRef *CredentialReference `json:"credentialRef,omitempty"`
|
||||
// CredentialVersion 只在创建并回读成功后记录,不表示凭据当前仍可用。
|
||||
// 省略表示尚未确认;已有凭据不能仅凭读取成功补记确认。
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +optional
|
||||
CredentialVersion int64 `json:"credentialVersion,omitempty"`
|
||||
// Phase 暂不冻结供应子阶段枚举;它不是操作授权或绑定的替代记录。
|
||||
// +optional
|
||||
Phase string `json:"phase,omitempty"`
|
||||
@@ -42,7 +50,10 @@ type PostgreSQLDatabaseStatus struct {
|
||||
// +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:validation:XValidation:rule="!has(self.status) || !has(self.status.credentialVersion) || has(self.status.credentialRef)",message="credentialVersion requires credentialRef"
|
||||
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.status) && has(oldSelf.status.credentialRef)) || (has(self.status) && has(self.status.credentialRef) && self.status.credentialRef == oldSelf.status.credentialRef)",message="recorded credentialRef cannot change or be removed"
|
||||
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.status) && has(oldSelf.status.credentialVersion)) || (has(self.status) && has(self.status.credentialVersion) && self.status.credentialVersion == oldSelf.status.credentialVersion)",message="confirmed credentialVersion cannot change or be removed"
|
||||
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.spec.tenantRef) || (has(oldSelf.status) && (has(oldSelf.status.instanceUID) || has(oldSelf.status.credentialRef)))) || (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`
|
||||
|
||||
@@ -61,6 +61,7 @@ func TestDatabaseAPI(t *testing.T) {
|
||||
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) { testCredentialStatus(t, client) })
|
||||
t.Run("仓库示例", func(t *testing.T) { testSamples(t, client, scheme) })
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,11 @@ func (in *PostgreSQLDatabaseSpec) DeepCopy() *PostgreSQLDatabaseSpec {
|
||||
// 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.CredentialRef != nil {
|
||||
in, out := &in.CredentialRef, &out.CredentialRef
|
||||
*out = new(CredentialReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
|
||||
Reference in New Issue
Block a user