Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
138145b
Update explorer common structures and functions
roshanmaskey Apr 9, 2026
8011b02
Refactor containerd for readability and style, and function updates
roshanmaskey Apr 9, 2026
de70348
Updated as per PR feedback
roshanmaskey Apr 13, 2026
a6015e1
Merge pull request #37 from roshanmaskey/v0.6.0
roshanmaskey Apr 13, 2026
3a90e95
Refactored docker module and updated logs
roshanmaskey Apr 14, 2026
e5de61b
Removed comment as per feedback
roshanmaskey Apr 14, 2026
1a31218
Merge pull request #38 from roshanmaskey/v0.6.0
roshanmaskey Apr 14, 2026
ff50d03
Adding support for podman containers
roshanmaskey Apr 14, 2026
2ee8d9a
Updated as per feedback and few other fixes
roshanmaskey Apr 28, 2026
c8a07cc
Updated comment to make Close function clear
roshanmaskey May 5, 2026
178cbee
Merge pull request #39 from roshanmaskey/v0.6.0
roshanmaskey May 5, 2026
3a08150
Adding Type() function to return container type and fixed docker Cont…
roshanmaskey May 20, 2026
2a093dc
Remove blank line in Type method
roshanmaskey May 20, 2026
54ca202
Merge pull request #40 from roshanmaskey/v0.6.0
roshanmaskey May 20, 2026
64e9c68
Fixed scanning upper directory
roshanmaskey May 21, 2026
312a053
Merge pull request #41 from roshanmaskey/v0.6.0
roshanmaskey May 26, 2026
e931667
Refactored commands
roshanmaskey May 27, 2026
de92b73
Update cmd/commands/drift.go
roshanmaskey May 28, 2026
46a2d7c
Update cmd/commands/export.go
roshanmaskey May 28, 2026
2caafd0
Update cmd/commands/list.go
roshanmaskey May 28, 2026
8c3de1f
Update cmd/commands/mount.go
roshanmaskey May 28, 2026
12cc7bf
Update cmd/commands/mount_all.go
roshanmaskey May 28, 2026
f06e791
Update cmd/commands/utils.go
roshanmaskey May 28, 2026
b2ab7e5
Update cmd/commands/info.go
roshanmaskey May 28, 2026
c43d3fd
Merge pull request #43 from roshanmaskey/v0.6.0
roshanmaskey May 28, 2026
b79dbe9
porting resolveSanpshotter by dfjxs
roshanmaskey May 31, 2026
9287e9d
adding mouting support Docker version 29
roshanmaskey May 31, 2026
c66a95b
added package comment
roshanmaskey May 31, 2026
fc644c8
Merge pull request #44 from roshanmaskey/v0.6.0
roshanmaskey Jun 3, 2026
e155489
updated podman to check custom storage graphroot
roshanmaskey Jun 3, 2026
53374ec
Merge pull request #45 from roshanmaskey/v0.6.0
roshanmaskey Jun 4, 2026
5146567
Replace export-all and mount-all with export --all and mount --all
roshanmaskey Jun 4, 2026
130b1ce
updated README.md and go version in .golangci.yaml and go.mod
roshanmaskey Jun 4, 2026
c6d4494
Merge pull request #46 from roshanmaskey/v0.6.0
roshanmaskey Jun 5, 2026
68b0f12
refactor export function and removed duplicate code blocks
roshanmaskey Jun 6, 2026
cd5d2ff
Merge pull request #48 from roshanmaskey/v0.6.0
roshanmaskey Jun 9, 2026
1b1bfd4
Merge branch 'main' into v0.6.0
roshanmaskey Jun 9, 2026
60becc8
Update podman.go
roshanmaskey Jun 9, 2026
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: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ linters:
- gosec
run:
timeout: 5m
go: '1.18'
go: '1.25.10'
skip-dirs:
- script
485 changes: 229 additions & 256 deletions README.md

Large diffs are not rendered by default.

45 changes: 23 additions & 22 deletions cmd/commands/drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"strings"
"text/tabwriter"

"github.com/google/container-explorer/explorers"

