Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
oderwat committed Oct 27, 2024
0 parents commit b4e450a
Show file tree
Hide file tree
Showing 9 changed files with 1,643 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker-inspector-*
cmd/docker-inspector/internal-inspector
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Hans Raaf

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.PHONY: all clean

BINARY_NAME=docker-inspector
INTERNAL_BINARY=internal-inspector

all: clean $(BINARY_NAME)

clean:
rm -f $(BINARY_NAME) cmd/docker-inspector/$(INTERNAL_BINARY)

# Build the internal Linux inspector first
cmd/docker-inspector/$(INTERNAL_BINARY): cmd/internal-inspector/main.go
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o cmd/docker-inspector/$(INTERNAL_BINARY) ./cmd/internal-inspector

# Build the main wrapper for the current platform
$(BINARY_NAME): cmd/docker-inspector/$(INTERNAL_BINARY)
go build -o $(BINARY_NAME) ./cmd/docker-inspector

# Build for specific platforms
.PHONY: darwin linux windows
darwin: clean cmd/docker-inspector/$(INTERNAL_BINARY)
GOOS=darwin GOARCH=amd64 go build -o $(BINARY_NAME)-darwin ./cmd/docker-inspector

linux: clean cmd/docker-inspector/$(INTERNAL_BINARY)
GOOS=linux GOARCH=amd64 go build -o $(BINARY_NAME)-linux ./cmd/docker-inspector

windows: clean cmd/docker-inspector/$(INTERNAL_BINARY)
GOOS=windows GOARCH=amd64 go build -o $(BINARY_NAME).exe ./cmd/docker-inspector
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Docker Image Inspector

A command-line tool to inspect the contents of Docker images without having to manually create containers or extract tar files. The tool creates a temporary container, inspects its filesystem, and cleans up automatically.

## Features

- Cross-platform: Runs on macOS, Linux, and Windows (with Linux containers)
- Inspects any Docker image without modifying it
- Recursive directory listing
- Glob pattern support (including `**/`) for finding specific files
- MD5 checksum calculation for files
- JSON output option for automated processing
- Detailed summaries of files, directories, and sizes
- Clean handling of special filesystems (/proc, /sys, etc.)
- Modification time handling for reliable diffs

## Installation

1. Clone the repository
2. Build for your platform:
```bash
# For macOS
make darwin

# For Linux
make linux

# For Windows
make windows
```

## Usage

Basic usage:
```bash
./docker-inspector nginx:latest
```

With options:
```bash
# Find specific files
./docker-inspector nginx:latest --glob "**/*.conf"

# Calculate MD5 checksums
./docker-inspector nginx:latest --md5

# Output as JSON
./docker-inspector nginx:latest --json > nginx-files.json

# Inspect specific path
./docker-inspector nginx:latest --path /etc/nginx

# Keep container for further inspection
./docker-inspector nginx:latest --keep
```

### Comparing Images

To compare the contents of two images or two runs of the same image:

```bash
# With modification times (might show differences due to container startup)
./docker-inspector nginx:latest --json --md5 > run1.txt
./docker-inspector nginx:latest --json --md5 > run2.txt
diff run1.txt run2.txt

# Without modification times (more reliable for structural comparisons)
./docker-inspector nginx:latest --json --md5 --no-times > run1.txt
./docker-inspector nginx:latest --json --md5 --no-times > run2.txt
diff run1.txt run2.txt
```

Note: Files like /etc/resolv.conf typically show modification time differences between runs due to container startup configuration. Using --md5 helps identify actual content changes regardless of timestamps.

### Options

```
--path Path inside the container to inspect (default: "/")
--json Output in JSON format
--summary Show summary statistics
--glob Glob pattern for matching files (supports **/)
--md5 Calculate MD5 checksums for files
--keep Keep the temporary container after inspection
--no-times Exclude modification times from output (useful for diffs)
```

## How It Works

The tool:
1. Creates a temporary container from the specified image
2. Copies a specialized Linux inspector binary into the container
3. Executes the inspector inside the container
4. Collects and formats the results
5. Automatically cleans up the container (unless --keep is specified)

## Building from Source

