Skip to content

Commit 712f840

Browse files
committed
support for extraction of data from an docker image
1 parent 2a3e5e9 commit 712f840

File tree

2 files changed

+391
-20
lines changed

2 files changed

+391
-20
lines changed

cmd/docker-inspector/main.go

Lines changed: 216 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import (
88
"os"
99
"os/exec"
1010
"path/filepath"
11+
"runtime"
12+
"strconv"
13+
"strings"
14+
"syscall"
1115
"text/tabwriter"
1216
)
1317

@@ -24,6 +28,12 @@ type Args struct {
2428
MD5 bool `arg:"--md5" help:"calculate MD5 checksums for files"`
2529
Keep bool `arg:"--keep" help:"keep the temporary container after inspection"`
2630
NoTimes bool `arg:"--no-times" help:"exclude modification times from output"`
31+
// for extraction
32+
OutputDir string `arg:"--output-dir" help:"extract matching files to this directory"`
33+
StripComponents int `arg:"--strip-components" help:"strip NUMBER leading components from file names"`
34+
PreserveOwner bool `arg:"--preserve-owner" help:"preserve user/group information when extracting"`
35+
PreservePermissions bool `arg:"--preserve-perms" help:"preserve file permissions when extracting"`
36+
PreserveAll bool `arg:"--preserve-all" help:"preserve all file attributes"`
2737
}
2838

