Skip to content

Commit 6bdd765

Browse files
Lorygoldrst0git
authored andcommitted
Added the diff command
Signed-off-by: Lorygold <lory.goldoni@gmail.com>
1 parent 50f94b4 commit 6bdd765

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

cmd/diff.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
7+
"github.com/checkpoint-restore/checkpointctl/internal"
8+
metadata "github.com/checkpoint-restore/checkpointctl/lib"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// creates the command
13+
func Diff() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "diff <checkpointA> <checkpointB>",
16+
Short: "Compare two container checkpoints and display differences",
17+
Args: cobra.ExactArgs(2),
18+
RunE: diff,
19+
}
20+
21+
flags := cmd.Flags()
22+
flags.StringVar(
23+
format,
24+
"format",
25+
"tree",
26+
"Specify output format: tree or json",
27+
)
28+
flags.BoolVar(
29+
psTreeCmd,
30+
"ps-tree-cmd",
31+
false,
32+
"Include full command lines in process tree diff",
33+
)
34+
flags.BoolVar(
35+
psTreeEnv,
36+
"ps-tree-env",
37+
false,
38+
"Include environment variables in process tree diff",
39+
)
40+
flags.BoolVar(
41+
files,
42+
"files",
43+
false,
44+
"Include file descriptors in the diff",
45+
)
46+
flags.BoolVar(
47+
sockets,
48+
"sockets",
49+
false,
50+
"Include sockets in the diff",
51+
)
52+
53+
return cmd
54+
}
55+
56+
// diff executes the checkpoint diff logic
57+
func diff(cmd *cobra.Command, args []string) error {
58+
checkA := args[0]
59+
checkB := args[1]
60+
61+
requiredFiles := []string{
62+
metadata.SpecDumpFile,
63+
metadata.ConfigDumpFile,
64+
}
65+
66+
if *files || *sockets || *psTreeCmd || *psTreeEnv {
67+
// Include all files necessary for deep diffs
68+
for _, f := range []string{"files.img", "fs-", "ids-", "fdinfo-", "pagemap-", "pages-", "mm-", "pstree.img", "core-"} {
69+
requiredFiles = append(requiredFiles, filepath.Join(metadata.CheckpointDirectory, f))
70+
}
71+
}
72+
73+
// Load tasks from both checkpoints
74+
tasksAVal, err := internal.CreateTasks([]string{checkA}, requiredFiles)
75+
if err != nil {
76+
return fmt.Errorf("failed to load checkpointA: %w", err)
77+
}
78+
defer internal.CleanupTasks(tasksAVal)
79+
80+
tasksBVal, err := internal.CreateTasks([]string{checkB}, requiredFiles)
81+
if err != nil {
82+
return fmt.Errorf("failed to load checkpointB: %w", err)
83+
}
84+
defer internal.CleanupTasks(tasksBVal)
85+
86+
// Convert []Task → []*Task for DiffTasks
87+
tasksA := make([]*internal.Task, len(tasksAVal))
88+
for i := range tasksAVal {
89+
tasksA[i] = &tasksAVal[i]
90+
}
91+
92+
tasksB := make([]*internal.Task, len(tasksBVal))
93+
for i := range tasksBVal {
94+
tasksB[i] = &tasksBVal[i]
95+
}
96+
97+
// Compute diff
98+
diffTasks, err := internal.DiffTasks(tasksA, tasksB, *psTreeCmd, *psTreeEnv, *files, *sockets)
99+
if err != nil {
100+
return fmt.Errorf("failed to compute diff: %w", err)
101+
}
102+
103+
// Render output
104+
switch *format {
105+
case "tree":
106+
return internal.RenderDiffTreeView(diffTasks)
107+
case "json":
108+
return internal.RenderDiffJSONView(diffTasks)
109+
default:
110+
return fmt.Errorf("invalid output format: %s", *format)
111+
}
112+
}

