diff --git a/docs/database/README.md b/docs/database/README.md index 2855d14..8e6c24d 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -21,15 +21,17 @@ Database 是 Ayatori 首批实际产品领域之一。第一个迁移切片只 代码被移动到 Ayatori 的 `internal/database/domain/instance`,测试 import 和文档链接相应更新; 首个后续切片按已批准合同增加 Instance extension observation:观测与当前 target 绑定,进入重新 -验证或删除时失效,且支持判定不授权 Tenant provisioning。其余 Ready/observation 行为仍应先更新 -合同与测试再实现,不能把旧运行链路接回该模型。 +验证或删除时失效,且支持判定不授权 Tenant provisioning。后续 Ready 切片实现管理能力判定、 +registry 准备决策与完整回读、Ready 重验及本轮 evidence 前置检查;沿用已批准合同,不能把旧运行 +链路接回该模型。各层验证边界见 [Instance 领域规格](domain-instance.md)。 ## 边界 - 领域层不依赖 Kubernetes types、数据库 driver 或凭据 provider。 - CredentialReference 只携带管理 Secret 的名称与字段映射,不包含 Secret 内容或 OpenBao path。 - Instance checkpoint 不是外部事实;实际能力必须由 application/adapter 观察后交给领域对象判断。 -- 当前代码不授权 Tenant provisioning,也不表示 Database API 已经可用。 +- 当前代码只检查 Instance 供应前置条件,不授予 Tenant 所有权或外部写入权限,也不表示 + Database API 已经可用。 ## 设计入口 diff --git a/docs/database/domain-instance.md b/docs/database/domain-instance.md index 0b36fc9..f84389e 100644 --- a/docs/database/domain-instance.md +++ b/docs/database/domain-instance.md @@ -2,7 +2,7 @@ 状态:Draft,含已确认决策。日期:2026-09-13。 -上层合并边界见 [ADR-0008](../decisions/0008-merge-Ayatori Database controller.md)。本文只展开 Instance,不包含 Tenant 的供应 +上层合并边界见 [ADR-0008](../decisions/0008-merge-postgresql-tenant-operator.md)。本文只展开 Instance,不包含 Tenant 的供应 实现,也不新增 CRD 字段。设计签名用于评审职责与行为,不是待复制的 Go 接口代码。 ## 1. 对象职责与生命周期 @@ -228,3 +228,26 @@ Ready --registry 需修复/保存--> InitializingRegistry 均已确认。其他决策及未决项见总体草案,不增加后台清扫器或状态字段。 批准本对象结构不等于批准这些未决行为,也不意味着立刻实现完整供应链路。 + +## 8. 领域实现与验证边界(2026-09-21) + +`internal/database/domain/instance` 按上述方法合同实现 Ready 纯判定。输入分别表达连接、 +metadata、role、database、grant、extension 管理能力,以及 registry 的未观察、缺失、需迁移、 +可用、不兼容和不可访问状态。检查零值或未知值按证据不足处理;操作失败只使用封闭的安全 +类别,不接收驱动错误。`Snapshot.Failure` 是 Condition 映射的领域输入,不新增 CRD/status 字段。 + +管理能力分别指目标连接可用、服务器 metadata 可读,以及执行规格 §7 所要求的角色、数据库、 +授权和扩展管理操作的能力;不是仅凭版本查询或扩展可用列表判定权限。具体 SQL 权限探测矩阵、 +最小权限角色和扩展权限例外仍须在 PostgreSQL adapter 切片定义并用真实后端验证。 +registry 不兼容独立保留为领域失败类别,不将其误报为权限不足;公开 Condition Reason 的映射 +留待 API/application 切片按原合同评审。 + +领域测试验证完整回读、缺少检查项、状态重建、重复判定、目标不匹配、配置变化、依赖失败、 +registry 丢失/不兼容、操作结果不确定和删除限制。只有本轮完整能力判定通过后,Instance +前置条件检查才通过;这不授予 Tenant 所有权,也不替代实际写入前的并发校验。 + +本切片不新增 controller、adapter、Secret 读取或外部生命周期操作。checkpoint 保存失败、 +resourceVersion 冲突、watch 与 finalizer 事件链需由后续 application/envtest 验证;SQL 探测、 +registry 初始化/迁移、超时后的真实状态回读和并发幂等由 PostgreSQL 集成测试验证;Secret +变化后的连接刷新由 Kubernetes API 加真实 PostgreSQL 的集成测试验证。纯领域测试不能证明 +Database 已可运行或这些集成合同已完成。 diff --git a/internal/database/domain/instance/instance.go b/internal/database/domain/instance/instance.go index ef5ff8b..466b681 100644 --- a/internal/database/domain/instance/instance.go +++ b/internal/database/domain/instance/instance.go @@ -38,22 +38,22 @@ const ( ) // Snapshot contains persisted observations only, without credentials or live evidence. -// Failure detail mapping will be added with capability assessment, not intent transitions. type Snapshot struct { Phase Phase ObservedRevision int64 Readiness Readiness ReportedVersion string + Failure Failure } // Instance protects registration state and pure lifecycle transitions. // Reconstitution does not establish live capability evidence, even for a Ready snapshot. -// This initial slice deliberately exposes no operation that authorizes provisioning. type Instance struct { target ObservationTarget snapshot Snapshot deleting bool extensions ExtensionSupport + evidence *CapabilityObservation } func Reconstitute(target ObservationTarget, snapshot Snapshot, deleting bool) (*Instance, error) { @@ -85,6 +85,8 @@ func (i *Instance) BeginValidation() error { i.snapshot.Phase = PhaseValidating i.snapshot.Readiness = Unknown i.extensions = ExtensionSupport{} + i.evidence = nil + i.snapshot.Failure = NoFailure return nil } @@ -100,6 +102,8 @@ func (i *Instance) BeginDeletion() error { i.snapshot.Phase = PhaseDeleting i.snapshot.Readiness = Unknown i.extensions = ExtensionSupport{} + i.evidence = nil + i.snapshot.Failure = NoFailure return nil } diff --git a/internal/database/domain/instance/readiness.go b/internal/database/domain/instance/readiness.go new file mode 100644 index 0000000..fea8ec7 --- /dev/null +++ b/internal/database/domain/instance/readiness.go @@ -0,0 +1,263 @@ +/* +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 instance + +import "errors" + +// Failure 只表示安全类别;驱动错误、凭据和 Condition 文案留在应用边界。 +type Failure uint8 + +const ( + NoFailure Failure = iota + ObservationIncomplete + DependencyUnavailable + AuthenticationFailed + InsufficientPrivileges + RegistryIncompatible + RegistryNotUsable +) + +// CheckResult 的零值表示未观察,不能视为成功。 +type CheckResult uint8 + +const ( + CheckUnobserved CheckResult = iota + CheckPassed + CheckUnavailable + CheckAuthenticationFailed + CheckInsufficientPrivileges +) + +// ManagementChecks 分别记录所需能力;SQL 探测和同轮次关联由 adapter/application 保证。 +// Extensions 不代表任意扩展均可安装;具体请求仍需支持检查、执行及回读。 +type ManagementChecks struct { + Connection CheckResult + Metadata CheckResult + Roles CheckResult + Databases CheckResult + Grants CheckResult + Extensions CheckResult +} + +func (c ManagementChecks) failure() Failure { + for _, check := range []CheckResult{c.Connection, c.Metadata, c.Roles, c.Databases, c.Grants, c.Extensions} { + switch check { + case CheckPassed: + case CheckUnavailable: + return DependencyUnavailable + case CheckAuthenticationFailed: + return AuthenticationFailed + case CheckInsufficientPrivileges: + return InsufficientPrivileges + default: + return ObservationIncomplete + } + } + return NoFailure +} + +type RegistryState uint8 + +const ( + RegistryUnobserved RegistryState = iota + RegistryAbsent + RegistryNeedsMigration + RegistryUsable + RegistryUnsupported + RegistryUnavailable +) + +// CapabilityObservation 是值对象,不包含连接、凭据或可变集合。 +type CapabilityObservation struct { + target ObservationTarget + version string + checks ManagementChecks + registry RegistryState +} + +func NewCapabilityObservation(target ObservationTarget, version string, + checks ManagementChecks, registry RegistryState, +) (CapabilityObservation, error) { + if err := target.Validate(); err != nil { + return CapabilityObservation{}, err + } + return CapabilityObservation{target: target, version: version, checks: checks, registry: registry}, nil +} + +func (o CapabilityObservation) managementFailure() Failure { + if failure := o.checks.failure(); failure != NoFailure { + return failure + } + if o.version == "" { + return ObservationIncomplete + } + return NoFailure +} + +func (o CapabilityObservation) registryFailure() Failure { + switch o.registry { + case RegistryUsable: + return NoFailure + case RegistryAbsent, RegistryNeedsMigration: + return RegistryNotUsable + case RegistryUnsupported: + return RegistryIncompatible + case RegistryUnavailable: + return DependencyUnavailable + default: + return ObservationIncomplete + } +} + +type PreparationDecision uint8 + +const ( + PreparationDenied PreparationDecision = iota + PreparationAllowed + AlreadyUsable +) + +func (i *Instance) acceptObservation(o CapabilityObservation, phase Phase) error { + if !i.target.Matches(o.target) { + return errors.New("capability observation target does not match instance") + } + if i.deleting || i.snapshot.Phase != phase { + return errors.New("capability observation is not allowed in current lifecycle") + } + return nil +} + +func (i *Instance) fail(failure Failure) { + i.evidence = nil + i.extensions = ExtensionSupport{} + i.snapshot.Readiness = NotReady + i.snapshot.Failure = failure + i.snapshot.ObservedRevision = i.target.Revision().Value() +} + +// AssessManagement 只推进意图,不执行 registry 写入,也不完成 observedRevision。 +func (i *Instance) AssessManagement(o CapabilityObservation) error { + if err := i.acceptObservation(o, PhaseValidating); err != nil { + return err + } + if failure := o.managementFailure(); failure != NoFailure { + i.fail(failure) + return nil + } + if failure := o.registryFailure(); failure != NoFailure && failure != RegistryNotUsable { + i.fail(failure) + return nil + } + i.snapshot.Phase = PhaseInitializingRegistry + i.snapshot.Readiness = Unknown + i.snapshot.Failure = NoFailure + i.evidence = nil + return nil +} + +// PlanRegistryPreparation 不证明 checkpoint 已落盘;应用层必须先保存意图再执行写入。 +func (i *Instance) PlanRegistryPreparation(o CapabilityObservation) (PreparationDecision, error) { + if err := i.acceptObservation(o, PhaseInitializingRegistry); err != nil { + return PreparationDenied, err + } + if failure := o.managementFailure(); failure != NoFailure { + i.fail(failure) + return PreparationDenied, nil + } + switch o.registry { + case RegistryUsable: + return AlreadyUsable, nil + case RegistryAbsent, RegistryNeedsMigration: + return PreparationAllowed, nil + default: + i.fail(o.registryFailure()) + return PreparationDenied, nil + } +} + +// RegistryPreparationResult 只能是安全失败或完整回读,不能表达裸操作成功。 +type RegistryPreparationResult struct { + observation CapabilityObservation + failure Failure +} + +func RegistryReadBack(o CapabilityObservation) RegistryPreparationResult { + return RegistryPreparationResult{observation: o} +} + +func RegistryPreparationFailed(target ObservationTarget, failure Failure) (RegistryPreparationResult, error) { + if err := target.Validate(); err != nil { + return RegistryPreparationResult{}, err + } + if failure < ObservationIncomplete || failure > RegistryNotUsable { + return RegistryPreparationResult{}, errors.New("registry preparation requires a known failure category") + } + return RegistryPreparationResult{observation: CapabilityObservation{target: target}, failure: failure}, nil +} + +func (i *Instance) AssessRegistryResult(result RegistryPreparationResult) error { + o := result.observation + if err := i.acceptObservation(o, PhaseInitializingRegistry); err != nil { + return err + } + if result.failure != NoFailure { + i.fail(result.failure) + return nil + } + i.assessComplete(o) + return nil +} + +func (i *Instance) assessComplete(o CapabilityObservation) { + if failure := o.managementFailure(); failure != NoFailure { + i.fail(failure) + return + } + if failure := o.registryFailure(); failure != NoFailure { + i.fail(failure) + return + } + i.snapshot = Snapshot{Phase: PhaseReady, ObservedRevision: i.target.Revision().Value(), + Readiness: Ready, ReportedVersion: o.version} + i.evidence = &o +} + +// AssessReadiness 每轮接收完整事实,失败立即撤销本轮供应能力。 +func (i *Instance) AssessReadiness(o CapabilityObservation) error { + if err := i.acceptObservation(o, PhaseReady); err != nil { + return err + } + if i.snapshot.ObservedRevision != i.target.Revision().Value() { + return i.BeginValidation() + } + if o.managementFailure() != NoFailure || o.registry == RegistryUnavailable { + i.snapshot.Phase = PhaseValidating + } else if o.registryFailure() != NoFailure { + i.snapshot.Phase = PhaseInitializingRegistry + } + i.assessComplete(o) + return nil +} + +// RequireProvisioningReady 仅检查 Instance 前置条件,不授予 Tenant 所有权或外部写入许可。 +func (i *Instance) RequireProvisioningReady() error { + if i.deleting || i.evidence == nil || i.snapshot.Phase != PhaseReady || i.snapshot.Readiness != Ready || + i.snapshot.ObservedRevision != i.target.Revision().Value() { + return errors.New("instance is not ready for provisioning") + } + return nil +} diff --git a/internal/database/domain/instance/readiness_test.go b/internal/database/domain/instance/readiness_test.go new file mode 100644 index 0000000..ad0feac --- /dev/null +++ b/internal/database/domain/instance/readiness_test.go @@ -0,0 +1,352 @@ +/* +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 instance_test + +import ( + "testing" + + "git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance" +) + +const testServerVersion = "17.6" + +func completeChecks() instance.ManagementChecks { + return instance.ManagementChecks{ + Connection: instance.CheckPassed, Metadata: instance.CheckPassed, + Roles: instance.CheckPassed, Databases: instance.CheckPassed, + Grants: instance.CheckPassed, Extensions: instance.CheckPassed, + } +} + +func capability(t *testing.T, value *instance.Instance, checks instance.ManagementChecks, + registry instance.RegistryState, +) instance.CapabilityObservation { + t.Helper() + o, err := instance.NewCapabilityObservation(value.Target(), testServerVersion, checks, registry) + if err != nil { + t.Fatal(err) + } + return o +} + +func readyInstance(t *testing.T) *instance.Instance { + t.Helper() + i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false) + if err := i.AssessRegistryResult(instance.RegistryReadBack(capability(t, i, completeChecks(), instance.RegistryUsable))); err != nil { + t.Fatal(err) + } + if err := i.RequireProvisioningReady(); err != nil { + t.Fatal(err) + } + return i +} + +func TestReadinessRequiresCompleteReadBack(t *testing.T) { + i := lifecycleInstance(t, instance.Snapshot{}, false) + if err := i.BeginValidation(); err != nil { + t.Fatal(err) + } + absent := capability(t, i, completeChecks(), instance.RegistryAbsent) + if err := i.AssessManagement(absent); err != nil { + t.Fatal(err) + } + if s := i.Snapshot(); s.Phase != instance.PhaseInitializingRegistry || s.ObservedRevision != 0 || s.Readiness != instance.Unknown { + t.Fatalf("management observation prematurely concluded readiness: %+v", s) + } + for range 2 { + decision, err := i.PlanRegistryPreparation(absent) + if err != nil || decision != instance.PreparationAllowed { + t.Fatalf("preparation: %v, %v", decision, err) + } + if i.RequireProvisioningReady() == nil { + t.Fatal("preparation authorized provisioning") + } + } + if err := i.AssessRegistryResult(instance.RegistryReadBack(absent)); err != nil { + t.Fatal(err) + } + if i.Snapshot().Failure != instance.RegistryNotUsable || i.RequireProvisioningReady() == nil { + t.Fatal("absent registry accepted as ready") + } + usable := capability(t, i, completeChecks(), instance.RegistryUsable) + decision, err := i.PlanRegistryPreparation(usable) + if err != nil || decision != instance.AlreadyUsable { + t.Fatalf("retry after external preparation: %v, %v", decision, err) + } + if err := i.AssessRegistryResult(instance.RegistryReadBack(usable)); err != nil { + t.Fatal(err) + } + if s := i.Snapshot(); s.Readiness != instance.Ready || s.ReportedVersion != testServerVersion || s.ObservedRevision != i.Target().Revision().Value() { + t.Fatalf("complete observation not accepted: %+v", s) + } + if err := i.RequireProvisioningReady(); err != nil { + t.Fatal(err) + } +} + +// 重启只恢复 checkpoint;依赖稍后恢复时必须重新取得完整事实。 +func TestReadinessRecoveryAndInvalidation(t *testing.T) { + i := readyInstance(t) + restored, err := instance.Reconstitute(i.Target(), i.Snapshot(), false) + if err != nil { + t.Fatal(err) + } + if restored.RequireProvisioningReady() == nil { + t.Fatal("persisted Ready fabricated fresh evidence") + } + for range 2 { + if err := restored.AssessReadiness(capability(t, restored, completeChecks(), instance.RegistryUsable)); err != nil { + t.Fatal(err) + } + if err := restored.RequireProvisioningReady(); err != nil { + t.Fatal(err) + } + } + if err := restored.BeginValidation(); err != nil { + t.Fatal(err) + } + if restored.RequireProvisioningReady() == nil { + t.Fatal("validation retained evidence") + } + deleted, err := instance.Reconstitute(i.Target(), i.Snapshot(), true) + if err != nil { + t.Fatal(err) + } + if deleted.RequireProvisioningReady() == nil { + t.Fatal("deletion allowed provisioning") + } + if err := deleted.BeginDeletion(); err != nil { + t.Fatal(err) + } + if deleted.RequireProvisioningReady() == nil { + t.Fatal("deleting checkpoint allowed provisioning") + } +} + +func TestEachManagementCheckIsRequired(t *testing.T) { + for field := range 6 { + for _, result := range []instance.CheckResult{instance.CheckUnobserved, instance.CheckUnavailable, + instance.CheckAuthenticationFailed, instance.CheckInsufficientPrivileges, 255} { + checks := completeChecks() + fields := []*instance.CheckResult{&checks.Connection, &checks.Metadata, &checks.Roles, + &checks.Databases, &checks.Grants, &checks.Extensions} + *fields[field] = result + i := readyInstance(t) + if err := i.AssessReadiness(capability(t, i, checks, instance.RegistryUsable)); err != nil { + t.Fatal(err) + } + if s := i.Snapshot(); s.Phase != instance.PhaseValidating || s.Readiness != instance.NotReady || + s.Failure == instance.NoFailure || i.RequireProvisioningReady() == nil { + t.Fatalf("check %d result %d accepted: %+v", field, result, s) + } + } + } +} + +func TestRegistryDecisionsAndReadinessLoss(t *testing.T) { + for _, tc := range []struct { + state instance.RegistryState + decision instance.PreparationDecision + failure instance.Failure + }{ + {instance.RegistryUsable, instance.AlreadyUsable, instance.NoFailure}, + {instance.RegistryAbsent, instance.PreparationAllowed, instance.RegistryNotUsable}, + {instance.RegistryNeedsMigration, instance.PreparationAllowed, instance.RegistryNotUsable}, + {instance.RegistryUnsupported, instance.PreparationDenied, instance.RegistryIncompatible}, + {instance.RegistryUnavailable, instance.PreparationDenied, instance.DependencyUnavailable}, + {instance.RegistryUnobserved, instance.PreparationDenied, instance.ObservationIncomplete}, + {255, instance.PreparationDenied, instance.ObservationIncomplete}, + } { + i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false) + o := capability(t, i, completeChecks(), tc.state) + decision, err := i.PlanRegistryPreparation(o) + if err != nil || decision != tc.decision { + t.Fatalf("registry %d: %v, %v", tc.state, decision, err) + } + i = readyInstance(t) + if err := i.AssessReadiness(o); err != nil { + t.Fatal(err) + } + if i.Snapshot().Failure != tc.failure { + t.Fatalf("registry %d: %+v", tc.state, i.Snapshot()) + } + if tc.state != instance.RegistryUsable { + wantPhase := instance.PhaseInitializingRegistry + if tc.state == instance.RegistryUnavailable { + wantPhase = instance.PhaseValidating + } + if i.Snapshot().Phase != wantPhase || i.RequireProvisioningReady() == nil { + t.Fatal("registry drift retained readiness") + } + } + } +} + +func TestInitializationRejectsIncompleteOrFailedManagement(t *testing.T) { + for _, tc := range []struct { + checks instance.ManagementChecks + registry instance.RegistryState + failure instance.Failure + }{ + {instance.ManagementChecks{}, instance.RegistryUsable, instance.ObservationIncomplete}, + {completeChecks(), instance.RegistryUnsupported, instance.RegistryIncompatible}, + {completeChecks(), instance.RegistryUnavailable, instance.DependencyUnavailable}, + } { + i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseValidating}, false) + if err := i.AssessManagement(capability(t, i, tc.checks, tc.registry)); err != nil { + t.Fatal(err) + } + if s := i.Snapshot(); s.Phase != instance.PhaseValidating || s.Failure != tc.failure || + s.ObservedRevision != i.Target().Revision().Value() || s.Readiness != instance.NotReady { + t.Fatalf("invalid management accepted: %+v", s) + } + } + i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false) + decision, err := i.PlanRegistryPreparation(capability(t, i, instance.ManagementChecks{}, instance.RegistryAbsent)) + if err != nil || decision != instance.PreparationDenied || i.Snapshot().Failure != instance.ObservationIncomplete { + t.Fatal("incomplete management allowed registry writes") + } + if err := i.AssessRegistryResult(instance.RegistryReadBack(capability(t, i, completeChecks(), instance.RegistryUsable))); err != nil { + t.Fatal(err) + } + if err := i.RequireProvisioningReady(); err != nil { + t.Fatal("dependency recovery did not restore readiness", err) + } +} + +func TestReadinessMethodsRejectWrongPhaseAndDeletion(t *testing.T) { + for _, deleting := range []bool{false, true} { + for _, phase := range []instance.Phase{instance.PhasePending, instance.PhaseValidating, + instance.PhaseInitializingRegistry, instance.PhaseReady, instance.PhaseDeleting} { + for _, operation := range []struct { + phase instance.Phase + apply func(*instance.Instance, instance.CapabilityObservation) error + }{ + {instance.PhaseValidating, (*instance.Instance).AssessManagement}, + {instance.PhaseReady, (*instance.Instance).AssessReadiness}, + {instance.PhaseInitializingRegistry, func(i *instance.Instance, o instance.CapabilityObservation) error { + _, err := i.PlanRegistryPreparation(o) + return err + }}, + {instance.PhaseInitializingRegistry, func(i *instance.Instance, o instance.CapabilityObservation) error { + return i.AssessRegistryResult(instance.RegistryReadBack(o)) + }}, + } { + if !deleting && operation.phase == phase { + continue + } + i := lifecycleInstance(t, instance.Snapshot{Phase: phase}, deleting) + before := i.Snapshot() + if err := operation.apply(i, capability(t, i, completeChecks(), instance.RegistryUsable)); err == nil { + t.Fatalf("phase %s deleting=%t accepted operation for %s", phase, deleting, operation.phase) + } + if i.Snapshot() != before { + t.Fatal("rejected operation mutated snapshot") + } + } + } + } +} + +func TestOldGenerationObservationDoesNotReplaceEvidence(t *testing.T) { + i := readyInstance(t) + target := i.Target() + revision, err := instance.NewRevision(target.Revision().Value() + 1) + if err != nil { + t.Fatal(err) + } + other, err := instance.NewObservationTarget(target.Identity(), revision, target.Definition()) + if err != nil { + t.Fatal(err) + } + o, err := instance.NewCapabilityObservation(other, testServerVersion, completeChecks(), instance.RegistryUsable) + if err != nil { + t.Fatal(err) + } + before := i.Snapshot() + if err := i.AssessReadiness(o); err == nil || i.Snapshot() != before { + t.Fatal("mismatched generation observation was accepted") + } + if err := i.RequireProvisioningReady(); err != nil { + t.Fatal("rejected unrelated input changed previously accepted evidence", err) + } +} + +func TestPreparationFailureCannotEstablishReadiness(t *testing.T) { + i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false) + for _, failure := range []instance.Failure{instance.DependencyUnavailable, instance.AuthenticationFailed, + instance.InsufficientPrivileges, instance.RegistryIncompatible} { + result, err := instance.RegistryPreparationFailed(i.Target(), failure) + if err != nil { + t.Fatal(err) + } + if err := i.AssessRegistryResult(result); err != nil { + t.Fatal(err) + } + if s := i.Snapshot(); s.Failure != failure || s.Readiness != instance.NotReady || + s.Phase != instance.PhaseInitializingRegistry || i.RequireProvisioningReady() == nil { + t.Fatalf("failed operation accepted: %+v", s) + } + } + for _, failure := range []instance.Failure{instance.NoFailure, 255} { + if _, err := instance.RegistryPreparationFailed(i.Target(), failure); err == nil { + t.Fatal("invalid failure accepted") + } + } +} + +func TestCapabilityInputsAndLifecycleGuards(t *testing.T) { + i := readyInstance(t) + if _, err := instance.NewCapabilityObservation(instance.ObservationTarget{}, testServerVersion, + completeChecks(), instance.RegistryUsable); err == nil { + t.Fatal("invalid target accepted") + } + if _, err := instance.RegistryPreparationFailed(instance.ObservationTarget{}, instance.DependencyUnavailable); err == nil { + t.Fatal("invalid failure target accepted") + } + for _, method := range []func(instance.CapabilityObservation) error{ + i.AssessManagement, i.AssessReadiness, + func(o instance.CapabilityObservation) error { _, err := i.PlanRegistryPreparation(o); return err }, + func(o instance.CapabilityObservation) error { + return i.AssessRegistryResult(instance.RegistryReadBack(o)) + }, + } { + before := i.Snapshot() + if err := method(instance.CapabilityObservation{}); err == nil || i.Snapshot() != before { + t.Fatal("mismatched observation accepted or mutated state") + } + } + old := i.Snapshot() + old.ObservedRevision = 0 + changed := lifecycleInstance(t, old, false) + if err := changed.AssessReadiness(capability(t, changed, completeChecks(), instance.RegistryUsable)); err != nil { + t.Fatal(err) + } + if s := changed.Snapshot(); s.Phase != instance.PhaseValidating || s.ObservedRevision != 0 || s.Readiness != instance.Unknown { + t.Fatalf("changed generation accepted old checkpoint: %+v", s) + } + o, err := instance.NewCapabilityObservation(i.Target(), "", completeChecks(), instance.RegistryUsable) + if err != nil { + t.Fatal(err) + } + if err := i.AssessReadiness(o); err != nil { + t.Fatal(err) + } + if i.Snapshot().Failure != instance.ObservationIncomplete { + t.Fatal("missing version accepted") + } +}