Skip to content

Commit 01b951b

Browse files
authored
migration(nodes): from cld to cldf (#249)
Migrates nodes logic from cld pkg/migrations/nodes. https://smartcontract-it.atlassian.net/browse/CLD-475
1 parent ecbd906 commit 01b951b

File tree

3 files changed

+293
-0
lines changed

3 files changed

+293
-0
lines changed

.changeset/better-singers-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
refactor: migrates nodes management logic from cld

engine/cld/nodes/nodes.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package migrations
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"maps"
7+
"os"
8+
"slices"
9+
)
10+
11+
// Nodes represents a set of node IDs. This is used to keep track of which nodes
12+
// are available for a given domain. This struct is serialized to JSON and
13+
// stored in each domain directory in the `nodes.json` file
14+
type Nodes struct {
15+
Nodes map[string]struct{} `json:"nodes"`
16+
}
17+
18+
// NewNodes creates a new Nodes struct with the given node IDs.
19+
func NewNodes(nodeIDs []string) *Nodes {
20+
nodes := &Nodes{
21+
Nodes: make(map[string]struct{}),
22+
}
23+
24+
for _, id := range nodeIDs {
25+
nodes.Nodes[id] = struct{}{}
26+
}
27+
28+
return nodes
29+
}
30+
31+
// Add adds a node ID to the set. It is No-op if already exists.
32+
func (n *Nodes) Add(nodeID string) {
33+
n.Nodes[nodeID] = struct{}{}
34+
}
35+
36+
// Keys returns the node IDs as a slice.
37+
func (n *Nodes) Keys() []string {
38+
return slices.Collect(maps.Keys(n.Nodes))
39+
}
40+
41+
// LoadNodesFromFile loads nodes from a JSON file at the specified path.
42+
func LoadNodesFromFile(filePath string) (*Nodes, error) {
43+
data, err := os.ReadFile(filePath)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to read file: %w", err)
46+
}
47+
48+
var nodes Nodes
49+
if err = json.Unmarshal(data, &nodes); err != nil {
50+
return nil, fmt.Errorf("failed to unmarshal JSON for nodes: %w", err)
51+
}
52+
53+
return &nodes, nil
54+
}
55+
56+
// SaveToFile saves the nodes to a JSON file at the specified path.
57+
// If the file already exists, the new nodes will be merged with the existing nodes.
58+
// If a node with the same id already exists, it will be overwritten.
59+
func (n *Nodes) SaveToFile(filePath string) error {
60+
_, err := os.Stat(filePath)
61+
62+
// if the file already exists, load the existing nodes and merge with the new nodes
63+
// if the node already exists, overwrite it
64+
if err == nil {
65+
existingNodes, err2 := LoadNodesFromFile(filePath)
66+
if err2 != nil {
67+
return err2
68+
}
69+
70+
// Add existing nodes to current nodes (current nodes take precedence)
71+
for _, enodeKey := range existingNodes.Keys() {
72+
n.Add(enodeKey)
73+
}
74+
}
75+
76+
data, err := json.MarshalIndent(n, "", " ")
77+
if err != nil {
78+
return fmt.Errorf("failed to marshal JSON: %w", err)
79+
}
80+
81+
return os.WriteFile(filePath, data, 0600)
82+
}
83+
84+
// SaveNodeIDsToFile creates a new Nodes struct from node IDs and saves it to the specified path.
85+
func SaveNodeIDsToFile(filePath string, nodeIDs []string) error {
86+
nodes := NewNodes(nodeIDs)
87+
return nodes.SaveToFile(filePath)
88+
}

