Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions checkpointctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func main() {

rootCommand.AddCommand(cmd.BuildCmd())

rootCommand.AddCommand(cmd.Diff())

rootCommand.Version = version

if err := rootCommand.Execute(); err != nil {
Expand Down
112 changes: 112 additions & 0 deletions cmd/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package cmd

import (
"fmt"
"path/filepath"

"github.com/checkpoint-restore/checkpointctl/internal"
metadata "github.com/checkpoint-restore/checkpointctl/lib"
"github.com/spf13/cobra"
)

// creates the command
func Diff() *cobra.Command {
cmd := &cobra.Command{
Use: "diff <checkpointA> <checkpointB>",
Short: "Show changes between two container checkpoints",
Args: cobra.ExactArgs(2),
RunE: diff,
}

flags := cmd.Flags()
flags.StringVar(
format,
"format",
"tree",
"Specify output format: tree or json",
)
flags.BoolVar(
psTreeCmd,
"ps-tree-cmd",
false,
"Include full command lines in process tree diff",
)
flags.BoolVar(
psTreeEnv,
"ps-tree-env",
false,
"Include environment variables in process tree diff",
)
flags.BoolVar(
files,
"files",
false,
"Include file descriptors in the diff",
)
flags.BoolVar(
sockets,
"sockets",
false,
"Include sockets in the diff",
)

return cmd
}

// diff executes the checkpoint diff logic
func diff(cmd *cobra.Command, args []string) error {
checkA := args[0]
checkB := args[1]

requiredFiles := []string{
metadata.SpecDumpFile,
metadata.ConfigDumpFile,
}

if *files || *sockets || *psTreeCmd || *psTreeEnv {
// Include all files necessary for deep diffs
for _, f := range []string{"files.img", "fs-", "ids-", "fdinfo-", "pagemap-", "pages-", "mm-", "pstree.img", "core-"} {
requiredFiles = append(requiredFiles, filepath.Join(metadata.CheckpointDirectory, f))
}
}

// Load tasks from both checkpoints
tasksAVal, err := internal.CreateTasks([]string{checkA}, requiredFiles)
if err != nil {
return fmt.Errorf("failed to load checkpointA: %w", err)
}
defer internal.CleanupTasks(tasksAVal)

tasksBVal, err := internal.CreateTasks([]string{checkB}, requiredFiles)
if err != nil {
return fmt.Errorf("failed to load checkpointB: %w", err)
}
defer internal.CleanupTasks(tasksBVal)

// Convert []Task → []*Task for DiffTasks
tasksA := make([]*internal.Task, len(tasksAVal))
for i := range tasksAVal {
tasksA[i] = &tasksAVal[i]
}

tasksB := make([]*internal.Task, len(tasksBVal))
for i := range tasksBVal {
tasksB[i] = &tasksBVal[i]
}

// Compute diff
diffTasks, err := internal.DiffTasks(tasksA, tasksB, *psTreeCmd, *psTreeEnv, *files, *sockets)
if err != nil {
return fmt.Errorf("failed to compute diff: %w", err)
}

// Render output
switch *format {
case "tree":
return internal.RenderDiffTreeView(diffTasks)
case "json":
return internal.RenderDiffJSONView(diffTasks)
default:
return fmt.Errorf("invalid output format: %s", *format)
}
}
165 changes: 165 additions & 0 deletions internal/diff_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package internal

import (
"encoding/json"
"fmt"
"reflect"
)

// DiffStatus describes how a task changed between two checkpoints
type DiffStatus string

const (
Added DiffStatus = "added"
Removed DiffStatus = "removed"
Modified DiffStatus = "modified"
Unchanged DiffStatus = "unchanged"
)

// DiffTask represents the forensic diff of a single task/process
type DiffTask struct {
// Stable identifier for matching tasks across checkpoints
ID string `json:"id"`

// High-level classification of the change
Status DiffStatus `json:"status"`

// Task state in checkpoint A (nil if Added)
Before *Task `json:"before,omitempty"`

// Task state in checkpoint B (nil if Removed)
After *Task `json:"after,omitempty"`

// Fine-grained differences detected inside the task
Changes []DiffChange `json:"changes,omitempty"`
}

// DiffChange represents a single detected difference.
type DiffChange struct {
// Logical category of the change (pstree, files, sockets, env, cmdline, etc.)
Category string `json:"category"`

// Specific field or subcomponent that changed
Field string `json:"field"`

// Value in checkpoint A
Before any `json:"before,omitempty"`

// Value in checkpoint B
After any `json:"after,omitempty"`
}

// DiffTasks compares two sets of tasks and returns their differences.
func DiffTasks(
tasksA []*Task,
tasksB []*Task,
psTreeCmd bool,
psTreeEnv bool,
files bool,
sockets bool,
) ([]DiffTask, error) {
if tasksA == nil || tasksB == nil {
return nil, fmt.Errorf("nil task list provided")
}

// Index tasks by CheckpointFilePath for matching
indexA := make(map[string]*Task)
indexB := make(map[string]*Task)

for _, t := range tasksA {
indexA[t.CheckpointFilePath] = t
}
for _, t := range tasksB {
indexB[t.CheckpointFilePath] = t
}

var diffs []DiffTask

// Tasks present in A
for id, taskA := range indexA {
taskB, exists := indexB[id]

if !exists {
// Removed task
diffs = append(diffs, DiffTask{
ID: id,
Status: Removed,
Before: taskA,
})
continue
}

// Exists in both → compare
if reflect.DeepEqual(taskA, taskB) {
diffs = append(diffs, DiffTask{
ID: id,
Status: Unchanged,
Before: taskA,
After: taskB,
})
continue
}

// Modified task
diffs = append(diffs, DiffTask{
ID: id,
Status: Modified,
Before: taskA,
After: taskB,
Changes: []DiffChange{
{
Category: "task",
Field: "struct",
Before: taskA,
After: taskB,
},
},
})
}

// Tasks only in B → Added
for id, taskB := range indexB {
if _, exists := indexA[id]; exists {
continue
}

diffs = append(diffs, DiffTask{
ID: id,
Status: Added,
After: taskB,
})
}

return diffs, nil
}

// RenderDiffTreeView prints a human-readable tree of diff tasks
func RenderDiffTreeView(diffTasks []DiffTask) error {
for _, dt := range diffTasks {
fmt.Printf("\nTask ID: %s | Status: %s\n", dt.ID, dt.Status)

if dt.Before != nil {
fmt.Printf(" Before checkpoint: %s\n", dt.Before.CheckpointFilePath)
}
if dt.After != nil {
fmt.Printf(" After checkpoint: %s\n", dt.After.CheckpointFilePath)
}

for _, ch := range dt.Changes {
fmt.Printf(" Change: [%s] %s | Before: %v | After: %v\n",
ch.Category, ch.Field, ch.Before, ch.After)
}
}
return nil
}

// RenderDiffJSONView prints diff tasks in JSON format
func RenderDiffJSONView(diffTasks []DiffTask) error {
jsonData, err := json.MarshalIndent(diffTasks, "", " ")
if err != nil {
return err
}

fmt.Println(string(jsonData))
return nil
}
Loading