9a028ab907
管理后台节点页的「拉黑/恢复(draining/up)」与「一键换机(replace)」此前 注入的是 StubLifecycle/StubProvision(Ready()=false,UI 置灰、操作返回 服务不可用)。本次接真实实现: - RealLifecycle:复用 scheduler 的乐观锁状态流转(UPDATE nodes WHERE status=from + node_events + directory_version bump),手动运维与调度 自动循环共用同一条 canonical 路径;读当前状态作 from 守卫,幂等安全。 - RealProvision:委托 provision.Service.Replace 的 make-before-break 换机 (先拉起新节点再 drain 旧节点,容量不掉)。 - BuildServices:lifecycle 恒为真实;provision 在服务初始化成功时为真实, 否则回退 not-ready stub(无厂商凭证时点击换机给出明确错误)。 补 services_real_test.go:换机委托(空 UUID 起新编排)、错误透传、 非法目标状态拒绝、Ready 标志。server 全量 23 包测试通过。 e2e(真实换机)待机群 + 厂商凭证。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
61 lines
1.8 KiB
Go
61 lines
1.8 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/wangjia/pangolin/server/internal/provision"
|
|
)
|
|
|
|
// fakeReplacer records the arguments passed to Replace and returns canned values.
|
|
type fakeReplacer struct {
|
|
gotNodeID int64
|
|
gotUUID string
|
|
res *provision.ReplaceResult
|
|
err error
|
|
}
|
|
|
|
func (f *fakeReplacer) Replace(_ context.Context, nodeID int64, uuid string) (*provision.ReplaceResult, error) {
|
|
f.gotNodeID, f.gotUUID = nodeID, uuid
|
|
return f.res, f.err
|
|
}
|
|
|
|
func TestRealProvision_Replace_DelegatesWithFreshUUID(t *testing.T) {
|
|
f := &fakeReplacer{res: &provision.ReplaceResult{NewNodeID: 99}}
|
|
p := NewRealProvision(f)
|
|
|
|
if err := p.Replace(context.Background(), 42, "operator"); err != nil {
|
|
t.Fatalf("Replace: %v", err)
|
|
}
|
|
if f.gotNodeID != 42 {
|
|
t.Errorf("nodeID = %d, want 42", f.gotNodeID)
|
|
}
|
|
// Admin always starts a fresh orchestration: the service mints its own key.
|
|
if f.gotUUID != "" {
|
|
t.Errorf("replacementUUID = %q, want empty (fresh start)", f.gotUUID)
|
|
}
|
|
if !p.Ready() {
|
|
t.Error("RealProvision.Ready() = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestRealProvision_Replace_PropagatesError(t *testing.T) {
|
|
sentinel := errors.New("no vendor credentials")
|
|
p := NewRealProvision(&fakeReplacer{err: sentinel})
|
|
if err := p.Replace(context.Background(), 7, "operator"); !errors.Is(err, sentinel) {
|
|
t.Errorf("Replace error = %v, want %v", err, sentinel)
|
|
}
|
|
}
|
|
|
|
func TestRealLifecycle_RejectsUnsupportedTarget(t *testing.T) {
|
|
// Target validation happens before any DB access, so a nil db is safe here.
|
|
l := &RealLifecycle{}
|
|
if err := l.TransitionStatus(context.Background(), 1, "destroyed", "operator"); err == nil {
|
|
t.Error("TransitionStatus with target=destroyed should error, got nil")
|
|
}
|
|
if !l.Ready() {
|
|
t.Error("RealLifecycle.Ready() = false, want true")
|
|
}
|
|
}
|