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 }