engine/cld/nodes/nodes_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package migrations
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_Nodes_Add(t *testing.T) {
13+
t.Parallel()
14+
15+
// Check initial
16+
got := NewNodes([]string{"a", "b"})
17+
require.Len(t, got.Nodes, 2)
18+
require.Contains(t, got.Nodes, "a")
19+
require.Contains(t, got.Nodes, "b")
20+
21+
// Add a new node
22+
got.Add("c")
23+
require.Len(t, got.Nodes, 3)
24+
require.Contains(t, got.Nodes, "c")
25+
26+
// Add an existing node should not change anything
27+
got.Add("c")
28+
require.Len(t, got.Nodes, 3)
29+
require.Contains(t, got.Nodes, "c")
30+
}
31+
32+
func Test_Nodes_Keys(t *testing.T) {
33+
t.Parallel()
34+
35+
got := NewNodes([]string{"a", "b", "c"})
36+
require.ElementsMatch(t, got.Keys(), []string{"a", "b", "c"})
37+
}
38+
39+
func Test_LoadNodesFromFile(t *testing.T) {
40+
t.Parallel()
41+
42+
t.Run("loads nodes from valid JSON file", func(t *testing.T) {
43+
t.Parallel()
44+
45+
// Create a temporary file with valid nodes JSON
46+
tempDir := t.TempDir()
47+
filePath := filepath.Join(tempDir, "nodes.json")
48+
49+
expectedNodes := &Nodes{
50+
Nodes: map[string]struct{}{
51+
"node1": {},
52+
"node2": {},
53+
"node3": {},
54+
},
55+
}
56+
57+
data, err := json.MarshalIndent(expectedNodes, "", " ")
58+
require.NoError(t, err)
59+
require.NoError(t, os.WriteFile(filePath, data, 0600))
60+
61+
// Load nodes from file
62+
nodes, err := LoadNodesFromFile(filePath)
63+
require.NoError(t, err)
64+
require.NotNil(t, nodes)
65+
require.ElementsMatch(t, nodes.Keys(), []string{"node1", "node2", "node3"})
66+
})
67+
68+
t.Run("returns error for non-existent file", func(t *testing.T) {
69+
t.Parallel()
70+
71+
nodes, err := LoadNodesFromFile("non-existent-file.json")
72+
require.Error(t, err)
73+
require.Nil(t, nodes)
74+
require.Contains(t, err.Error(), "failed to read file")
75+
})
76+
77+
t.Run("returns error for invalid JSON", func(t *testing.T) {
78+
t.Parallel()
79+
80+
tempDir := t.TempDir()
81+
filePath := filepath.Join(tempDir, "invalid.json")
82+
83+
// Write invalid JSON
84+
require.NoError(t, os.WriteFile(filePath, []byte("invalid json"), 0600))
85+
86+
nodes, err := LoadNodesFromFile(filePath)
87+
require.Error(t, err)
88+
require.Nil(t, nodes)
89+
require.Contains(t, err.Error(), "failed to unmarshal JSON for nodes")
90+
})
91+
}
92+
93+
func Test_Nodes_SaveToFile(t *testing.T) {
94+
t.Parallel()
95+
96+
t.Run("saves nodes to new file", func(t *testing.T) {
97+
t.Parallel()
98+
99+
tempDir := t.TempDir()
100+
filePath := filepath.Join(tempDir, "nodes.json")
101+
102+
nodes := NewNodes([]string{"node1", "node2", "node3"})
103+
104+
err := nodes.SaveToFile(filePath)
105+
require.NoError(t, err)
106+
107+
// Verify file was created and contains correct data
108+
data, err := os.ReadFile(filePath)
109+
require.NoError(t, err)
110+
111+
var savedNodes Nodes
112+
require.NoError(t, json.Unmarshal(data, &savedNodes))
113+
require.ElementsMatch(t, savedNodes.Keys(), []string{"node1", "node2", "node3"})
114+
})
115+
116+
t.Run("merges with existing file", func(t *testing.T) {
117+
t.Parallel()
118+
119+
tempDir := t.TempDir()
120+
filePath := filepath.Join(tempDir, "nodes.json")
121+
122+
// Create existing nodes file
123+
existingNodes := NewNodes([]string{"existing1", "existing2"})
124+
err := existingNodes.SaveToFile(filePath)
125+
require.NoError(t, err)
126+
127+
// Save new nodes (should merge with existing)
128+
newNodes := NewNodes([]string{"new1", "new2", "existing1"}) // existing1 should not be duplicated
129+
err = newNodes.SaveToFile(filePath)
130+
require.NoError(t, err)
131+
132+
// Verify merged result
133+
loadedNodes, err := LoadNodesFromFile(filePath)
134+
require.NoError(t, err)
135+
require.ElementsMatch(t, loadedNodes.Keys(), []string{"existing1", "existing2", "new1", "new2"})
136+
})
137+
138+
t.Run("handles file permission errors gracefully", func(t *testing.T) {
139+
t.Parallel()
140+
141+
// Try to save to a directory that doesn't exist
142+
nodes := NewNodes([]string{"node1"})
143+
err := nodes.SaveToFile("/non-existent-dir/nodes.json")
144+
require.Error(t, err)
145+
})
146+
}
147+
148+
func Test_SaveNodeIDsToFile(t *testing.T) {
149+
t.Parallel()
150+
151+
t.Run("creates nodes and saves to file", func(t *testing.T) {
152+
t.Parallel()
153+
154+
tempDir := t.TempDir()
155+
filePath := filepath.Join(tempDir, "nodes.json")
156+
157+
nodeIDs := []string{"node1", "node2", "node3"}
158+
err := SaveNodeIDsToFile(filePath, nodeIDs)
159+
require.NoError(t, err)
160+
161+
// Verify file was created and contains correct data
162+
loadedNodes, err := LoadNodesFromFile(filePath)
163+
require.NoError(t, err)
164+
require.ElementsMatch(t, loadedNodes.Keys(), nodeIDs)
165+
})
166+
167+
t.Run("merges with existing file", func(t *testing.T) {
168+
t.Parallel()
169+
170+
tempDir := t.TempDir()
171+
filePath := filepath.Join(tempDir, "nodes.json")
172+
173+
// Create existing file
174+
err := SaveNodeIDsToFile(filePath, []string{"existing1", "existing2"})
175+
require.NoError(t, err)
176+
177+
// Save new nodes
178+
err = SaveNodeIDsToFile(filePath, []string{"new1", "new2", "existing1"})
179+
require.NoError(t, err)
180+
181+
// Verify merged result
182+
loadedNodes, err := LoadNodesFromFile(filePath)
183+
require.NoError(t, err)
184+
require.ElementsMatch(t, loadedNodes.Keys(), []string{"existing1", "existing2", "new1", "new2"})
185+
})
186+
187+
t.Run("handles empty node IDs", func(t *testing.T) {
188+
t.Parallel()
189+
190+
tempDir := t.TempDir()
191+
filePath := filepath.Join(tempDir, "nodes.json")
192+
193+
err := SaveNodeIDsToFile(filePath, []string{})
194+
require.NoError(t, err)
195+
196+
loadedNodes, err := LoadNodesFromFile(filePath)
197+
require.NoError(t, err)
198+
require.Empty(t, loadedNodes.Keys())
199+
})
200+
}

0 commit comments

Comments
 (0)