Skip to content

Commit 2a3e5e9

Browse files
committed
Added support to diff two images "on the fly" (also partially)
1 parent b4e450a commit 2a3e5e9

File tree

4 files changed

+398
-1233
lines changed

4 files changed

+398
-1233
lines changed

cmd/docker-inspector/diff.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Package diff provides functionality for comparing Docker image contents
2+
package main
3+
4+
import (
5+
"fmt"
6+
"strings"
7+
"time"
8+
)
9+
10+
// Mode specifies what attributes to compare
11+
type Mode int
12+
13+
const (
14+
// CompareAll includes all attributes including modification times
15+
CompareAll Mode = iota
16+
// CompareNoTimes excludes modification time comparisons
17+
CompareNoTimes
18+
)
19+
20+
// Change represents the type of difference found
21+
type Change string
22+
23+
const (
24+
Added Change = "added"
25+
Removed Change = "removed"
26+
Modified Change = "modified"
27+
)
28+
29+
// FileDiff represents a difference between two versions of a file
30+
type FileDiff struct {
31+
Path string `json:"path"`
32+
Type Change `json:"type"`
33+
OldFile FileInfo `json:"oldFile,omitempty"`
34+
NewFile FileInfo `json:"newFile,omitempty"`
35+
// Details contains human-readable descriptions of the changes
36+
Details []string `json:"details,omitempty"`
37+
}
38+
39+
// Summary contains statistical information about the differences
40+
type Summary struct {
41+
TotalDifferences int `json:"totalDifferences"`
42+
AddedFiles int `json:"addedFiles"`
43+
RemovedFiles int `json:"removedFiles"`
44+
ModifiedFiles int `json:"modifiedFiles"`
45+
}
46+
47+
// Result contains the complete diff information
48+
type Result struct {
49+
Differences []FileDiff `json:"differences"`
50+
Summary Summary `json:"summary"`
51+
}
52+
53+
// FileInfo mirrors the internal inspector's FileInfo structure
54+
type FileInfo struct {
55+
Path string `json:"path"`
56+
Size int64 `json:"size"`
57+
Mode string `json:"mode"`
58+
ModTime *time.Time `json:"modTime,omitempty"`
59+
IsDir bool `json:"isDir"`
60+
SymlinkTo string `json:"symlinkTo,omitempty"`
61+
User string `json:"user"`
62+
Group string `json:"group"`
63+
MD5 string `json:"md5,omitempty"`
64+
}
65+
66+
// Compare performs a comparison of two sets of FileInfo records
67+
func Compare(old, new []FileInfo, mode Mode) (*Result, error) {
68+
result := &Result{}
69+
70+
// Create maps for faster lookups
71+
oldFiles := make(map[string]FileInfo)
72+
newFiles := make(map[string]FileInfo)
73+
74+
// Skip special files and populate maps
75+
for _, f := range old {
76+
if !isSpecialFile(f.Path) {
77+
oldFiles[f.Path] = f
78+
}
79+
}
80+
for _, f := range new {
81+
if !isSpecialFile(f.Path) {
82+
newFiles[f.Path] = f
83+
}
84+
}
85+
86+
// Find removed files
87+
for path, oldFile := range oldFiles {
88+
if _, exists := newFiles[path]; !exists {
89+
diff := FileDiff{
90+
Path: path,
91+
Type: Removed,
92+
OldFile: oldFile,
93+
}
94+
result.Differences = append(result.Differences, diff)
95+
result.Summary.RemovedFiles++
96+
}
97+
}
98+
99+
// Find added and modified files
100+
for path, newFile := range newFiles {
101+
oldFile, exists := oldFiles[path]
102+
if !exists {
103+
diff := FileDiff{
104+
Path: path,
105+
Type: Added,
106+
NewFile: newFile,
107+
}
108+
result.Differences = append(result.Differences, diff)
109+
result.Summary.AddedFiles++
110+
continue
111+
}
112+
113+
// Check for modifications
114+
if differences := compareFiles(oldFile, newFile, mode); len(differences) > 0 {
115+
diff := FileDiff{
116+
Path: path,
117+
Type: Modified,
118+
OldFile: oldFile,
119+
NewFile: newFile,
120+
Details: differences,
121+
}
122+
result.Differences = append(result.Differences, diff)
123+
result.Summary.ModifiedFiles++
124+
}
125+
}
126+
127+
result.Summary.TotalDifferences = result.Summary.AddedFiles +
128+
result.Summary.RemovedFiles +
129+
result.Summary.ModifiedFiles
130+
131+
return result, nil
132+
}
133+
134+
// compareFiles returns a list of differences between two files
135+
func compareFiles(old, new FileInfo, mode Mode) []string {
136+
var differences []string
137+
138+
// Compare basic attributes
139+
if old.Size != new.Size {
140+
differences = append(differences,
141+
fmt.Sprintf("size changed: %d -> %d", old.Size, new.Size))
142+
}
143+
if old.Mode != new.Mode {
144+
differences = append(differences,
145+
fmt.Sprintf("permissions changed: %s -> %s", old.Mode, new.Mode))
146+
}
147+
if old.User != new.User || old.Group != new.Group {
148+
differences = append(differences,
149+
fmt.Sprintf("ownership changed: %s:%s -> %s:%s",
150+
old.User, old.Group, new.User, new.Group))
151+
}
152+
153+
// Compare modification times if requested
154+
if mode == CompareAll && old.ModTime != nil && new.ModTime != nil {
155+
if !old.ModTime.Equal(*new.ModTime) {
156+
differences = append(differences,
157+
fmt.Sprintf("modification time changed: %s -> %s",
158+
old.ModTime.Format(time.RFC3339),
159+
new.ModTime.Format(time.RFC3339)))
160+
}
161+
}
162+
163+
// Compare MD5 if available
164+
if old.MD5 != "" && new.MD5 != "" && old.MD5 != new.MD5 {
165+
differences = append(differences, "content changed (different MD5)")
166+
}
167+
168+
return differences
169+
}
170+
171+
// isSpecialFile returns true for files we want to ignore
172+
func isSpecialFile(path string) bool {
173+
return strings.HasPrefix(path, "/proc/") ||
174+
strings.HasPrefix(path, "/sys/") ||
175+
strings.HasPrefix(path, "/dev/") ||
176+
path == "/etc/resolv.conf" ||
177+
path == "/etc/hostname" ||
178+
path == "/etc/hosts"
179+
}

0 commit comments

Comments
 (0)