Files
pangolin/server/internal/scheduler/wiring_provision.go
T
wangjia 5cc63f9e28 feat(scheduler): provision 真实接线适配器(1/2) — orchestrate.ProvisionService over provision.Service
把 scheduler 的 stub provision 换成真实 provision.Service(#14)的适配器:
- 类型映射:orchestrate.NodeSpec → provision.NodeSpec(ProviderID string→int64、
  Role/Tier 转换、reality/name 字段);CreateNode 返回 node.UUID 作 orchestrate ID。
- uuid↔int64:Destroy/RotateIP 用 provision.Store.GetNodeByUUID 解析。
- ListProviders:tier→pool(pro=premium/free=consumable) + region 过滤 + enabled 过滤。
- 依赖小接口(provisionSvc/provisionResolver)以便单测;映射全单测覆盖
  (spec 映射/uuid 解析/pool+region+enabled 过滤)。
- e2e(真实创建/销毁节点)待有机群 + 厂商凭证后验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:14:54 +08:00

138 lines
4.2 KiB
Go

package scheduler
import (
"context"
"strconv"
"github.com/wangjia/pangolin/server/internal/provision"
"github.com/wangjia/pangolin/server/internal/scheduler/orchestrate"
)
// provisionSvc and provisionResolver are the slices of provision.Service /
// provision.Store the adapter needs. Declaring them as interfaces (rather than
// taking the concrete types) keeps the type-mapping logic unit-testable with
// fakes. *provision.Service satisfies provisionSvc; provision.Store satisfies
// provisionResolver.
type provisionSvc interface {
CreateNode(ctx context.Context, spec provision.NodeSpec, idempotencyKey string) (*provision.Node, error)
DestroyNode(ctx context.Context, id int64) error
RotateIP(ctx context.Context, id int64) (*provision.Node, error)
ListProviders(ctx context.Context, pool provision.Pool) ([]*provision.Provider, error)
}
type provisionResolver interface {
GetNodeByUUID(ctx context.Context, uuid string) (*provision.Node, error)
}
// provisionAdapter implements orchestrate.ProvisionService over the real
// provision.Service (#14). The orchestrate interface keys nodes by string ID
// (node UUID); provision keys by int64. The adapter bridges that, plus the
// NodeSpec / Provider type differences.
type provisionAdapter struct {
svc provisionSvc
store provisionResolver
}
// NewProvisionAdapter wires the adapter. svc is the provision service; store
// resolves a node UUID → its int64 id (provision.Store).
func NewProvisionAdapter(svc provisionSvc, store provisionResolver) orchestrate.ProvisionService {
return &provisionAdapter{svc: svc, store: store}
}
// CreateNode maps the spec and returns the new node's UUID as the orchestrate ID.
func (a *provisionAdapter) CreateNode(ctx context.Context, spec orchestrate.NodeSpec, idempotencyKey string) (string, error) {
var providerID int64
if spec.ProviderID != "" {
providerID, _ = strconv.ParseInt(spec.ProviderID, 10, 64)
}
n, err := a.svc.CreateNode(ctx, provision.NodeSpec{
Region: spec.Region,
Role: provision.Role(spec.Role),
Tier: provision.Tier(spec.Tier),
ProviderID: providerID,
NameZH: spec.NameZH,
NameEn: spec.NameEn,
RealitySNI: spec.RealitySNI,
RealityPBK: spec.RealityPBK,
HY2Port: spec.HY2Port,
}, idempotencyKey)
if err != nil {
return "", err
}
return n.UUID, nil
}
// DestroyNode resolves the UUID to an int64 id, then tears the node down.
func (a *provisionAdapter) DestroyNode(ctx context.Context, nodeID string) error {
id, err := a.resolveID(ctx, nodeID)
if err != nil {
return err
}
return a.svc.DestroyNode(ctx, id)
}
// RotateIP swaps the node's IP in place; the UUID is unchanged, so it is returned.
func (a *provisionAdapter) RotateIP(ctx context.Context, nodeID string) (string, error) {
id, err := a.resolveID(ctx, nodeID)
if err != nil {
return "", err
}
n, err := a.svc.RotateIP(ctx, id)
if err != nil {
return "", err
}
return n.UUID, nil
}
// ListProviders maps tier → pool, lists providers, and filters by region.
func (a *provisionAdapter) ListProviders(ctx context.Context, tier, region string) ([]orchestrate.ProviderInfo, error) {
ps, err := a.svc.ListProviders(ctx, poolForTier(tier))
if err != nil {
return nil, err
}
out := make([]orchestrate.ProviderInfo, 0, len(ps))
for _, p := range ps {
if !p.Enabled {
continue
}
if region != "" && !regionSupported(p.Regions, region) {
continue
}
out = append(out, orchestrate.ProviderInfo{
ID: strconv.FormatInt(p.ID, 10),
Regions: p.Regions,
})
}
return out, nil
}
func (a *provisionAdapter) resolveID(ctx context.Context, uuid string) (int64, error) {
n, err := a.store.GetNodeByUUID(ctx, uuid)
if err != nil {
return 0, err
}
return n.ID, nil
}
// poolForTier mirrors provision's tier→pool mapping: pro → premium, else consumable.
func poolForTier(tier string) provision.Pool {
if provision.Tier(tier) == provision.TierPro {
return provision.PoolPremium
}
return provision.PoolConsumable
}
// regionSupported reports whether region is in regions, treating an empty list
// as "all regions supported".
func regionSupported(regions []string, region string) bool {
if len(regions) == 0 {
return true
}
for _, r := range regions {
if r == region {
return true
}
}
return false
}