From eb7e0fbbfa1ed844bcdac02380caeca93b184353 Mon Sep 17 00:00:00 2001 From: warku123 Date: Fri, 22 May 2026 15:29:04 +0800 Subject: [PATCH 1/2] fix: attach shared docker network in network add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit network create wires every node's compose to the shared trond- network so siblings can resolve each other by container name. network add skipped this step — the new node landed only on its per-compose bridge and stayed at 0 peers, and any service on trond- (e.g. the monitoring stack) could not reach it either. Append trond- to node.Networks before render, mirroring the loop already used by network create. --- cmd/network/add.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/network/add.go b/cmd/network/add.go index c093cbfd..e0b4b398 100644 --- a/cmd/network/add.go +++ b/cmd/network/add.go @@ -3,6 +3,7 @@ package network import ( "fmt" "os" + "slices" "strings" "time" @@ -107,6 +108,14 @@ func runAdd(cmd *cobra.Command, args []string) error { } }() + // Auto-attach the shared docker network so the new node can resolve + // sibling container names for P2P peering. Without this it lands only + // on its per-compose bridge and stays at 0 peers. + sharedNet := "trond-" + addNetworkName + if !slices.Contains(node.Networks, sharedNet) { + node.Networks = append(node.Networks, sharedNet) + } + templateDir := findTemplatesDir() hocon, err := render.RenderHOCON(templateDir, parsed, node) if err != nil { From ebeec9ce93967ef0c8c881cb289a58e99f3b38ea Mon Sep 17 00:00:00 2001 From: warku123 Date: Fri, 22 May 2026 17:35:16 +0800 Subject: [PATCH 2/2] fix: auto-wire active_peers for newly added node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that the new node attaches to the shared trond- docker network (previous commit), it can resolve sibling container names — but it still has no active_peers entry, so it doesn't know which peers to dial. java-tron with discovery off (private network default) only connects to nodes listed in node.active, so without this the new node sits silent on the network despite being reachable. Populate the new node's active_peers from existing entries in state, mirroring autoWireActivePeers in network create. Because P2P connections are bidirectional once established, we only update the new node — existing nodes accept the incoming connection and broadcast back without needing a config rewrite or restart. state.ManagedNode gains a P2PPort field so add can read each sibling's listen port from state instead of re-parsing intent files (which add no longer has access to for nodes deployed earlier). network create now persists it too. Legacy state entries that predate the field get P2PPort=0 and are skipped from the active list — the field comment documents the same fallback the existing HTTPPort / GRPCPort fields use. --- cmd/network/add.go | 24 ++++++++++++++++++++++++ cmd/network/create.go | 1 + internal/state/types.go | 4 ++++ 3 files changed, 29 insertions(+) diff --git a/cmd/network/add.go b/cmd/network/add.go index e0b4b398..51aec890 100644 --- a/cmd/network/add.go +++ b/cmd/network/add.go @@ -116,6 +116,29 @@ func runAdd(cmd *cobra.Command, args []string) error { node.Networks = append(node.Networks, sharedNet) } + // Auto-populate active_peers from existing nodes in the network so the + // new node can dial into the running mesh. P2P connections are + // bidirectional once established, so we only update the new node — + // no need to reconfigure (and restart) existing siblings, they'll + // accept the incoming connection. Skip nodes whose P2PPort is zero + // (legacy state predating the field) and skip when the user + // explicitly supplied active_peers in the intent. + if node.NetworkOverrides.ActivePeers == nil { + var existingPeers []string + for _, n := range deployState.Nodes { + if !strings.HasPrefix(n.Name, prefix) { + continue + } + if n.P2PPort == 0 { + continue + } + existingPeers = append(existingPeers, fmt.Sprintf("%s:%d", n.Name, n.P2PPort)) + } + if len(existingPeers) > 0 { + node.NetworkOverrides.ActivePeers = &existingPeers + } + } + templateDir := findTemplatesDir() hocon, err := render.RenderHOCON(templateDir, parsed, node) if err != nil { @@ -158,6 +181,7 @@ func runAdd(cmd *cobra.Command, args []string) error { LastApplied: time.Now().UTC(), HTTPPort: node.Ports.HTTP, GRPCPort: node.Ports.GRPC, + P2PPort: node.Ports.P2P, InstallPath: node.InstallPath, Labels: node.Labels, }) diff --git a/cmd/network/create.go b/cmd/network/create.go index c29d58f9..163cd143 100644 --- a/cmd/network/create.go +++ b/cmd/network/create.go @@ -154,6 +154,7 @@ func runCreate(cmd *cobra.Command, args []string) error { LastApplied: time.Now().UTC(), HTTPPort: node.Ports.HTTP, GRPCPort: node.Ports.GRPC, + P2PPort: node.Ports.P2P, Labels: node.Labels, } store.UpsertNode(deployState, mn) diff --git a/internal/state/types.go b/internal/state/types.go index d5ff27bf..a387d778 100644 --- a/internal/state/types.go +++ b/internal/state/types.go @@ -22,6 +22,10 @@ type ManagedNode struct { // fields — callers must fall back to defaults when zero. HTTPPort int `json:"http_port,omitempty"` GRPCPort int `json:"grpc_port,omitempty"` + // P2PPort is the listen.port a sibling can dial to peer with this node. + // `network add` reads it from every existing entry to populate the new + // node's active_peers so it can immediately join the P2P mesh. + P2PPort int `json:"p2p_port,omitempty"` // Labels mirror intent.NodeSpec.Labels and survive across CLI sessions // so test harnesses can filter via `trond list --label key=value` // without touching the original intent file.