feat(provision): 弹性节点基建 Terraform + 一键更换 (tsk_6u0FxmbC7Yeq)
IaC 面 (infra/) 与控制面 (server/internal/provision) 双产出,落地 doc/04 §4 「节点是牲口」弹性拓扑与 make-before-break 一键更换。 server/internal/provision: - CloudAdapter 适配层 + Registry(首发 vultr 消耗品池 / hetzner 精品池各一); 厂商凭证仅从 PROVISION_<VENDOR>_* env 注入,不入库不入 git。 - ProvisionService:CreateNode(幂等键重放不重复开机)、DestroyNode(幂等)、 RotateIP(换 IP 不换机 + version bump)、ListProviders。 - Replace 一键更换:先建后拆,新机 up 先于旧机 draining(容量不下降), replacement_uuid 幂等键 + replacements 表分步记录,崩溃可续跑不重复。 - RotatePool:池内滚动轮换,并发度 1–2。 - cmd/nodectl CLI:create/destroy/rotate-ip/replace/rotate-pool/providers。 - 单测(mock 厂商 API + 内存 Store):幂等重放、make-before-break 时序断言、 开机失败/探活超时→destroyed+失败计数+告警钩子、崩溃续跑、RotatePool。 infra/: - terraform/:探针机 + 控制面基线模块化(probe / control-plane)+ README, 低频基线进 state,节点不进 Terraform。 - cloud-init/node.yaml.tmpl:节点引导模板(注入一次性 bootstrap token,task #5)。 - identity-isolation.md:身份隔离登记表(doc/06 §2 红线,无任何凭证)。 migrations/000008:nodes 增 provider_instance_id/elastic_ip_id、node_events 增 ip_rotated、provision_idempotency / replacements 表(附加式,不动现网)。 红线:仅面向新厂商池,绝不纳管现网生产 EC2(deploy/ marzban)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package provision
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock abstracts time for testability (Replace drains on a timer).
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
// Sleep blocks for d or until ctx is done, returning ctx.Err() on cancel.
|
||||
Sleep(ctx context.Context, d time.Duration) error
|
||||
}
|
||||
|
||||
// realClock is the production Clock.
|
||||
type realClock struct{}
|
||||
|
||||
func (realClock) Now() time.Time { return time.Now().UTC() }
|
||||
|
||||
func (realClock) Sleep(ctx context.Context, d time.Duration) error {
|
||||
t := time.NewTimer(d)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-t.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Prober verifies a node is reachable before it joins the directory.
|
||||
//
|
||||
// The full version (three-way probe cross-check, doc/04 §4.2) depends on the
|
||||
// probe fleet (task #15). Until then a simplified Prober is injected:
|
||||
// "heartbeat present + an overseas reachability check". Replace calls WaitReady
|
||||
// after a node is created; on success the node is promoted to up.
|
||||
type Prober interface {
|
||||
// WaitReady blocks until the node passes simplified probing, or returns an
|
||||
// error on timeout / ctx cancellation / probe failure.
|
||||
WaitReady(ctx context.Context, node *Node) error
|
||||
}
|
||||
|
||||
// AlertHook receives operational alerts (doc/04 §5.3 — TG bot in production).
|
||||
// Replace / CreateNode call Fire on boot failure, probe timeout, and capacity
|
||||
// protection breaches.
|
||||
type AlertHook interface {
|
||||
Fire(ctx context.Context, alert Alert)
|
||||
}
|
||||
|
||||
// AlertKind classifies an alert.
|
||||
type AlertKind string
|
||||
|
||||
const (
|
||||
AlertBootFailed AlertKind = "boot_failed"
|
||||
AlertProbeTimeout AlertKind = "probe_timeout"
|
||||
AlertReplaceFail AlertKind = "replace_failed"
|
||||
AlertCapacity AlertKind = "capacity_low"
|
||||
)
|
||||
|
||||
// Alert is a single operational alert payload.
|
||||
type Alert struct {
|
||||
Kind AlertKind
|
||||
NodeUUID string
|
||||
Pool Pool
|
||||
Message string
|
||||
// FailCount is the running failure counter for capacity-protection alerts.
|
||||
FailCount int
|
||||
}
|
||||
|
||||
// noopAlert discards alerts; used when no hook is configured.
|
||||
type noopAlert struct{}
|
||||
|
||||
func (noopAlert) Fire(context.Context, Alert) {}
|
||||
|
||||
// BootstrapIssuer issues one-time enrollment tokens (task #5).
|
||||
// *mtls.BootstrapTokenManager satisfies this interface.
|
||||
type BootstrapIssuer interface {
|
||||
IssueToken(ctx context.Context, nodeUUID string) (string, error)
|
||||
}
|
||||
|
||||
// CloudInitRenderer renders the node cloud-init document from a template,
|
||||
// injecting the node UUID and bootstrap token (template lives at
|
||||
// infra/cloud-init/node.yaml.tmpl, task #6).
|
||||
type CloudInitRenderer interface {
|
||||
Render(data CloudInitData) (string, error)
|
||||
}
|
||||
|
||||
// CloudInitData is the template context for the node cloud-init document.
|
||||
type CloudInitData struct {
|
||||
NodeUUID string
|
||||
BootstrapToken string
|
||||
Region string
|
||||
Role Role
|
||||
Tier Tier
|
||||
// ControlPlaneURL is the mTLS enrollment endpoint the agent registers to.
|
||||
ControlPlaneURL string
|
||||
}
|
||||
Reference in New Issue
Block a user