internal/diff_types.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package internal
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
)
8+
9+
// DiffStatus describes how a task changed between two checkpoints
10+
type DiffStatus string
11+
12+
const (
13+
Added DiffStatus = "added"
14+
Removed DiffStatus = "removed"
15+
Modified DiffStatus = "modified"
16+
Unchanged DiffStatus = "unchanged"
17+
)
18+
19+
// DiffTask represents the forensic diff of a single task/process
20+
type DiffTask struct {
21+
// Stable identifier for matching tasks across checkpoints
22+
ID string `json:"id"`
23+
24+
// High-level classification of the change
25+
Status DiffStatus `json:"status"`
26+
27+
// Task state in checkpoint A (nil if Added)
28+
Before *Task `json:"before,omitempty"`
29+
30+
// Task state in checkpoint B (nil if Removed)
31+
After *Task `json:"after,omitempty"`
32+
33+
// Fine-grained differences detected inside the task
34+
Changes []DiffChange `json:"changes,omitempty"`
35+
}
36+
37+
// DiffChange represents a single detected difference.
38+
type DiffChange struct {
39+
// Logical category of the change (pstree, files, sockets, env, cmdline, etc.)
40+
Category string `json:"category"`
41+
42+
// Specific field or subcomponent that changed
43+
Field string `json:"field"`
44+
45+
// Value in checkpoint A
46+
Before any `json:"before,omitempty"`
47+
48+
// Value in checkpoint B
49+
After any `json:"after,omitempty"`
50+
}
51+
52+
// DiffTasks compares two sets of tasks and returns their differences.
53+
func DiffTasks(
54+
tasksA []*Task,
55+
tasksB []*Task,
56+
psTreeCmd bool,
57+
psTreeEnv bool,
58+
files bool,
59+
sockets bool,
60+
) ([]DiffTask, error) {
61+
if tasksA == nil || tasksB == nil {
62+
return nil, fmt.Errorf("nil task list provided")
63+
}
64+
65+
// Index tasks by CheckpointFilePath for matching
66+
indexA := make(map[string]*Task)
67+
indexB := make(map[string]*Task)
68+
69+
for _, t := range tasksA {
70+
indexA[t.CheckpointFilePath] = t
71+
}
72+
for _, t := range tasksB {
73+
indexB[t.CheckpointFilePath] = t
74+
}
75+
76+
var diffs []DiffTask
77+
78+
// Tasks present in A
79+
for id, taskA := range indexA {
80+
taskB, exists := indexB[id]
81+
82+
if !exists {
83+
// Removed task
84+
diffs = append(diffs, DiffTask{
85+
ID: id,
86+
Status: Removed,
87+
Before: taskA,
88+
})
89+
continue
90+
}
91+
92+
// Exists in both → compare
93+
if reflect.DeepEqual(taskA, taskB) {
94+
diffs = append(diffs, DiffTask{
95+
ID: id,
96+
Status: Unchanged,
97+
Before: taskA,
98+
After: taskB,
99+
})
100+
continue
101+
}
102+
103+
// Modified task
104+
diffs = append(diffs, DiffTask{
105+
ID: id,
106+
Status: Modified,
107+
Before: taskA,
108+
After: taskB,
109+
Changes: []DiffChange{
110+
{
111+
Category: "task",
112+
Field: "struct",
113+
Before: taskA,
114+
After: taskB,
115+
},
116+
},
117+
})
118+
}
119+
120+
// Tasks only in B → Added
121+
for id, taskB := range indexB {
122+
if _, exists := indexA[id]; exists {
123+
continue
124+
}
125+
126+
diffs = append(diffs, DiffTask{
127+
ID: id,
128+
Status: Added,
129+
After: taskB,
130+
})
131+
}
132+
133+
return diffs, nil
134+
}
135+
136+
// RenderDiffTreeView prints a human-readable tree of diff tasks
137+
func RenderDiffTreeView(diffTasks []DiffTask) error {
138+
for _, dt := range diffTasks {
139+
fmt.Printf("\nTask ID: %s | Status: %s\n", dt.ID, dt.Status)
140+
141+
if dt.Before != nil {
142+
fmt.Printf(" Before checkpoint: %s\n", dt.Before.CheckpointFilePath)
143+
}
144+
if dt.After != nil {
145+
fmt.Printf(" After checkpoint: %s\n", dt.After.CheckpointFilePath)
146+
}
147+
148+
for _, ch := range dt.Changes {
149+
fmt.Printf(" Change: [%s] %s | Before: %v | After: %v\n",
150+
ch.Category, ch.Field, ch.Before, ch.After)
151+
}
152+
}
153+
return nil
154+
}
155+
156+
// RenderDiffJSONView prints diff tasks in JSON format
157+
func RenderDiffJSONView(diffTasks []DiffTask) error {
158+
jsonData, err := json.MarshalIndent(diffTasks, "", " ")
159+
if err != nil {
160+
return err
161+
}
162+
163+
fmt.Println(string(jsonData))
164+
return nil
165+
}

0 commit comments

Comments
 (0)