refactor: 集中显式注入并归位凭据领域规则
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
)
|
||||
|
||||
var ErrApplicationCredentialInvalid = errors.New("application credential is invalid")
|
||||
|
||||
var applicationIdentifier = regexp.MustCompile(`^[a-z][a-z0-9_]{0,62}$`)
|
||||
|
||||
// ApplicationCredential 是内存中的应用连接凭据,不得放入 CR 或普通日志。
|
||||
// 它与 Instance 管理凭据分开,固定输出交付合同中的七键,不生成带密码的 URI。
|
||||
type ApplicationCredential struct {
|
||||
username string
|
||||
password string
|
||||
database string
|
||||
endpoint instance.Endpoint
|
||||
}
|
||||
|
||||
func NewApplicationCredential(username, password, database string, endpoint instance.Endpoint) (ApplicationCredential, error) {
|
||||
if !applicationIdentifier.MatchString(username) || !applicationIdentifier.MatchString(database) || password == "" {
|
||||
return ApplicationCredential{}, ErrApplicationCredentialInvalid
|
||||
}
|
||||
if endpoint.Validate() != nil {
|
||||
return ApplicationCredential{}, ErrApplicationCredentialInvalid
|
||||
}
|
||||
return ApplicationCredential{
|
||||
username: username,
|
||||
password: password,
|
||||
database: database,
|
||||
endpoint: endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateApplicationCredential 仅供已获准首次创建凭据的供应步骤调用。
|
||||
// 不能在读取失败、写入结果不确定或重启后无条件重新调用。
|
||||
func GenerateApplicationCredential(username, database string, endpoint instance.Endpoint) (ApplicationCredential, error) {
|
||||
password := make([]byte, 32)
|
||||
rand.Read(password)
|
||||
return NewApplicationCredential(username, base64.RawURLEncoding.EncodeToString(password), database, endpoint)
|
||||
}
|
||||
|
||||
func (c ApplicationCredential) String() string { return "[redacted application credential]" }
|
||||
func (c ApplicationCredential) GoString() string { return c.String() }
|
||||
func (c ApplicationCredential) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"[redacted application credential]"`), nil
|
||||
}
|
||||
|
||||
// SecretData 只在凭据后端或数据库连接边界使用;返回值包含明文密码,禁止记录日志。
|
||||
// 每次返回独立 map,调用方不能修改已经构造的凭据。
|
||||
func (c ApplicationCredential) SecretData() map[string]any {
|
||||
endpoint := c.endpoint.Values()
|
||||
return map[string]any{
|
||||
"username": c.username,
|
||||
"password": c.password,
|
||||
"database": c.database,
|
||||
"host": endpoint.Host,
|
||||
"hostaddr": endpoint.HostAddr,
|
||||
"port": strconv.Itoa(endpoint.Port),
|
||||
"sslmode": string(endpoint.TLSMode),
|
||||
}
|
||||
}
|
||||
|
||||
func (c ApplicationCredential) Validate() error {
|
||||
_, err := NewApplicationCredential(c.username, c.password, c.database, c.endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
// MatchesTarget 只比较连接目标,不向用例暴露密码;管理库名不是应用连接目标的一部分。
|
||||
func (c ApplicationCredential) MatchesTarget(username, database string, endpoint instance.Endpoint) bool {
|
||||
actual, wanted := c.endpoint.Values(), endpoint.Values()
|
||||
return c.username == username && c.database == database && actual.Host == wanted.Host &&
|
||||
actual.HostAddr == wanted.HostAddr && actual.Port == wanted.Port && actual.TLSMode == wanted.TLSMode
|
||||
}
|
||||
|
||||
// ParseApplicationCredential 拒绝缺键、非字符串或非法连接参数,不回显后端内容。
|
||||
func ParseApplicationCredential(data map[string]any) (ApplicationCredential, error) {
|
||||
values := make(map[string]string, 7)
|
||||
for _, key := range []string{"username", "password", "database", "host", "hostaddr", "port", "sslmode"} {
|
||||
value, ok := data[key].(string)
|
||||
if !ok || value == "" {
|
||||
return ApplicationCredential{}, ErrApplicationCredentialInvalid
|
||||
}
|
||||
values[key] = value
|
||||
}
|
||||
port, err := strconv.Atoi(values["port"])
|
||||
if err != nil {
|
||||
return ApplicationCredential{}, ErrApplicationCredentialInvalid
|
||||
}
|
||||
endpoint, err := instance.NewEndpoint(instance.EndpointValues{
|
||||
Host: values["host"],
|
||||
HostAddr: values["hostaddr"],
|
||||
Port: port,
|
||||
ManagementDatabase: values["database"],
|
||||
TLSMode: instance.TLSMode(values["sslmode"]),
|
||||
})
|
||||
if err != nil {
|
||||
return ApplicationCredential{}, ErrApplicationCredentialInvalid
|
||||
}
|
||||
return NewApplicationCredential(values["username"], values["password"], values["database"], endpoint)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package application_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
)
|
||||
|
||||
func TestApplicationCredential(t *testing.T) {
|
||||
endpoint, err := instance.NewEndpoint(instance.EndpointValues{
|
||||
Host: "postgres.example", HostAddr: "192.0.2.1", Port: 5432,
|
||||
ManagementDatabase: "postgres", TLSMode: instance.TLSVerifyFull,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := application.GenerateApplicationCredential("owner", "app", endpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := application.GenerateApplicationCredential("owner", "app", endpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := first.SecretData()
|
||||
if len(data) != 7 || data["password"] == second.SecretData()["password"] || len(data["password"].(string)) != 43 {
|
||||
t.Fatal("expected seven keys and independent 256-bit passwords")
|
||||
}
|
||||
parsed, err := application.ParseApplicationCredential(data)
|
||||
if err != nil || !maps.Equal(parsed.SecretData(), data) {
|
||||
t.Fatal("credential did not round trip")
|
||||
}
|
||||
encoded, err := json.Marshal(first)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, output := range []string{fmt.Sprint(first), fmt.Sprintf("%+v", first), fmt.Sprintf("%#v", first), string(encoded)} {
|
||||
if strings.Contains(output, data["password"].(string)) {
|
||||
t.Fatal("credential formatting leaked the password")
|
||||
}
|
||||
}
|
||||
data["password"] = "changed"
|
||||
if first.SecretData()["password"] == "changed" {
|
||||
t.Fatal("caller mutated credential")
|
||||
}
|
||||
for key := range data {
|
||||
invalid := maps.Clone(data)
|
||||
delete(invalid, key)
|
||||
if _, err := application.ParseApplicationCredential(invalid); err == nil {
|
||||
t.Fatalf("accepted missing %s", key)
|
||||
}
|
||||
invalid[key] = 42
|
||||
if _, err := application.ParseApplicationCredential(invalid); err == nil {
|
||||
t.Fatalf("accepted non-string %s", key)
|
||||
}
|
||||
}
|
||||
if (application.ApplicationCredential{}).Validate() == nil {
|
||||
t.Fatal("accepted zero credential")
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/binding"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,54 +16,28 @@ var (
|
||||
ErrCredentialUncertain = errors.New("credential creation outcome is uncertain; manual resolution required")
|
||||
)
|
||||
|
||||
const (
|
||||
CredentialsReady = "CredentialsReady"
|
||||
CredentialCreationStarted = "CreationStarted"
|
||||
CredentialPrepared = "CredentialPrepared"
|
||||
)
|
||||
|
||||
type CredentialLocation struct {
|
||||
Mount string
|
||||
Path string
|
||||
}
|
||||
|
||||
// CredentialStore 只表达本用例需要的凭据操作,不提供覆盖或删除。
|
||||
// version=0 的读取只用于观察是否已有值,成功不能作为认领依据。
|
||||
type CredentialStore interface {
|
||||
ProvisionLocation(string) (CredentialLocation, error)
|
||||
ReadCredential(context.Context, CredentialLocation, int64) (ApplicationCredential, error)
|
||||
CreateCredential(context.Context, CredentialLocation, ApplicationCredential) (int64, error)
|
||||
}
|
||||
|
||||
type CredentialInstance struct {
|
||||
binding.Instance
|
||||
Generation int64
|
||||
Endpoint instance.Endpoint
|
||||
ProvisionLocation(string) (credentialdomain.Location, error)
|
||||
ReadCredential(context.Context, credentialdomain.Location, int64) (credentialdomain.ApplicationCredential, error)
|
||||
CreateCredential(context.Context, credentialdomain.Location, credentialdomain.ApplicationCredential) (int64, error)
|
||||
}
|
||||
|
||||
// CredentialRecord 是同一轮观察的事实,状态中永远不保存密码。
|
||||
type CredentialRecord struct {
|
||||
Database BindingDatabase
|
||||
Tenant *BindingTenant
|
||||
Instance *CredentialInstance
|
||||
DatabaseProtected bool
|
||||
TenantProtected bool
|
||||
Status CredentialStatus
|
||||
}
|
||||
|
||||
type CredentialStatus struct {
|
||||
Location *CredentialLocation
|
||||
Version int64
|
||||
Ready bool
|
||||
Reason string
|
||||
Message string
|
||||
credentialdomain.Target
|
||||
Revision string
|
||||
TenantGeneration int64
|
||||
InstanceGeneration int64
|
||||
Status credentialdomain.State
|
||||
}
|
||||
|
||||
// CredentialResources 的写入必须检查 Database UID/resourceVersion,保留其他状态。
|
||||
// CheckCurrent 在外部操作前后回读本轮三个资源,拒绝陈旧快照;它不是跨系统事务。
|
||||
type CredentialResources interface {
|
||||
Load(context.Context, string) (*CredentialRecord, error)
|
||||
Save(context.Context, *CredentialRecord, CredentialStatus) (*CredentialRecord, error)
|
||||
Save(context.Context, *CredentialRecord, credentialdomain.State) (*CredentialRecord, error)
|
||||
CheckCurrent(context.Context, *CredentialRecord) error
|
||||
}
|
||||
|
||||
@@ -75,37 +48,35 @@ type CredentialPreparation struct {
|
||||
|
||||
func (s CredentialPreparation) Reconcile(ctx context.Context, name string) error {
|
||||
record, err := s.Resources.Load(ctx, name)
|
||||
if err != nil || record == nil || record.Database.Source != "Provision" {
|
||||
if err != nil || record == nil || !record.RequiresPreparation() {
|
||||
return err
|
||||
}
|
||||
// 未完成创建的重入不猜测后端结果。即使进程在实际发请求前退出,也需要人工核实。
|
||||
if record.Status.Version == 0 && record.Status.Reason == binding.Conflict {
|
||||
return nil // 保留首次冲突的具体原因,不因后端恢复而重入创建。
|
||||
if state, canContinue := record.Status.Resume(); !canContinue {
|
||||
if state == record.Status {
|
||||
return nil
|
||||
}
|
||||
return s.report(ctx, record, state.Phase, state.Message)
|
||||
}
|
||||
if record.Status.Version == 0 && record.Status.Reason == CredentialCreationStarted {
|
||||
return s.report(ctx, record, binding.Conflict,
|
||||
"凭据创建未留下成功确认;请核对固定位置与后端历史并人工处理,未重新生成密码")
|
||||
}
|
||||
if issue := record.check(); issue != nil {
|
||||
return s.report(ctx, record, issue.Reason, issue.Message)
|
||||
if issue := record.Check(); issue != nil {
|
||||
return s.report(ctx, record, issue.Phase, issue.Message)
|
||||
}
|
||||
location, err := s.Store.ProvisionLocation(record.Database.Identity.UID)
|
||||
if err != nil {
|
||||
return s.report(ctx, record, binding.DependencyUnavailable, "凭据存储位置配置无效,未执行外部写入")
|
||||
return s.report(ctx, record, credentialdomain.Unavailable, "凭据存储位置配置无效,未执行外部写入")
|
||||
}
|
||||
if issue := record.Status.CheckLocation(location); issue != nil {
|
||||
return s.report(ctx, record, issue.Phase, issue.Message)
|
||||
}
|
||||
if record.Status.Location == nil {
|
||||
status := record.Status
|
||||
status.Location = &location
|
||||
status.Ready, status.Reason, status.Message = false, "LocationPinned", "凭据位置已固定,等待创建"
|
||||
status = status.WithPhase(credentialdomain.Pinned, "凭据位置已固定,等待创建")
|
||||
record, err = s.Resources.Save(ctx, record, status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if *record.Status.Location != location {
|
||||
return s.report(ctx, record, binding.DependencyUnavailable,
|
||||
"部署配置与固定凭据位置不一致;请恢复原 mount/path 配置,未迁移或改密")
|
||||
}
|
||||
if record.Status.Version > 0 {
|
||||
if record.Status.Confirmed() {
|
||||
return s.observe(ctx, record)
|
||||
}
|
||||
return s.create(ctx, record)
|
||||
@@ -113,19 +84,18 @@ func (s CredentialPreparation) Reconcile(ctx context.Context, name string) error
|
||||
|
||||
func (s CredentialPreparation) create(ctx context.Context, record *CredentialRecord) error {
|
||||
_, err := s.Store.ReadCredential(ctx, *record.Status.Location, 0)
|
||||
if err == nil || errors.Is(err, ErrApplicationCredentialInvalid) {
|
||||
return s.report(ctx, record, binding.Conflict, "固定位置已有未确认的凭据;请人工核实,未认领或覆盖")
|
||||
if err == nil || errors.Is(err, credentialdomain.ErrApplicationCredentialInvalid) {
|
||||
return s.report(ctx, record, credentialdomain.Conflict, "固定位置已有未确认的凭据;请人工核实,未认领或覆盖")
|
||||
}
|
||||
if !errors.Is(err, ErrCredentialNotFound) {
|
||||
return s.report(ctx, record, binding.DependencyUnavailable, "创建前无法确认凭据位置是否为空,等待依赖恢复")
|
||||
return s.report(ctx, record, credentialdomain.Unavailable, "创建前无法确认凭据位置是否为空,等待依赖恢复")
|
||||
}
|
||||
credential, err := GenerateApplicationCredential(record.Database.LoginRole, record.Database.Name, record.Instance.Endpoint)
|
||||
credential, err := credentialdomain.GenerateApplicationCredential(record.Database.LoginRole, record.Database.Name, record.Instance.Endpoint)
|
||||
if err != nil {
|
||||
return s.report(ctx, record, "InvalidTarget", "应用凭据目标无效,未执行外部写入")
|
||||
return s.report(ctx, record, credentialdomain.InvalidTarget, "应用凭据目标无效,未执行外部写入")
|
||||
}
|
||||
status := record.Status
|
||||
status.Ready, status.Reason = false, CredentialCreationStarted
|
||||
status.Message = "凭据创建已开始;尚无成功确认时不得重入创建"
|
||||
status = status.WithPhase(credentialdomain.Creating, "凭据创建已开始;尚无成功确认时不得重入创建")
|
||||
record, err = s.Resources.Save(ctx, record, status)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -136,46 +106,47 @@ func (s CredentialPreparation) create(ctx context.Context, record *CredentialRec
|
||||
version, err := s.Store.CreateCredential(ctx, *record.Status.Location, credential)
|
||||
if errors.Is(err, ErrCredentialUnavailable) {
|
||||
// 适配器只在明确未执行写入(认证拒绝或请求前取消)时返回此错误。
|
||||
return s.report(ctx, record, binding.DependencyUnavailable, "凭据创建在执行前被拒绝,等待认证或权限恢复")
|
||||
return s.report(ctx, record, credentialdomain.Unavailable, "凭据创建在执行前被拒绝,等待认证或权限恢复")
|
||||
}
|
||||
if err != nil || version != 1 {
|
||||
return s.report(ctx, record, binding.Conflict,
|
||||
if err != nil {
|
||||
return s.report(ctx, record, credentialdomain.Conflict,
|
||||
"凭据创建冲突或结果不确定;请核对固定位置的版本历史,未认领、覆盖或重新生成密码")
|
||||
}
|
||||
confirmed, issue := record.Status.Created(version)
|
||||
if issue != nil {
|
||||
return s.report(ctx, record, issue.Phase, issue.Message)
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
status = record.Status
|
||||
status.Version, status.Ready, status.Reason = version, true, CredentialPrepared
|
||||
status.Message = "凭据已创建并回读确认;尚未创建 PostgreSQL 资源或交付给 Tenant"
|
||||
_, err = s.Resources.Save(ctx, record, status)
|
||||
_, err = s.Resources.Save(ctx, record, confirmed)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s CredentialPreparation) observe(ctx context.Context, record *CredentialRecord) error {
|
||||
credential, err := s.Store.ReadCredential(ctx, *record.Status.Location, record.Status.Version)
|
||||
if errors.Is(err, ErrCredentialConflict) || errors.Is(err, ErrCredentialNotFound) {
|
||||
return s.report(ctx, record, binding.Conflict, "已确认凭据消失、版本变化或内容无效;请人工核实,未生成替代密码")
|
||||
return s.report(ctx, record, credentialdomain.Conflict, "已确认凭据消失、版本变化或内容无效;请人工核实,未生成替代密码")
|
||||
}
|
||||
if err != nil {
|
||||
return s.report(ctx, record, binding.DependencyUnavailable, "已确认凭据暂时无法读取;保留确认版本,等待依赖恢复")
|
||||
return s.report(ctx, record, credentialdomain.Unavailable, "已确认凭据暂时无法读取;保留确认版本,等待依赖恢复")
|
||||
}
|
||||
if !credential.MatchesTarget(record.Database.LoginRole, record.Database.Name, record.Instance.Endpoint) {
|
||||
return s.report(ctx, record, binding.Conflict, "已确认凭据与当前 Instance/database/loginRole 不一致;请人工核实,未修改凭据")
|
||||
if issue := record.CheckCredential(credential); issue != nil {
|
||||
return s.report(ctx, record, issue.Phase, issue.Message)
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
status := record.Status
|
||||
status.Ready, status.Reason = true, CredentialPrepared
|
||||
status.Phase = credentialdomain.Prepared
|
||||
status.Message = "已确认凭据可读取;尚未验证 PostgreSQL 资源或完成 Tenant 交付"
|
||||
_, err = s.Resources.Save(ctx, record, status)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s CredentialPreparation) report(ctx context.Context, record *CredentialRecord, reason, message string) error {
|
||||
func (s CredentialPreparation) report(ctx context.Context, record *CredentialRecord, phase credentialdomain.Phase, message string) error {
|
||||
status := record.Status
|
||||
status.Ready, status.Reason = false, reason
|
||||
status.Phase = phase
|
||||
status.Message = fmt.Sprintf("Database %s:%s", record.Database.Identity.Name, message)
|
||||
_, err := s.Resources.Save(ctx, record, status)
|
||||
return err
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/binding"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
)
|
||||
@@ -25,14 +27,14 @@ func preparationRecord(t *testing.T) *CredentialRecord {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &CredentialRecord{
|
||||
Database: BindingDatabase{Database: binding.Database{
|
||||
Database: binding.Database{
|
||||
Identity: database, Instance: preparationInstanceName, InstanceUID: preparationInstanceUID, Name: bindingTestName, LoginRole: bindingTestName, Source: "Provision", Tenant: &tenant,
|
||||
}},
|
||||
Tenant: &BindingTenant{
|
||||
},
|
||||
Tenant: &binding.Tenant{
|
||||
Identity: tenant, Phase: binding.Bound, Database: &database,
|
||||
Request: binding.Request{Provision: &binding.ProvisionRequest{Instance: preparationInstanceName}},
|
||||
},
|
||||
Instance: &CredentialInstance{Identity: binding.Identity{Name: preparationInstanceName, UID: preparationInstanceUID}, Ready: true, Endpoint: endpoint},
|
||||
Instance: &credentialdomain.Instance{Identity: binding.Identity{Name: preparationInstanceName, UID: preparationInstanceUID}, Ready: true, Endpoint: endpoint},
|
||||
DatabaseProtected: true, TenantProtected: true,
|
||||
}
|
||||
}
|
||||
@@ -48,7 +50,7 @@ func (r *memoryCredentialResources) Load(context.Context, string) (*CredentialRe
|
||||
return ©, nil
|
||||
}
|
||||
|
||||
func (r *memoryCredentialResources) Save(_ context.Context, record *CredentialRecord, status CredentialStatus) (*CredentialRecord, error) {
|
||||
func (r *memoryCredentialResources) Save(_ context.Context, record *CredentialRecord, status credentialdomain.State) (*CredentialRecord, error) {
|
||||
if r.saveError != nil {
|
||||
return nil, r.saveError
|
||||
}
|
||||
@@ -69,16 +71,16 @@ type preparationStore struct {
|
||||
createError error
|
||||
}
|
||||
|
||||
func (*preparationStore) ProvisionLocation(uid string) (CredentialLocation, error) {
|
||||
return CredentialLocation{Mount: "applications", Path: "database/" + uid}, nil
|
||||
func (*preparationStore) ProvisionLocation(uid string) (credentialdomain.Location, error) {
|
||||
return credentialdomain.Location{Mount: "applications", Path: "database/" + uid}, nil
|
||||
}
|
||||
|
||||
func (s *preparationStore) ReadCredential(context.Context, CredentialLocation, int64) (ApplicationCredential, error) {
|
||||
func (s *preparationStore) ReadCredential(context.Context, credentialdomain.Location, int64) (credentialdomain.ApplicationCredential, error) {
|
||||
s.reads++
|
||||
return ApplicationCredential{}, s.readError
|
||||
return credentialdomain.ApplicationCredential{}, s.readError
|
||||
}
|
||||
|
||||
func (s *preparationStore) CreateCredential(context.Context, CredentialLocation, ApplicationCredential) (int64, error) {
|
||||
func (s *preparationStore) CreateCredential(context.Context, credentialdomain.Location, credentialdomain.ApplicationCredential) (int64, error) {
|
||||
s.creates++
|
||||
if s.createError != nil {
|
||||
return 0, s.createError
|
||||
@@ -159,7 +161,7 @@ func TestCredentialPreparationWriteBoundary(t *testing.T) {
|
||||
if err := service.Reconcile(t.Context(), resources.record.Database.Identity.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.creates != test.wantCreates || resources.record.Status.Reason != binding.Conflict {
|
||||
if store.creates != test.wantCreates || resources.record.Status.Phase != credentialdomain.Conflict {
|
||||
t.Fatal("未确认创建重入时不得生成替代密码")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package application
|
||||
|
||||
import "git.ddupan.top/panxiao81/ayatori/internal/database/domain/binding"
|
||||
|
||||
func (r *CredentialRecord) check() *binding.Issue {
|
||||
database := r.Database
|
||||
if database.Deleting || database.Phase == binding.Deleting || database.Phase == "Released" {
|
||||
return &binding.Issue{Reason: "PreparationStopped", Message: "Database 正在删除或已释放;保留凭据与 finalizer,不执行供应或清理"}
|
||||
}
|
||||
if database.Tenant == nil || r.Tenant == nil || r.Tenant.Database == nil {
|
||||
return &binding.Issue{Reason: binding.DependencyUnavailable, Message: "等待 Database 与 Tenant 双向绑定完成"}
|
||||
}
|
||||
if *database.Tenant != r.Tenant.Identity || *r.Tenant.Database != database.Identity {
|
||||
return &binding.Issue{Reason: binding.Conflict, Message: "双向绑定的名称或 UID 不匹配,未创建凭据"}
|
||||
}
|
||||
if r.Tenant.Deleting || r.Tenant.Phase != binding.Bound || !r.DatabaseProtected || !r.TenantProtected {
|
||||
return &binding.Issue{Reason: "PreparationStopped", Message: "Tenant 未完成绑定、正在删除或缺少 finalizer 保护,未创建凭据"}
|
||||
}
|
||||
target, err := r.Tenant.Request.Resolve(r.Tenant.Identity)
|
||||
if err != nil || (target.Provision != nil && !database.MatchesProvision(target, r.Tenant.Identity)) || target.Name != database.Identity.Name {
|
||||
return &binding.Issue{Reason: binding.Conflict, Message: "Tenant 申请与 Database 目标不一致,未创建凭据"}
|
||||
}
|
||||
if r.Instance == nil || database.InstanceUID == "" {
|
||||
return &binding.Issue{Reason: binding.DependencyUnavailable, Message: "等待 Instance 与已记录的实例身份"}
|
||||
}
|
||||
return r.Instance.Check(&database.Database)
|
||||
}
|
||||
Reference in New Issue
Block a user