2939
func (Args) Version() string {
@@ -90,6 +100,36 @@ func runInspector(image string, args Args) ([]byte, error) {
90100
dockerArgs = append(dockerArgs, "--rm")
91101
}
92102

103+
// If output directory is specified, mount it
104+
if args.OutputDir != "" {
105+
// Convert to absolute path
106+
absPath, err := filepath.Abs(args.OutputDir)
107+
if err != nil {
108+
return nil, fmt.Errorf("failed to get absolute path for output dir: %v", err)
109+
}
110+
111+
// Create the output directory if it doesn't exist
112+
if err := os.Mkdir(absPath, 0755); err != nil && !os.IsExist(err) {
113+
return nil, fmt.Errorf("failed to create output directory: %v", err)
114+
}
115+
116+
dockerArgs = append(dockerArgs,
117+
"-v", fmt.Sprintf("%s:/inspect-target", absPath))
118+
}
119+
120+
/*
121+
// Add capabilities if we need to preserve ownership
122+
if args.OutputDir != "" && args.PreserveOwner {
123+
// Option 1: Full privileged mode (more than we need, but guaranteed to work)
124+
//dockerArgs = append(dockerArgs, "--privileged")
125+
// Option 2: Just the capabilities we need (more secure)
126+
dockerArgs = append(dockerArgs,
127+
"--cap-add=CHOWN",
128+
"--cap-add=DAC_OVERRIDE",
129+
"--cap-add=DAC_READ_SEARCH")
130+
}
131+
*/
132+
93133
// Mount the inspector and set it as entrypoint
94134
dockerArgs = append(dockerArgs,
95135
"-v", fmt.Sprintf("%s:/inspect:ro", inspectorPath),
@@ -109,13 +149,22 @@ func runInspector(image string, args Args) ([]byte, error) {
109149
if args.Path != "/" {
110150
dockerArgs = append(dockerArgs, "--path", args.Path)
111151
}
112-
152+
if args.OutputDir != "" {
153+
dockerArgs = append(dockerArgs, "--output-dir", "/inspect-target")
154+
dockerArgs = append(dockerArgs, "--strip-components", fmt.Sprintf("%d", args.StripComponents))
155+
if args.PreserveOwner {
156+
dockerArgs = append(dockerArgs, "--preserve-owner")
157+
}
158+
if args.PreservePermissions {
159+
dockerArgs = append(dockerArgs, "--preserve-perms")
160+
}
161+
}
113162
// Create a pipe for capturing stdout while also displaying it
114163
cmd := exec.Command("docker", dockerArgs...)
115164
cmd.Stderr = os.Stderr
165+
cmd.Stderr = os.Stderr
116166
output, err := cmd.Output()
117167
return output, err
118-
119168
/*
120169
// This is a version that lets us debug what the docker command is printing
121170
stdout, err := cmd.StdoutPipe()
@@ -161,6 +210,18 @@ func main() {
161210

162211
arg.MustParse(&args)
163212

213+
if args.PreserveAll {
214+
args.PreserveOwner = true
215+
args.PreservePermissions = true
216+
}
217+
// check if we actually can handle the owner preservation
218+
if runtime.GOOS == "darwin" && args.OutputDir != "" && args.PreserveOwner {
219+
if !isOwnershipSupported(args.OutputDir) {
220+
fmt.Fprintf(os.Stderr, "filesystem of %q does not support ownership changes\n", args.OutputDir)
221+
os.Exit(1)
222+
}
223+
}
224+
164225
// Run inspection on first image
165226
files1JSON, err := runInspector(args.Image1, args)
166227
if err != nil {
@@ -214,11 +275,11 @@ func main() {
214275
os.Exit(1)
215276
}
216277
} else {
278+
var files1 []FileInfo
217279
if args.JSON {
218280
// we just print what we got
219281
fmt.Print(string(files1JSON))
220282
} else {
221-
var files1 []FileInfo
222283
if err := json.Unmarshal(files1JSON, &files1); err != nil {
223284
fmt.Fprintf(os.Stderr, "failed to parse inspection results: %v", err)
224285
os.Exit(1)
@@ -277,5 +338,157 @@ func main() {
277338
fmt.Printf("Files: %d\n", fileCount)
278339
}
279340
}
341+
342+
// If we're on macOS and files were copied with ownership preservation requested,
343+
// fix ownership using sudo
344+
if runtime.GOOS == "darwin" && args.OutputDir != "" &&
345+
args.PreserveOwner {
346+
// Test if ownership changes are supported
347+
if args.JSON {
348+
if err := json.Unmarshal(files1JSON, &files1); err != nil {
349+
fmt.Fprintf(os.Stderr, "failed to parse inspection results: %v", err)
350+
os.Exit(1)
351+
}
352+
}
353+
fmt.Fprintf(os.Stderr, "\nFixing file ownership on macOS...")
354+
if err := fixOwnershipWithSudo(files1, args.OutputDir, args.StripComponents); err != nil {
355+
fmt.Fprintf(os.Stderr, "\nError fixing ownership: %v\n", err)
356+
os.Exit(1)
357+
}
358+
fmt.Fprintf(os.Stderr, " Done!\n")
359+
}
360+
}
361+
}
362+
363+
// In main.go, modify the ownership fixing:
364+
func fixOwnershipWithSudo(files []FileInfo, outputDir string, stripComponents int) error {
365+
// Build a script of chown commands
366+
var commands strings.Builder
367+
commands.WriteString("#!/bin/bash\n")
368+
369+
for _, file := range files {
370+
// Get the adjusted path based on strip components
371+
destPath := getDestPath(file.Path, stripComponents)
372+
if destPath == "" {
373+
continue
374+
}
375+
376+
// Extract UID/GID from the user/group strings
377+
uid, err := extractID(file.User)
378+
if err != nil {
379+
fmt.Fprintf(os.Stderr, "Warning: Could not extract UID from %q: %v\n", file.User, err)
380+
continue
381+
}
382+
gid, err := extractID(file.Group)
383+
if err != nil {
384+
fmt.Fprintf(os.Stderr, "Warning: Could not extract GID from %q: %v\n", file.Group, err)
385+
continue
386+
}
387+
388+
fullDestPath := filepath.Join(outputDir, destPath)
389+
// Use -h to handle symlinks correctly
390+
fmt.Fprintf(&commands, "chown -h %d:%d %q\n", uid, gid, fullDestPath)
391+
}
392+
393+
// Create a temporary script file
394+
scriptFile, err := os.CreateTemp("", "docker-inspector-*.sh")
395+
if err != nil {
396+
return fmt.Errorf("failed to create script file: %v", err)
397+
}
398+
defer os.Remove(scriptFile.Name())
399+
400+
if err := os.WriteFile(scriptFile.Name(), []byte(commands.String()), 0700); err != nil {
401+
return fmt.Errorf("failed to write script: %v", err)
402+
}
403+
404+
//fmt.Println(commands.String())
405+
// Run the script with sudo
406+
cmd := exec.Command("sudo", "/bin/bash", scriptFile.Name())
407+
cmd.Stdout = os.Stdout
408+
cmd.Stderr = os.Stderr
409+
410+
if err := cmd.Run(); err != nil {
411+
return fmt.Errorf("failed to fix ownership: %v", err)
412+
}
413+
414+
return nil
415+
}
416+
417+
func getDestPath(sourcePath string, stripComponents int) string {
418+
// Split path into components
419+
parts := strings.Split(strings.TrimPrefix(sourcePath, "/"), "/")
420+
421+
// Strip leading components
422+
if stripComponents >= len(parts) {
423+
return ""
424+
}
425+
426+
return "/" + filepath.Join(parts[stripComponents:]...)
427+
}
428+
429+
func extractID(s string) (int, error) {
430+
// Find the last pair of parentheses
431+
openIdx := strings.LastIndex(s, "(")
432+
closeIdx := strings.LastIndex(s, ")")
433+
if openIdx == -1 || closeIdx == -1 || openIdx >= closeIdx {
434+
return 0, fmt.Errorf("no ID found in %q", s)
435+
}
436+
437+
// Extract and parse the ID
438+
idStr := s[openIdx+1 : closeIdx]
439+
id, err := strconv.Atoi(idStr)
440+
if err != nil {
441+
return 0, fmt.Errorf("invalid ID in %q: %v", s, err)
442+
}
443+
return id, nil
444+
}
445+
446+
func isOwnershipSupported(dir string) bool {
447+
// Convert to absolute path
448+
absPath, err := filepath.Abs(dir)
449+
if err != nil {
450+
return false
451+
}
452+
453+
created := false
454+
stat, err := os.Stat(absPath)
455+
if err != nil {
456+
// Create the output directory if it doesn't exist
457+
if err := os.Mkdir(absPath, 0755); err != nil {
458+
return false
459+
}
460+
created = true
461+
}
462+
defer func() {
463+
if created {
464+
if err := os.Remove(absPath); err != nil {
465+
fmt.Fprintf(os.Stderr, "failed to remove %q: %v", absPath, err)
466+
}
467+
}
468+
}()
469+
470+
testFile, err := os.CreateTemp(dir, ".ownership-test-*")
471+
if err != nil {
472+
return false
473+
}
474+
testPath := testFile.Name()
475+
testFile.Close()
476+
defer os.Remove(testPath)
477+
478+
fmt.Fprintf(os.Stderr, "Checking filesystem of %q for ownership support (requires sudo)...\n", dir)
479+
// Try to change ownership to root:root
480+
if err := exec.Command("sudo", "chown", "999:999", testPath).Run(); err != nil {
481+
return false
482+
}
483+
484+
// Read back the ownership
485+
stat, err = os.Stat(testPath)
486+
if err != nil {
487+
return false
488+
}
489+
490+
if sys, ok := stat.Sys().(*syscall.Stat_t); ok {
491+
return sys.Uid == 999 && sys.Gid == 999
280492
}
493+
return false
281494
}

0 commit comments

Comments
 (0)