log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
Expand All @@ -35,11 +37,11 @@ var DriftCommand = cli.Command{
ArgsUsage: "[containerID]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "filter",
Name: "filter, f",
Usage: "comma separated label filter using key=value pair",
},
cli.BoolFlag{
Name: "mount-support-containers",
Name: "mount-support-containers, s",
Usage: "mount Kubernetes supporting containers",
},
},
Expand All @@ -48,8 +50,8 @@ var DriftCommand = cli.Command{
if runtime.GOOS != "linux" {
return fmt.Errorf("feature is only supported on Linux")
}
output := clictx.GlobalString("output")
outputfile := clictx.GlobalString("output-file")
output := GlobalConfig.Output
outputfile := GlobalConfig.OutputFile
filter := clictx.String("filter")

// Getting container ID positional arg
Expand All @@ -58,27 +60,25 @@ var DriftCommand = cli.Command{
containerID = clictx.Args().First()
}

ctx, exp, cancel, err := explorerEnvironment(clictx)
if err != nil {
return err
}
defer cancel()

drifts, err := exp.ContainerDrift(ctx, filter, !clictx.Bool("mount-support-containers"), containerID)
if err != nil {
log.WithField("message", err).Error("retrieving container drift")
if output == "json" && outputfile != "" {
data := []string{}
writeOutputFile(data, outputfile)
var allDrifts []explorers.Drift

exps := GetExplorers()
for _, xplr := range exps {
drifts, err := xplr.ContainerDrift(GlobalConfig.Context, filter, !clictx.Bool("mount-support-containers"), containerID)
if err != nil {
engineName := xplr.Type()
log.WithField("message", err).Errorf("retrieving %s container drift", engineName)
} else if drifts != nil {
allDrifts = append(allDrifts, drifts...)
}
return nil
}

// Handle output formats
if strings.ToLower(output) == "json" {
if outputfile != "" {
writeOutputFile(drifts, outputfile)
writeOutputFile(allDrifts, outputfile)
} else {
printAsJSON(drifts)
printAsJSON(allDrifts)
}
return nil
}
Expand All @@ -89,10 +89,10 @@ var DriftCommand = cli.Command{

if output == "table" {
// Define the header
fmt.Fprintf(tw, "CONTAINER ID\tADDED/MODIFIED\tDELETED\n")
fmt.Fprintf(tw, "CONTAINER TYPE\tCONTAINER ID\tADDED/MODIFIED\tDELETED\n")
}

for _, drift := range drifts {
for _, drift := range allDrifts {
switch strings.ToLower(output) {
case "json_line":
printAsJSONLine(drift)
Expand All @@ -116,7 +116,8 @@ var DriftCommand = cli.Command{
displayAddedOrModifiedFiles := strings.Join(addedOrModifiedFiles, ", ")
displayInaccessibleFiles := strings.Join(inaccessibleFiles, ", ")

displayValues := fmt.Sprintf("%s\t%s\t%s",
displayValues := fmt.Sprintf("%s\t%s\t%s\t%s",
drift.ContainerType,
drift.ContainerID,
displayAddedOrModifiedFiles,
displayInaccessibleFiles,
Expand Down
249 changes: 106 additions & 143 deletions cmd/commands/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,185 +18,148 @@ package commands

import (
"context"
"fmt"
"encoding/json"
"os"
"path/filepath"
"strings"

"github.com/containerd/containerd/namespaces"
"github.com/google/container-explorer/explorers"
"github.com/google/container-explorer/explorers/containerd"
"github.com/google/container-explorer/explorers/docker"
"github.com/urfave/cli"

containerdConfig "github.com/containerd/containerd/services/server/config"
dockerConfig "github.com/docker/docker/daemon/config"
log "github.com/sirupsen/logrus"
)

const (
containerdRootDir = "/var/lib/containerd"
dockerRootDir = "/var/lib/docker"
defaultContainerdRootDir = "/var/lib/containerd"
defaultDockerRootDir = "/var/lib/docker"
)

// explorerEnvironment returns a ContainerExplorer interface.
// Containers managed using containerd and docker implement ContainerExplorer
// interface.
func explorerEnvironment(clictx *cli.Context) (context.Context, explorers.ContainerExplorer, func(), error) {
ctx, cancel := context.WithCancel(context.Background())

imageroot := clictx.GlobalString("image-root")
containerdroot := clictx.GlobalString("containerd-root")
dockerroot := clictx.GlobalString("docker-root")
metadatafile := clictx.GlobalString("metadata-file")
snapshotfile := clictx.GlobalString("snapshot-metadata-file")
layercache := clictx.GlobalString("layer-cache")

// Read support container data if provided using global switch.
var sc *explorers.SupportContainer
if clictx.GlobalString("support-container-data") != "" {
var err error
sc, err = explorers.NewSupportContainer(clictx.GlobalString("support-container-data"))
if err != nil {
log.Errorf("getting new support container: %v", err)
}
}
// RuntimeConfig holds the global configuration for container-explorer.
type RuntimeConfig struct {
Context context.Context
ImageRootDir string
ContainerdRootDir string
DockerRootDir string
PodmanRootDir string
LayerCache string
SupportContainerData *explorers.SupportContainer
Output string
OutputFile string
Debug bool
}

// Handle docker managed containers.
//
// Use the global flag --docker-managed to specify container
// managed using docker. This includes Kubernetes containers
// managed using docker.
if clictx.GlobalBool("docker-managed") {
if dockerroot == "" && imageroot == "" {
fmt.Printf("Missing required argument. Use --image-root or --docker-root\n")
os.Exit(1)
// GlobalConfig is the package-level configuration object.
var GlobalConfig RuntimeConfig

// InitializeRuntime sets up the global configuration from the CLI context.
func InitializeRuntime(clictx *cli.Context) error {
GlobalConfig.Context = context.Background()
GlobalConfig.Debug = clictx.GlobalBool("debug")
GlobalConfig.ImageRootDir = clictx.GlobalString("image-root")
GlobalConfig.ContainerdRootDir = clictx.GlobalString("containerd-root")
GlobalConfig.DockerRootDir = clictx.GlobalString("docker-root")
GlobalConfig.LayerCache = clictx.GlobalString("layer-cache")
GlobalConfig.Output = clictx.GlobalString("output")
GlobalConfig.OutputFile = clictx.GlobalString("output-file")

// Read support container data if provided.
supportContainerFile := clictx.GlobalString("support-container-data")
sc, err := explorers.NewSupportContainer(supportContainerFile)
if err != nil {
log.Errorf("getting new support container: %v", err)
}
GlobalConfig.SupportContainerData = sc

// Handle docker managed containers root.
if GlobalConfig.DockerRootDir == "" {
if GlobalConfig.ImageRootDir != "" {
dockerDataDir := getDockerDataRoot(GlobalConfig.ImageRootDir)
GlobalConfig.DockerRootDir = filepath.Join(GlobalConfig.ImageRootDir, strings.Replace(dockerDataDir, "/", "", 1))
} else if GlobalConfig.ContainerdRootDir != "" {
parentDir := filepath.Dir(strings.TrimSuffix(GlobalConfig.ContainerdRootDir, "/"))
GlobalConfig.DockerRootDir = filepath.Join(parentDir, "docker")
}
}

if imageroot != "" && dockerroot == "" {
dockerroot = filepath.Join(
imageroot,
strings.Replace(dockerRootDir, "/", "", 1),
)
// Handle containerd managed containers root.
if GlobalConfig.ContainerdRootDir == "" {
if GlobalConfig.ImageRootDir != "" {
containerdDataDir := getContainerdDataDir(GlobalConfig.ImageRootDir)
GlobalConfig.ContainerdRootDir = filepath.Join(GlobalConfig.ImageRootDir, strings.Replace(containerdDataDir, "/", "", 1))
} else if GlobalConfig.DockerRootDir != "" {
parentDir := filepath.Dir(strings.TrimSuffix(GlobalConfig.DockerRootDir, "/"))
GlobalConfig.ContainerdRootDir = filepath.Join(parentDir, "containerd")
}

log.WithFields(log.Fields{
"imageroot": imageroot,
"containerdroot": containerdroot,
"dockerroot": dockerroot,
"manifestfile": metadatafile,
"snapshotfile": snapshotfile,
"sc": &sc,
}).Debug("docker container environment")

de, _ := docker.NewExplorer(dockerroot, containerdroot, metadatafile, snapshotfile, sc)
return ctx, de, func() {
cancel()
}, nil
}

// Handle containerd managed containers.
//
// The default is containerd managed containers. This includes
// Kubernetes managed containers.
if containerdroot == "" && imageroot == "" {
fmt.Printf("Missing required arguments. Use --image-root or --containerd-root\n")
os.Exit(1)
if !clictx.GlobalBool("use-layer-cache") {
GlobalConfig.LayerCache = ""
}

if imageroot != "" && containerdroot == "" {
containerdroot = filepath.Join(
imageroot,
strings.Replace(containerdRootDir, "/", "", 1),
)
log.WithFields(log.Fields{
"imageRoot": GlobalConfig.ImageRootDir,
"containerdRoot": GlobalConfig.ContainerdRootDir,
"dockerRoot": GlobalConfig.DockerRootDir,
"layercache": GlobalConfig.LayerCache,
"supportContainerData": GlobalConfig.SupportContainerData,
"debug": GlobalConfig.Debug,
}).Debug("runtime configuration initialized")

return nil
}

// getDockerDataRoot returns Docker data-root directory.
// Returns custom path if configured, otherwise returns the default path.
func getDockerDataRoot(imageRootDir string) string {
configPath := filepath.Join(imageRootDir, "etc", "docker", "daemon.json")

if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.WithFields(log.Fields{"configPath": configPath, "error": err}).Debug("reading docker config")
return defaultDockerRootDir
}

if metadatafile == "" {
metadatafile = filepath.Join(containerdroot, "io.containerd.metadata.v1.bolt", "meta.db")
data, err := os.ReadFile(configPath)
if err != nil {
log.WithFields(log.Fields{"configPath": configPath, "error": err}).Debug("reading docker config file")
return defaultDockerRootDir
}

log.WithFields(log.Fields{
"imageroot": imageroot,
"containerdroot": containerdroot,
"dockerroot": dockerroot,
"manifestfile": metadatafile,
"snapshotfile": snapshotfile,
}).Debug("containerd container environment")
var cfg dockerConfig.Config

if !clictx.GlobalBool("use-layer-cache") {
layercache = ""
}
cde, err := containerd.NewExplorer(imageroot, containerdroot, metadatafile, snapshotfile, layercache, sc)
err = json.Unmarshal(data, &cfg)
if err != nil {
return ctx, nil, func() { cancel() }, err
log.WithFields(log.Fields{"configPath": configPath, "error": err}).Debug("unmarshalling docker config")
return defaultDockerRootDir
}
return ctx, cde, func() {
cancel()
}, nil
}

func parseRuntimeConfig(clictx *cli.Context) (context.Context, map[string]interface{}, error) {
// Global options
namespace := clictx.GlobalString("namespace")
imageRootDir := clictx.GlobalString("image-root")
containerdRootDir := clictx.GlobalString("containerd-root")
dockerRootDir := clictx.GlobalString("docker-root")
metadataFile := clictx.GlobalString("metadata-file")
snapshotFile := clictx.GlobalString("snapshot-metadata-file")
layerCache := clictx.GlobalString("layer-cache")
useLayerCache := clictx.GlobalBool("use-layer-cache")
supportDataFile := clictx.GlobalString("support-container-data")

ctx := context.Background()
ctx = namespaces.WithNamespace(ctx, namespace)

if imageRootDir == "" && containerdRootDir == "" && dockerRootDir == "" {
return ctx, nil, fmt.Errorf("Missing required arguments. Use --image-root, --containerd-root or --docker-root")
if cfg.Root == "" {
return defaultDockerRootDir
}

if containerdRootDir == "" && imageRootDir != "" {
containerdRootDir = filepath.Join(imageRootDir, "var", "lib", "containerd")
}
return cfg.Root
}

if dockerRootDir == "" && imageRootDir != "" {
dockerRootDir = filepath.Join(imageRootDir, "var", "lib", "docker")
// getContainerDataDir returns containerd root directory.
// Returns custom path if configured, otherwise returns the default path.
func getContainerdDataDir(imageRootDir string) string {
configPath := filepath.Join(imageRootDir, "etc", "containerd", "config.toml")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.WithFields(log.Fields{"configPath": configPath, "error": err}).Debug("reading containerd config")
return defaultContainerdRootDir
}

if metadataFile == "" {
metadataFile = filepath.Join(containerdRootDir, "io.containerd.metadata.v1.bolt", "meta.db")
}
var cfg containerdConfig.Config

if !useLayerCache {
layerCache = ""
if err := containerdConfig.LoadConfig(configPath, &cfg); err != nil {
log.WithFields(log.Fields{"configPath": configPath, "error": err}).Debug("parsing containerd config")
return defaultContainerdRootDir
}

log.WithFields(log.Fields{
"imageRootDir": imageRootDir,
"containerdRootDir": containerdRootDir,
"dockerRootDir": dockerRootDir,
"metadataFile": metadataFile,
"snapshotFile": snapshotFile,
"layerCache": layerCache,
"useLayerCache": useLayerCache,
"supportDataFile": supportDataFile,
}).Debug("container-explorer runtime configuration settings")

runtimeConfig := make(map[string]interface{})
runtimeConfig["namespace"] = namespace
runtimeConfig["imageRootDir"] = imageRootDir
runtimeConfig["containerdRootDir"] = containerdRootDir
runtimeConfig["dockerRootDir"] = dockerRootDir
runtimeConfig["metadataFile"] = metadataFile
runtimeConfig["snapshotFile"] = snapshotFile
runtimeConfig["layerCache"] = layerCache

var err error
var sc *explorers.SupportContainer
if supportDataFile != "" {
sc, err = explorers.NewSupportContainer(clictx.GlobalString("support-container-data"))
if err != nil {
log.Errorf("getting new support container: %v", err)
}
if cfg.Root == "" {
return defaultContainerdRootDir
}
runtimeConfig["supportContainer"] = sc

return ctx, runtimeConfig, nil
}
return cfg.Root
}
Loading
Loading