feat(backend): admin 手动运维接真实 lifecycle/provision
管理后台节点页的「拉黑/恢复(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>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user