Skip to content

Commit bd2d683

Browse files
committed
feat(dvb): add diff command to show YAML vs current state differences
1 parent 0135d7f commit bd2d683

2 files changed

Lines changed: 350 additions & 0 deletions

File tree

cmd/dvb/diff.go

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
// cmd/dvb/diff.go
2+
package main
3+
4+
import (
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
v1 "github.com/altuslabsxyz/devnet-builder/api/proto/gen/v1"
12+
"github.com/altuslabsxyz/devnet-builder/internal/config"
13+
"github.com/fatih/color"
14+
"github.com/spf13/cobra"
15+
"gopkg.in/yaml.v3"
16+
)
17+
18+
func newDiffCmd() *cobra.Command {
19+
var (
20+
filePath string
21+
output string
22+
)
23+
24+
cmd := &cobra.Command{
25+
Use: "diff",
26+
Short: "Show differences between YAML config and current state",
27+
Long: `Show what changes would be made by applying a YAML configuration.
28+
29+
The diff command compares the desired state (from YAML) with the current state
30+
and displays the differences without making any changes. This is useful for
31+
previewing what 'dvb apply' would do.
32+
33+
Examples:
34+
# Show diff for a single file
35+
dvb diff -f devnet.yaml
36+
37+
# Show diff for all YAML files in a directory
38+
dvb diff -f ./devnets/
39+
40+
# Output in JSON format
41+
dvb diff -f devnet.yaml -o json`,
42+
RunE: func(cmd *cobra.Command, args []string) error {
43+
if daemonClient == nil {
44+
return fmt.Errorf("daemon not running - start with: devnetd")
45+
}
46+
47+
if filePath == "" {
48+
return fmt.Errorf("--file/-f is required")
49+
}
50+
51+
// Load YAML files
52+
devnets, err := loadDiffYAMLFiles(filePath)
53+
if err != nil {
54+
return fmt.Errorf("failed to load YAML: %w", err)
55+
}
56+
57+
if len(devnets) == 0 {
58+
return fmt.Errorf("no valid devnet configurations found in %s", filePath)
59+
}
60+
61+
// Process each devnet
62+
hasChanges := false
63+
for _, yamlDevnet := range devnets {
64+
changed, err := showDiff(cmd, yamlDevnet, output)
65+
if err != nil {
66+
return err
67+
}
68+
if changed {
69+
hasChanges = true
70+
}
71+
}
72+
73+
if !hasChanges {
74+
color.Green("\n✓ No changes detected")
75+
}
76+
77+
return nil
78+
},
79+
}
80+
81+
cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to YAML file or directory (required)")
82+
cmd.Flags().StringVarP(&output, "output", "o", "text", "Output format: text, json")
83+
84+
return cmd
85+
}
86+
87+
// loadDiffYAMLFiles loads devnet configurations from a file or directory
88+
func loadDiffYAMLFiles(path string) ([]*config.YAMLDevnet, error) {
89+
info, err := os.Stat(path)
90+
if err != nil {
91+
return nil, fmt.Errorf("cannot access %s: %w", path, err)
92+
}
93+
94+
var files []string
95+
if info.IsDir() {
96+
entries, err := os.ReadDir(path)
97+
if err != nil {
98+
return nil, err
99+
}
100+
for _, entry := range entries {
101+
if entry.IsDir() {
102+
continue
103+
}
104+
name := entry.Name()
105+
if strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") {
106+
files = append(files, filepath.Join(path, name))
107+
}
108+
}
109+
} else {
110+
files = []string{path}
111+
}
112+
113+
var devnets []*config.YAMLDevnet
114+
for _, file := range files {
115+
parsed, err := parseDiffYAMLFile(file)
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to parse %s: %w", file, err)
118+
}
119+
devnets = append(devnets, parsed...)
120+
}
121+
122+
return devnets, nil
123+
}
124+
125+
// parseDiffYAMLFile parses a YAML file that may contain multiple documents
126+
func parseDiffYAMLFile(path string) ([]*config.YAMLDevnet, error) {
127+
f, err := os.Open(path)
128+
if err != nil {
129+
return nil, err
130+
}
131+
defer f.Close()
132+
133+
var devnets []*config.YAMLDevnet
134+
decoder := yaml.NewDecoder(f)
135+
136+
for docIndex := 0; ; docIndex++ {
137+
var doc config.YAMLDevnet
138+
err := decoder.Decode(&doc)
139+
if err == io.EOF {
140+
break
141+
}
142+
if err != nil {
143+
return nil, fmt.Errorf("document %d: %w", docIndex, err)
144+
}
145+
146+
if err := doc.Validate(); err != nil {
147+
return nil, fmt.Errorf("validation failed for document %d in %s: %w", docIndex, path, err)
148+
}
149+
150+
devnets = append(devnets, &doc)
151+
}
152+
153+
return devnets, nil
154+
}
155+
156+
// showDiff displays the differences for a single devnet
157+
func showDiff(cmd *cobra.Command, yamlDevnet *config.YAMLDevnet, output string) (bool, error) {
158+
ctx := cmd.Context()
159+
name := yamlDevnet.Metadata.Name
160+
161+
// Check if devnet exists
162+
existing, err := daemonClient.GetDevnet(ctx, name)
163+
exists := err == nil && existing != nil
164+
165+
if !exists {
166+
// Devnet doesn't exist - would be created
167+
return printCreateDiff(yamlDevnet, output)
168+
}
169+
170+
// Compare existing with desired
171+
return printUpdateDiff(yamlDevnet, existing, output)
172+
}
173+
174+
// printCreateDiff shows what would be created
175+
func printCreateDiff(yamlDevnet *config.YAMLDevnet, output string) (bool, error) {
176+
name := yamlDevnet.Metadata.Name
177+
178+
fmt.Printf("\n")
179+
color.Green("+ devnet/%s (create)", name)
180+
fmt.Printf("\n")
181+
182+
// Show spec details
183+
printFieldDiff("network", "", yamlDevnet.Spec.Network, true)
184+
if yamlDevnet.Spec.NetworkType != "" {
185+
printFieldDiff("networkType", "", yamlDevnet.Spec.NetworkType, true)
186+
}
187+
if yamlDevnet.Spec.NetworkVersion != "" {
188+
printFieldDiff("networkVersion", "", yamlDevnet.Spec.NetworkVersion, true)
189+
}
190+
191+
validators := yamlDevnet.Spec.Validators
192+
if validators == 0 {
193+
validators = 4
194+
}
195+
printFieldDiff("validators", "", fmt.Sprintf("%d", validators), true)
196+
197+
if yamlDevnet.Spec.FullNodes > 0 {
198+
printFieldDiff("fullNodes", "", fmt.Sprintf("%d", yamlDevnet.Spec.FullNodes), true)
199+
}
200+
201+
mode := yamlDevnet.Spec.Mode
202+
if mode == "" {
203+
mode = "docker"
204+
}
205+
printFieldDiff("mode", "", mode, true)
206+
207+
// Show nodes that would be created
208+
fmt.Printf("\n Nodes:\n")
209+
for i := 0; i < validators; i++ {
210+
color.Green(" + %s-%d (validator)", name, i)
211+
}
212+
for i := 0; i < yamlDevnet.Spec.FullNodes; i++ {
213+
color.Green(" + %s-full-%d (full)", name, i)
214+
}
215+
216+
// Show labels if present
217+
if len(yamlDevnet.Metadata.Labels) > 0 {
218+
fmt.Printf("\n Labels:\n")
219+
for k, v := range yamlDevnet.Metadata.Labels {
220+
color.Green(" + %s: %s", k, v)
221+
}
222+
}
223+
224+
return true, nil
225+
}
226+
227+
// printUpdateDiff shows what would change for an existing devnet
228+
func printUpdateDiff(yamlDevnet *config.YAMLDevnet, existing *v1.Devnet, output string) (bool, error) {
229+
name := yamlDevnet.Metadata.Name
230+
hasChanges := false
231+
232+
// Compare specs
233+
desiredSpec := diffYamlToProtoSpec(&yamlDevnet.Spec)
234+
existingSpec := existing.Spec
235+
236+
changes := []fieldChange{}
237+
238+
// Compare plugin/network
239+
if desiredSpec.Plugin != existingSpec.Plugin {
240+
changes = append(changes, fieldChange{"network", existingSpec.Plugin, desiredSpec.Plugin})
241+
}
242+
243+
// Compare network type
244+
if desiredSpec.NetworkType != existingSpec.NetworkType {
245+
changes = append(changes, fieldChange{"networkType", existingSpec.NetworkType, desiredSpec.NetworkType})
246+
}
247+
248+
// Compare validators
249+
if desiredSpec.Validators != existingSpec.Validators {
250+
changes = append(changes, fieldChange{"validators", fmt.Sprintf("%d", existingSpec.Validators), fmt.Sprintf("%d", desiredSpec.Validators)})
251+
}
252+
253+
// Compare full nodes
254+
if desiredSpec.FullNodes != existingSpec.FullNodes {
255+
changes = append(changes, fieldChange{"fullNodes", fmt.Sprintf("%d", existingSpec.FullNodes), fmt.Sprintf("%d", desiredSpec.FullNodes)})
256+
}
257+
258+
// Compare mode
259+
if desiredSpec.Mode != existingSpec.Mode {
260+
changes = append(changes, fieldChange{"mode", existingSpec.Mode, desiredSpec.Mode})
261+
}
262+
263+
// Compare SDK version
264+
if desiredSpec.SdkVersion != existingSpec.SdkVersion && desiredSpec.SdkVersion != "" {
265+
changes = append(changes, fieldChange{"sdkVersion", existingSpec.SdkVersion, desiredSpec.SdkVersion})
266+
}
267+
268+
if len(changes) == 0 {
269+
// No spec changes
270+
color.White(" devnet/%s (no changes)", name)
271+
return false, nil
272+
}
273+
274+
hasChanges = true
275+
fmt.Printf("\n")
276+
color.Yellow("~ devnet/%s (update)", name)
277+
fmt.Printf("\n")
278+
279+
for _, c := range changes {
280+
printFieldDiff(c.field, c.oldVal, c.newVal, false)
281+
}
282+
283+
// Show node changes if validator/fullnode count changed
284+
if desiredSpec.Validators != existingSpec.Validators || desiredSpec.FullNodes != existingSpec.FullNodes {
285+
fmt.Printf("\n Nodes:\n")
286+
287+
// Validator changes
288+
if desiredSpec.Validators > existingSpec.Validators {
289+
for i := existingSpec.Validators; i < desiredSpec.Validators; i++ {
290+
color.Green(" + %s-%d (validator)", name, i)
291+
}
292+
} else if desiredSpec.Validators < existingSpec.Validators {
293+
for i := desiredSpec.Validators; i < existingSpec.Validators; i++ {
294+
color.Red(" - %s-%d (validator)", name, i)
295+
}
296+
}
297+
298+
// Full node changes
299+
if desiredSpec.FullNodes > existingSpec.FullNodes {
300+
for i := existingSpec.FullNodes; i < desiredSpec.FullNodes; i++ {
301+
color.Green(" + %s-full-%d (full)", name, i)
302+
}
303+
} else if desiredSpec.FullNodes < existingSpec.FullNodes {
304+
for i := desiredSpec.FullNodes; i < existingSpec.FullNodes; i++ {
305+
color.Red(" - %s-full-%d (full)", name, i)
306+
}
307+
}
308+
}
309+
310+
return hasChanges, nil
311+
}
312+
313+
type fieldChange struct {
314+
field string
315+
oldVal string
316+
newVal string
317+
}
318+
319+
// printFieldDiff prints a single field diff
320+
func printFieldDiff(field, oldVal, newVal string, isNew bool) {
321+
if isNew {
322+
color.Green(" + %s: %s", field, newVal)
323+
} else {
324+
color.Red(" - %s: %s", field, oldVal)
325+
color.Green(" + %s: %s", field, newVal)
326+
}
327+
}
328+
329+
// diffYamlToProtoSpec converts YAML spec to proto DevnetSpec (for diff comparison)
330+
func diffYamlToProtoSpec(yamlSpec *config.YAMLDevnetSpec) *v1.DevnetSpec {
331+
spec := &v1.DevnetSpec{
332+
Plugin: yamlSpec.Network,
333+
NetworkType: yamlSpec.NetworkType,
334+
Validators: int32(yamlSpec.Validators),
335+
FullNodes: int32(yamlSpec.FullNodes),
336+
Mode: yamlSpec.Mode,
337+
SdkVersion: yamlSpec.NetworkVersion,
338+
}
339+
340+
// Set defaults
341+
if spec.Validators == 0 {
342+
spec.Validators = 4
343+
}
344+
if spec.Mode == "" {
345+
spec.Mode = "docker"
346+
}
347+
348+
return spec
349+
}

cmd/dvb/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func main() {
5959
newVersionCmd(),
6060
newDaemonCmd(),
6161
newApplyCmd(),
62+
newDiffCmd(),
6263
newDeployCmd(),
6364
newListCmd(),
6465
newStatusCmd(),

0 commit comments

Comments
 (0)