-
Notifications
You must be signed in to change notification settings - Fork 26
Added the diff command - for enabling forensic analysis of checkpoint differences in Kubernetes
#192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Lorygold
wants to merge
1
commit into
checkpoint-restore:main
Choose a base branch
from
Lorygold:add_diff_command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Added the diff command - for enabling forensic analysis of checkpoint differences in Kubernetes
#192
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.