mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +08:00
704 lines
22 KiB
Go
704 lines
22 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go-backend/internal/store/repo"
|
|
"go-backend/internal/ws"
|
|
)
|
|
|
|
const peerShareResourcePrefix = "peer-share-"
|
|
|
|
func peerShareResourceName(shareID int64, kind, original string) string {
|
|
return fmt.Sprintf("%s%d-%s-%s", peerShareResourcePrefix, shareID, kind, base64.RawURLEncoding.EncodeToString([]byte(original)))
|
|
}
|
|
|
|
func parsePeerShareServiceName(name string) (shareID int64, originalName string, ok bool) {
|
|
if !strings.HasPrefix(name, peerShareResourcePrefix) {
|
|
return
|
|
}
|
|
parts := strings.SplitN(strings.TrimPrefix(name, peerShareResourcePrefix), "-", 3)
|
|
if len(parts) != 3 || parts[1] != "service" {
|
|
return
|
|
}
|
|
shareID, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil || shareID <= 0 {
|
|
return 0, "", false
|
|
}
|
|
decoded, err := base64.RawURLEncoding.DecodeString(parts[2])
|
|
if err != nil || len(decoded) == 0 {
|
|
return 0, "", false
|
|
}
|
|
return shareID, string(decoded), true
|
|
}
|
|
|
|
func federationResourceCommandKind(cmd string) (kind, action string) {
|
|
lower := strings.ToLower(cmd)
|
|
for _, entry := range []struct{ suffix, kind string }{{"service", "service"}, {"chains", "chain"}, {"climiters", "climiter"}, {"limiters", "limiter"}} {
|
|
if strings.HasSuffix(lower, entry.suffix) {
|
|
return entry.kind, strings.TrimSuffix(lower, entry.suffix)
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// Every peer-supplied reference is resolved within the same share namespace.
|
|
// Composite traffic limiters use a comma-separated list in GOST.
|
|
func scopePeerResourceReferences(value interface{}, shareID int64) {
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
for key, child := range v {
|
|
if strings.EqualFold(key, "chains") {
|
|
if names, ok := child.([]interface{}); ok {
|
|
for i, name := range names {
|
|
if raw, ok := name.(string); ok {
|
|
names[i] = peerShareResourceName(shareID, "chain", raw)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
kind := ""
|
|
switch strings.ToLower(key) {
|
|
case "chain":
|
|
kind = "chain"
|
|
case "limiter":
|
|
kind = "limiter"
|
|
case "climiter":
|
|
kind = "climiter"
|
|
}
|
|
if raw, ok := child.(string); ok && kind != "" && strings.TrimSpace(raw) != "" {
|
|
names := strings.Split(raw, ",")
|
|
for i, name := range names {
|
|
names[i] = peerShareResourceName(shareID, kind, strings.TrimSpace(name))
|
|
}
|
|
v[key] = strings.Join(names, ",")
|
|
} else {
|
|
scopePeerResourceReferences(child, shareID)
|
|
}
|
|
}
|
|
case []interface{}:
|
|
for _, child := range v {
|
|
scopePeerResourceReferences(child, shareID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func peerResourceDeletePayload(item repo.PeerShareResource) interface{} {
|
|
if item.Kind == "service" {
|
|
return map[string]interface{}{"services": []string{item.RuntimeName}}
|
|
}
|
|
key := item.Kind
|
|
if key == "climiter" {
|
|
key = "limiter"
|
|
}
|
|
return map[string]interface{}{key: item.RuntimeName}
|
|
}
|
|
func peerResourceDeleteCommand(kind string) string {
|
|
switch kind {
|
|
case "service":
|
|
return "DeleteService"
|
|
case "chain":
|
|
return "DeleteChains"
|
|
case "climiter":
|
|
return "DeleteCLimiters"
|
|
default:
|
|
return "DeleteLimiters"
|
|
}
|
|
}
|
|
|
|
// preparePeerResourceCommand validates and writes desired state before the node
|
|
// sees any command, closing the flow-report race even without a reservation.
|
|
func (h *Handler) preparePeerResourceCommand(share *repo.PeerShare, cmd string, data interface{}) ([]repo.PeerShareResource, error) {
|
|
kind, action := federationResourceCommandKind(cmd)
|
|
if kind == "" {
|
|
return nil, fmt.Errorf("command not allowed")
|
|
}
|
|
raw, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var decoded interface{}
|
|
if err = json.Unmarshal(raw, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
var configs []map[string]interface{}
|
|
var names []string
|
|
setting := action == "add" || action == "update"
|
|
if setting {
|
|
if kind == "service" {
|
|
configs = extractFederationServiceEntries(decoded)
|
|
} else if m, ok := decoded.(map[string]interface{}); ok {
|
|
if nested, ok := m["data"].(map[string]interface{}); ok {
|
|
configs = []map[string]interface{}{nested}
|
|
} else {
|
|
configs = []map[string]interface{}{m}
|
|
}
|
|
}
|
|
if len(configs) == 0 {
|
|
return nil, fmt.Errorf("resource configuration is required")
|
|
}
|
|
for _, c := range configs {
|
|
names = append(names, strings.TrimSpace(asString(c["name"])))
|
|
}
|
|
} else {
|
|
if kind == "service" {
|
|
if m, ok := decoded.(map[string]interface{}); ok {
|
|
for _, v := range asAnySlice(m["services"]) {
|
|
names = append(names, strings.TrimSpace(asString(v)))
|
|
}
|
|
}
|
|
} else {
|
|
if s, ok := decoded.(string); ok {
|
|
names = []string{strings.TrimSpace(s)}
|
|
} else if m, ok := decoded.(map[string]interface{}); ok {
|
|
key := kind
|
|
if key == "climiter" {
|
|
key = "limiter"
|
|
}
|
|
names = []string{strings.TrimSpace(asString(m[key]))}
|
|
}
|
|
}
|
|
if len(names) == 0 {
|
|
return nil, fmt.Errorf("resource name is required")
|
|
}
|
|
}
|
|
requestedNames := make(map[string]bool, len(names))
|
|
for _, name := range names {
|
|
requestedNames[name] = true
|
|
}
|
|
items := make([]repo.PeerShareResource, 0, len(names))
|
|
seen := map[string]bool{}
|
|
for i, name := range names {
|
|
if name == "" || strings.HasPrefix(name, peerShareResourcePrefix) {
|
|
return nil, fmt.Errorf("invalid peer resource name")
|
|
}
|
|
if seen[name] {
|
|
return nil, fmt.Errorf("duplicate resource name")
|
|
}
|
|
seen[name] = true
|
|
old, err := h.repo.GetPeerShareResource(share.ID, kind, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item := repo.PeerShareResource{ShareID: share.ID, NodeID: share.NodeID, Kind: kind, OriginalName: name, RuntimeName: peerShareResourceName(share.ID, kind, name), DesiredState: "active", UpdatedTime: time.Now().UnixMilli()}
|
|
if old != nil {
|
|
item = *old
|
|
item.Applied = 0
|
|
item.UpdatedTime = time.Now().UnixMilli()
|
|
}
|
|
if setting {
|
|
if old == nil && kind == "service" {
|
|
legacy, err := h.peerShareLegacyServiceNames(share, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(legacy) > 0 {
|
|
encoded, _ := json.Marshal(legacy)
|
|
item.LegacyNames = string(encoded)
|
|
item.LegacyServiceBase = normalizeForwardRuntimeServiceName(name)
|
|
}
|
|
}
|
|
config := configs[i]
|
|
if err := validatePeerResourceReferences(config, kind); err != nil {
|
|
return nil, err
|
|
}
|
|
scopePeerResourceReferences(config, share.ID)
|
|
config["name"] = item.RuntimeName
|
|
encoded, err := json.Marshal(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.Config = string(encoded)
|
|
item.DesiredState = "active"
|
|
item.ReleaseLegacyFamily = false
|
|
} else {
|
|
if old == nil {
|
|
// Delete callers send candidate base/TCP/UDP names. Missing names
|
|
// are safe no-ops; never forward a raw legacy name to the node.
|
|
if action == "delete" {
|
|
if kind != "service" {
|
|
continue
|
|
}
|
|
legacy, err := h.peerShareLegacyServiceNames(share, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(legacy) == 0 {
|
|
continue
|
|
}
|
|
encoded, _ := json.Marshal(legacy)
|
|
item.LegacyNames = string(encoded)
|
|
item.LegacyServiceBase = normalizeForwardRuntimeServiceName(name)
|
|
} else {
|
|
return nil, fmt.Errorf("service %q not found", name)
|
|
}
|
|
}
|
|
if old != nil && old.DesiredState == "deleted" && action != "delete" {
|
|
return nil, fmt.Errorf("service %q not found", name)
|
|
}
|
|
switch action {
|
|
case "delete":
|
|
item.DesiredState = "deleted"
|
|
base := normalizeForwardRuntimeServiceName(name)
|
|
if requestedNames[base] && requestedNames[base+"_tcp"] && requestedNames[base+"_udp"] {
|
|
item.ReleaseLegacyFamily = true
|
|
}
|
|
case "pause":
|
|
item.DesiredState = "paused"
|
|
case "resume":
|
|
if item.Config == "" {
|
|
return nil, fmt.Errorf("resource has no saved configuration")
|
|
}
|
|
item.DesiredState = "active"
|
|
default:
|
|
return nil, fmt.Errorf("command not allowed")
|
|
}
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
// Ownership and desired config commit atomically. A failed resource write
|
|
// must not rename a legacy binding and expose its remaining transports.
|
|
err = h.repo.WithPeerShareResourceTransaction(func(tx *repo.Repository) error {
|
|
if setting && kind == "service" {
|
|
scoped := make([]interface{}, 0, len(items))
|
|
for _, item := range items {
|
|
var c map[string]interface{}
|
|
_ = json.Unmarshal([]byte(item.Config), &c)
|
|
scoped = append(scoped, c)
|
|
}
|
|
binder := &Handler{repo: tx}
|
|
if err := binder.bindPeerShareForwardRuntimeServices(share, scoped); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.SavePeerShareResources(items)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (h *Handler) applyPeerShareResource(item repo.PeerShareResource) (ws.CommandResult, error) {
|
|
var result ws.CommandResult
|
|
var err error
|
|
if item.LegacyNames != "" {
|
|
var names []string
|
|
if err := json.Unmarshal([]byte(item.LegacyNames), &names); err != nil {
|
|
return result, err
|
|
}
|
|
share, loadErr := h.repo.GetPeerShare(item.ShareID)
|
|
if loadErr != nil {
|
|
return result, loadErr
|
|
}
|
|
if share == nil {
|
|
return result, fmt.Errorf("legacy resource ownership missing")
|
|
}
|
|
for _, name := range names {
|
|
owned, checkErr := h.peerShareLegacyServiceNames(share, name)
|
|
if checkErr != nil {
|
|
return result, checkErr
|
|
}
|
|
if len(owned) == 0 {
|
|
return result, fmt.Errorf("legacy resource ownership missing")
|
|
}
|
|
}
|
|
if _, err = h.sendNodeCommand(item.NodeID, "DeleteService", map[string]interface{}{"services": names}, false, true); err != nil {
|
|
return result, err
|
|
}
|
|
if err = h.repo.ClearPeerShareResourceLegacyNames(item.ShareID, item.Kind, item.OriginalName); err != nil {
|
|
return result, err
|
|
}
|
|
}
|
|
if item.DesiredState == "deleted" || item.DesiredState == "paused" {
|
|
// The provider retains the paused configuration durably. Keep the
|
|
// listener absent on reconnect instead of briefly starting it before
|
|
// sending a second pause command. Resume reapplies the saved config.
|
|
result, err = h.sendNodeCommand(item.NodeID, peerResourceDeleteCommand(item.Kind), peerResourceDeletePayload(item), false, true)
|
|
} else {
|
|
var config map[string]interface{}
|
|
if err = json.Unmarshal([]byte(item.Config), &config); err != nil {
|
|
return result, err
|
|
}
|
|
if item.Kind == "service" {
|
|
result, err = h.sendNodeCommand(item.NodeID, "UpdateService", []interface{}{config}, false, false)
|
|
} else {
|
|
suffix := "Limiters"
|
|
key := "limiter"
|
|
if item.Kind == "chain" {
|
|
suffix = "Chains"
|
|
key = "chain"
|
|
}
|
|
if item.Kind == "climiter" {
|
|
suffix = "CLimiters"
|
|
}
|
|
result, err = h.sendNodeCommand(item.NodeID, "Add"+suffix, config, false, false)
|
|
if err != nil && isAlreadyExistsMessage(err.Error()) {
|
|
result, err = h.sendNodeCommand(item.NodeID, "Update"+suffix, map[string]interface{}{key: item.RuntimeName, "data": config}, false, false)
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if item.Kind == "service" && item.DesiredState == "deleted" {
|
|
// TCP and UDP may share one reservation. Release only after all variants
|
|
// have an acknowledged tombstone.
|
|
items, listErr := h.repo.ListPeerShareResourcesByNode(item.NodeID)
|
|
if listErr != nil {
|
|
return result, listErr
|
|
}
|
|
base := normalizeForwardRuntimeServiceName(item.OriginalName)
|
|
for _, other := range items {
|
|
if other.ShareID == item.ShareID && other.Kind == "service" && other.OriginalName != item.OriginalName && normalizeForwardRuntimeServiceName(other.OriginalName) == base && (other.DesiredState != "deleted" || other.Applied == 0) {
|
|
return result, h.repo.MarkPeerShareResourceApplied(item.ShareID, item.Kind, item.OriginalName)
|
|
}
|
|
}
|
|
legacyBase := item.LegacyServiceBase
|
|
if legacyBase == "" {
|
|
for _, other := range items {
|
|
if other.ShareID == item.ShareID && normalizeForwardRuntimeServiceName(other.OriginalName) == base && other.LegacyServiceBase != "" {
|
|
legacyBase = other.LegacyServiceBase
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if legacyBase != "" && item.ReleaseLegacyFamily {
|
|
share, loadErr := h.repo.GetPeerShare(item.ShareID)
|
|
if loadErr != nil {
|
|
return result, loadErr
|
|
}
|
|
if share == nil {
|
|
return result, fmt.Errorf("share ownership missing")
|
|
}
|
|
if _, checkErr := h.peerShareLegacyServiceNames(share, legacyBase); checkErr != nil {
|
|
return result, checkErr
|
|
}
|
|
if _, deleteErr := h.sendNodeCommand(item.NodeID, "DeleteService", map[string]interface{}{"services": buildForwardServiceDeleteNames([]string{legacyBase})}, false, true); deleteErr != nil {
|
|
return result, deleteErr
|
|
}
|
|
}
|
|
if legacyBase != "" && !item.ReleaseLegacyFamily {
|
|
return result, h.repo.MarkPeerShareResourceApplied(item.ShareID, item.Kind, item.OriginalName)
|
|
}
|
|
runtimes, listErr := h.repo.ListActivePeerShareRuntimesByShareID(item.ShareID)
|
|
if listErr != nil {
|
|
return result, listErr
|
|
}
|
|
return result, h.repo.WithPeerShareResourceTransaction(func(tx *repo.Repository) error {
|
|
for _, runtime := range runtimes {
|
|
sid, original, scoped := parsePeerShareServiceName(runtime.ServiceName)
|
|
ownedScoped := scoped && sid == item.ShareID && normalizeForwardRuntimeServiceName(original) == base
|
|
ownedLegacy := !scoped && runtime.Role == "forward" && item.ReleaseLegacyFamily && legacyBase != "" && normalizeForwardRuntimeServiceName(runtime.ServiceName) == legacyBase
|
|
if ownedScoped || ownedLegacy {
|
|
if err = tx.CompletePeerShareRuntimeRelease(runtime.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if legacyBase != "" && item.ReleaseLegacyFamily {
|
|
if err := tx.ClearPeerShareResourceLegacyFamily(item.ShareID, legacyBase); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.MarkPeerShareResourceApplied(item.ShareID, item.Kind, item.OriginalName)
|
|
})
|
|
}
|
|
return result, h.repo.MarkPeerShareResourceApplied(item.ShareID, item.Kind, item.OriginalName)
|
|
}
|
|
|
|
func orderPeerShareResources(items []repo.PeerShareResource) {
|
|
rank := func(item repo.PeerShareResource) int {
|
|
if item.DesiredState == "deleted" {
|
|
if item.Kind == "service" {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
if item.Kind == "service" {
|
|
return 4
|
|
}
|
|
if item.Kind == "chain" {
|
|
return 3
|
|
}
|
|
return 2
|
|
}
|
|
sort.SliceStable(items, func(i, j int) bool { return rank(items[i]) < rank(items[j]) })
|
|
}
|
|
|
|
func (h *Handler) reconcilePeerShareResourcesOnNode(nodeID int64) error {
|
|
return h.reconcilePeerShareResources(nodeID, false)
|
|
}
|
|
func (h *Handler) retryPendingPeerShareResourcesOnNode(nodeID int64) error {
|
|
return h.reconcilePeerShareResources(nodeID, true)
|
|
}
|
|
func (h *Handler) reconcilePeerShareResources(nodeID int64, pendingOnly bool) error {
|
|
h.peerResourceMu.Lock()
|
|
defer h.peerResourceMu.Unlock()
|
|
items, err := h.repo.ListPeerShareResourcesByNode(nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtimes, err := h.repo.ListActivePeerShareRuntimesByNode(nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pending := map[string]bool{}
|
|
for _, runtime := range runtimes {
|
|
if runtime.ReleasePending != 0 {
|
|
sid, name, ok := parsePeerShareServiceName(runtime.ServiceName)
|
|
if ok {
|
|
pending[fmt.Sprintf("%d:%s", sid, normalizeForwardRuntimeServiceName(name))] = true
|
|
}
|
|
}
|
|
}
|
|
groups := map[int64][]repo.PeerShareResource{}
|
|
var ids []int64
|
|
for _, item := range items {
|
|
if _, ok := groups[item.ShareID]; !ok {
|
|
ids = append(ids, item.ShareID)
|
|
}
|
|
groups[item.ShareID] = append(groups[item.ShareID], item)
|
|
}
|
|
var failures []error
|
|
for _, shareID := range ids {
|
|
share, err := h.repo.GetPeerShare(shareID)
|
|
if err != nil {
|
|
failures = append(failures, err)
|
|
continue
|
|
}
|
|
expired := share == nil || share.IsActive != 1 || (share.ExpiryTime > 0 && share.ExpiryTime <= time.Now().UnixMilli()) || isPeerShareFlowExceeded(share)
|
|
group := groups[shareID]
|
|
var changed []repo.PeerShareResource
|
|
for i := range group {
|
|
item := &group[i]
|
|
if (expired || (item.Kind == "service" && pending[fmt.Sprintf("%d:%s", item.ShareID, normalizeForwardRuntimeServiceName(item.OriginalName))])) && (item.DesiredState != "deleted" || item.Applied == 0 || item.LegacyServiceBase != "") {
|
|
item.DesiredState = "deleted"
|
|
item.ReleaseLegacyFamily = true
|
|
item.Applied = 0
|
|
changed = append(changed, *item)
|
|
}
|
|
}
|
|
if err := h.repo.SavePeerShareResources(changed); err != nil {
|
|
failures = append(failures, err)
|
|
continue
|
|
}
|
|
orderPeerShareResources(group)
|
|
dependencyFailed := false
|
|
deletionFailed := false
|
|
for _, item := range group {
|
|
if item.Applied == 1 && (pendingOnly || item.DesiredState == "deleted") {
|
|
continue
|
|
}
|
|
if dependencyFailed && item.Kind == "service" && item.DesiredState != "deleted" {
|
|
continue
|
|
}
|
|
if deletionFailed && item.Kind != "service" && item.DesiredState == "deleted" {
|
|
continue
|
|
}
|
|
if _, err := h.applyPeerShareResource(item); err != nil {
|
|
failures = append(failures, fmt.Errorf("share %d %s %s: %w", item.ShareID, item.Kind, item.OriginalName, err))
|
|
if item.Kind != "service" {
|
|
dependencyFailed = true
|
|
}
|
|
if item.DesiredState == "deleted" {
|
|
deletionFailed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errors.Join(failures...)
|
|
}
|
|
|
|
func (h *Handler) releasePeerShareResources(shareID int64) error {
|
|
h.peerResourceMu.Lock()
|
|
defer h.peerResourceMu.Unlock()
|
|
share, err := h.repo.GetPeerShare(shareID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if share == nil {
|
|
return nil
|
|
}
|
|
all, err := h.repo.ListPeerShareResourcesByNode(share.NodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var items []repo.PeerShareResource
|
|
for _, item := range all {
|
|
if item.ShareID == shareID && !(item.DesiredState == "deleted" && item.Applied == 1 && item.LegacyServiceBase == "") {
|
|
item.DesiredState = "deleted"
|
|
item.ReleaseLegacyFamily = true
|
|
item.Applied = 0
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
if err = h.repo.SavePeerShareResources(items); err != nil {
|
|
return err
|
|
}
|
|
orderPeerShareResources(items)
|
|
for _, item := range items {
|
|
if _, err = h.applyPeerShareResource(item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A legacy name is eligible for migration only when its runtime registration
|
|
// proves unique ownership. An identically numbered local forward is ambiguous.
|
|
func (h *Handler) peerShareLegacyServiceNames(share *repo.PeerShare, original string) ([]string, error) {
|
|
base := normalizeForwardRuntimeServiceName(original)
|
|
runtimes, err := h.repo.ListActiveForwardPeerShareRuntimesByNodeAndServiceName(share.NodeID, base)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
owned := false
|
|
for _, runtime := range runtimes {
|
|
if runtime.ShareID != share.ID {
|
|
return nil, fmt.Errorf("legacy resource %q has ambiguous ownership", original)
|
|
}
|
|
if runtime.ReleasePending != 0 {
|
|
return nil, fmt.Errorf("runtime release is pending")
|
|
}
|
|
owned = true
|
|
}
|
|
if !owned {
|
|
resources, err := h.repo.ListPeerShareResourcesByNode(share.NodeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, resource := range resources {
|
|
if resource.ShareID == share.ID && resource.LegacyServiceBase == base {
|
|
owned = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !owned {
|
|
return nil, nil
|
|
}
|
|
if id, _, _, ok := parseFlowServiceIDs(base); ok {
|
|
local, err := h.repo.GetForwardRecord(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if local != nil {
|
|
return nil, fmt.Errorf("legacy resource %q collides with a local forward", original)
|
|
}
|
|
}
|
|
// Only delete the requested transport. Other variants remain registered to
|
|
// their legacy runtime until they are independently migrated.
|
|
return []string{original}, nil
|
|
}
|
|
|
|
// releasePeerShareForwardRuntimeResources removes exact persisted transport
|
|
// names belonging to a single forward reservation, retaining failed tombstones.
|
|
func (h *Handler) releasePeerShareForwardRuntimeResources(runtime *repo.PeerShareRuntime) error {
|
|
h.peerResourceMu.Lock()
|
|
defer h.peerResourceMu.Unlock()
|
|
shareID, original, ok := parsePeerShareServiceName(runtime.ServiceName)
|
|
if !ok || shareID != runtime.ShareID {
|
|
return fmt.Errorf("runtime is not a scoped shared forward")
|
|
}
|
|
all, err := h.repo.ListPeerShareResourcesByNode(runtime.NodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var items []repo.PeerShareResource
|
|
for _, item := range all {
|
|
if item.ShareID == runtime.ShareID && item.Kind == "service" && normalizeForwardRuntimeServiceName(item.OriginalName) == normalizeForwardRuntimeServiceName(original) {
|
|
item.DesiredState = "deleted"
|
|
item.ReleaseLegacyFamily = true
|
|
item.Applied = 0
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
if len(items) == 0 {
|
|
return fmt.Errorf("shared forward resource ownership is missing")
|
|
}
|
|
if err = h.repo.SavePeerShareResources(items); err != nil {
|
|
return err
|
|
}
|
|
for _, item := range items {
|
|
if _, err = h.applyPeerShareResource(item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Registry kinds without an owned command API cannot be referenced by peers.
|
|
func validatePeerResourceReferences(value interface{}, kind string) error {
|
|
var walk func(interface{}) error
|
|
walk = func(value interface{}) error {
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
for key, child := range v {
|
|
switch strings.ToLower(key) {
|
|
case "auther", "authers", "admission", "admissions", "bypass", "bypasses", "resolver", "hosts", "rlimiter", "logger", "loggers", "observer", "recorders", "hop", "sd":
|
|
if child != nil {
|
|
empty := false
|
|
switch x := child.(type) {
|
|
case string:
|
|
empty = strings.TrimSpace(x) == ""
|
|
case []interface{}:
|
|
empty = len(x) == 0
|
|
}
|
|
if !empty {
|
|
return fmt.Errorf("unsupported shared registry reference: %s", key)
|
|
}
|
|
}
|
|
case "forwarder":
|
|
if f, ok := child.(map[string]interface{}); ok {
|
|
for field, value := range f {
|
|
if strings.EqualFold(field, "name") && strings.TrimSpace(asString(value)) != "" {
|
|
return fmt.Errorf("named shared forwarder references are unsupported")
|
|
}
|
|
}
|
|
}
|
|
case "hops":
|
|
for _, hop := range asMapSlice(child) {
|
|
if _, ok := hop["nodes"]; !ok {
|
|
return fmt.Errorf("shared chains require inline hop nodes")
|
|
}
|
|
for _, loader := range []string{"file", "redis", "http", "plugin"} {
|
|
if hop[loader] != nil {
|
|
return fmt.Errorf("shared hop loaders are unsupported")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case []interface{}:
|
|
for _, child := range v {
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if kind == "limiter" || kind == "climiter" {
|
|
if config, ok := value.(map[string]interface{}); ok {
|
|
for _, loader := range []string{"file", "redis", "http", "plugin"} {
|
|
if config[loader] != nil {
|
|
return fmt.Errorf("shared limiter loaders are unsupported")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return walk(value)
|
|
}
|