复用现有 k3s 调度和 Job 生命周期,增加 worker 接入与批准、受保护节点身份、归档传输、持久迁移锁及活动 PVC 切换;同步管理员 API、CLI、面板和隔离规则。分布式模式默认关闭,保持单机兼容。 验证:Go 全量测试与 vet;面板 874 个测试、lint/build;Linux VM 安装器测试、清单服务端 dry-run、网络命名空间防火墙实测。A/B/C 三机 WireGuard、Velocity 和迁移验收仍待完成。
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"felis.lolicon.best/internal/distributed"
|
|
"felis.lolicon.best/internal/naming"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
)
|
|
|
|
type Distribution interface {
|
|
Nodes(context.Context) ([]distributed.Node, error)
|
|
ValidateNode(context.Context, string) error
|
|
BeginMigration(context.Context, string, string, string) (distributed.Operation, error)
|
|
Migration(context.Context, string, string) (distributed.Operation, error)
|
|
RetryMigration(context.Context, string, string) (distributed.Operation, error)
|
|
}
|
|
|
|
func (a *API) distributedReady(w http.ResponseWriter, r *http.Request) bool {
|
|
if a.Distribution == nil {
|
|
writeError(w, r, newError(503, "distributed_unavailable", "distributed deployment is not configured"))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (a *API) handleNodes(w http.ResponseWriter, r *http.Request) {
|
|
if !a.distributedReady(w, r) {
|
|
return
|
|
}
|
|
nodes, err := a.Distribution.Nodes(r.Context())
|
|
if err != nil {
|
|
writeError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"nodes": nodes})
|
|
}
|
|
|
|
func (a *API) handleMigration(w http.ResponseWriter, r *http.Request) {
|
|
if !a.distributedReady(w, r) {
|
|
return
|
|
}
|
|
server := r.PathValue("name")
|
|
if err := naming.ValidateServerName(server); err != nil {
|
|
writeError(w, r, newError(400, "bad_name", "%v", err))
|
|
return
|
|
}
|
|
rec, err := a.Repo.ServerByName(r.Context(), server)
|
|
if err != nil {
|
|
a.writeLookupError(w, r, err)
|
|
return
|
|
}
|
|
if rec.Retire != nil {
|
|
writeError(w, r, errServerRetiring)
|
|
return
|
|
}
|
|
var body struct {
|
|
TargetNode string `json:"targetNode"`
|
|
}
|
|
if err := decodeJSON(w, r, &body); err != nil {
|
|
writeError(w, r, err)
|
|
return
|
|
}
|
|
if err := a.Distribution.ValidateNode(r.Context(), body.TargetNode); err != nil {
|
|
writeError(w, r, newError(400, "bad_node", "%v", err))
|
|
return
|
|
}
|
|
op, err := a.Distribution.BeginMigration(r.Context(), server, body.TargetNode, rec.OwnerID)
|
|
if err != nil {
|
|
a.writeMigrationError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, op)
|
|
}
|
|
|
|
func (a *API) handleMigrationStatus(w http.ResponseWriter, r *http.Request) {
|
|
if !a.distributedReady(w, r) {
|
|
return
|
|
}
|
|
op, err := a.Distribution.Migration(r.Context(), r.PathValue("name"), r.PathValue("id"))
|
|
if err != nil {
|
|
a.writeMigrationError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, op)
|
|
}
|
|
|
|
func (a *API) handleMigrationRetry(w http.ResponseWriter, r *http.Request) {
|
|
if !a.distributedReady(w, r) {
|
|
return
|
|
}
|
|
rec, err := a.Repo.ServerByName(r.Context(), r.PathValue("name"))
|
|
if err != nil {
|
|
a.writeLookupError(w, r, err)
|
|
return
|
|
}
|
|
if rec.Retire != nil {
|
|
writeError(w, r, errServerRetiring)
|
|
return
|
|
}
|
|
op, err := a.Distribution.RetryMigration(r.Context(), r.PathValue("name"), r.PathValue("id"))
|
|
if err != nil {
|
|
a.writeMigrationError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, op)
|
|
}
|
|
|
|
func (a *API) writeMigrationError(w http.ResponseWriter, r *http.Request, err error) {
|
|
switch {
|
|
case errors.Is(err, distributed.ErrBusy):
|
|
writeError(w, r, newError(409, "migration_busy", "%v", err))
|
|
case errors.Is(err, distributed.ErrNotFound), apierrors.IsNotFound(err):
|
|
writeError(w, r, newError(404, "not_found", "%v", err))
|
|
case apierrors.IsConflict(err):
|
|
writeError(w, r, newError(409, "conflict", "retry after refreshing migration status"))
|
|
default:
|
|
writeError(w, r, err)
|
|
}
|
|
}
|