Requires:
- Go 1.21 or later
- Docker running with Linux containers
- make

```bash
# Build for current platform
make

# Or for specific platform
make darwin # For macOS
make linux # For Linux
make windows # For Windows
```

## Credits

Most of the implementation work was done by Claude (Anthropic) in a conversation about Docker image inspection requirements and cross-platform Go development. The original concept and requirements were provided by the repository owner.

## License

[MIT-License](LICENSE.txt)
134 changes: 134 additions & 0 deletions cmd/docker-inspector/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package main

import (
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/alexflint/go-arg"
)

//go:embed internal-inspector
var internalInspector []byte

type Args struct {
Image string `arg:"positional,required" help:"docker image to inspect"`
Path string `arg:"--path" default:"/" help:"path inside the container to inspect"`
JSON bool `arg:"--json" help:"output in JSON format"`
Summary bool `arg:"--summary" help:"show summary statistics"`
Pattern string `arg:"--glob" help:"glob pattern for matching files (supports **/)"`
MD5 bool `arg:"--md5" help:"calculate MD5 checksums for files"`
Keep bool `arg:"--keep" help:"keep the temporary container after inspection"`
NoTimes bool `arg:"--no-times" help:"exclude modification times from output"`
}

func (Args) Version() string {
return "docker-inspector 1.0.0"
}

func (Args) Description() string {
return "Docker image content inspector - examines files and directories inside a container image"
}

func runInspector(containerID string, args Args) error {
// Create a temporary directory for the inspector
tempDir, err := os.MkdirTemp("", "docker-inspector-*")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

// Write the embedded Linux inspector to the temp directory
inspectorPath := filepath.Join(tempDir, "internal-inspector")
if err := os.WriteFile(inspectorPath, internalInspector, 0755); err != nil {
return fmt.Errorf("failed to write inspector: %v", err)
}

// Copy the inspector to the container
copyCmd := exec.Command("docker", "cp", inspectorPath, fmt.Sprintf("%s:/inspect", containerID))
if output, err := copyCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to copy inspector to container: %v\n%s", err, output)
}

// Build the command arguments
dockerArgs := []string{"exec", containerID, "/inspect"}
if args.JSON {
dockerArgs = append(dockerArgs, "--json")
}
if args.Summary {
dockerArgs = append(dockerArgs, "--summary")
}
if args.Pattern != "" {
dockerArgs = append(dockerArgs, "--glob", args.Pattern)
}
if args.MD5 {
dockerArgs = append(dockerArgs, "--md5")
}
if args.NoTimes {
dockerArgs = append(dockerArgs, "--no-times")
}
if args.Path != "/" {
dockerArgs = append(dockerArgs, "--path", args.Path)
}

cmd := exec.Command("docker", dockerArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

func main() {
var args Args
// Set defaults
args.Summary = false
args.Path = "/"

arg.MustParse(&args)

// First, ensure the image exists or can be pulled
pullCmd := exec.Command("docker", "pull", args.Image)
pullCmd.Stderr = os.Stderr
if err := pullCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to pull image %s: %v\n", args.Image, err)
os.Exit(1)
}

// Start a temporary container
startCmd := exec.Command("docker", "run", "-d", "--entrypoint", "sleep", args.Image, "3600")
output, err := startCmd.CombinedOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to start container: %v\n%s\n", err, output)
os.Exit(1)
}

containerID := strings.TrimSpace(string(output))
if containerID == "" {
fmt.Fprintf(os.Stderr, "Failed to get container ID\n")
os.Exit(1)
}

// Give the container a moment to start
time.Sleep(1 * time.Second)

// Ensure container cleanup unless --keep is specified
if !args.Keep {
defer func() {
stopCmd := exec.Command("docker", "rm", "-f", containerID)
stopCmd.Run()
}()
}

// Run the inspection
if err := runInspector(containerID, args); err != nil {
fmt.Fprintf(os.Stderr, "Inspection failed: %v\n", err)
os.Exit(1)
}

if args.Keep {
fmt.Printf("\nContainer ID: %s\n", containerID)
}
}
Loading

0 comments on commit b4e450a

Please sign in to comment.