Files
nginx-ui/api/cluster/sync.go
0xJacky 9f70d47b35 feat(cluster): unify multi-node configuration synchronization
Deployment to other nodes was file by file, manual, and always validated
against the local Nginx. One synchronization engine now backs every path.

- Directory deployment: a directory carries its own deployment targets and
  every file below it inherits them, so a whole tree replicates in one
  request per node (#1559).
- Node sync: one action pushes all configurations, sites and streams to the
  selected nodes, which is what a freshly added node needs (#1484).
- Namespace replication: the namespace record is mirrored to its member nodes
  and synced sites and streams carry the namespace name, so every node groups
  them identically (#1744).
- Automatic sync: a namespace can switch to the auto strategy with its own
  interval and a cron job re-pushes its content (#1582).
- deploy_mode=remote no longer touches the local Nginx: no sites-enabled
  symlink, no local nginx -t and no local reload. The deployment intent moves
  to the database, and moving a site into a remote namespace detaches its
  leftover local symlink (#1505).

The receiving side gains a batch config endpoint that writes every file and
reloads once, with a per-file fallback for nodes that predate it. Disabling a
site or stream became idempotent so converging a node reports no spurious
failures, renaming only tests and reloads when the enabled tree actually
changed, and stream delete now removes the stream record instead of a site one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:07:34 +08:00

92 lines
2.4 KiB
Go

package cluster
import (
"net/http"
"github.com/0xJacky/Nginx-UI/internal/clustersync"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"github.com/uozi-tech/cosy"
"gorm.io/gen/field"
)
// SyncNodes pushes the local configurations, sites and streams to the selected
// nodes. A freshly added node is brought up to date with a single request.
func SyncNodes(c *gin.Context) {
var json struct {
NodeIDs []uint64 `json:"node_ids" binding:"required"`
Configs *bool `json:"configs"`
Sites *bool `json:"sites"`
Streams *bool `json:"streams"`
Overwrite *bool `json:"overwrite"`
}
if !cosy.BindAndValid(c, &json) {
return
}
scope := clustersync.FullScope()
if json.Configs != nil {
scope.Configs = *json.Configs
}
if json.Sites != nil {
scope.Sites = *json.Sites
}
if json.Streams != nil {
scope.Streams = *json.Streams
}
if json.Overwrite != nil {
scope.Overwrite = *json.Overwrite
}
summary, err := clustersync.SyncNodes(c, json.NodeIDs, scope)
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, summary)
}
// SyncNamespace replicates a namespace and all of its sites and streams to every
// node of that namespace, so each node ends up with the same content.
func SyncNamespace(c *gin.Context) {
summary, err := clustersync.SyncNamespace(c, cast.ToUint64(c.Param("id")))
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, summary)
}
// UpsertNamespace is the receiving end of namespace replication. It creates the
// namespace when the node does not know it yet and keeps its local node
// membership and deploy mode untouched.
func UpsertNamespace(c *gin.Context) {
var json struct {
Name string `json:"name" binding:"required"`
PostSyncAction string `json:"post_sync_action" binding:"omitempty,oneof=none reload_nginx"`
UpstreamTestType string `json:"upstream_test_type" binding:"omitempty,oneof=local remote mirror"`
}
if !cosy.BindAndValid(c, &json) {
return
}
n := query.Namespace
namespace, err := n.Assign(field.Attrs(&model.Namespace{
Name: json.Name,
PostSyncAction: json.PostSyncAction,
UpstreamTestType: json.UpstreamTestType,
})).Where(n.Name.Eq(json.Name)).FirstOrCreate()
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, namespace)
}