P8(2): inbound node reassignment (move) with backend re-homing
An inbound's 部署到 node can now be changed after creation; the row, clients, client_traffics and subscription output stay untouched (same inbound id throughout). Contract: the update payload's nodeId counts only when the key was explicitly sent — the controller probes the raw JSON body and sets Inbound.NodeIDProvided; the wire adapter now always emits nodeId (null = local) so clearing the picker is an explicit move-to-local, while callers that omit nodeId keep the stored assignment (legacy behaviour). UpdateInbound resolves the target before the port-conflict check (so conflicts are scoped to the target node), rejects moves to a missing node, re-derives auto tags under the target node's prefix, validates the tag against the target node's namespace, and marks BOTH nodes dirty: the source node's next reconcile sweeps the old tag (no silent ghost — the flag persists until a reconcile succeeds, so an offline side just defers), the target's pushes the inbound. Agent-bound nodes converge via GetXrayConfigForAgent on the agent's next config poll. A local side has no reconcile loop, so it is applied directly post-commit (Del/Add on the local runtime, restart fallback). Frontend: the deploy-to Select is no longer disabled in edit mode (protocol stays locked); candidate rules unchanged (online nodes only). Tests: panel->panel move with reconcile sweep/push assertions, auto-tag prefix regeneration, agent-config flip in both directions, omitted-nodeId legacy contract, same-node no-op, missing-node rejection, node->local move.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -222,6 +224,27 @@ func (a *InboundController) bulkDelInbounds(c *gin.Context) {
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
// requestHasJSONKey reports whether the request body is a JSON object that
|
||||
// explicitly carries key — present with a null value counts. The body is
|
||||
// restored so the normal bind can read it again. Non-JSON bodies (form
|
||||
// posts) report false for every key.
|
||||
func requestHasJSONKey(c *gin.Context, key string) bool {
|
||||
if c.Request == nil || c.Request.Body == nil {
|
||||
return false
|
||||
}
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil || len(body) == 0 {
|
||||
return false
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
var probe map[string]json.RawMessage
|
||||
if json.Unmarshal(body, &probe) != nil {
|
||||
return false
|
||||
}
|
||||
_, ok := probe[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// updateInbound updates an existing inbound configuration.
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
@@ -232,13 +255,16 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{
|
||||
Id: id,
|
||||
}
|
||||
// An explicitly sent nodeId (even null) is a (re)assignment request —
|
||||
// UpdateInbound migrates the inbound when it differs from the stored
|
||||
// assignment. An omitted nodeId keeps the stored assignment, so detect
|
||||
// the key's presence on the raw body before binding consumes it.
|
||||
nodeIDProvided := requestHasJSONKey(c, "nodeId")
|
||||
if !middleware.BindAndValidateInto(c, inbound) {
|
||||
return
|
||||
}
|
||||
// Same NodeID=0 → nil normalisation as addInbound. UpdateInbound
|
||||
// loads the existing row's NodeID from DB anyway (Phase 1 doesn't
|
||||
// support migrating an inbound between nodes), but normalising here
|
||||
// keeps the wire shape consistent.
|
||||
inbound.NodeIDProvided = nodeIDProvided
|
||||
// Same NodeID=0 → nil normalisation as addInbound.
|
||||
if inbound.NodeID != nil && *inbound.NodeID == 0 {
|
||||
inbound.NodeID = nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user