feat: 实现 Instance Ready 领域判定与恢复规则
Verify / test (pull_request) Successful in 7m20s
Verify / lint (pull_request) Successful in 7m51s

This commit is contained in:
2026-09-21 07:49:41 +00:00
parent 984c0aee73
commit cd0d3a70ae
5 changed files with 650 additions and 6 deletions
+5 -3
View File
@@ -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 已经可用。
## 设计入口
+24 -1
View File
@@ -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 已可运行或这些集成合同已完成。
@@ -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
}
@@ -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
}
@@ -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")
}
}