diff --git a/.golangci.yaml b/.golangci.yaml index 1eecf82..7295a9a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -19,6 +19,6 @@ linters: - gosec run: timeout: 5m - go: '1.18' + go: '1.25.10' skip-dirs: - script diff --git a/README.md b/README.md index 4997656..05b1e4c 100644 --- a/README.md +++ b/README.md @@ -1,307 +1,280 @@ -# Container Explorer +# Container Explorer (`container-explorer`) -Container Explorer (container-explorer) is a tool to explore containers of a -disk image. Container Explorer supports exploring containers managed using -containerd and docker container runtimes. Container Explorer attempts to -provide the familiar output generated by tools like ctr and docker. +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -Container Explorer provides the following functionalities: +**Container Explorer** (built as `ce`) is a standalone Go utility for exploring, +analyzing, and performing forensics on container runtimes (such as **containerd**, **Docker**, +and **Podman**). -- Exploring namespaces -- Exploring containers -- Exploring images -- Exploring snapshots -- Exploring contents -- Exploring container drift -- Mounting containers -- Support JSON output +Container Explorer operates **completely offline**. It directly parses the low-level metadata databases, +storage directories, and snapshot layers on disk (e.g., in `/var/lib/containerd` or `/var/lib/docker`). +This design makes it a highly powerful utility for digital forensics, incident response, VM disk image analysis, +and low-level troubleshooting. -You can build the Container Explorer using the instruction at -[Build Container Explorer](#build-container-explorer). +--- -If you don't want to build, the binaries are available on -. +## Key Features -## Usage +- **Offline Forensics**: Analyze container configurations and filesystems without needing container + daemons (`dockerd`, `containerd`, or `podman`) to be running. +- **Multi-Runtime Support**: Auto-detects and supports **containerd** (Bbolt DB), **Docker** (JSON + configurations), and **Podman** (SQLite database state). +- **Disk Image Analysis**: Point the tool to a mounted offline root filesystem (`--image-root`) + from a VM or disk snapshot to explore containers on that image. +- **Filesystem Mounting**: Mount container filesystems (OverlayFS merged view) locally to inspect, + search, or copy files. +- **Drift Detection**: Detect filesystem drift (modified, added, deleted, or executable files) + between the running container and its original base image. +- **Container Exporting**: Export container filesystems as raw disk images (`.raw`) or tar archives + (`.tar.gz`) for secondary analysis. +- **Kubernetes Awareness**: Filter out or isolate Kubernetes infrastructure/support containers + (e.g., `pause` containers) using label filters or predefined configuration files. -The figure below shows the output of the container-explorer --help command. +--- -```text -NAME: - container-explorer - A standalone utility to explore container details - -USAGE: - main [global options] command [command options] [arguments...] - -VERSION: - 0.4.0 - -DESCRIPTION: - A standalone utility to explore container details. - - Container explorer supports exploring containers managed using containerd and - docker. The utility also supports exploring containers created and managed using - Kubernetes. - - -COMMANDS: - list, ls lists container related information - info show internal information - mount mount a container to a mount point - mount-all, mount_all mount all containers - drift, diff identifies container filesystem changes - export export a container - export-all, export_all export all containers as image or archive - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - --debug enable debug messages - --containerd-root value, -c value specify containerd root directory - --image-root value, -i value specify mount point for a disk image - --metadata-file value, -m value specify the path to containerd metadata file i.e. meta.db - --snapshot-metadata-file value, -s value specify the path to containerd snapshot metadata file i.e. metadata.db. - --use-layer-cache attempt to use cached layers where layers are symlinks - --layer-cache value cached layer folder within the snapshot root (default: "layers") - --namespace value, -n value specify container namespace (default: "default") - --docker-managed specify docker manages standalone or Kubernetes containers - --docker-root value specify docker root directory. This is only used with flag --docker-managed - --support-container-data value a yaml file containing information about support containers - --output value output format in json, table. Default is table (default: "table") - --output-file value, -o value output file to save the content - --help, -h show help - --version, -v print the version -``` +## Architecture and Design -Container Explorer helps you explore containers on a mounted disk image. Let's -assume we have a clone of the Google Kubernetes Engine (GKE) node attached on a -forensic VM as `/dev/sdb`. +Container Explorer parses metadata databases directly: +- **containerd**: Reads the BoltDB metadata file (`io.containerd.metadata.v1.bolt/meta.db`). +- **Docker**: Reads container configurations (`config.v2.json`), repositories JSON, and image files directly + from the Docker root directory—no SQLite database is used by the Docker explorer. +- **Podman**: Reads SQLite database state (`db.sql`) and storage configurations directly from Podman's storage + root (typically `/var/lib/containers/storage`). -1. List the disk partition table. +It reconstructs the container layer stack to perform file system operations like mounting, drift +detection, and exporting, completely bypassing the container engine. - ```shell - sudo fdisk -l /dev/sdb - ``` +### Filesystem & Snapshotter Support - The output of the `fdisk` command. +- **OverlayFS**: For standard containers (Docker, containerd, Podman) using OverlayFS, the tool reconstructs + the layered filesystem using the `lowerdir` and `upperdir` directories to establish a merged view. +- **Native Filesystem**: For containerd containers using the `native` snapshotter: + - **Mounting**: Automatically resolved via its snapshot ID in the metadata database, and mounted + as a read-only bind mount (`rbind, ro`) directly from the host's native snapshot directory + (e.g., `/var/lib/containerd/io.containerd.snapshotter.v1.native/snapshots/`). + - **Exporting**: Works out-of-the-box as it leverages the native bind mount mechanism. + - **Drift Detection**: Since `native` snapshots represent complete directory copies rather than + layered diff overlays, drift detection is bypassed. - ```text - Disk /dev/sdb: 10 GiB, 10737418240 bytes, 20971520 sectors - Units: sectors of 1 * 512 = 512 bytes - Sector size (logical/physical): 512 bytes / 512 bytes - I/O size (minimum/optimal): 512 bytes / 512 bytes - Disklabel type: gpt - Disk identifier: 7C818738-EDF0-B246-960D-0E7EE8655B06 - - Device Start End Sectors Size Type - /dev/sdb1 8704000 20971486 12267487 5.8G Linux filesystem - /dev/sdb2 20480 53247 32768 16M ChromeOS kernel - /dev/sdb3 4509696 8703999 4194304 2G ChromeOS root fs - /dev/sdb4 53248 86015 32768 16M ChromeOS kernel - /dev/sdb5 315392 4509695 4194304 2G ChromeOS root fs - /dev/sdb6 16448 16448 1 512B ChromeOS kernel - /dev/sdb7 16449 16449 1 512B ChromeOS root fs - /dev/sdb8 86016 118783 32768 16M Linux filesystem - /dev/sdb9 16450 16450 1 512B ChromeOS reserved - /dev/sdb10 16451 16451 1 512B ChromeOS reserved - /dev/sdb11 64 16447 16384 8M BIOS boot - /dev/sdb12 249856 315391 65536 32M EFI System - ``` - -2. Mount the `/dev/sdb1` as read-only disk on mount point `/mnt/case`. - - ```shell - sudo mount -o ro,noload,noexec /dev/sdb1 /mnt/case - ``` - -3. Use `container-explorer` to explore the mounted image. - - ```shell - sudo ce -i /mnt/case --support-container-data supportcontainer.yaml list containers - ``` - -4. Mount an individual container or all containers - - Mount a container to mount point `/mnt/container`. - - ```shell - sudo ce -i /mnt/case –support-container-data supportcontainer.yaml -n k8s.io mount f3c910583a81e7441e2cbd209b72afa4740e676ff8d82f2c74fdc5c78e179c10 /container - ``` - - Mount all containers to mount point `/mnt/container`. Mounting all - containers will create sub-directories using container ID as directory name. - - ```shell - sudo ce -i /mnt/case –support-container-data supportcontainer.yaml mount-all /mnt/container - ``` - -5. List the mounted containers within `/mnt/container/`. - - ```shell - sudo ls -l /mnt/container - ``` - - The output of the command. - - ```text - drwxr-xr-x 1 root root 4096 Feb 5 08:55 3544209cfda893703458d7d0a6a65970bfb46e9be6a60faa1e4e9d0adae11b55 - drwxr-xr-x 1 root root 4096 Feb 5 08:54 3646fe81507be0510e9191d7e34adbeb751e7ecd86f7e1657289968828c5c8e3 - drwxr-xr-x 1 root root 4096 Feb 5 08:54 68a04caa81f9a4265e53a83b50874faca5a7c8400ee0c064d40d81cde6f03b86 - drwxr-xr-x 1 root root 4096 Feb 5 09:14 6f68aeae9c0288c2412f793d3a7b85efac189786ed8da2bdce9f88d39827fb80 - drwxr-xr-x 1 root root 4096 Feb 5 08:55 7227972ec83761790a65c137239c48817a26b8ad85be74b1ecf751656a2a61be - drwxr-xr-x 1 root root 4096 Feb 5 09:13 cc9bc4f6c6b35b8a3616d8b4586741d8dc148c62b394d276dfab7572ee5aa542 - drwxr-xr-x 1 root root 4096 Feb 5 09:13 d3d1ff8c4ef39acbdf0a44bee6c326786309e408942d6a2d42cbaa1661bac77f - drwxr-xr-x 1 root root 4096 Feb 5 08:54 f3c910583a81e7441e2cbd209b72afa4740e676ff8d82f2c74fdc5c78e179c10 - ``` - -6. See filesystem changes - - ```shell - sudo ce -i /mnt/case/ --output json --support-container-data supportcontainer.yaml drift - ``` - - In order to see drift of a particular container, supply the container ID with drift/diff - - ```shell - sudo ce -i /mnt/case/ --output json --support-container-data supportcontainer.yaml drift f3c910583a81e7441e2cbd209b72afa4740e676ff8d82f2c74fdc5c78e179c10 - ``` - -6. Use your favorite forensic tool to process mounted containers. - -## Mounting Disk Image - -Let's assume you have a GKE node disk image as `clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img`. - -1. List the partition table. - - ```shell - sudo fdisk -l clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img - ``` - - The output of the `fdisk -l` command. - - ```text - Disk clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img: 10 GiB, 10737418240 bytes, 20971520 sectors - Units: sectors of 1 * 512 = 512 bytes - Sector size (logical/physical): 512 bytes / 512 bytes - I/O size (minimum/optimal): 512 bytes / 512 bytes - Disklabel type: gpt - Disk identifier: 7C818738-EDF0-B246-960D-0E7EE8655B06 - - Device Start End Sectors Size Type - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img1 8704000 20971486 12267487 5.8G Linux filesystem - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img2 20480 53247 32768 16M ChromeOS kernel - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img3 4509696 8703999 4194304 2G ChromeOS root fs - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img4 53248 86015 32768 16M ChromeOS kernel - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img5 315392 4509695 4194304 2G ChromeOS root fs - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img6 16448 16448 1 512B ChromeOS kernel - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img7 16449 16449 1 512B ChromeOS root fs - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img8 86016 118783 32768 16M Linux filesystem - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img9 16450 16450 1 512B ChromeOS reserved - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img10 16451 16451 1 512B ChromeOS reserved - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img11 64 16447 16384 8M BIOS boot - clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img12 249856 315391 65536 32M EFI System - ``` +--- -2. Mount the first partition (Linux Filesystem) +## Installation - ```shell - sudo mount -o ro,noload,noexec,offset=$((8704000*512)) clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img /mnt/case - ``` +### Prerequisites -## Docker Containers +To compile or run Container Explorer, you need: +- **Go** (version 1.25 or later is recommended) +- **Linux operating system** (required for mount, export, and drift detection capabilities) -Container Explorer supports exploring Docker managed containers. Use -`--docker-managed` global flag to explore Docker containers. +### Compiling from Source -```shell -sudo ce -i /mnt/case --support-container-data supportcontainer.yaml --docker-managed list containers +Clone the repository and build the binary: + +```bash +git clone https://github.com/google/container-explorer.git +cd container-explorer +go build -o ce cmd/main.go ``` -Container Explorer supports the following operation on Docker containers: +The output binary is named `ce`. -- Listing containers -- Listing images -- Mounting an individual container -- Mounting all containers -- Excluding containers by image, hostname, and labels +### Using the Setup Script -## Excluding Containers +A setup script is provided in the `script/` directory to download and install releases: -When a GKE cluster is created, several containers are created to support the -Kubernetes. These clusters are used to support Kubernetes only and may not be -interesting for the investigation. +```bash +sudo ./script/setup.sh install +``` -The Kubernetes support containers are hidden by default when the global flag `--support-container-data=supportcontainer.yaml` is used. +--- -The `supportcontainer.yaml` contains the commonly known hostname, image, and -labels used to identify the support containers. +## Command Line Interface (CLI) Reference -When `--support-container-data` is used, the `list` and `mount-all` commands -automatically ignores the known support containers where applicable. You can use -`--show-support-containers` and `--mount-support-containers` to display and -mount the support containers. +### Global Flags -### Filtering Containers +```text +GLOBAL FLAGS: + --debug, -d Enable debug messages + --containerd-root value, -c value Specify containerd root directory + --docker-root value, -D value Specify docker root directory + --image-root value, -i value Specify mount point for an offline disk image + --use-layer-cache, -u Attempt to use cached layers where layers are symlinks + --layer-cache value, -l value Cached layer folder within the snapshot root (default: "layers") + --support-container-data value, -s value + A yaml file containing criteria for Kubernetes support containers + --output value Output format: json, table (default: "table") + --output-file value, -o value Output file to save the content + --help, -h Show help +``` -Container Explorer supports filtering containers using the labels. This is particularly handy while reviewing GKE containers. Filter supports comma separated key/value pairs. The filter `--filter io.cri-containerd.kind=container` lists containerd containers. +> [!IMPORTANT] +> The global flags `--containerd-root` and `--docker-root` do not have built-in CLI defaults. +> Instead, they are dynamically inferred relative to the `--image-root` flag (e.g., as `/var/lib/containerd` and +> `/var/lib/docker`) if it is set. If `--image-root` is not specified, you must supply these paths explicitly; +> otherwise, the tool cannot locate runtime database files and the corresponding explorer will fail to initialize. + +--- + +## Commands + +### 1. `list` (or `ls`) +Lists container-related objects and information. Output results can be printed as a table (default) or +exported in JSON format using global output flags. + +#### Subcommands: +- `namespaces` (aliases: `namespace`, `ns`): List namespaces (only implemented for containerd). +- `containers` (aliases: `container`): List containers across runtimes. + - `-f, --filter`: Comma-separated label filter (e.g., `key=value`). + - `-s, --show-support-containers`: Show Kubernetes supporting/infra containers. + - `-L, --no-labels`: Hide labels in table view. + - `--updated`: Show container updated timestamp. + - `-p, --ports`: Show exposed ports. + - `-r, --running`: Placeholder flag (UI defined, but not wired in the backend). +- `images` (aliases: `image`, `img`): List container images. +- `contents` (aliases: `content`): List containerd content addressable stores (only implemented for containerd). +- `snapshots` (aliases: `snapshot`, `sn`): List container layers/snapshots (only implemented for containerd). + - `-P, --full-overlay-path`: Display full OverlayFS directory paths on the host. +- `tasks` (aliases: `task`): List container execution tasks/processes. + +*Example:* +```bash +# List all Docker and containerd containers +sudo ./ce --image-root /mnt/disk1 list containers + +# List snapshots with full host OverlayFS paths +sudo ./ce --image-root /mnt/disk1 list snapshots -P + +# Export the list of all containers to a JSON file +sudo ./ce --image-root /mnt/disk1 --output json --output-file output/container_list.json list containers +``` -The command below shows containers in pod namespace `default`. +--- -```shell -/opt/container-explorer/bin/ce -i /mnt list containers --filter io.cri-containerd.kind=container,io.kubernetes.pod.namespace=default +### 2. `info` / `inspect` +Retrieves internal metadata and OCI specifications for a container. + +Both commands display the OCI specs for a target container. While `info container` requires a subcommand structure, the standalone `inspect` command acts as a shortcut that yields the same output. + +```bash +sudo ./ce --image-root /mnt/disk1 info container +# Or using the inspect shortcut: +sudo ./ce --image-root /mnt/disk1 inspect ``` -## Installing Container Explorer +**Flags:** +- `-s, --spec`: Print only the container's OCI runtime configuration (`config.json` equivalent). -Follow the steps below to install a pre-compiled Container Explorer on Linux systems. +--- -1. Download setup script `setup.sh` which is located at `https://github.com/google/container-explorer/blob/main/script/setup.sh` +### 3. `mount` +Mounts a container's merged filesystem view using OverlayFS to a local target directory. - ```shell - wget https://raw.githubusercontent.com/google/container-explorer/main/script/setup.sh - ``` +```bash +sudo ./ce --image-root /mnt/disk1 mount [flag] [container-id] +``` -2. Run the script with `root` privileges. +**Flags / Arguments:** +- `--all`: Mount all matching containers under the target mount point. +- `-e, --container-engine`: Specify engine (`docker`, `containerd`, `podman`, `all`). +- `-f, --filter`: Filter by container label. +- `-s, --mount-support-containers`: Include Kubernetes support containers. + +*Example:* +```bash +sudo mkdir /mnt/container_inspect +sudo ./ce --image-root /mnt/disk1 mount 4b8d7c2a /mnt/container_inspect +# You can now browse the live merged filesystem of the container under /mnt/container_inspect +``` - ```shell - sudo bash setup.sh install - ``` +--- - Container Explorer files will be created at `/opt/container-explorer` +### 4. `drift` (or `diff`) +Identifies filesystem drift (additions, modifications, deletions, and newly introduced executables) +in the container compared to its base image layer. Just like `list`, results can be exported in JSON +format using the global `--output json` flag. -3. Run Container Explorer +```bash +sudo ./ce --image-root /mnt/disk1 drift +``` - ```shell - /opt/container-explorer/bin/ce -h - ``` +**Flags:** +- `-f, --filter`: Comma-separated label filter. +- `-s, --mount-support-containers`: Analyze Kubernetes support containers. - **Note**: `supportcontainer.yaml` is located at `/opt/container-explorer/etc/supportcontainer.yaml` +*Example:* +```bash +# Print container drift to stdout in JSON format +sudo ./ce --image-root /mnt/disk1 --output json drift 4b8d7c2a -## Build Container Explorer +# Export container drift to a JSON file +sudo ./ce --image-root /mnt/disk1 --output json --output-file output/drift_report.json drift 4b8d7c2a +``` -Follow the steps below to compile the Container Explorer. +--- -1. Verify Golang version is 1.20 or above +### 5. `export` +Exports container filesystems to a target directory as raw disk images or tar archives. - ```shell - go version - ``` +```bash +sudo ./ce --image-root /mnt/disk1 export [flag] +``` + +**Flags:** +- `-i, --image`: Export container filesystem as raw `.img` file (default). +- `-a, --archive`: Export container filesystem as `.tar` archive. +- `--all`: Export all containers. +- `-e, --container-engine`: Choose container engine (`docker`, `containerd`, `podman`, `all`). +- `-f, --filter`: Label filter. +- `-s, --export-support-containers`: Export Kubernetes support containers. + +*Example:* +```bash +sudo ./ce --image-root /mnt/disk1 export -a 4b8d7c2a /tmp/container_exports/ +# Generates a tar archive of the container's filesystem in /tmp/container_exports/ +``` + +--- + +## Limitations & Feature Matrix + +Because Container Explorer operates as an offline forensic tool by reading filesystem stores directly, support for specific operations varies across container engines depending on database types and implementation status. -2. Clone Container Explorer github project +| Feature / Command | containerd | Docker | Podman | +| :--- | :--- | :--- | :--- | +| **`list namespaces`** | ✅ Supported | ❌ Not implemented (returns empty) | ❌ Not implemented (returns empty) | +| **`list containers`** | ✅ Supported | ✅ Supported | ✅ Supported | +| **`list images`** | ✅ Supported | ✅ Supported | ✅ Supported | +| **`list contents`** | ✅ Supported | ❌ Not implemented (stubbed) | ❌ Not implemented (stubbed) | +| **`list snapshots`** | ✅ Supported | ❌ Not implemented (stubbed) | ❌ Not implemented (stubbed) | +| **`list tasks`** | ✅ Supported | ✅ Supported | ✅ Supported | +| **`mount` (OverlayFS)** | ✅ Supported | ✅ Supported | ✅ Supported | +| **`mount` (Native FS)** | ✅ Supported | ➖ N/A | ➖ N/A | +| **`drift` (OverlayFS)** | ✅ Supported | ✅ Supported | ✅ Supported | +| **`drift` (Native FS)** | ➖ Bypassed | ➖ N/A | ➖ N/A | +| **`export`** | ✅ Supported | ✅ Supported | ✅ Supported | + +--- + +## Forensic Investigation Guide + +### Investigating a Mounted VM Disk +If you have mounted an external VM disk or disk snapshot at `/mnt/disk1`: + +```bash +# Analyze containerd and Docker runtimes under the mounted disk image +sudo ./ce --image-root /mnt/disk1 list containers + +# Check for container filesystem drift on the disk image +sudo ./ce --image-root /mnt/disk1 drift +``` - ```shell - git clone https://github.com/google/container-explorer - ``` +--- -3. Compile the code +## Contributing - ```shell - cd container-explorer - go build -ldflags '-s -w' -o $HOME/ce cmd/main.go - ``` +We welcome contributions to this project! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on +our code of conduct and the submission process. -4. Run container-explorer +## License - ```bash - $HOME/ce -h - ``` +This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. diff --git a/cmd/commands/drift.go b/cmd/commands/drift.go index 21291ec..8cd6009 100644 --- a/cmd/commands/drift.go +++ b/cmd/commands/drift.go @@ -23,6 +23,8 @@ import ( "strings" "text/tabwriter" + "github.com/google/container-explorer/explorers" + log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -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", }, }, @@ -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 @@ -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 } @@ -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) @@ -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, diff --git a/cmd/commands/env.go b/cmd/commands/env.go index 3aaa98f..3f2b36d 100644 --- a/cmd/commands/env.go +++ b/cmd/commands/env.go @@ -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 -} \ No newline at end of file + return cfg.Root +} diff --git a/cmd/commands/export.go b/cmd/commands/export.go index 82cb669..a8e87d1 100644 --- a/cmd/commands/export.go +++ b/cmd/commands/export.go @@ -19,10 +19,9 @@ package commands import ( "fmt" "runtime" + "strings" "github.com/google/container-explorer/explorers" - "github.com/google/container-explorer/explorers/containerd" - "github.com/google/container-explorer/explorers/docker" log "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -30,126 +29,42 @@ import ( var ExportCommand = cli.Command{ Name: "export", - Usage: "export a container as image or archive", - Description: "export a container as image or archive", - ArgsUsage: "ID OUTPUTDIR", + Usage: "export a container or all containers as image or archive", + Description: "export a container or all containers as image or archive", + ArgsUsage: "[flag] [ID] OUTPUTDIR", Flags: []cli.Flag{ cli.BoolFlag{ - Name: "image", + Name: "image, i", Usage: "output container as raw image", }, cli.BoolFlag{ - Name: "archive", + Name: "archive, a", Usage: "output container as archive", }, - }, - Action: func(clictx *cli.Context) error { - - // Export a container is only supported on a Linux operating system. - if runtime.GOOS != "linux" { - return fmt.Errorf("exporting a container is only supported on Linux") - } - - if clictx.NArg() < 2 { - return fmt.Errorf("container ID and output directory are required") - } - - containerID := clictx.Args().First() - outputDir := clictx.Args().Get(1) - - exportAsImage := clictx.Bool("image") - exportAsArchive := clictx.Bool("archive") - - // At least one options is required. If not provided by user - // export as image file. - if !exportAsArchive && !exportAsImage { - exportAsImage = true - } - - exportOptions := make(map[string]bool) - exportOptions["image"] = exportAsImage - exportOptions["archive"] = exportAsArchive - - // Process container-explorer runtime arguments - ctx, runtimeConfig, err := parseRuntimeConfig(clictx) - if err != nil { - return err - } - - namespace := runtimeConfig["namespace"].(string) - imageRootDir := runtimeConfig["imageRootDir"].(string) - containerdRootDir := runtimeConfig["containerdRootDir"].(string) - dockerRootDir := runtimeConfig["dockerRootDir"].(string) - metadataFile := runtimeConfig["metadataFile"].(string) - snapshotFile := runtimeConfig["snapshotFile"].(string) - layercache := runtimeConfig["layerCache"].(string) - sc := runtimeConfig["supportContainer"].(*explorers.SupportContainer) - - log.WithFields(log.Fields{ - "namespace": namespace, - "containerID": containerID, - "outputDir": outputDir, - "exportAsImage": exportAsImage, - "exportAsArchive": exportAsArchive, - }).Debug("Processing export request") - - cXplr, err := containerd.NewExplorer(imageRootDir, containerdRootDir, metadataFile, snapshotFile, layercache, sc) - if err == nil { - if err := cXplr.ExportContainer(ctx, containerID, outputDir, exportOptions); err != nil { - log.Errorf("exporting %s as containerd container: %v", containerID, err) - } - } else { - log.Errorf("getting containerd explorer: %v", err) - } - - dXplr, err := docker.NewExplorer(dockerRootDir, containerdRootDir, metadataFile, snapshotFile, sc) - if err == nil { - if err := dXplr.ExportContainer(ctx, containerID, outputDir, exportOptions); err != nil { - log.Errorf("exporting %s as Docker container: %v", containerID, err) - } - } else { - log.Errorf("getting Docker explorer: %v", err) - } - - // default return - return nil - }, -} - -var ExportAllCommand = cli.Command{ - Name: "export-all", - Aliases: []string{"export_all"}, - Usage: "export all containers as image or archive", - Description: "export all containers as image or archive", - ArgsUsage: "OUTPUTDIR", - Flags: []cli.Flag{ cli.BoolFlag{ - Name: "image", - Usage: "output container as raw image", + Name: "all", + Usage: "export all containers", }, - cli.BoolFlag{ - Name: "archive", - Usage: "output container as archive", + cli.StringFlag{ + Name: "container-engine, e", + Usage: "supported container engine containerd, docker, and podman", + Value: "all", }, cli.StringFlag{ - Name: "filter", + Name: "filter, f", Usage: "comma separated label filter using key=value", }, cli.BoolFlag{ - Name: "export-support-containers", + Name: "export-support-containers, s", Usage: "export Kubernetes supporting containers", }, }, Action: func(clictx *cli.Context) error { - // Exporting containers only supported on a Linux operating system. - if runtime.GOOS != "linux" { - return fmt.Errorf("exporting containers is only supported on Linux") - } - if clictx.NArg() < 1 { - return fmt.Errorf("output directory is required") + // Export a container is only supported on a Linux operating system. + if runtime.GOOS != "linux" { + return fmt.Errorf("exporting a container is only supported on Linux") } - outputDir := clictx.Args().First() exportAsImage := clictx.Bool("image") exportAsArchive := clictx.Bool("archive") @@ -164,53 +79,59 @@ var ExportAllCommand = cli.Command{ exportOptions["image"] = exportAsImage exportOptions["archive"] = exportAsArchive - filterString := clictx.String("filter") - filterMap := getFilterMap(filterString) - - exportSupportContainers := clictx.Bool("export-support-containers") + if clictx.Bool("all") { + if clictx.NArg() < 1 { + return fmt.Errorf("output directory is required") + } + outputDir := clictx.Args().First() + containerEngine := clictx.String("container-engine") + + filterString := clictx.String("filter") + filterMap := getFilterMap(filterString) + + exportSupportContainers := clictx.Bool("export-support-containers") + + log.WithFields(log.Fields{ + "containerEngine": containerEngine, + "exportAsImage": exportAsImage, + "exportAsArchive": exportAsArchive, + "filter": filterString, + "exportSupportContainers": exportSupportContainers, + }).Debug("exporting all containers") + + exps := GetExplorers() + for _, xplr := range exps { + engineName := xplr.Type() + if containerEngine == "all" || strings.ToLower(containerEngine) == engineName { + if err := xplr.ExportAllContainers(GlobalConfig.Context, outputDir, exportOptions, filterMap, exportSupportContainers); err != nil { + log.Errorf("exporting all %s containers as image or archive: %v", engineName, err) + } + } + } + return nil + } - // Process container-explorer runtime arguments - ctx, runtimeConfig, err := parseRuntimeConfig(clictx) - if err != nil { - return err + if clictx.NArg() < 2 { + return fmt.Errorf("container ID and output directory are required") } - namespace := runtimeConfig["namespace"].(string) - imageRootDir := runtimeConfig["imageRootDir"].(string) - containerdRootDir := runtimeConfig["containerdRootDir"].(string) - dockerRootDir := runtimeConfig["dockerRootDir"].(string) - metadataFile := runtimeConfig["metadataFile"].(string) - snapshotFile := runtimeConfig["snapshotFile"].(string) - layercache := runtimeConfig["layerCache"].(string) - sc := runtimeConfig["supportContainer"].(*explorers.SupportContainer) + containerID := clictx.Args().First() + outputDir := clictx.Args().Get(1) log.WithFields(log.Fields{ - "namespace": namespace, - "outputDir": outputDir, - "exportAsImage": exportAsImage, + "containerID": containerID, + "outputDir": outputDir, + "exportAsImage": exportAsImage, "exportAsArchive": exportAsArchive, - }).Debug("Processing export-all request") + }).Debug("processing export request") - // Exporting all containerd containers - cXplr, err := containerd.NewExplorer(imageRootDir, containerdRootDir, metadataFile, snapshotFile, layercache, sc) - if err == nil { - if err := cXplr.ExportAllContainers(ctx, outputDir, exportOptions, filterMap, exportSupportContainers); err != nil { - log.Errorf("exporting all containerd containers as image or archive: %v", err) - } - } else { - log.Errorf("getting containerd explorer: %v", err) - } + matched, err := ForMatchingContainer(GlobalConfig.Context, containerID, func(xplr explorers.ContainerExplorer) error { + return xplr.ExportContainer(GlobalConfig.Context, containerID, outputDir, exportOptions) + }) - // Exporting all Docker containers - dXplr, err := docker.NewExplorer(dockerRootDir, containerdRootDir, metadataFile, snapshotFile, sc) - if err == nil { - if err := dXplr.ExportAllContainers(ctx, outputDir, exportOptions, filterMap, exportSupportContainers); err != nil { - log.Errorf("exporting all Docker containers as image or archive: %v", err) - } - } else { - log.Errorf("getting Docker explorer: %v", err) + if !matched { + return fmt.Errorf("no matching container") } - - return nil + return err }, -} \ No newline at end of file +} diff --git a/cmd/commands/info.go b/cmd/commands/info.go index edb5c5a..7c46bcd 100644 --- a/cmd/commands/info.go +++ b/cmd/commands/info.go @@ -17,12 +17,12 @@ limitations under the License. package commands import ( - "encoding/json" "fmt" + "github.com/google/container-explorer/explorers" + log "github.com/sirupsen/logrus" - "github.com/containerd/containerd/namespaces" "github.com/urfave/cli" ) @@ -41,7 +41,7 @@ var infoContainer = cli.Command{ Description: "show container internal information", Flags: []cli.Flag{ cli.BoolFlag{ - Name: "spec", + Name: "spec, s", Usage: "show only container spec", }, }, @@ -51,48 +51,56 @@ var infoContainer = cli.Command{ return fmt.Errorf("container id is required") } - var ( - namespace string - containerid string - ) + containerID := clictx.Args().First() + spec := clictx.Bool("spec") - namespace = clictx.GlobalString("namespace") - containerid = clictx.Args().First() + matched, err := ForMatchingContainer(GlobalConfig.Context, containerID, func(xplr explorers.ContainerExplorer) error { + info, err := xplr.InfoContainer(GlobalConfig.Context, containerID, spec) + if err != nil { + return err + } + printAsJSON(info) + return nil + }) - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - log.Fatal(err) + if !matched { + log.Errorf("container %s not found", containerID) } - defer cancel() + return err + }, +} - ctx = namespaces.WithNamespace(ctx, namespace) +var InspectCommand = cli.Command{ + Name: "inspect", + Usage: "show container internal information", + Description: "show container internal information", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "spec, s", + Usage: "show only container spec", + }, + }, + Action: func(clictx *cli.Context) error { - info, err := exp.InfoContainer(ctx, containerid, clictx.Bool("spec")) - if err != nil { - log.Fatal(err) + if clictx.NArg() < 1 { + return fmt.Errorf("container id is required") } - printAsJSON(info) - - return nil - }, -} - -func printAsJSON(v interface{}) { - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - log.Error("error marshaling to JSON", err) - return - } + containerID := clictx.Args().First() + spec := clictx.Bool("spec") - fmt.Println(string(b)) -} + matched, err := ForMatchingContainer(GlobalConfig.Context, containerID, func(xplr explorers.ContainerExplorer) error { + info, err := xplr.InfoContainer(GlobalConfig.Context, containerID, spec) + if err != nil { + return err + } + printAsJSON(info) + return nil + }) -func printAsJSONLine(v interface{}) { - b, err := json.Marshal(v) - if err != nil { - log.Error("error marshaling to json_line", err) - return - } - fmt.Println(string(b)) + if !matched { + log.Errorf("container %s not found", containerID) + } + return err + }, } diff --git a/cmd/commands/list.go b/cmd/commands/list.go index 66d2059..95bfcef 100644 --- a/cmd/commands/list.go +++ b/cmd/commands/list.go @@ -17,14 +17,14 @@ limitations under the License. package commands import ( - "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" "strings" "text/tabwriter" + "github.com/google/container-explorer/explorers" + log "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -39,7 +39,7 @@ var ListCommand = cli.Command{ Subcommands: cli.Commands{ listNamespaces, listContainers, - listContent, + listContents, listImages, listSnapshots, listTasks, @@ -52,21 +52,22 @@ var listNamespaces = cli.Command{ Usage: "list all namespaces", Description: "list all namespaces", Action: func(clictx *cli.Context) error { + exps := GetExplorers() + fmt.Println("NAMESPACE") + for _, xplr := range exps { + // Currently namespaces are only relevant for containerd. + if xplr.Type() != "containerd" { + continue + } - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - log.Fatal(err) - } - defer cancel() - - nss, err := exp.ListNamespaces(ctx) - if err != nil { - log.Fatal(err) - } + nss, err := xplr.ListNamespaces(GlobalConfig.Context) + if err != nil { + log.Fatal(err) + } - fmt.Println("NAMESPACE") - for _, ns := range nss { - fmt.Println(ns) + for _, ns := range nss { + fmt.Println(ns) + } } return nil @@ -80,15 +81,15 @@ var listContainers = cli.Command{ Description: "list containers for all namespaces", Flags: []cli.Flag{ cli.StringFlag{ - Name: "filter", + Name: "filter, f", Usage: "comma separated label filter using key=value pair", }, cli.BoolFlag{ - Name: "show-support-containers", + Name: "show-support-containers, s", Usage: "show supporting containers created by Kubernetes", }, cli.BoolFlag{ - Name: "no-labels", + Name: "no-labels, L", Usage: "hide container labels", }, cli.BoolFlag{ @@ -96,38 +97,54 @@ var listContainers = cli.Command{ Usage: "show updated timestamp", }, cli.BoolFlag{ - Name: "ports", + Name: "ports, p", Usage: "show exposed ports", }, cli.BoolFlag{ - Name: "running", + Name: "running, r", Usage: "show running docker managed containers", }, }, Action: func(clictx *cli.Context) error { - output := clictx.GlobalString("output") - outputfile := clictx.GlobalString("output-file") + output := GlobalConfig.Output + outputfile := GlobalConfig.OutputFile filters := clictx.String("filter") - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - log.WithField("message", err).Error("setting environment") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + containermap := make(map[string]explorers.Container) + exps := GetExplorers() + + // First pass: Collect all containers + for _, xplr := range exps { + engineContainers, err := xplr.ListContainers(GlobalConfig.Context) + if err != nil { + engineName := xplr.Type() + log.WithField("message", err).Errorf("listing %s containers", engineName) + continue } - return nil - } - defer cancel() - - containers, err := exp.ListContainers(ctx) - if err != nil { - log.WithField("message", err).Error("listing containers") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + + for _, c := range engineContainers { + // Merging logic: Docker might enrich containerd containers + if existing, ok := containermap[c.ID]; ok { + // Enriching with name, pid, status if they are more complete + if existing.Name == "" { + existing.Name = c.Name + } + if existing.ProcessID == 0 { + existing.ProcessID = c.ProcessID + } + if existing.Status == "" { + existing.Status = c.Status + } + containermap[c.ID] = existing + } else { + containermap[c.ID] = c + } } - return nil + } + + var containers []explorers.Container + for _, container := range containermap { + containers = append(containers, container) } // Filter containers @@ -162,6 +179,7 @@ var listContainers = cli.Command{ containers = filteredContainers } + // Handling JSON output if strings.ToLower(output) == "json" { if outputfile != "" { writeOutputFile(containers, outputfile) @@ -171,11 +189,12 @@ var listContainers = cli.Command{ return nil } + // Handling table output tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) defer tw.Flush() if output == "table" { - displayFields := "NAMESPACE\tTYPE\tCONTAINER ID\tCONTAINER HOSTNAME\tIMAGE\tCREATED AT\tPID\tSTATUS" + displayFields := "CONTAINER TYPE\tNAMESPACE\tCONTAINER ID\tCONTAINER NAME\tIMAGE\tCREATED AT\tPID\tSTATUS" // show updated timestamp if clictx.Bool("updated") { displayFields = fmt.Sprintf("%v\tUPDATED AT", displayFields) @@ -184,10 +203,6 @@ var listContainers = cli.Command{ if clictx.Bool("ports") { displayFields = fmt.Sprintf("%v\tEXPOSED PORTS", displayFields) } - // display docker container name - if clictx.GlobalBool("docker-managed") { - displayFields = fmt.Sprintf("%v\tNAME", displayFields) - } // show labels if !clictx.Bool("no-labels") { displayFields = fmt.Sprintf("%v\tLABELS", displayFields) @@ -197,40 +212,25 @@ var listContainers = cli.Command{ for _, container := range containers { // Show Kubernetes support containers created - // by GKE, EKS, and AKS if !clictx.Bool("show-support-containers") && container.SupportContainer { log.WithFields(log.Fields{ "namespace": container.Namespace, - "containerid": container.ID, + "containerID": container.ID, "supportcontainer": container.SupportContainer, - }).Info("skip support container") + }).Info("skipping support container") continue } - // Show only running containers. - // - // This is currently supported only on a docker managed containers. - if clictx.GlobalBool("docker-managed") && clictx.Bool("running") { - if !container.Running { - log.WithFields(log.Fields{ - "containerid": container.ID, - "image": container.Image, - }).Info("skip container that was not running") - - continue - } - } - switch strings.ToLower(output) { case "json_line": printAsJSONLine(container) default: displayValues := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s", - container.Namespace, container.ContainerType, + container.Namespace, container.ID, - container.Hostname, + container.Name, container.Image, container.CreatedAt.Format(tsLayout), container.ProcessID, @@ -244,17 +244,12 @@ var listContainers = cli.Command{ if clictx.Bool("ports") { displayValues = fmt.Sprintf("%v\t%s", displayValues, arrayToString(container.ExposedPorts)) } - // show docker container name - if clictx.GlobalBool("docker-managed") { - displayValues = fmt.Sprintf("%v\t%s", displayValues, strings.Replace(container.Runtime.Name, "/", "", 1)) - } // show labels values if !clictx.Bool("no-labels") { displayValues = fmt.Sprintf("%v\t%v", displayValues, labelString(container.Labels)) } fmt.Fprintf(tw, "%v\n", displayValues) } - } return nil @@ -268,7 +263,7 @@ var listImages = cli.Command{ Description: "list images for all namespaces", Flags: []cli.Flag{ cli.BoolFlag{ - Name: "show-support-containers", + Name: "show-support-containers, s", Usage: "show Kubernetes support container images", }, cli.BoolFlag{ @@ -276,50 +271,44 @@ var listImages = cli.Command{ Usage: "show updated timestamp", }, cli.BoolFlag{ - Name: "no-labels", + Name: "no-labels, L", Usage: "hide image labels", }, }, Action: func(clictx *cli.Context) error { - output := clictx.GlobalString("output") - outputfile := clictx.GlobalString("output-file") - - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - log.WithField("message", err).Error("setting environment") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) - } - return nil - } - defer cancel() - - images, err := exp.ListImages(ctx) - if err != nil { - log.WithField("message", err).Error("listing images") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + output := GlobalConfig.Output + outputfile := GlobalConfig.OutputFile + + var containerImages []explorers.Image + exps := GetExplorers() + + for _, xplr := range exps { + engineImages, err := xplr.ListImages(GlobalConfig.Context) + if err != nil { + engineName := xplr.Type() + log.WithField("message", err).Errorf("listing %s images", engineName) + continue } - return nil + containerImages = append(containerImages, engineImages...) } + // Handle JSON output if strings.ToLower(output) == "json" { if outputfile != "" { - writeOutputFile(images, outputfile) + writeOutputFile(containerImages, outputfile) } else { - printAsJSON(images) + printAsJSON(containerImages) } return nil } + // Handle table output tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) defer tw.Flush() // Setting table output if strings.ToLower(output) == "table" { - displayFields := "NAMESPACE\tNAME\tCREATED AT\tDIGEST\tTYPE" + displayFields := "CONTAINER TYPE\tNAMESPACE\tNAME\tCREATED AT\tDIGEST\tTYPE" if clictx.Bool("updated") { displayFields = fmt.Sprintf("%v\tUPDATED AT", displayFields) } @@ -330,7 +319,7 @@ var listImages = cli.Command{ fmt.Fprintf(tw, "%v\n", displayFields) } - for _, image := range images { + for _, image := range containerImages { if !clictx.Bool("show-support-containers") && image.SupportContainerImage { log.WithFields(log.Fields{ "namespace": image.Namespace, @@ -343,7 +332,8 @@ var listImages = cli.Command{ case "json_line": printAsJSONLine(image) default: - displayValues := fmt.Sprintf("%s\t%s\t%s\t%s\t%s", + displayValues := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s", + image.ContainerType, image.Namespace, image.Name, image.CreatedAt.Format(tsLayout), @@ -363,58 +353,53 @@ var listImages = cli.Command{ }, } -var listContent = cli.Command{ - Name: "content", +var listContents = cli.Command{ + Name: "contents", Aliases: []string{"content"}, Usage: "list content for all namespaces", Description: "list content for all namespaces", Action: func(clictx *cli.Context) error { - output := clictx.GlobalString("output") - outputfile := clictx.GlobalString("outputfile") - - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - log.WithField("message", err).Error("setting environment") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) - } - return nil - } - defer cancel() - - content, err := exp.ListContent(ctx) - if err != nil { - log.WithField("message", err).Error("listing content") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + output := GlobalConfig.Output + outputfile := GlobalConfig.OutputFile + + var containerContents []explorers.Content + exps := GetExplorers() + + for _, xplr := range exps { + engineContents, err := xplr.ListContent(GlobalConfig.Context) + if err != nil { + engineName := xplr.Type() + log.WithField("message", err).Errorf("listing %s content", engineName) + continue } - return nil + containerContents = append(containerContents, engineContents...) } + // Handling JSON output if strings.ToLower(output) == "json" { if outputfile != "" { - writeOutputFile(content, outputfile) + writeOutputFile(containerContents, outputfile) } else { - printAsJSON(content) + printAsJSON(containerContents) } return nil } + // Handling table output tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) defer tw.Flush() if strings.ToLower(output) == "table" { - fmt.Fprintf(tw, "NAMESPACE\tDIGEST\tSIZE\tCREATED AT\tUPDATED AT\tLABELS\n") + fmt.Fprintf(tw, "CONTAINER TYPE\tNAMESPACE\tDIGEST\tSIZE\tCREATED AT\tUPDATED AT\tLABELS\n") } - for _, c := range content { + for _, c := range containerContents { switch strings.ToLower(output) { case "json_line": printAsJSONLine(c) default: - fmt.Fprintf(tw, "%s\t%s\t%v\t%v\t%v\t%s\n", + fmt.Fprintf(tw, "%s\t%s\t%s\t%v\t%v\t%v\t%s\n", + c.ContainerType, c.Namespace, c.Digest, c.Size, @@ -436,75 +421,69 @@ var listSnapshots = cli.Command{ Description: "list snapshots for all namespaces", Flags: []cli.Flag{ cli.BoolFlag{ - Name: "no-labels", + Name: "no-labels, L", Usage: "hide snapshot labels", }, cli.BoolFlag{ - Name: "full-overlay-path", + Name: "full-overlay-path, P", Usage: "show overlay full path", }, }, Action: func(clictx *cli.Context) error { - output := clictx.GlobalString("output") - outputfile := clictx.GlobalString("outputfile") - - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - //log.Fatal(err) - log.WithField("message", err).Error("setting environment") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + output := GlobalConfig.Output + outputfile := GlobalConfig.OutputFile + + var containerSnapshotKeyInfos []explorers.SnapshotKeyInfo + exps := GetExplorers() + + for _, xplr := range exps { + engineSnapshots, err := xplr.ListSnapshots(GlobalConfig.Context) + if err != nil { + engineName := xplr.Type() + log.WithField("message", err).Errorf("listing %s snapshots", engineName) + continue } - return nil - } - defer cancel() - - ss, err := exp.ListSnapshots(ctx) - if err != nil { - //log.Fatal(err) - log.WithField("message", err).Error("listing snapshot") - if output == "json" && outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + + // Add full overlay path if requested + if clictx.Bool("full-overlay-path") { + for i := range engineSnapshots { + engineSnapshots[i].OverlayPath = filepath.Join(xplr.SnapshotRoot(engineSnapshots[i].Snapshotter), engineSnapshots[i].OverlayPath) + } } - return nil + + containerSnapshotKeyInfos = append(containerSnapshotKeyInfos, engineSnapshots...) } + // Handling JSON output if strings.ToLower(output) == "json" { if outputfile != "" { - writeOutputFile(ss, outputfile) + writeOutputFile(containerSnapshotKeyInfos, outputfile) } else { - printAsJSON(ss) + printAsJSON(containerSnapshotKeyInfos) } return nil } + // Handling Table output tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) defer tw.Flush() // Setting table output header if strings.ToLower(output) == "table" { - displayFields := "NAMESPACE\tSNAPSHOTTER\tCREATED AT\tUPDATED AT\tKIND\tNAME\tPARENT\tLAYER PATH" + displayFields := "CONTAINER TYPE\tNAMESPACE\tSNAPSHOTTER\tCREATED AT\tUPDATED AT\tKIND\tNAME\tPARENT\tLAYER PATH" if !clictx.Bool("no-labels") { displayFields = fmt.Sprintf("%s\tLABELS", displayFields) } fmt.Fprintf(tw, "%v\n", displayFields) } - for _, s := range ss { - ssfilepath := filepath.Join(exp.SnapshotRoot(s.Snapshotter), s.OverlayPath) - + for _, s := range containerSnapshotKeyInfos { switch strings.ToLower(output) { case "json_line": - s.OverlayPath = ssfilepath printAsJSONLine(s) default: - if clictx.Bool("full-overlay-path") { - s.OverlayPath = ssfilepath - } - - displayValue := fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v", + displayValue := fmt.Sprintf("%s\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v", + s.ContainerType, s.Namespace, s.Snapshotter, s.CreatedAt.Format(tsLayout), @@ -532,50 +511,66 @@ var listTasks = cli.Command{ Usage: "list tasks", Description: "list container tasks", Action: func(clictx *cli.Context) error { - output := clictx.GlobalString("output") - outputfile := clictx.GlobalString("outputfile") + output := GlobalConfig.Output + outputfile := GlobalConfig.OutputFile - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - log.WithField("message", err).Error("setting environment") - if outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + taskMap := make(map[string]explorers.Task) + exps := GetExplorers() + + for _, xplr := range exps { + engineTasks, err := xplr.ListTasks(GlobalConfig.Context) + if err != nil { + engineName := xplr.Type() + log.WithField("message", err).Errorf("listing %s tasks", engineName) + continue } - return nil - } - defer cancel() - tasks, err := exp.ListTasks(ctx) - if err != nil { - log.WithField("message", err).Error("listing task") - if outputfile != "" { - data := []string{} - writeOutputFile(data, outputfile) + for _, t := range engineTasks { + if existing, ok := taskMap[t.Name]; ok { + if existing.PID == 0 { + existing.PID = t.PID + } + if existing.Status == "" { + existing.Status = t.Status + } + taskMap[t.Name] = existing + } else { + taskMap[t.Name] = t + } } - return nil } + var containerTasks []explorers.Task + for _, task := range taskMap { + containerTasks = append(containerTasks, task) + } + + // Handling JSON output if strings.ToLower(output) == "json" { - printAsJSON(tasks) + if outputfile != "" { + writeOutputFile(containerTasks, outputfile) + } else { + printAsJSON(containerTasks) + } return nil } + // Handling table output tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) defer tw.Flush() - displayFields := "NAMESPACE\tCONTAINER ID\tCONTAINER TYPE\tPID\tSTATUS" + displayFields := "CONTAINER TYPE\tNAMESPACE\tCONTAINER ID\tPID\tSTATUS" fmt.Fprintf(tw, "%v\n", displayFields) - for _, t := range tasks { + for _, t := range containerTasks { switch strings.ToLower(output) { case "json_line": printAsJSONLine(t) default: displayValues := fmt.Sprintf("%v\t%v\t%v\t%v\t%v", + t.ContainerType, t.Namespace, t.Name, - t.ContainerType, t.PID, t.Status, ) @@ -585,34 +580,3 @@ var listTasks = cli.Command{ return nil }, } - -// labelString retruns a string of comma separated key-value pairs. -func labelString(labels map[string]string) string { - var lablestrings []string - - for k, v := range labels { - lablestrings = append(lablestrings, strings.Join([]string{k, v}, "=")) - } - return strings.Join(lablestrings, ",") -} - -// arrayToString returns a string of comma separated value of an array. -func arrayToString(array []string) string { - var result string - - for i, val := range array { - if i == 0 { - result = val - continue - } - result = fmt.Sprintf("%s,%s", result, val) - } - - return result -} - -// writeOutputFile writes JSON data to specified file. -func writeOutputFile(v interface{}, outputfile string) { - data, _ := json.Marshal(v) - ioutil.WriteFile(outputfile, data, 0644) -} diff --git a/cmd/commands/mount.go b/cmd/commands/mount.go index d464b97..2a65d4b 100644 --- a/cmd/commands/mount.go +++ b/cmd/commands/mount.go @@ -19,17 +19,38 @@ package commands import ( "fmt" "runtime" + "strings" + + "github.com/google/container-explorer/explorers" - "github.com/containerd/containerd/namespaces" log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) var MountCommand = cli.Command{ Name: "mount", - Usage: "mount a container to a mount point", - Description: "mount a container to a mount point", - ArgsUsage: "ID MOUNTPOINT", + Usage: "mount a container or all containers to a mount point", + Description: "mount a container or all containers to a mount point", + ArgsUsage: "[flag] [ID] MOUNTPOINT", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all", + Usage: "mount all containers", + }, + cli.StringFlag{ + Name: "container-engine, e", + Usage: "supported container engines are docker, containerd, and podman", + Value: "all", + }, + cli.StringFlag{ + Name: "filter, f", + Usage: "comma separated label filter using key=value pair", + }, + cli.BoolFlag{ + Name: "mount-support-containers, s", + Usage: "mount Kubernetes supporting containers", + }, + }, Action: func(clictx *cli.Context) error { // Mounting a container is only supported on a Linux operating system. @@ -37,33 +58,49 @@ var MountCommand = cli.Command{ return fmt.Errorf("mounting a container is only supported on Linux") } + if clictx.Bool("all") { + if clictx.NArg() < 1 { + return fmt.Errorf("mount point is required") + } + + mountpoint := clictx.Args().First() + containerEngine := clictx.String("container-engine") + filter := clictx.String("filter") + skipSupportContainer := !clictx.Bool("mount-support-containers") + + log.WithFields(log.Fields{ + "containerEngine": containerEngine, + "filter": filter, + "skipSupportContainer": skipSupportContainer, + }).Debug("mounting all containers") + + exps := GetExplorers() + for _, xplr := range exps { + engineName := xplr.Type() + if containerEngine == "all" || strings.ToLower(containerEngine) == engineName { + if err := xplr.MountAllContainers(GlobalConfig.Context, mountpoint, filter, skipSupportContainer); err != nil { + log.Errorf("mounting %s containers: %v", engineName, err) + } + } + } + return nil + } + + // Mount individual container if clictx.NArg() < 2 { return fmt.Errorf("container id and mount point are required") } - namespace := clictx.GlobalString("namespace") - containerid := clictx.Args().First() + containerID := clictx.Args().First() mountpoint := clictx.Args().Get(1) - log.WithFields(log.Fields{ - "namespace": namespace, - "containerid": containerid, - "mountpoint": mountpoint, - }).Debug("user provided mount options") + matched, err := ForMatchingContainer(GlobalConfig.Context, containerID, func(xplr explorers.ContainerExplorer) error { + return xplr.MountContainer(GlobalConfig.Context, containerID, mountpoint) + }) - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - return err + if !matched { + log.Errorf("container %s not found", containerID) } - defer cancel() - - ctx = namespaces.WithNamespace(ctx, namespace) - - if err := exp.MountContainer(ctx, containerid, mountpoint); err != nil { - return err - } - - // default return - return nil + return err }, } diff --git a/cmd/commands/mount_all.go b/cmd/commands/mount_all.go index 502ab25..eb9934c 100644 --- a/cmd/commands/mount_all.go +++ b/cmd/commands/mount_all.go @@ -19,7 +19,9 @@ package commands import ( "fmt" "runtime" + "strings" + log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -31,11 +33,16 @@ var MountAllCommand = cli.Command{ ArgsUsage: "[flag] MOUNT_POINT", Flags: []cli.Flag{ cli.StringFlag{ - Name: "filter", + Name: "container-engine, e", + Usage: "support container engines are docker, containerd, and podman", + Value: "all", + }, + cli.StringFlag{ + 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", }, }, @@ -50,18 +57,26 @@ var MountAllCommand = cli.Command{ } mountpoint := clictx.Args().First() + containerEngine := clictx.String("container-engine") filter := clictx.String("filter") + skipSupportContainer := !clictx.Bool("mount-support-containers") - ctx, exp, cancel, err := explorerEnvironment(clictx) - if err != nil { - return err - } - defer cancel() + log.WithFields(log.Fields{ + "containerEngine": containerEngine, + "filter": filter, + "skipSupportContainer": skipSupportContainer, + }).Debug("mounting all containers") - if err := exp.MountAllContainers(ctx, mountpoint, filter, !clictx.Bool("mount-support-containers")); err != nil { - return err + exps := GetExplorers() + for _, xplr := range exps { + engineName := xplr.Type() + if containerEngine == "all" || strings.ToLower(containerEngine) == engineName { + if err := xplr.MountAllContainers(GlobalConfig.Context, mountpoint, filter, skipSupportContainer); err != nil { + log.Errorf("mounting %s containers: %v", engineName, err) + } + } } - // default - return nil + + return nil // default }, } diff --git a/cmd/commands/utils.go b/cmd/commands/utils.go index da2e363..7c03039 100644 --- a/cmd/commands/utils.go +++ b/cmd/commands/utils.go @@ -16,7 +16,67 @@ limitations under the License. package commands -import "strings" +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/google/container-explorer/explorers" + "github.com/google/container-explorer/explorers/containerd" + "github.com/google/container-explorer/explorers/docker" + "github.com/google/container-explorer/explorers/podman" + + log "github.com/sirupsen/logrus" +) + +// GetExplorers returns a slice of all initialized container explorers. +func GetExplorers() []explorers.ContainerExplorer { + var allExplorers []explorers.ContainerExplorer + + // Docker + dkrxplr, err := docker.NewExplorer(GlobalConfig.ImageRootDir, GlobalConfig.ContainerdRootDir, GlobalConfig.DockerRootDir) + if err != nil { + log.Debugf("unable to get docker explorer: %v", err) + } else { + allExplorers = append(allExplorers, dkrxplr) + } + + // Podman + pmxplr, err := podman.NewExplorer(GlobalConfig.ImageRootDir) + if err != nil { + log.Debugf("unable to get podman explorer: %v", err) + } else { + allExplorers = append(allExplorers, pmxplr) + } + + // Containerd + ctrxplr, err := containerd.NewExplorer(GlobalConfig.ImageRootDir, GlobalConfig.ContainerdRootDir, GlobalConfig.DockerRootDir, GlobalConfig.LayerCache, GlobalConfig.SupportContainerData) + if err != nil { + log.Debugf("unable to get containerd explorer: %v", err) + } else { + allExplorers = append(allExplorers, ctrxplr) + } + + return allExplorers +} + +// ForMatchingContainer finds an explorer that has the given containerID and executes the provided function. +func ForMatchingContainer(ctx context.Context, containerID string, fn func(explorers.ContainerExplorer) error) (bool, error) { + exps := GetExplorers() + for _, exp := range exps { + container, err := exp.GetContainerByID(ctx, containerID) + if err != nil { + log.Debugf("error checking container ID %s in explorer: %v", containerID, err) + continue + } + if container != nil { + return true, fn(exp) + } + } + return false, fmt.Errorf("container %s not found", containerID) +} func getFilterMap(filter string) map[string]string { if filter == "" { @@ -32,4 +92,54 @@ func getFilterMap(filter string) map[string]string { } } return filterMap -} \ No newline at end of file +} + +func printAsJSON(v any) { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + log.Error("error marshaling to JSON", err) + return + } + + fmt.Println(string(b)) +} + +func printAsJSONLine(v any) { + b, err := json.Marshal(v) + if err != nil { + log.Error("error marshaling to json_line", err) + return + } + fmt.Println(string(b)) +} + +// labelString returns a string of comma separated key-value pairs. +func labelString(labels map[string]string) string { + var lablestrings []string + + for k, v := range labels { + lablestrings = append(lablestrings, strings.Join([]string{k, v}, "=")) + } + return strings.Join(lablestrings, ",") +} + +// arrayToString returns a string of comma separated value of an array. +func arrayToString(array []string) string { + var result string + + for i, val := range array { + if i == 0 { + result = val + continue + } + result = fmt.Sprintf("%s,%s", result, val) + } + + return result +} + +// writeOutputFile writes JSON data to specified file. +func writeOutputFile(v any, outputfile string) { + data, _ := json.Marshal(v) + os.WriteFile(outputfile, data, 0644) +} diff --git a/cmd/main.go b/cmd/main.go index c3d3f0c..3b09d72 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,7 +25,7 @@ import ( ) const ( - VERSION = "0.4.1" + VERSION = "0.6.0" ) func init() { @@ -39,35 +39,16 @@ func main() { app.Name = "container-explorer" app.Version = VERSION - app.Usage = "A standalone utility to explore container details" - app.Description = `A standalone utility to explore container details. - - Container explorer supports exploring containers managed using containerd and - docker. The utility also supports exploring containers created and managed using - Kubernetes. + app.Usage = "A standalone utility for exploring container details" + app.Description = `A standalone utility for exploring container details. + Container Explorer supports containers managed by containerd, Docker, podman, + and Kubernetes. ` app.Flags = []cli.Flag{ cli.BoolFlag{ - Name: "debug", + Name: "debug, d", Usage: "enable debug messages", }, - - // Removing the default containerd-root value - // - // A bug was discovered when analyzing docker managed containers and - // the `containerd-root` default value was set. - // - // The bug occurs only when the analysis host had containerd running - // and docker-root path is specified rather than image path. - // - // i.e container-explorer --docker-managed --docker-root - // - // Since the default containerd-root is set to /var/lib/containerd, - // container-explorer attempts access manifest in /var/lib/containerd. - // This leads to inaccurate information or issue accessing locked file. - // - // Workaround: Remove the default values in flag and specify in env.go - // as required. cli.StringFlag{ Name: "containerd-root, c", Usage: "specify containerd root directory", @@ -76,38 +57,21 @@ func main() { Name: "image-root, i", Usage: "specify mount point for a disk image", }, - cli.StringFlag{ - Name: "metadata-file, m", - Usage: "specify the path to containerd metadata file i.e. meta.db", - }, - cli.StringFlag{ - Name: "snapshot-metadata-file, s", - Usage: "specify the path to containerd snapshot metadata file i.e. metadata.db.", - }, cli.BoolFlag{ - Name: "use-layer-cache", + Name: "use-layer-cache, u", Usage: "attempt to use cached layers where layers are symlinks", }, cli.StringFlag{ - Name: "layer-cache", + Name: "layer-cache, l", Usage: "cached layer folder within the snapshot root", Value: "layers", }, cli.StringFlag{ - Name: "namespace, n", - Usage: "specify container namespace", - Value: "default", - }, - cli.BoolFlag{ - Name: "docker-managed", - Usage: "specify docker manages standalone or Kubernetes containers", - }, - cli.StringFlag{ - Name: "docker-root", - Usage: "specify docker root directory. This is only used with flag --docker-managed", + Name: "docker-root, D", + Usage: "specify docker root directory", }, cli.StringFlag{ - Name: "support-container-data", + Name: "support-container-data, s", Usage: "a yaml file containing information about support containers", }, cli.StringFlag{ @@ -124,18 +88,17 @@ func main() { app.Commands = []cli.Command{ cecommands.ListCommand, cecommands.InfoCommand, + cecommands.InspectCommand, cecommands.MountCommand, - cecommands.MountAllCommand, cecommands.DriftCommand, cecommands.ExportCommand, - cecommands.ExportAllCommand, } - app.Before = func(context *cli.Context) error { - if context.GlobalBool("debug") { + app.Before = func(clictx *cli.Context) error { + if clictx.GlobalBool("debug") { log.SetLevel(log.DebugLevel) } - return nil + return cecommands.InitializeRuntime(clictx) } err := app.Run(os.Args) diff --git a/explorers/container.go b/explorers/container.go index b9aac05..2f41a3c 100644 --- a/explorers/container.go +++ b/explorers/container.go @@ -16,11 +16,14 @@ limitations under the License. package explorers -import "github.com/containerd/containerd/containers" +import ( + "github.com/containerd/containerd/containers" +) // Container provides information about a container. type Container struct { Namespace string + Name string Hostname string ImageBase string SupportContainer bool @@ -28,7 +31,22 @@ type Container struct { ProcessID int Status string + // RuntimeInfo structure + // Name string + // Options []string + // containerd specific fields + // containers.Container structure + // ID string + // Labels map[string]string + // Image string + // Runtime RuntimeInfo + // Spec typeurl.Any + // Snapshotter string + // CreatedAt time.Time + // UpdatedAt time.Time + // Extensions map[string]typeurl.Any + // SandboxID string containers.Container // docker specific fields diff --git a/explorers/containerd/bucket.go b/explorers/containerd/bucket.go index 69c917c..60e2859 100644 --- a/explorers/containerd/bucket.go +++ b/explorers/containerd/bucket.go @@ -53,10 +53,10 @@ func getSnapshottersBucket(tx *bolt.Tx, namespace string) *bolt.Bucket { return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots) } -func getSnapshotKeyBucket(tx *bolt.Tx, namespace, snapshotter, snapshotkey string) *bolt.Bucket { - return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots, []byte(snapshotter), []byte(snapshotkey)) +func getsnapshotKeyBucket(tx *bolt.Tx, namespace, snapshotter, snapshotKey string) *bolt.Bucket { + return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots, []byte(snapshotter), []byte(snapshotKey)) } -func getOverlaySnapshotBucket(tx *bolt.Tx, snapshotkey string) *bolt.Bucket { - return getBucket(tx, bucketKeyVersion, bucketKeyObjectSnapshots, []byte(snapshotkey)) +func getOverlaySnapshotBucket(tx *bolt.Tx, snapshotKey string) *bolt.Bucket { + return getBucket(tx, bucketKeyVersion, bucketKeyObjectSnapshots, []byte(snapshotKey)) } diff --git a/explorers/containerd/containerd.go b/explorers/containerd/containerd.go index d14580c..30164b9 100644 --- a/explorers/containerd/containerd.go +++ b/explorers/containerd/containerd.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package containerd implements the ContainerExplorer interface for exploring containerd managed containers. package containerd import ( @@ -25,12 +26,13 @@ import ( "os/exec" "path/filepath" "strings" - "syscall" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/metadata" "github.com/containerd/containerd/namespaces" + "github.com/google/container-explorer/explorers" + "github.com/google/container-explorer/utils" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -39,33 +41,44 @@ import ( ) type explorer struct { - imageroot string // mounted image path - root string // containerd root - manifest string // path to manifest database file i.e. meta.db - snapshot string // path to snapshot database file i.e. metadata.db - layercache string // layer cache folder within snapshot root - mdb *bolt.DB // manifest database - sc *explorers.SupportContainer // support container structure object + imageRoot string // mounted image path + containerdRoot string + dockerRoot string + manifestFile string // path to manifest database file i.e. meta.db + snapshotFile string + layercache string // layer cache folder within snapshot root + mdb *bolt.DB // manifest database + sc *explorers.SupportContainer // support container structure object } // NewExplorer returns a ContainerExplorer interface to explore containerd. -func NewExplorer(imageroot string, root string, manifest string, snapshot string, layercache string, sc *explorers.SupportContainer) (explorers.ContainerExplorer, error) { +func NewExplorer(imageRoot string, containerdRoot string, dockerRoot string, layercache string, sc *explorers.SupportContainer) (explorers.ContainerExplorer, error) { opt := &bolt.Options{ ReadOnly: true, } - db, err := bolt.Open(manifest, 0444, opt) + + if _, err := utils.PathExists(containerdRoot); err != nil { + return nil, fmt.Errorf("contained root directory does not exist") + } + + manifestFile := filepath.Join(containerdRoot, "io.containerd.metadata.v1.bolt", "meta.db") + if _, err := utils.PathExists(manifestFile); err != nil { + return nil, fmt.Errorf("containerd manifest file meta.db does not exist") + } + + db, err := bolt.Open(manifestFile, 0444, opt) if err != nil { return &explorer{}, err } return &explorer{ - imageroot: imageroot, - root: root, - manifest: manifest, - snapshot: snapshot, - layercache: layercache, - mdb: db, - sc: sc, + imageRoot: imageRoot, + containerdRoot: containerdRoot, + dockerRoot: dockerRoot, + manifestFile: manifestFile, + layercache: layercache, + mdb: db, + sc: sc, }, nil } @@ -74,11 +87,15 @@ func NewExplorer(imageroot string, root string, manifest string, snapshot string // Containerd requires snapshot database metadata.db which is stored within // the snapshot root directory. // -// The default snapshot root directrion location for containerd is +// The default snapshot root directory location for containerd is // /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs func (e *explorer) SnapshotRoot(snapshotter string) string { - dirs, _ := filepath.Glob(filepath.Join(e.root, "*")) - snapshotRoot := "" + snapshotRoot := "unknown" + if snapshotter == "" { + return snapshotRoot + } + + dirs, _ := filepath.Glob(filepath.Join(e.containerdRoot, "*")) for _, dir := range dirs { if strings.Contains(strings.ToLower(dir), strings.ToLower(snapshotter)) { filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { @@ -95,7 +112,7 @@ func (e *explorer) SnapshotRoot(snapshotter string) string { return snapshotRoot } } - return "unknown" + return snapshotRoot } // ListNamespace returns namespaces. @@ -124,7 +141,7 @@ func (e *explorer) ListNamespaces(ctx context.Context) ([]string, error) { // // In containerd the container information is stored in metadata file meta.db. func (e *explorer) ListContainers(ctx context.Context) ([]explorers.Container, error) { - var cecontainers []explorers.Container + var ceContainers []explorers.Container nss, err := e.ListNamespaces(ctx) if err != nil { @@ -142,29 +159,28 @@ func (e *explorer) ListContainers(ctx context.Context) ([]explorers.Container, e } for _, result := range results { - cectr := convertToContainerExplorerContainer(ns, result) - cectr.ImageBase = imageBasename(cectr.Image) - cectr.SupportContainer = e.sc.IsSupportContainer(cectr) - - task, err := e.GetContainerTask(ctx, cectr) + ceCtr := convertToContainerExplorerContainer(ns, result) + ceCtr.ImageBase = imageBasename(ceCtr.Image) + ceCtr.SupportContainer = e.sc.IsSupportContainer(ceCtr) + ceTask, err := e.GetContainerTask(ctx, ceCtr) if err != nil { - log.WithField("containerid", cectr.ID).Error("failed getting container task") + log.WithField("containerID", ceCtr.ID).Error("failed getting container task") } - cectr.ProcessID = task.PID - cectr.ContainerType = task.ContainerType - cectr.Status = task.Status + ceCtr.ProcessID = ceTask.PID + ceCtr.ContainerType = ceTask.ContainerType + ceCtr.Status = ceTask.Status - cecontainers = append(cecontainers, cectr) + ceContainers = append(ceContainers, ceCtr) } } - return cecontainers, nil + return ceContainers, nil } // ListImages returns the information about content. // // In containerd, the image information is stored in metadata file meta.db. func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { - var ceimages []explorers.Image + var ceImages []explorers.Image nss, err := e.ListNamespaces(ctx) if err != nil { @@ -182,21 +198,22 @@ func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { } for _, result := range results { - ceimages = append(ceimages, explorers.Image{ + ceImages = append(ceImages, explorers.Image{ Namespace: ns, + ContainerType: "containerd", SupportContainerImage: e.sc.SupportContainerImage(imageBasename(result.Name)), Image: result, }) } } - return ceimages, nil + return ceImages, nil } // ListContent returns the information about content. // // In containerd, the content information is stored in metadata file meta.db. func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) { - var cecontent []explorers.Content + var ceContent []explorers.Content nss, err := e.ListNamespaces(ctx) if err != nil { @@ -214,14 +231,14 @@ func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) } for _, result := range results { - cecontent = append(cecontent, explorers.Content{ + ceContent = append(ceContent, explorers.Content{ Namespace: ns, Info: result, }) } } - return cecontent, nil + return ceContent, nil } // ListSnapshots returns the snapshot information. @@ -245,7 +262,7 @@ func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) // // Snapshot ID is required when mounting the container. func (e *explorer) ListSnapshots(ctx context.Context) ([]explorers.SnapshotKeyInfo, error) { - var cesnapshots []explorers.SnapshotKeyInfo + var ceSnapshots []explorers.SnapshotKeyInfo nss, err := e.ListNamespaces(ctx) if err != nil { @@ -256,14 +273,18 @@ func (e *explorer) ListSnapshots(ctx context.Context) ([]explorers.SnapshotKeyIn opts := bolt.Options{ ReadOnly: true, } - ssdb, err := bolt.Open(e.snapshot, 0444, &opts) - if err != nil { - log.WithFields(log.Fields{ - "snapshotfile": e.snapshot, - }).Error(err) + + var ssdb *bolt.DB + if e.snapshotFile != "" { + ssdb, err = bolt.Open(e.snapshotFile, 0444, &opts) + if err != nil { + log.WithFields(log.Fields{ + "snapshotFile": e.snapshotFile, + }).Error(err) + } } - store := NewSnaptshotStore(e.root, e.layercache, e.mdb, ssdb) + store := NewSnapshotStore(e.containerdRoot, e.layercache, e.mdb, ssdb) for _, ns := range nss { ctx = namespaces.WithNamespace(ctx, ns) @@ -273,36 +294,36 @@ func (e *explorer) ListSnapshots(ctx context.Context) ([]explorers.SnapshotKeyIn return nil, err } - cesnapshots = append(cesnapshots, results...) + ceSnapshots = append(ceSnapshots, results...) } - return cesnapshots, nil + return ceSnapshots, nil } // ListTasks returns container tasks status func (e *explorer) ListTasks(ctx context.Context) ([]explorers.Task, error) { - if e.imageroot == "" { - log.Error("image-root is empty. Unable to list tasks.") + if e.imageRoot == "" { + log.Error("image-root is empty: unable to list tasks") return nil, nil } // Holds container task information. - var cetasks []explorers.Task + var ceTasks []explorers.Task ctrs, err := e.ListContainers(ctx) if err != nil { return nil, fmt.Errorf("failed to list containers: %w", err) } for _, ctr := range ctrs { - cetask, err := e.GetContainerTask(ctx, ctr) + ceTask, err := e.GetContainerTask(ctx, ctr) if err != nil { return nil, fmt.Errorf("failed getting a container's task: %w", err) } - cetasks = append(cetasks, cetask) + ceTasks = append(ceTasks, ceTask) } - return cetasks, nil + return ceTasks, nil } // GetContainerTask returns container task @@ -314,96 +335,96 @@ func (e *explorer) GetContainerTask(ctx context.Context, ctr explorers.Container if err != nil { return explorers.Task{}, fmt.Errorf("failed getting container spec for %s container: %w", ctr.ID, err) } - ctrspec := v.(spec.Spec) + ctrSpec := v.(spec.Spec) - var cgroupspath string - var containertype string + var cgroupsPath string + var containerType string // Compute cgroup path for docker and containerd containers - if strings.Contains(ctrspec.Linux.CgroupsPath, "docker") { - containertype = "docker" + if strings.Contains(ctrSpec.Linux.CgroupsPath, "docker") { + containerType = "docker" // compute for docker // // Spec file `config.json` contains key cgroupsPath as `system.slice:docker:`. // The path maps on file system to `/sys/fs/cgroup/system.slice/docker-.scope`. - m := strings.Split(ctrspec.Linux.CgroupsPath, ":") + m := strings.Split(ctrSpec.Linux.CgroupsPath, ":") if len(m) != 3 { return explorers.Task{}, fmt.Errorf("expecting pattern system.slice:docker: and got %d fields", len(m)) } // docker cgroup directory i.e. system.slice - cgroupns := m[0] + cgroupNS := m[0] // container cgroup information - cgroupctrdir := fmt.Sprintf("%s-%s.scope", m[1], m[2]) + cgroupCtrDir := fmt.Sprintf("%s-%s.scope", m[1], m[2]) // abolute path to container cgroup directory - cgroupspath = filepath.Join(e.imageroot, "sys", "fs", "cgroup", cgroupns, cgroupctrdir) + cgroupsPath = filepath.Join(e.imageRoot, "sys", "fs", "cgroup", cgroupNS, cgroupCtrDir) } else { - containertype = "containerd" + containerType = "containerd" // compute for containerd // // Spec file contains "cgroupsPath": "/default/", - cgroupspath = filepath.Join(e.imageroot, "sys", "fs", "cgroup", ctrspec.Linux.CgroupsPath) + cgroupsPath = filepath.Join(e.imageRoot, "sys", "fs", "cgroup", ctrSpec.Linux.CgroupsPath) } // Verify the path actually exist on the system. // If a container is deleted then cgroup may not exist for the container - if !explorers.PathExists(cgroupspath, false) { + if !explorers.PathExists(cgroupsPath, false) { log.WithFields(log.Fields{ - "containerid": ctr.ID, - "cgroupspath": cgroupspath, + "containerID": ctr.ID, + "cgroupsPath": cgroupsPath, }).Debug("container cgroup path does not exit") return explorers.Task{ Namespace: ctr.Namespace, Name: ctr.ID, - ContainerType: containertype, + ContainerType: containerType, Status: "UNKNOWN", }, nil } - status, err := explorers.GetTaskStatus(cgroupspath) + status, err := explorers.GetTaskStatus(cgroupsPath) if err != nil { // Only print the error message. // The default return should contain status UNKNOWN - log.WithField("containerid", ctr.ID).Error("failed getting container status for container: ", err) + log.WithField("containerID", ctr.ID).Errorf("failed getting container status for container: %v", err) } // Get container process ID - ctrpid := explorers.GetTaskPID(cgroupspath) - if ctrpid == -1 && containertype == "containerd" { + ctrPID := explorers.GetTaskPID(cgroupsPath) + if ctrPID == -1 && containerType == "containerd" { state, err := e.GetContainerState(ctx, ctr) if err != nil { - log.WithField("containerid", ctr.ID).Error("failed getting container state") + log.WithField("containerID", ctr.ID).Error("failed getting container state") } if state.InitProcessPid != 0 { - ctrpid = state.InitProcessPid + ctrPID = state.InitProcessPid } } return explorers.Task{ Namespace: ctr.Namespace, Name: ctr.ID, - PID: ctrpid, - ContainerType: containertype, + PID: ctrPID, + ContainerType: containerType, Status: status, }, nil } // GetContainerState returns container runtime state func (e *explorer) GetContainerState(ctx context.Context, ctr explorers.Container) (explorers.State, error) { - statedir := filepath.Join(e.imageroot, "run", "containerd", "runc", ctr.Namespace, ctr.ID) - if !explorers.PathExists(statedir, false) { - return explorers.State{}, fmt.Errorf("container state directory %s did not exist", statedir) + stateDir := filepath.Join(e.imageRoot, "run", "containerd", "runc", ctr.Namespace, ctr.ID) + if !explorers.PathExists(stateDir, false) { + return explorers.State{}, fmt.Errorf("container state directory %s did not exist", stateDir) } - statefile := filepath.Join(statedir, "state.json") - if !explorers.PathExists(statefile, true) { - return explorers.State{}, fmt.Errorf("container state file %s did not exist", statefile) + stateFile := filepath.Join(stateDir, "state.json") + if !explorers.PathExists(stateFile, true) { + return explorers.State{}, fmt.Errorf("container state file %s did not exist", stateFile) } - data, err := os.ReadFile(statefile) + data, err := os.ReadFile(stateFile) if err != nil { return explorers.State{}, err } @@ -415,13 +436,31 @@ func (e *explorer) GetContainerState(ctx context.Context, ctr explorers.Containe return state, nil } -// InfoContainer returns container internal information. -func (e *explorer) InfoContainer(ctx context.Context, containerid string, spec bool) (interface{}, error) { +// getContainerStoreInfo finds the container across all namespaces and returns it and its namespace +func (e *explorer) getContainerStoreInfo(ctx context.Context, containerID string) (containers.Container, string, error) { + nss, err := e.ListNamespaces(ctx) + if err != nil { + return containers.Container{}, "", err + } + store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) - container, err := store.Get(ctx, containerid) + for _, ns := range nss { + nsCtx := namespaces.WithNamespace(ctx, ns) + container, err := store.Get(nsCtx, containerID) + if err == nil { + return container, ns, nil + } + } + + return containers.Container{}, "", fmt.Errorf("no matching container") +} + +// InfoContainer returns container internal information. +func (e *explorer) InfoContainer(ctx context.Context, containerID string, spec bool) (any, error) { + container, _, err := e.getContainerStoreInfo(ctx, containerID) if err != nil { - return nil, err + return nil, fmt.Errorf("getting container %s: %w", containerID, err) } if container.Spec != nil && container.Spec.GetValue() != nil { @@ -438,7 +477,7 @@ func (e *explorer) InfoContainer(ctx context.Context, containerid string, spec b // Return container and spec info return struct { containers.Container - Spec interface{} `json:"Spec,omitempty"` + Spec any `json:"Spec,omitempty"` }{ Container: container, Spec: v, @@ -550,16 +589,16 @@ func (e *explorer) resolveSnapshotter(ctx context.Context, container *containers } // MountContainer mounts a container to the specified path -func (e *explorer) MountContainer(ctx context.Context, containerid string, mountpoint string) error { - store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) - - container, err := store.Get(ctx, containerid) +func (e *explorer) MountContainer(ctx context.Context, containerID string, mountpoint string) error { + container, ns, err := e.getContainerStoreInfo(ctx, containerID) if err != nil { return fmt.Errorf("failed getting container information %v", err) } - if err := e.resolveSnapshotter(ctx, &container); err != nil { - return fmt.Errorf("failed to resolve snapshotter: %w", err) + ctx = namespaces.WithNamespace(ctx, ns) + + if err = e.resolveSnapshotter(ctx, &container); err != nil { + return fmt.Errorf("failed resolving snapshotter: %w", err) } // Snapshot database metadata.db access @@ -567,36 +606,38 @@ func (e *explorer) MountContainer(ctx context.Context, containerid string, mount ReadOnly: true, } - if e.snapshot == "" { + if e.snapshotFile == "" { snapshotterFolder := e.SnapshotRoot(container.Snapshotter) if snapshotterFolder != "unknown" { - e.snapshot = filepath.Join(snapshotterFolder, "metadata.db") + e.snapshotFile = filepath.Join(snapshotterFolder, "metadata.db") } } log.WithFields(log.Fields{ - "snapshotter": container.Snapshotter, - "snapshotKey": container.SnapshotKey, - "image": container.Image, - "snapshotterFolder": e.snapshot, - }).Debug("container snapshotter") - - ssdb, err := bolt.Open(e.snapshot, 0444, &opts) + "containerID": containerID, + "namespace": ns, + "snapshotter": container.Snapshotter, + "snapshotKey": container.SnapshotKey, + "image": container.Image, + "snapshotterFile": e.snapshotFile, + }).Debug("containerd container snapshotter") + + ssDB, err := bolt.Open(e.snapshotFile, 0444, &opts) if err != nil { - return fmt.Errorf("failed to open snapshot database %v", err) + return fmt.Errorf("failed opening %s snapshot database %v", container.Snapshotter, err) } // snapshot store - ssstore := NewSnaptshotStore(e.root, e.layercache, e.mdb, ssdb) + ssStore := NewSnapshotStore(e.containerdRoot, e.layercache, e.mdb, ssDB) var mountArgs []string hasWorkDir := false - snapshotRoot, _ := filepath.Split(e.snapshot) + snapshotRoot, _ := filepath.Split(e.snapshotFile) matches, _ := filepath.Glob(filepath.Join(snapshotRoot, "snapshots/*/work")) if len(matches) > 0 { hasWorkDir = true } if container.Snapshotter == "native" { - upperdir, err := ssstore.NativePath(ctx, container) + upperdir, err := ssStore.NativePath(ctx, container) log.WithFields(log.Fields{ "upperdir": upperdir, }).Debug("native directories") @@ -605,7 +646,7 @@ func (e *explorer) MountContainer(ctx context.Context, containerid string, mount } mountArgs = []string{"-t", "bind", upperdir, mountpoint, "-o", "rbind,ro"} } else if hasWorkDir { - lowerdir, upperdir, workdir, err := ssstore.OverlayPath(ctx, container) + lowerdir, upperdir, workdir, err := ssStore.OverlayPath(ctx, container) log.WithFields(log.Fields{ "lowerdir": lowerdir, "upperdir": upperdir, @@ -624,7 +665,7 @@ func (e *explorer) MountContainer(ctx context.Context, containerid string, mount mountopts := fmt.Sprintf("ro,lowerdir=%s:%s", upperdir, lowerdir) mountArgs = []string{"-t", "overlay", "overlay", "-o", mountopts, mountpoint} } else { - log.Error("Unsupported snapshotter ", container.Snapshotter) + log.Errorf("unsupported snapshotter: %s", container.Snapshotter) } log.Debug("container mount command ", mountArgs) @@ -632,17 +673,15 @@ func (e *explorer) MountContainer(ctx context.Context, containerid string, mount cmd := exec.Command("mount", mountArgs...) out, err := cmd.CombinedOutput() if err != nil { - log.Errorf("running mount command %v", err) + log.Errorf("running mount command: %v", err) - if strings.Contains(err.Error(), " 32") { - log.Error("invalid overlayfs lowerdir path. Use --debug to view lowerdir path") - } + log.Error("invalid overlayfs lowerdir path: use --debug to view lowerdir path") return err } if string(out) != "" { - log.Info("mount command output ", string(out)) + log.Infof("mount command output: %s", string(out)) } // default @@ -659,12 +698,17 @@ func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, fi filters := strings.Split(filter, ",") for _, ctr := range ctrs { + // Skip Docker-managed containers to avoid double mounting (as they will be mounted by the Docker explorer) + if ctr.Namespace == "moby" { + continue + } + // Skip Kubernetes suppot containers if skipsupportcontainers && ctr.SupportContainer { log.WithFields(log.Fields{ "namespace": ctr.Namespace, - "containerid": ctr.ID, - }).Info("skip mounting Kubernetes containers") + "containerID": ctr.ID, + }).Info("skipping Kubernetes containers") continue } @@ -700,16 +744,16 @@ func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, fi if err := os.MkdirAll(ctrmountpoint, 0755); err != nil { log.WithFields(log.Fields{ "namespace": ctr.Namespace, - "containerid": ctr.ID, + "containerID": ctr.ID, "mountpoint": mountpoint, }).Error("creating mount point for a container") - log.WithField("containerid", ctr.ID).Warn("skipping container mount") + log.WithField("containerID", ctr.ID).Warn("skipping container mount") continue } // Clear snapshot database for each container - e.snapshot = "" + e.snapshotFile = "" ctx = namespaces.WithNamespace(ctx, ctr.Namespace) if err := e.MountContainer(ctx, ctr.ID, ctrmountpoint); err != nil { return err @@ -720,58 +764,6 @@ func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, fi return nil } -// ScanDiffDirectory identifies added or modified files in the diff directory -func ScanDiffDirectory(diffDir string) (addedOrModified []explorers.FileInfo, inaccessibleFiles []explorers.FileInfo, err error) { - err = filepath.Walk(diffDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - if fileinfo, err := explorers.GetFileInfo(info, path, diffDir); err == nil { - inaccessibleFiles = append(inaccessibleFiles, *fileinfo) - } - - return nil // Continue walking despite the error - } - if !info.IsDir() { - fileinfo, err := explorers.GetFileInfo(info, path, diffDir) - if err != nil { - return err - } - - // Check if the file is a whiteout files - if info.Mode()&os.ModeCharDevice != 0 { - if stat, ok := info.Sys().(*syscall.Stat_t); ok { - rdev := stat.Rdev - - // Extract major and minor device numbers - major := (rdev >> 8) & 0xfff - minor := (rdev & 0xff) | ((rdev >> 12) & 0xfff00) - - if major == 0 && minor == 0 { - // This is a whiteout file - inaccessibleFiles = append(inaccessibleFiles, *fileinfo) - - return nil - } - } - } - - // Check if the file is not a symbolic link - if info.Mode()&os.ModeSymlink == 0 { - // Check if the file has executable permissions - mode := info.Mode().Perm() - if mode&0111 != 0 { - // The file is executable by owner, group, or others - fileinfo.FileType = "executable" - } - } - - addedOrModified = append(addedOrModified, *fileinfo) - } - return nil - }) - - return -} - // ContainerDrift finds drifted files from all the containers func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsupportcontainers bool, containerID string) ([]explorers.Drift, error) { var drifts []explorers.Drift @@ -783,6 +775,12 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor filters := strings.Split(filter, ",") for _, ctr := range ctrs { + // Temporary fix to remove duplicate errors for Docker engine 29 containers + // TODO: Build support for Docker engine 29. + if ctr.Namespace == "moby" { + continue + } + // If containerID is supplied & doesn't match skip if containerID != "" && ctr.ID != containerID { continue @@ -792,8 +790,8 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor if skipsupportcontainers && ctr.SupportContainer { log.WithFields(log.Fields{ "namespace": ctr.Namespace, - "containerid": ctr.ID, - }).Info("skip mounting Kubernetes containers") + "containerID": ctr.ID, + }).Info("skipping Kubernetes containers") continue } @@ -824,13 +822,14 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor continue } - e.snapshot = "" + e.snapshotFile = "" ctx = namespaces.WithNamespace(ctx, ctr.Namespace) store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) container, err := store.Get(ctx, ctr.ID) if err != nil { - return nil, fmt.Errorf("failed getting container information %v", err) + log.WithFields(log.Fields{"containerID": ctr.ID, "error": err}).Error("getting container information") + continue } if err := e.resolveSnapshotter(ctx, &container); err != nil { return nil, fmt.Errorf("failed to resolve snapshotter: %w", err) @@ -839,26 +838,27 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor opts := bolt.Options{ ReadOnly: true, } - if e.snapshot == "" { + if e.snapshotFile == "" { snapshotterFolder := e.SnapshotRoot(container.Snapshotter) if snapshotterFolder != "unknown" { - e.snapshot = filepath.Join(snapshotterFolder, "metadata.db") + e.snapshotFile = filepath.Join(snapshotterFolder, "metadata.db") } } log.WithFields(log.Fields{ "snapshotter": container.Snapshotter, "snapshotKey": container.SnapshotKey, "image": container.Image, - "snapshotterFolder": e.snapshot, + "snapshotterFolder": e.snapshotFile, }).Debug("container snapshotter") - ssdb, err := bolt.Open(e.snapshot, 0444, &opts) + ssdb, err := bolt.Open(e.snapshotFile, 0444, &opts) if err != nil { - return nil, fmt.Errorf("failed to open snapshot database %v", err) + log.WithFields(log.Fields{"containerID": ctr.ID, "error": err}).Error("failed to open snapshot database") + continue } // snapshot store - ssstore := NewSnaptshotStore(e.root, e.layercache, e.mdb, ssdb) + ssstore := NewSnapshotStore(e.containerdRoot, e.layercache, e.mdb, ssdb) hasWorkDir := false - snapshotRoot, _ := filepath.Split(e.snapshot) + snapshotRoot, _ := filepath.Split(e.snapshotFile) matches, _ := filepath.Glob(filepath.Join(snapshotRoot, "snapshots/*/work")) if len(matches) > 0 { hasWorkDir = true @@ -869,7 +869,8 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor "upperdir": upperdir, }).Debug("native directories") if err != nil { - return nil, fmt.Errorf("failed to get native path %v", err) + log.WithFields(log.Fields{"containerID": ctr.ID, "error": err}).Error("failed to get native path") + continue } } else if hasWorkDir { lowerdir, upperdir, workdir, err := ssstore.OverlayPath(ctx, container) @@ -880,19 +881,22 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor }).Debug("overlay directories") log.WithFields(log.Fields{ - "container ID": ctr.ID, - }).Debug("Checking drift for container") + "containerID": ctr.ID, + }).Debug("checking container drift") if err != nil { - return nil, fmt.Errorf("failed to get overlay path %v", err) + log.WithFields(log.Fields{"containerID": ctr.ID, "error": err}).Error("failed to get overlay path") + continue } if lowerdir == "" { - return nil, fmt.Errorf("lowerdir is empty") + log.WithFields(log.Fields{"containerID": ctr.ID}).Error("lowerdir is empty") + continue } // Scan upperdir - addedOrModified, inaccessibleFiles, err := ScanDiffDirectory(upperdir) + addedOrModified, inaccessibleFiles, err := explorers.ScanDiffDirectory(upperdir) if err != nil { - return nil, fmt.Errorf("failed to scan diff directory: %v", err) + log.WithFields(log.Fields{"containerID": ctr.ID, "error": err}).Error("failed to scan diff directory") + continue } drift := explorers.Drift{ @@ -915,7 +919,7 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor } } } else { - log.Error("Unsupported snapshotter ", container.Snapshotter) + log.Error("unsupported snapshotter ", container.Snapshotter) } } @@ -928,6 +932,30 @@ func (e *explorer) Close() error { return e.mdb.Close() } +func (e *explorer) GetContainerByID(ctx context.Context, containerID string) (*explorers.Container, error) { + container, ns, err := e.getContainerStoreInfo(ctx, containerID) + if err != nil { + return nil, err + } + + cectr := convertToContainerExplorerContainer(ns, container) + cectr.ImageBase = imageBasename(cectr.Image) + cectr.SupportContainer = e.sc.IsSupportContainer(cectr) + task, err := e.GetContainerTask(ctx, cectr) + if err != nil { + log.WithField("containerID", cectr.ID).Error("failed getting container task") + } + cectr.ProcessID = task.PID + cectr.ContainerType = task.ContainerType + cectr.Status = task.Status + + return &cectr, nil +} + +func (e *explorer) Type() string { + return "containerd" +} + // convertToContainerExplorerContainer returns a Container object which is // superset of containers.Container object. func convertToContainerExplorerContainer(ns string, ctr containers.Container) explorers.Container { @@ -962,13 +990,14 @@ func convertToContainerExplorerContainer(ns string, ctr containers.Container) ex return explorers.Container{ Namespace: ns, + Name: ctr.ID, Hostname: hostname, Container: ctr, } } // parseSpec parses containerd spec and returns the information as JSON. -func parseSpec(data []byte) (interface{}, error) { +func parseSpec(data []byte) (any, error) { var v spec.Spec json.Unmarshal(data, &v) return v, nil diff --git a/explorers/containerd/export.go b/explorers/containerd/export.go index 40b27e1..6d8cb11 100644 --- a/explorers/containerd/export.go +++ b/explorers/containerd/export.go @@ -21,8 +21,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" - "strings" "github.com/containerd/containerd/namespaces" "github.com/google/container-explorer/explorers" @@ -31,7 +29,7 @@ import ( ) // ExportContainer exports a container either as a raw image or an archive. -func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOption map[string]bool) error { +func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOptions map[string]bool) error { // Check if the specified containerID exists. containerExists := false @@ -47,7 +45,7 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp if err != nil { log.WithFields(log.Fields{ "namespace": containerNamespace, - "error": err, + "error": err, }).Warnf("error listing containers in namespace") continue @@ -66,7 +64,7 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp if !containerExists { log.WithFields(log.Fields{ "containerID": containerID, - "namespace": containerNamespace, + "namespace": containerNamespace, }).Debug("no container in namespace") continue @@ -74,9 +72,9 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp // Continue the following if a matching containerID is found. log.WithFields(log.Fields{ - "containerID": targetContainer.ID, - "name": targetContainer.Runtime.Name, - "namespace": targetContainer.Namespace, + "containerID": targetContainer.ID, + "name": targetContainer.Runtime.Name, + "namespace": targetContainer.Namespace, "containerType": targetContainer.ContainerType, }).Info("container found") @@ -98,59 +96,59 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp break } } - log.Infof("Attempting to mount container %s to %s", targetContainer.ID, mountpoint) + log.Infof("attempting to mount container %s to %s", targetContainer.ID, mountpoint) if err := e.MountContainer(ctx, targetContainer.ID, mountpoint); err != nil { // If mountpoint was created, attempt to clean it up. _ = os.Remove(mountpoint) // Best effort removal return fmt.Errorf("failed to mount container %s: %w", targetContainer.ID, err) } - log.Infof("Successfully mounted container %s to %s", targetContainer.ID, mountpoint) + log.Infof("successfully mounted container %s to %s", targetContainer.ID, mountpoint) // Defer unmount and cleanup of the mountpoint defer func() { - log.Infof("Cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) + log.Infof("cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) unmountCmd := exec.Command("umount", mountpoint) unmountCmdOutput, unmountErr := unmountCmd.CombinedOutput() // Run and get output/error if unmountErr != nil { - log.Warnf("Failed to unmount %s: %v. Output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) + log.Warnf("failed to unmount %s: %v; output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) } else { - log.Infof("Successfully unmounted %s. Output: %s", mountpoint, string(unmountCmdOutput)) + log.Infof("successfully unmounted %s; output: %s", mountpoint, string(unmountCmdOutput)) } if rmErr := os.Remove(mountpoint); rmErr != nil { - log.Warnf("Failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) + log.Warnf("failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) } else { - log.Infof("Successfully removed mountpoint directory %s", mountpoint) + log.Infof("successfully removed mountpoint directory %s", mountpoint) } }() - if exportOption["image"] { - log.Infof("Exporting container %s as a raw image to %s", targetContainer.ID, outputDir) - if err := exportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { + if exportOptions["image"] { + log.Infof("exporting container %s as a raw image to %s", targetContainer.ID, outputDir) + if err := utils.ExportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { return fmt.Errorf("failed to export container %s as raw image: %w", targetContainer.ID, err) } - log.Infof("Successfully exported container %s as a raw image.", targetContainer.ID) + log.Infof("successfully exported container %s as a raw image", targetContainer.ID) } - if exportOption["archive"] { - log.Infof("Exporting container %s as an archive to %s", targetContainer.ID, outputDir) - if err := exportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { + if exportOptions["archive"] { + log.Infof("exporting container %s as an archive to %s", targetContainer.ID, outputDir) + if err := utils.ExportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { return fmt.Errorf("failed to export container %s as archive: %w", targetContainer.ID, err) } - log.Infof("Successfully exported container %s as an archive.", targetContainer.ID) + log.Infof("successfully exported container %s as an archive", targetContainer.ID) } } if !containerExists { - log.Infof("Container %s not found containerd containers", containerID) + log.Infof("container %s not found containerd containers", containerID) } return nil } // ExportAllContainers exports all containerd containers to specified output directory. -func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOption map[string]bool, filter map[string]string, exportSupportContainers bool) error { +func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOptions map[string]bool, filter map[string]string, exportSupportContainers bool) error { containerNamespaces, err := e.ListNamespaces(ctx) if err != nil { return fmt.Errorf("listing namespaces: %w", err) @@ -163,29 +161,29 @@ func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, ex if err != nil { log.WithFields(log.Fields{ "namespace": containerNamespace, - "error": err, + "error": err, }).Warnf("error listing containers in namespace") continue } log.WithFields(log.Fields{ - "namespace": containerNamespace, + "namespace": containerNamespace, "container_count": len(containers), }).Debug("containerd containers in namespace") for _, container := range containers { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, }).Debug("processing containerd container for export") - if !exportSupportContainers && container.SupportContainer{ + if !exportSupportContainers && container.SupportContainer { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, }).Debug("skipping Kubernetes support containers") continue @@ -193,20 +191,20 @@ func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, ex if utils.IncludeContainer(container, filter) { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, }).Debug("ignoring containerd container for export") - err := e.ExportContainer(ctx, container.ID, outputDir, exportOption) + err := e.ExportContainer(ctx, container.ID, outputDir, exportOptions) if err != nil { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, - "error": err, + "error": err, }).Error("error exporting containerd container") } } @@ -216,211 +214,3 @@ func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, ex // Default return nil } - -// exportContainerImage creates a raw disk image file of a calculated size based on -// the content of the mountpoint, formats it to ext4, and saves it to outputDir. -func exportContainerImage(ctx context.Context, containerID string, mountpoint string, outputDir string) error { - // 1. Calculate the required size for the image. - contentSize, err := utils.CalculateDirectorySize(mountpoint) - if err != nil { - return fmt.Errorf("failed to calculate content size for %s: %w", mountpoint, err) - } - log.Infof("Calculated content size for %s: %d bytes", mountpoint, contentSize) - - // Add overhead for filesystem structures (e.g., 20MB base + 5% of content size for inodes, metadata) - overhead := int64(20*1024*1024) + (contentSize / 20) - imageSize := contentSize + overhead - log.Infof("Target image size for %s: %d bytes (content: %d, overhead: %d)", containerID, imageSize, contentSize, overhead) - - imageFileName := fmt.Sprintf("%s.raw", containerID) - imageFilePath := filepath.Join(outputDir, imageFileName) - - log.WithFields(log.Fields{ - "containerID": containerID, - "imageFilePath": imageFilePath, - "imageSize": imageSize, - }).Info("Preparing to create and format disk image") - - // 2. Create the image file - imgFile, err := os.Create(imageFilePath) - if err != nil { - return fmt.Errorf("failed to create image file %s: %w", imageFilePath, err) - } - - // 3. Set the image file size - if err := imgFile.Truncate(imageSize); err != nil { - imgFile.Close() // Attempt to close before returning - return fmt.Errorf("failed to truncate image file %s to size %d: %w", imageFilePath, imageSize, err) - } - - // 4. Sync and Close the file before formatting - if err := imgFile.Sync(); err != nil { - imgFile.Close() // Attempt to close before returning - log.Warnf("failed to sync image file %s after truncation: %v", imageFilePath, err) - } - if err := imgFile.Close(); err != nil { - return fmt.Errorf("failed to close image file %s before formatting: %w", imageFilePath, err) - } - log.Infof("Successfully created and sized image file: %s", imageFilePath) - - // 5. Format the image file as ext4 - log.WithFields(log.Fields{ - "imageFilePath": imageFilePath, - }).Info("Formatting image as ext4...") - - mkfsCmd := exec.CommandContext(ctx, "mkfs.ext4", "-F", "-q", imageFilePath) - mkfsOutput, err := mkfsCmd.CombinedOutput() - if err != nil { - log.WithFields(log.Fields{ - "command": mkfsCmd.String(), - "output": string(mkfsOutput), - "error": err, - }).Error("mkfs.ext4 command failed") - return fmt.Errorf("mkfs.ext4 failed for %s: %w. Output: %s", imageFilePath, err, string(mkfsOutput)) - } - - log.WithFields(log.Fields{ - "imageFilePath": imageFilePath, - "output": string(mkfsOutput), - }).Info("Successfully formatted image as ext4") - - // 6. Mount the formatted image, copy data, then unmount. - log.Infof("Preparing to copy data from %s to image %s", mountpoint, imageFilePath) - - imageMountDir, err := os.MkdirTemp(outputDir, fmt.Sprintf("%s-img-mount-*.d", containerID)) - if err != nil { - return fmt.Errorf("failed to create temporary mount directory for image %s: %w", imageFilePath, err) - } - log.Infof("Created temporary image mount directory: %s", imageMountDir) - - var loopDevice string - var imageSuccessfullyMounted bool = false - - // Defer cleanup actions in LIFO order (unmount image, detach loop, remove temp dir) - defer func() { - if imageSuccessfullyMounted { - log.Infof("Unmounting image from %s", imageMountDir) - umountCmd := exec.Command("umount", imageMountDir) // Use non-contextual command for cleanup - // Best effort unmount - if umountErr := umountCmd.Run(); umountErr != nil { - umountOutput, _ := umountCmd.CombinedOutput() // Get output for logging - log.Warnf("Failed to unmount image filesystem from %s: %v. Output: %s", imageMountDir, umountErr, string(umountOutput)) - } else { - log.Infof("Successfully unmounted image filesystem from %s", imageMountDir) - } - } - - if loopDevice != "" { - log.Infof("Detaching loop device %s for image %s", loopDevice, imageFilePath) - losetupDetachCmd := exec.Command("losetup", "-d", loopDevice) // Use non-contextual command for cleanup - // Best effort detach - if detachErr := losetupDetachCmd.Run(); detachErr != nil { - detachOutput, _ := losetupDetachCmd.CombinedOutput() // Get output for logging - log.Warnf("Failed to detach loop device %s: %v. Output: %s", loopDevice, detachErr, string(detachOutput)) - } else { - log.Infof("Successfully detached loop device %s", loopDevice) - } - } - - log.Infof("Removing temporary image mount directory %s", imageMountDir) - if err := os.RemoveAll(imageMountDir); err != nil { - log.Warnf("Failed to remove temporary image mount directory %s: %v", imageMountDir, err) - } - }() - - // 6.1. Setup loop device - log.Infof("Setting up loop device for %s", imageFilePath) - losetupCmd := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath) - loopDeviceBytes, err := losetupCmd.Output() // Use Output to capture stdout, which is the loop device path - if err != nil { - // If Output() fails, CombinedOutput() can give more info if stderr was involved - losetupCombinedOutput, _ := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath).CombinedOutput() - log.Errorf("losetup -f --show %s failed: %v. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) - return fmt.Errorf("losetup -f --show %s failed: %w. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) - } - loopDevice = strings.TrimSpace(string(loopDeviceBytes)) - if loopDevice == "" { - log.Errorf("losetup -f --show %s returned an empty loop device path.", imageFilePath) - return fmt.Errorf("losetup -f --show %s returned an empty loop device path", imageFilePath) - } - log.Infof("Image %s associated with loop device %s", imageFilePath, loopDevice) - - // 6.2. Mount the loop device - log.Infof("Mounting loop device %s to %s", loopDevice, imageMountDir) - mountImageCmd := exec.CommandContext(ctx, "mount", loopDevice, imageMountDir) - mountImageOutput, err := mountImageCmd.CombinedOutput() - if err != nil { - log.Errorf("Failed to mount %s to %s: %v. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) - return fmt.Errorf("failed to mount loop device %s to %s: %w. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) - } - imageSuccessfullyMounted = true // Set flag for deferred cleanup - log.Infof("Successfully mounted %s to %s. Output: %s", loopDevice, imageMountDir, string(mountImageOutput)) - - // 6.3. Copy content from container's mountpoint to the image's mountpoint - // Source path: mountpoint + "/." to copy contents of the directory, not the directory itself. - sourcePathFiles, _ := filepath.Glob(filepath.Join(mountpoint, "*")) - - for _, sourcePathForCopy := range sourcePathFiles { - log.Infof("Copying contents from %s to %s using 'cp -a'", sourcePathForCopy, imageMountDir) - - copyCmd := exec.Command("cp", "-a", sourcePathForCopy, imageMountDir) - copyOutput, err := copyCmd.CombinedOutput() - if err != nil { - log.Errorf("Failed to copy data from %s to %s: %v. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) - return fmt.Errorf("failed to copy data from %s to %s: %w. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) - } - log.Infof("Successfully copied data from %s to %s. Output: %s", sourcePathForCopy, imageMountDir, string(copyOutput)) - } - - // 6.4. Sync filesystem buffers to ensure all data is written to the image - log.Info("Syncing filesystem buffers for the image.") - syncCmd := exec.CommandContext(ctx, "sync") - if syncErr := syncCmd.Run(); syncErr != nil { - // This is usually not fatal but good to log. - syncOutput, _ := syncCmd.CombinedOutput() // Get output for logging - log.Warnf("sync command failed after copying to image: %v. Output: %s", syncErr, string(syncOutput)) - } else { - log.Info("Filesystem buffers synced.") - } - - log.Infof("Image %s successfully created, formatted, and populated.", imageFilePath) - - return nil -} - -// exportContainerArchive creates a .tar.gz archive of the content of the mountpoint. -func exportContainerArchive(ctx context.Context, containerID string, mountpoint string, outputDir string) error { - archiveFileName := fmt.Sprintf("%s.tar.gz", containerID) - archiveFilePath := filepath.Join(outputDir, archiveFileName) - - log.WithFields(log.Fields{ - "containerID": containerID, - "mountpoint": mountpoint, - "archiveFilePath": archiveFilePath, - }).Info("Preparing to create container archive") - - // Command: tar -czf -C . - // -c: create - // -z: gzip - // -f: file - // -C : change to directory before processing files - // .: process all files in the current directory (which is due to -C) - tarCmd := exec.CommandContext(ctx, "tar", "-czf", archiveFilePath, "-C", mountpoint, ".") - - tarOutput, err := tarCmd.CombinedOutput() - if err != nil { - log.WithFields(log.Fields{ - "command": tarCmd.String(), - "output": string(tarOutput), - "error": err, - }).Error("tar command failed") - return fmt.Errorf("failed to create archive %s: %w. Output: %s", archiveFilePath, err, string(tarOutput)) - } - - log.WithFields(log.Fields{ - "archiveFilePath": archiveFilePath, - "output": string(tarOutput), - }).Info("Successfully created container archive") - - return nil -} \ No newline at end of file diff --git a/explorers/containerd/snapshot.go b/explorers/containerd/snapshot.go index 7c022aa..55f78b1 100644 --- a/explorers/containerd/snapshot.go +++ b/explorers/containerd/snapshot.go @@ -56,7 +56,7 @@ type snapshotStore struct { // Snapshot path in snapshot database: metadata.db/v1/snapshots/ // - id - Snapshot file system ID i.e. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs // - kind - ACTIVE vs COMMITTED -func NewSnaptshotStore(root string, layercache string, db *bolt.DB, sdb *bolt.DB) *snapshotStore { +func NewSnapshotStore(root string, layercache string, db *bolt.DB, sdb *bolt.DB) *snapshotStore { return &snapshotStore{ root: root, layercache: layercache, @@ -75,7 +75,7 @@ func (s *snapshotStore) List(ctx context.Context) ([]explorers.SnapshotKeyInfo, // Overlay snapshot bucket if s.sdb == nil { - log.Warn("handle to snapshot database does not exist") + log.Debug("snapshot database handle does not exist") } var skinfos []explorers.SnapshotKeyInfo @@ -101,9 +101,10 @@ func (s *snapshotStore) List(ctx context.Context) ([]explorers.SnapshotKeyInfo, return ssbkt.ForEach(func(k1, v1 []byte) error { var ( skinfo = explorers.SnapshotKeyInfo{ - Namespace: namespace, - Snapshotter: string(k), - Key: string(k1), + ContainerType: "containerd", + Namespace: namespace, + Snapshotter: string(k), + Key: string(k1), } // snapshot key bucket that contains information about a @@ -111,7 +112,7 @@ func (s *snapshotStore) List(ctx context.Context) ([]explorers.SnapshotKeyInfo, kbkt = ssbkt.Bucket(k1) ) - if err := readMetaSnapshotKey(&skinfo, kbkt); err != nil { + if err := readMetasnapshotKey(&skinfo, kbkt); err != nil { return err } @@ -130,7 +131,7 @@ func (s *snapshotStore) List(ctx context.Context) ([]explorers.SnapshotKeyInfo, }).Info("empty metata.db snapshot key bucket") return nil } - readOverlaySnapshotKey(&skinfo, skbkt) + readOverlaysnapshotKey(&skinfo, skbkt) return nil }) @@ -155,7 +156,7 @@ func (s *snapshotStore) NativePath(ctx context.Context, container containers.Con return "", fmt.Errorf("snapshot database handler (metadata.db) is nil") } - snapshotkeys, err := s.SnapshotKeys(ctx, container) + snapshotKeys, err := s.snapshotKeys(ctx, container) if err != nil { return "", fmt.Errorf("could not get snapshot keys for container %s", container.ID) } @@ -166,13 +167,13 @@ func (s *snapshotStore) NativePath(ctx context.Context, container containers.Con ) snapshotroot = snapshotRootDir(s.root, container.Snapshotter) - // Read snapshot metadata (metadata.db) snapshotkey bucket + // Read snapshot metadata (metadata.db) snapshotKey bucket // and extract value of key "id". // // The value of "id" specifies snapshot path in overlayfs // i.e. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs if err := s.sdb.View(func(tx *bolt.Tx) error { - upperdirID, err := getSnapshotID(tx, snapshotkeys[0]) + upperdirID, err := getSnapshotID(tx, snapshotKeys[0]) if err != nil { return err } @@ -190,7 +191,7 @@ func (s *snapshotStore) OverlayPath(ctx context.Context, container containers.Co return "", "", "", fmt.Errorf("snapshot database handler (metadata.db) is nil") } - snapshotkeys, err := s.SnapshotKeys(ctx, container) + snapshotKeys, err := s.snapshotKeys(ctx, container) if err != nil { return "", "", "", fmt.Errorf("could not get snapshot keys for container %s", container.ID) } @@ -203,13 +204,13 @@ func (s *snapshotStore) OverlayPath(ctx context.Context, container containers.Co ) snapshotroot = snapshotRootDir(s.root, container.Snapshotter) - // Read snapshot metadata (metadata.db) snapshotkey bucket + // Read snapshot metadata (metadata.db) snapshotKey bucket // and extract value of key "id". // // The value of "id" specifies snapshot path in overlayfs // i.e. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs if err := s.sdb.View(func(tx *bolt.Tx) error { - upperdirID, err := getSnapshotID(tx, snapshotkeys[0]) + upperdirID, err := getSnapshotID(tx, snapshotKeys[0]) if err != nil { return err } @@ -229,7 +230,7 @@ func (s *snapshotStore) OverlayPath(ctx context.Context, container containers.Co } // compute lowerdir - for _, ssk := range snapshotkeys[1:] { + for _, ssk := range snapshotKeys[1:] { id, err := getSnapshotID(tx, ssk) if err != nil { return err @@ -262,32 +263,32 @@ func (s *snapshotStore) OverlayPath(ctx context.Context, container containers.Co return lowerdir, upperdir, workdir, nil } -// SnapshotKeys returns the snapshot keys for a container. -func (s *snapshotStore) SnapshotKeys(ctx context.Context, container containers.Container) ([]string, error) { +// snapshotKeys returns the snapshot keys for a container. +func (s *snapshotStore) snapshotKeys(ctx context.Context, container containers.Container) ([]string, error) { namespace, err := namespaces.NamespaceRequired(ctx) if err != nil { return nil, fmt.Errorf("failed to get namespace from context %v", err) } - var snapshotkeys []string + var snapshotKeys []string if err := s.db.View(func(tx *bolt.Tx) error { ssk := container.SnapshotKey for { - bkt := getSnapshotKeyBucket(tx, namespace, container.Snapshotter, ssk) + bkt := getsnapshotKeyBucket(tx, namespace, container.Snapshotter, ssk) log.WithFields(log.Fields{ "namespace": namespace, "snapshotter": container.Snapshotter, - "snapshotkey": ssk, + "snapshotKey": ssk, }).Debug("snapshot key bucket") if bkt == nil { - return fmt.Errorf("empty meta.db snapshotkey bucket") + return fmt.Errorf("empty meta.db snapshotKey bucket") } name := string(bkt.Get(bucketKeyName)) parent := string(bkt.Get(bucketKeyParent)) - snapshotkeys = append(snapshotkeys, name) + snapshotKeys = append(snapshotKeys, name) if parent == "" { break @@ -299,11 +300,11 @@ func (s *snapshotStore) SnapshotKeys(ctx context.Context, container containers.C }); err != nil { return nil, err } - return snapshotkeys, nil + return snapshotKeys, nil } -// readMetaSnapshotKey parses the snapshot key key-value pairs in meta.db -func readMetaSnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) error { +// readMetasnapshotKey parses the snapshot key key-value pairs in meta.db +func readMetasnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) error { boltutil.ReadTimestamps(bkt, &skinfo.CreatedAt, &skinfo.UpdatedAt) skinfo.Name = string(bkt.Get(bucketKeyName)) @@ -313,8 +314,8 @@ func readMetaSnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) er return nil } -// readOverlaySnapshotKey parses the snapshot key key-value pairs in metadata.db -func readOverlaySnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) error { +// readOverlaysnapshotKey parses the snapshot key key-value pairs in metadata.db +func readOverlaysnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) error { boltutil.ReadTimestamps(bkt, &skinfo.CreatedAt, &skinfo.UpdatedAt) parent := string(bkt.Get(bucketKeyParent)) @@ -355,10 +356,10 @@ func readOverlaySnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) // getSnapshotID returns snapshot key ID. // // This returns value of metadata.db/v1/snapshots//id -func getSnapshotID(tx *bolt.Tx, snapshotkey string) (uint64, error) { - bkt := getOverlaySnapshotBucket(tx, snapshotkey) +func getSnapshotID(tx *bolt.Tx, snapshotKey string) (uint64, error) { + bkt := getOverlaySnapshotBucket(tx, snapshotKey) if bkt == nil { - return 0, fmt.Errorf("empty snapshotkey bucket %s", snapshotkey) + return 0, fmt.Errorf("empty snapshotKey bucket %s", snapshotKey) } id, _ := binary.Uvarint(bkt.Get(bucketKeyID)) diff --git a/explorers/content.go b/explorers/content.go index f5e96a2..a20601e 100644 --- a/explorers/content.go +++ b/explorers/content.go @@ -20,6 +20,7 @@ import "github.com/containerd/containerd/content" // Content provides information about containers' content type Content struct { - Namespace string + Namespace string + ContainerType string // Indicate if the container is containerd, docker, podman content.Info } diff --git a/explorers/docker/config.go b/explorers/docker/config.go index d6ca365..a8987f1 100644 --- a/explorers/docker/config.go +++ b/explorers/docker/config.go @@ -22,7 +22,7 @@ import "time" type State struct { Running bool Paused bool - Resarting bool + Restarting bool OOMKilled bool RemovalInProgress bool Dead bool @@ -31,12 +31,12 @@ type State struct { Error string StartedAt time.Time FinishedAt time.Time - Health interface{} + Health any } // Config holds docker runtime config type Config struct { - ExposedPorts map[string]interface{} + ExposedPorts map[string]any Hostname string Domainname string User string @@ -49,18 +49,18 @@ type Config struct { Env []string Cmd []string Image string - Volumes interface{} - WorkingDir interface{} - EntryPoint interface{} - OnBuild interface{} + Volumes any + WorkingDir any + EntryPoint any + OnBuild any Labels map[string]string } // Bridge represents docker networks bridge structure type Bridge struct { - IPAMConfig interface{} - Links interface{} - Aliases interface{} + IPAMConfig any + Links any + Aliases any NetworkID string EndpointID string Gateway string @@ -80,26 +80,26 @@ type NetworkSettings struct { HairpinMode bool LinkLocalIPv6Address string LinkLocalIPv6PrefixLen int - Networks map[string]interface{} - Service map[string]interface{} - Ports map[string]interface{} + Networks map[string]any + Service map[string]any + Ports map[string]any SandboxKey string - SecondaryIPAddresses interface{} - SecondaryIPv6Addresses interface{} + SecondaryIPAddresses any + SecondaryIPv6Addresses any IsAnonymousEndpoint bool HasSwarmEndpoint bool } // ConfigFile represents docker config.v2.json structure type ConfigFile struct { - StreamConfig map[string]interface{} + StreamConfig map[string]any State State ID string Created time.Time Managed bool Path string Args []string - ContainerConfig map[string]interface{} + ContainerConfig map[string]any Config Config Image string NetworkSettings NetworkSettings @@ -111,8 +111,8 @@ type ConfigFile struct { RestartCount int64 HasBeenRestartedBefore bool HasBeenManuallyStopped bool - MountPoints map[string]interface{} - SecretReferences interface{} + MountPoints map[string]any + SecretReferences any AppArmorProfile string HostnamePath string HostsPath string diff --git a/explorers/docker/docker.go b/explorers/docker/docker.go index 78702ff..ee78fa9 100644 --- a/explorers/docker/docker.go +++ b/explorers/docker/docker.go @@ -14,23 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package docker implements the ContainerExplorer interface for exploring Docker managed containers. package docker import ( "context" + "encoding/binary" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" - "syscall" "time" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/images" "github.com/containerd/containerd/metadata" + "github.com/google/container-explorer/explorers" + "github.com/google/container-explorer/utils" + + "github.com/containerd/containerd/v2/core/mount" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" log "github.com/sirupsen/logrus" @@ -39,9 +44,9 @@ import ( const ( configV2Filename = "config.v2.json" - containersDirName = "containers" + containerDirName = "containers" lowerdirName = "lower" - repositoriesDirName = "image" + imageDirName = "image" repositoriesFileName = "repositories.json" storageOverlay2 = "overlay2" ) @@ -55,43 +60,74 @@ type ImageRepository struct { } type explorer struct { - root string // docker root directory - containerdroot string - manifest string - snapshot string - mdb *bolt.DB // manifest database file + imageRoot string // Image root directory + containerdRoot string // containerd root directory + dockerRoot string // Docker root directory + manifestPath string // Containerd manifest path. io.containerd.manifest.v1.bolt/meta.db + manifestDB *bolt.DB // Containerd manifest database handle + snapshotPath string // Docker29+ overlayfs snapshotter database (containerd's overlayfs snapshotter) + snapshotDB *bolt.DB // Containerd overlayfs snapshotter database handle sc *explorers.SupportContainer // support container object } // NewExplorer returns a ContainerExplorer interface to explorer docker managed // containers. -func NewExplorer(root string, containerdroot string, manifest string, snapshot string, sc *explorers.SupportContainer) (explorers.ContainerExplorer, error) { - var db *bolt.DB +func NewExplorer(imageRoot string, containerdRoot string, dockerRoot string) (explorers.ContainerExplorer, error) { + if _, err := utils.PathExists(dockerRoot); err != nil { + return nil, fmt.Errorf("docker root directory does not exist") + } + + // Checking if containerd directory exists + var mdb *bolt.DB + var sdb *bolt.DB var err error - if fileExists(containerdroot) { - opt := &bolt.Options{ - ReadOnly: true, + if containerdRoot == "" { + return nil, fmt.Errorf("containerd root directory is empty") + } + + manifestPath := filepath.Join(containerdRoot, "io.containerd.metadata.v1.bolt", "meta.db") + + if fileExists(manifestPath) { + mdb, err = bolt.Open(manifestPath, 0444, &bolt.Options{ReadOnly: true}) + if err != nil { + return &explorer{}, err } - db, err = bolt.Open(manifest, 0444, opt) + } + + // Starting with Docker version 29, Docker uses containerd's overlayfs snapshotter. + // Specifying the use of containerd overlayfs snapshotter for current implementation. + // This may change in the future. + // TODO: Better way to identify the snapshotter filesystem in use. + snapshotPath := filepath.Join(containerdRoot, "io.containerd.snapshotter.v1.overlayfs", "metadata.db") + if fileExists(snapshotPath) { + sdb, err = bolt.Open(snapshotPath, 0644, &bolt.Options{ReadOnly: true}) if err != nil { + if mdb != nil { + mdb.Close() + } return &explorer{}, err } } log.WithFields(log.Fields{ - "root": root, - "containerdroot": containerdroot, - "manifest": manifest, - "snapshot": snapshot, + "imageRootDir": imageRoot, + "containerdRootDir": containerdRoot, + "dockerRootDir": dockerRoot, + "manifestPath": manifestPath, + "snapshotPath": snapshotPath, }).Debug("new docker explorer") + sc, _ := explorers.NewSupportContainer("") + return &explorer{ - root: root, - containerdroot: containerdroot, - manifest: manifest, - snapshot: snapshot, - mdb: db, + imageRoot: imageRoot, + containerdRoot: containerdRoot, + dockerRoot: dockerRoot, + manifestPath: manifestPath, + manifestDB: mdb, + snapshotPath: snapshotPath, + snapshotDB: sdb, sc: sc, }, nil } @@ -109,8 +145,8 @@ func (e *explorer) ListNamespaces(ctx context.Context) ([]string, error) { // Namespaces in metadata file i.e. meta.db // in /var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db - if e.mdb != nil { - err := e.mdb.View(func(tx *bolt.Tx) error { + if e.manifestDB != nil { + err := e.manifestDB.View(func(tx *bolt.Tx) error { store := metadata.NewNamespaceStore(tx) results, err := store.List(ctx) if err != nil { @@ -123,47 +159,66 @@ func (e *explorer) ListNamespaces(ctx context.Context) ([]string, error) { return nil, err } } - // TODO(rmaskey): implement the function return nss, nil } +// GetContainerByID returns a Container for a given container ID. +func (e *explorer) GetContainerByID(ctx context.Context, containerID string) (*explorers.Container, error) { + containers, err := e.ListContainers(ctx) + if err != nil { + return nil, err + } + + for _, container := range containers { + if container.ID == containerID { + return &container, nil + } + } + + return nil, fmt.Errorf("no matching container") +} + +// Type returns the container runtime type, which is "docker". +func (e *explorer) Type() string { + return "docker" +} + // GetContainerIDs returns container ID -func (e *explorer) GetContainerIDs(ctx context.Context, containerdir string) ([]string, error) { - containerpaths, err := filepath.Glob(filepath.Join(containerdir, "*")) +func (e *explorer) GetContainerIDs(ctx context.Context, containerDir string) ([]string, error) { + containerPaths, err := filepath.Glob(filepath.Join(e.dockerRoot, containerDirName, "*")) if err != nil { return nil, err } - var containerids []string - for _, containerpath := range containerpaths { - _, containerid := filepath.Split(containerpath) - containerids = append(containerids, containerid) + var containerIDs []string + for _, containerPath := range containerPaths { + _, containerID := filepath.Split(containerPath) + containerIDs = append(containerIDs, containerID) } - return containerids, nil + return containerIDs, nil } // ListContainers returns container information. func (e *explorer) ListContainers(ctx context.Context) ([]explorers.Container, error) { - containersdir := filepath.Join(e.root, containersDirName) + containerDir := filepath.Join(e.dockerRoot, containerDirName) log.WithFields(log.Fields{ - "dockerroot": e.root, - "containersdir": containersdir, + "dockerRoot": e.dockerRoot, + "containerDir": containerDir, }).Debug("docker containers directory") - containerids, err := e.GetContainerIDs(ctx, containersdir) + containerIDs, err := e.GetContainerIDs(ctx, containerDir) if err != nil { return nil, err } var cecontainers []explorers.Container - for _, containerid := range containerids { - cectr, err := e.GetCEContainer(ctx, containerid) + for _, containerID := range containerIDs { + cectr, err := e.GetCEContainer(ctx, containerID) if err != nil { return nil, err } - cecontainers = append(cecontainers, cectr) } @@ -179,7 +234,7 @@ func (e *explorer) ListContainers(ctx context.Context) ([]explorers.Container, e type rootfs struct { Rfstype string `json:"type"` - DiffIds []string `json:"diff_ids"` + DiffIDs []string `json:"diff_ids"` } // Refer to struct History @@ -214,36 +269,45 @@ func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { // Docker version 2 // // Check for valid image repositories directory - repositoriesdir := filepath.Join(e.root, repositoriesDirName) - if !fileExists(repositoriesdir) { - return nil, fmt.Errorf("valid image repositories directory %s not found", repositoriesdir) + repositoriesDir := filepath.Join(e.dockerRoot, imageDirName) + if !fileExists(repositoriesDir) { + return nil, fmt.Errorf("valid image repositories directory %s not found", repositoriesDir) } - storagedirs, err := filepath.Glob(filepath.Join(repositoriesdir, "*")) + storageDirs, err := filepath.Glob(filepath.Join(repositoriesDir, "*")) if err != nil { return nil, fmt.Errorf("listing storage directories %v", err) } var ceimages []explorers.Image - for _, storagedir := range storagedirs { - _, storagename := filepath.Split(storagedir) - repositoriesfile := filepath.Join(storagedir, repositoriesFileName) + for _, storageDir := range storageDirs { + _, storageName := filepath.Split(storageDir) + repositoriesFile := filepath.Join(storageDir, repositoriesFileName) log.WithFields(log.Fields{ - "storagename": storagename, - "storagedir": storagedir, - "repositoriesfile": repositoriesfile, + "storageName": storageName, + "storageDir": storageDir, + "repositoriesFile": repositoriesFile, }).Debug("image repository file") - data, err := os.ReadFile(repositoriesfile) + data, err := os.ReadFile(repositoriesFile) if err != nil { - return nil, fmt.Errorf("failed read repository file %v. %v", repositoriesfile, err) + log.WithFields(log.Fields{ + "storageName": storageName, + "repositoriesFile": repositoriesFile, + "error": err, + }).Debug("repositories.json does not exist") + continue } var r ImageRepository if err := json.Unmarshal(data, &r); err != nil { - return nil, fmt.Errorf("unmarshalling image repository file %s. %v", repositoriesfile, err) + log.WithFields(log.Fields{ + "repositoriesFile": repositoriesFile, + "message": err, + }).Debug("unmarshalling repositories.json") + continue } for _, distvalue := range r.Repositories { @@ -255,16 +319,17 @@ func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { }, } - if storagename == storageOverlay2 { - imagecontent, err := readImageContent(storagename, storagedir, image.Target.Digest) + if storageName == storageOverlay2 { + imageContent, err := readImageContent(storageName, storageDir, image.Target.Digest) if err != nil { - log.Error("reading image content file ", err) + log.Errorf("reading image content file: %v", err) } else { - image.CreatedAt = imagecontent.Created + image.CreatedAt = imageContent.Created } } ceimages = append(ceimages, explorers.Image{ + ContainerType: "docker", Image: image, SupportContainerImage: e.sc.SupportContainerImage(imageBasename(image.Name)), }) @@ -278,7 +343,7 @@ func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { // ListContent returns content information. func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) { // TODO(rmaskey): implement the function - fmt.Printf("INFO: listing content not implemented\n\n") + log.Info("listing docker content not implemented") return nil, nil } @@ -286,83 +351,142 @@ func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) // ListSnapshots returns snapshot information. func (e *explorer) ListSnapshots(ctx context.Context) ([]explorers.SnapshotKeyInfo, error) { // TODO(rmaskey): implement the function - fmt.Printf("INFO: listing snapshots not implemented\n\n") + log.Info("listing docker snapshots is not implemented") return nil, nil } // ListTasks returns container task status func (e *explorer) ListTasks(cxt context.Context) ([]explorers.Task, error) { - // TODO(rmaskey): implement the function - fmt.Printf("INFO: listing task status not implemented\n\n") - var tasks []explorers.Task + + containerPaths, err := filepath.Glob(filepath.Join(e.dockerRoot, "containers", "*")) + if err != nil { + return nil, fmt.Errorf("listing docker container directories: %w", err) + } + + for _, containerPath := range containerPaths { + configFile := filepath.Join(containerPath, "config.v2.json") + + configData, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("reading container config: %w", err) + } + + var config ConfigFile + if err := json.Unmarshal(configData, &config); err != nil { + return nil, fmt.Errorf("unmarshalling config.v2.json: %w", err) + } + + var status string + if config.State.Paused { + status = "paused" + } else if config.State.Running { + status = "running" + } + + task := explorers.Task{ + ContainerType: "docker", + Name: config.ID, + PID: int(config.State.Pid), + Status: status, + } + + tasks = append(tasks, task) + } + return tasks, nil } // InfoContainer returns container internal information. -func (e *explorer) InfoContainer(ctx context.Context, containerid string, spec bool) (interface{}, error) { - // TODO(rmaskey): implement the function - fmt.Printf("INFO: container info not implemented\n\n") +func (e *explorer) InfoContainer(ctx context.Context, containerID string, spec bool) (any, error) { + _, err := e.GetContainerByID(ctx, containerID) + if err != nil { + return nil, fmt.Errorf("getting container %s: %w", containerID, err) + } - return nil, nil + container, err := e.ReadContainerConfig(ctx, containerID) + if err != nil { + return nil, fmt.Errorf("reading container config: %w", err) + } + + return container, nil } -// MountContainer mounts a container to the specified path -func (e *explorer) MountContainer(ctx context.Context, containerid string, mountpoint string) error { - container, err := e.GetContainer(ctx, containerid) +// MountContainer mounts a container's filesystem layers to the specified mountpoint path. +func (e *explorer) MountContainer(ctx context.Context, containerID string, mountpoint string) error { + container, err := e.ReadContainerConfig(ctx, containerID) if err != nil { - return fmt.Errorf("getting container %v", err) + return fmt.Errorf("reading container config: %w", err) + } + + switch container.Driver { + case "overlay2": + return e.mountDockerV2Container(ctx, container, containerID, mountpoint) + case "overlayfs": + return e.mountDockerV29Container(ctx, container, containerID, mountpoint) + default: + return fmt.Errorf("unsupported storage driver: %s", container.Driver) } - containerMountIDPath := filepath.Join(e.root, repositoriesDirName, container.Driver, "layerdb", "mounts", containerid, "mount-id") +} + +// mountDockerV2Container mounts a container to the specified path +func (e *explorer) mountDockerV2Container(ctx context.Context, container ConfigFile, containerID string, mountpoint string) error { + containerMountIDPath := filepath.Join(e.dockerRoot, imageDirName, container.Driver, "layerdb", "mounts", containerID, "mount-id") log.WithField("containerMountIDPath", containerMountIDPath).Debug("container mount-id path") mountIDByte, err := os.ReadFile(containerMountIDPath) if err != nil { return fmt.Errorf("reading container mount-id") } - mountID := string(mountIDByte) + mountID := strings.TrimSpace(string(mountIDByte)) log.WithField("mount-id", mountID).Debug("container mount-id") // build container lower directory - lowerdirpath := filepath.Join(e.root, container.Driver, mountID, lowerdirName) + lowerdirpath := filepath.Join(e.dockerRoot, container.Driver, mountID, lowerdirName) log.WithField("lowerdirpath", lowerdirpath).Debug("container lowerdir path") data, err := os.ReadFile(lowerdirpath) if err != nil { return fmt.Errorf("reading lower file %v", err) } - var lowerdir string - for i, ldir := range strings.Split(string(data), ":") { - ldirpath := filepath.Join(e.root, container.Driver, ldir) - if i == 0 { - lowerdir = ldirpath - continue - } - lowerdir = fmt.Sprintf("%s:%s", lowerdir, ldirpath) + // Computing lowerdir for mounting + var lowerDirs []string + for _, ldir := range strings.Split(strings.TrimSpace(string(data)), ":") { + lowerDirs = append(lowerDirs, filepath.Join(e.dockerRoot, container.Driver, ldir)) } + lowerDir := strings.Join(lowerDirs, ":") - upperdir := filepath.Join(e.root, container.Driver, mountID, "diff") - workdir := filepath.Join(e.root, container.Driver, mountID, "work") + // Getting upperdir for mounting + upperData, err := os.ReadFile(filepath.Join(e.dockerRoot, container.Driver, mountID, "link")) + if err != nil { + return fmt.Errorf("reading link file %v", err) + } + upperDir := filepath.Join(e.dockerRoot, container.Driver, "l", strings.TrimSpace(string(upperData))) log.WithFields(log.Fields{ - "lowerdir": lowerdir, - "upperdir": upperdir, - "workdir": workdir, + "lowerdir": lowerDir, + "upperdir": upperDir, }).Debug("container overlay directories") // mounting container - mountopts := fmt.Sprintf("ro,lowerdir=%s:%s", upperdir, lowerdir) + mountopts := fmt.Sprintf("ro,lowerdir=%s:%s", upperDir, lowerDir) mountargs := []string{"-t", "overlay", "overlay", "-o", mountopts, mountpoint} cmd := exec.Command("mount", mountargs...) out, err := cmd.CombinedOutput() if err != nil { - log.Errorf("running mount command %v", mountargs) + log.Errorf("running mount command: %v", mountargs) if strings.Contains(err.Error(), " 32") { - return fmt.Errorf("invalid lowerdir path %v. Use --debug to view lowerdir path", err) + if string(out) != "" { + return fmt.Errorf("invalid lowerdir path %v; output: %s", err, strings.TrimSpace(string(out))) + } + return fmt.Errorf("invalid lowerdir path %v: use --debug to view lowerdir path", err) + } + if string(out) != "" { + return fmt.Errorf("executing mount command %v; output: %s", err, strings.TrimSpace(string(out))) } return fmt.Errorf("executing mount command %v", err) } @@ -374,34 +498,181 @@ func (e *explorer) MountContainer(ctx context.Context, containerid string, mount return nil } +func (e *explorer) GetOverlayfsLayers(namespace string, containerID string) (string, []string, error) { + var overlayPath string + var activeBucketName string + var activeID uint64 + var upperPath string + var lowerPaths []string + + if e.snapshotDB == nil { + return "", nil, fmt.Errorf("no access to snapshot database %s", e.snapshotPath) + } + + overlayPath = filepath.Dir(e.snapshotPath) + + err := e.snapshotDB.View(func(tx *bolt.Tx) error { + // Open the top-level "v1" Bucket + v1Bucket := tx.Bucket([]byte("v1")) + if v1Bucket == nil { + return fmt.Errorf("top-level bucket v1 not found in database") + } + + // Open the "snapshots" sub-bucket + snapshotsBucket := v1Bucket.Bucket([]byte("snapshots")) + if snapshotsBucket == nil { + return fmt.Errorf("level-two bucket snapshots not found in database") + } + + // 1. Local snapshot bucket matching the container ID. + // Match namespace if provided otherwise just match container ID. + // The key/bucket name format in bbolt is typically "/" + var matches []string + err := snapshotsBucket.ForEach(func(k, v []byte) error { + if v == nil { // Sub-bucket + sName := string(k) + parts := strings.Split(sName, "/") + if len(parts) == 3 && parts[0] == namespace { + if parts[2] == containerID { + activeBucketName = sName + return nil + } + // Allow matching short container ID (prefix match) + if strings.HasPrefix(parts[2], containerID) { + matches = append(matches, sName) + } + } + } + + return nil + }) + if err != nil { + return fmt.Errorf("checking snapshot bucket: %w", err) + } + + if activeBucketName == "" { + if len(matches) == 0 { + return fmt.Errorf("snapshot matching %q not found in namespace %q", containerID, namespace) + } else if len(matches) > 1 { + return fmt.Errorf("multiple snapshots match prefix %q in namespace %q: %v", containerID, namespace, matches) + } + activeBucketName = matches[0] + } + + // 2. Trace the parent chain to reconstruct the overlay layers + currentBucketName := activeBucketName + visited := make(map[string]bool) + for currentBucketName != "" { + if visited[currentBucketName] { + return fmt.Errorf("cycle detected in snapshot parent chain at %q", currentBucketName) + } + visited[currentBucketName] = true + + b := snapshotsBucket.Bucket([]byte(currentBucketName)) + if b == nil { + return fmt.Errorf("failed to open snapshot bucket: %s", currentBucketName) + } + + idBytes := b.Get([]byte("id")) + if len(idBytes) == 0 { + return fmt.Errorf("key id not found in for snapshot %q", currentBucketName) + } + dirID, _ := binary.Uvarint(idBytes) + + parentBytes := b.Get([]byte("parent")) + parentName := string(parentBytes) + + if currentBucketName == activeBucketName { + activeID = dirID + } else { + fsPath := filepath.Join(overlayPath, "snapshots", fmt.Sprintf("%d", dirID), "fs") + lowerPaths = append(lowerPaths, fsPath) + } + + currentBucketName = parentName + } + + return nil + }) + + if err != nil { + return "", nil, fmt.Errorf("getting snapshot: %w", err) + } + + activeSnapDir := filepath.Join(overlayPath, "snapshots", fmt.Sprintf("%d", activeID)) + upperPath = filepath.Join(activeSnapDir, "fs") + + return upperPath, lowerPaths, nil +} + +// mountDockerV29Container mounts container layers for Docker version 29+ using containerd's overlayfs snapshotter metadata. +func (e *explorer) mountDockerV29Container(ctx context.Context, container ConfigFile, containerID string, mountpoint string) error { + upperdir, lowerPaths, err := e.GetOverlayfsLayers("moby", containerID) + if err != nil { + return fmt.Errorf("getting overlay layers: %w", err) + } + + // Checking mount point + absMountPoint, err := filepath.Abs(mountpoint) + if err != nil { + return fmt.Errorf("failed to resolve absolute mount path: %w", err) + } + + // Create the mountpoint if it does not exist + if _, err := os.Stat(absMountPoint); os.IsNotExist(err) { + if err := os.MkdirAll(absMountPoint, 0o755); err != nil { + return fmt.Errorf("failed to create mount point directory: %w", err) + } + } + + // Prepare standard readonly overlayfs mount options + options := []string{ + "ro", + fmt.Sprintf("lowerdir=%s:%s", upperdir, strings.Join(lowerPaths, ":")), + } + mounts := []mount.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: options, + }, + } + + if err := mount.All(mounts, absMountPoint); err != nil { + return fmt.Errorf("read-only overlay mount failed: %w", err) + } + + return nil +} + // MountAllContainers mounts all the containers func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, filter string, skipsupportcontainers bool) error { - containersdir := filepath.Join(e.root, containersDirName) - log.WithField("containersdir", containersdir).Debug("docker containers directory") + containerDir := filepath.Join(e.dockerRoot, containerDirName) + log.WithField("containerDir", containerDir).Debug("docker containers directory") - containerids, err := e.GetContainerIDs(ctx, containersdir) + containerIDs, err := e.GetContainerIDs(ctx, containerDir) if err != nil { return fmt.Errorf("failed listing containers ID %v", err) } - if containerids == nil { + if containerIDs == nil { return fmt.Errorf("no container ID returned") } filters := strings.Split(filter, ",") - for _, containerid := range containerids { - cecontainer, err := e.GetCEContainer(ctx, containerid) + for _, containerID := range containerIDs { + cecontainer, err := e.GetCEContainer(ctx, containerID) if err != nil { - log.WithField("containerid", containerid).Error("getting container details") - log.WithField("containerid", containerid).Warn("skipping container mount") + log.WithField("containerID", containerID).Error("getting container details") + log.WithField("containerID", containerID).Warn("skipping container mount") continue } if skipsupportcontainers && cecontainer.SupportContainer { log.WithFields(log.Fields{ "namespace": cecontainer.Namespace, - "containerid": cecontainer.ID, - }).Info("skip mounting Kubernetes support container") + "containerID": cecontainer.ID, + }).Info("skipping Kubernetes support container") continue } @@ -436,16 +707,16 @@ func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, fi if err := os.MkdirAll(ctrmountpoint, 0755); err != nil { log.WithFields(log.Fields{ "namespace": cecontainer.Namespace, - "containerid": cecontainer.ID, + "containerID": cecontainer.ID, "mountpoint": ctrmountpoint, }).Error("creating mountpoint for container") - log.WithField("containerid", containerid).Warn("skippoing container mount") + log.WithField("containerID", containerID).Warn("skippoing container mount") continue } - if err := e.MountContainer(ctx, containerid, ctrmountpoint); err != nil { + if err := e.MountContainer(ctx, containerID, ctrmountpoint); err != nil { log.WithFields(log.Fields{ - "containerid": containerid, + "containerID": containerID, "message": err.Error(), }).Error("mounting container") } @@ -455,82 +726,29 @@ func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, fi return nil } -// ScanDiffDirectory identifies added or modified files in the diff directory -func ScanDiffDirectory(diffDir string) (addedOrModified []explorers.FileInfo, inaccessibleFiles []explorers.FileInfo, err error) { - err = filepath.Walk(diffDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - fileinfo, err := explorers.GetFileInfo(info, path, diffDir) - if err != nil { - return err - } - - inaccessibleFiles = append(inaccessibleFiles, *fileinfo) - - return nil // Continue walking despite the error - } - if !info.IsDir() { - fileinfo, err := explorers.GetFileInfo(info, path, diffDir) - if err != nil { - return err - } - - // Check if the file is a whiteout files - if info.Mode()&os.ModeCharDevice != 0 { - if stat, ok := info.Sys().(*syscall.Stat_t); ok { - rdev := stat.Rdev - - // Extract major and minor device numbers - major := (rdev >> 8) & 0xfff - minor := (rdev & 0xff) | ((rdev >> 12) & 0xfff00) - - if major == 0 && minor == 0 { - // Whiteout file - inaccessibleFiles = append(inaccessibleFiles, *fileinfo) - - return nil - } - } - } - - // Check if the file is not a symbolic link - if info.Mode()&os.ModeSymlink == 0 { - // Check if the file has executable permissions - mode := info.Mode().Perm() - if mode&0111 != 0 { - // The file is executable by owner, group, or others - fileinfo.FileType = "executable" - } - } - - addedOrModified = append(addedOrModified, *fileinfo) - } - return nil - }) - - return -} - // ContainerDrift finds drifted files from all the containers func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsupportcontainers bool, containerID string) ([]explorers.Drift, error) { var drifts []explorers.Drift - containersdir := filepath.Join(e.root, containersDirName) - log.WithField("containersdir", containersdir).Debug("docker containers directory") + containerDir := filepath.Join(e.dockerRoot, containerDirName) + log.WithField("containerDir", containerDir).Debug("docker containers directory") - containerids, err := e.GetContainerIDs(ctx, containersdir) + containerIDs, err := e.GetContainerIDs(ctx, containerDir) if err != nil { - return nil, fmt.Errorf("failed listing containers ID %v", err) + return nil, fmt.Errorf("failed listing container IDs %v", err) } - if containerids == nil { - return nil, fmt.Errorf("no container ID returned") + if containerIDs == nil { + return nil, fmt.Errorf("no container IDs returned") } filters := strings.Split(filter, ",") - for _, containerid := range containerids { - cecontainer, err := e.GetCEContainer(ctx, containerid) + for _, id := range containerIDs { + cecontainer, err := e.GetCEContainer(ctx, id) if err != nil { - log.WithField("containerid", containerid).Error("getting container details") - log.WithField("containerid", containerid).Warn("skipping container mount") + log.WithFields(log.Fields{ + "containerID": id, + "message": err.Error(), + }).Warn("unable to get container details. Skipping container mount") continue } @@ -542,8 +760,8 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor if skipsupportcontainers && cecontainer.SupportContainer { log.WithFields(log.Fields{ "namespace": cecontainer.Namespace, - "containerid": cecontainer.ID, - }).Info("skip mounting Kubernetes support container") + "containerID": cecontainer.ID, + }).Info("skipping Kubernetes support container") continue } @@ -573,64 +791,67 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor continue } - container, err := e.GetContainer(ctx, cecontainer.ID) - if err != nil { - return nil, fmt.Errorf("getting container %v", err) - } - - containerMountIDPath := filepath.Join(e.root, repositoriesDirName, container.Driver, "layerdb", "mounts", cecontainer.ID, "mount-id") - log.WithField("containerMountIDPath", containerMountIDPath).Debug("container mount-id path") - - mountIDByte, err := os.ReadFile(containerMountIDPath) + container, err := e.ReadContainerConfig(ctx, cecontainer.ID) if err != nil { - return nil, fmt.Errorf("reading container mount-id") + log.WithFields(log.Fields{"containerID": cecontainer.ID, "error": err}).Error("getting container") + continue } - mountID := string(mountIDByte) - log.WithField("mount-id", mountID).Debug("container mount-id") + // Container upper directory for drift scanning + var upperDir string - // build container lower directory - lowerdirpath := filepath.Join(e.root, container.Driver, mountID, lowerdirName) - log.WithField("lowerdirpath", lowerdirpath).Debug("container lowerdir path") - data, err := os.ReadFile(lowerdirpath) - if err != nil { - return nil, fmt.Errorf("reading lower file %v", err) - } + switch container.Driver { + case "overlay2": + containerMountIDPath := filepath.Join(e.dockerRoot, imageDirName, container.Driver, "layerdb", "mounts", container.ID, "mount-id") - var lowerdir string - for i, ldir := range strings.Split(string(data), ":") { - ldirpath := filepath.Join(e.root, container.Driver, ldir) - if i == 0 { - lowerdir = ldirpath + mountIDByte, err := os.ReadFile(containerMountIDPath) + if err != nil { + log.WithFields(log.Fields{ + "containerID": container.ID, + "message": err, + }).Info("reading container mount-id") continue } - lowerdir = fmt.Sprintf("%s:%s", lowerdir, ldirpath) - } + mountID := strings.TrimSpace(string(mountIDByte)) - upperdir := filepath.Join(e.root, container.Driver, mountID, "diff") - workdir := filepath.Join(e.root, container.Driver, mountID, "work") + upperDirLinkFile := filepath.Join(e.dockerRoot, container.Driver, mountID, "link") - log.WithFields(log.Fields{ - "lowerdir": lowerdir, - "upperdir": upperdir, - "workdir": workdir, - }).Debug("container overlay directories") + linkData, err := os.ReadFile(upperDirLinkFile) + if err != nil { + log.WithFields(log.Fields{ + "containerID": container.ID, + "message": err, + }).Info("reading upperdir link file") + continue + } + upperDir = filepath.Join(e.dockerRoot, container.Driver, "l", strings.TrimSpace(string(linkData))) - log.WithFields(log.Fields{ - "container ID": cecontainer.ID, - }).Debug("checking drift for container") - if err != nil { - return nil, fmt.Errorf("failed to get overlay path %v", err) - } + case "overlayfs": + upperDir, _, err = e.GetOverlayfsLayers("moby", container.ID) + if err != nil { + log.WithFields(log.Fields{ + "containerID": container.ID, + "error": err, + }).Info("getting upperdir snapshot") + continue + } - if lowerdir == "" { - return nil, fmt.Errorf("lowerdir is empty") + default: + log.WithField("containerID", container.ID).Warn("unable to find upperdir") + log.WithFields(log.Fields{ + "containerType": e.Type(), + "containerID": container.ID, + "driver": container.Driver, + }).Info("unsupported driver") + upperDir = "" + continue } - // Scan upperdir - addedOrModified, inaccessibleFiles, err := ScanDiffDirectory(upperdir) + // ScanDiff + addedOrModified, inaccessibleFiles, err := explorers.ScanDiffDirectory(upperDir) if err != nil { - return nil, fmt.Errorf("failed to scan diff directory: %v", err) + log.WithFields(log.Fields{"containerID": container.ID, "error": err}).Error("failed to scan diff directory") + continue } drift := explorers.Drift{ ContainerID: cecontainer.ID, @@ -657,18 +878,35 @@ func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsuppor // Close releases internal resources. func (e *explorer) Close() error { - return e.mdb.Close() + var errs []string + // Close handle to io.containerd.manifest.v1.bolt.v1/meta.db + if e.manifestDB != nil { + if err := e.manifestDB.Close(); err != nil { + errs = append(errs, fmt.Sprintf("closing manifestDB: %v", err)) + } + } + + // Close handle to io.containerd.snapshotter.overlayfs.v1/metadata.db + if e.snapshotDB != nil { + if err := e.snapshotDB.Close(); err != nil { + errs = append(errs, fmt.Sprintf("closing snapshotDB: %v", err)) + } + } + if len(errs) > 0 { + return fmt.Errorf("close errors: %s", strings.Join(errs, "; ")) + } + return nil } -// GetContainer returns container configuration -func (e *explorer) GetContainer(ctx context.Context, containerid string) (ConfigFile, error) { - containerdir := filepath.Join(e.root, containersDirName, containerid) - log.WithField("containerdir", containerdir).Debug("container directory") - if !fileExists(containerdir) { +// ReadContainerConfig returns container configuration +func (e *explorer) ReadContainerConfig(ctx context.Context, containerID string) (ConfigFile, error) { + containerDir := filepath.Join(e.dockerRoot, containerDirName, containerID) + log.WithField("containerDir", containerDir).Debug("container directory") + if !fileExists(containerDir) { return ConfigFile{}, fmt.Errorf("container does not exist") } - containerConfigFile := filepath.Join(containerdir, configV2Filename) + containerConfigFile := filepath.Join(containerDir, configV2Filename) log.WithField("containerConfigFile", containerConfigFile).Debug("container configuration file") if !fileExists(containerConfigFile) { return ConfigFile{}, fmt.Errorf("container config file %s does not exist", configV2Filename) @@ -688,13 +926,13 @@ func (e *explorer) GetContainer(ctx context.Context, containerid string) (Config } // GetCEContainer returns ContainerExplorer container -func (e *explorer) GetCEContainer(ctx context.Context, containerid string) (explorers.Container, error) { +func (e *explorer) GetCEContainer(ctx context.Context, containerID string) (explorers.Container, error) { if imagerepo == nil { imagerepo, _ = e.GetRepositories(ctx) } // Get docker container configuration based on container ID - config, err := e.GetContainer(ctx, containerid) + config, err := e.ReadContainerConfig(ctx, containerID) if err != nil { return explorers.Container{}, err } @@ -708,13 +946,22 @@ func (e *explorer) GetCEContainer(ctx context.Context, containerid string) (expl } } - // Extrac imagebase name from image name + // Extract imagebase name from image name + if strings.HasPrefix(config.Name, "/") { + cectr.Name = strings.Replace(config.Name, "/", "", -1) + } else { + cectr.Name = config.Name + } + cectr.ImageBase = imageBasename(cectr.Image) - cectr.SupportContainer = e.sc.IsSupportContainer(cectr) + + // Support container is only relevant for GKE running containerd. + cectr.SupportContainer = false return cectr, nil } +// fileExists checks if a file or directory exists at the given path. func fileExists(path string) bool { _, err := os.Stat(path) return err == nil @@ -722,30 +969,30 @@ func fileExists(path string) bool { // GetRepositories returns mapping of image ID to name func (e *explorer) GetRepositories(ctx context.Context) (map[string]string, error) { - repositoriesdir := filepath.Join(e.root, repositoriesDirName) - if !fileExists(repositoriesdir) { - return nil, fmt.Errorf("image repository directory %s does not exist", repositoriesdir) + repositoriesDir := filepath.Join(e.dockerRoot, imageDirName) + if !fileExists(repositoriesDir) { + return nil, fmt.Errorf("image repository directory %s does not exist", repositoriesDir) } - storagedirs, err := filepath.Glob(filepath.Join(repositoriesdir, "*")) + storageDirs, err := filepath.Glob(filepath.Join(repositoriesDir, "*")) if err != nil { - return nil, fmt.Errorf("listing storage directories. %v", err) + return nil, fmt.Errorf("listing storage directories: %v", err) } - for _, storagedir := range storagedirs { - _, storagename := filepath.Split(storagedir) + for _, storageDir := range storageDirs { + _, storageName := filepath.Split(storageDir) - if storagename != "overlay2" { + if storageName != "overlay2" { // TODO(rmaskey): handle other storage - log.Warn("storage ", storagename, " currently not supported") + log.WithField("storageName", storageName).Info("storage not supported") continue } // Handle overlay2 storage - repositoriesfile := filepath.Join(storagedir, repositoriesFileName) - data, err := os.ReadFile(repositoriesfile) + repositoriesFile := filepath.Join(storageDir, repositoriesFileName) + data, err := os.ReadFile(repositoriesFile) if err != nil { - return nil, fmt.Errorf("failed reading repositories file %s. %v", repositoriesfile, err) + return nil, fmt.Errorf("failed reading repositories file %s: %v", repositoriesFile, err) } var r ImageRepository @@ -778,11 +1025,11 @@ func (e *explorer) GetRepositories(ctx context.Context) (map[string]string, erro // convertToContainerExplorerContainer maps docker config data to container // explorer container structure func convertToContainerExplorerContainer(config ConfigFile) explorers.Container { - var exposedports []string + var exposedPorts []string if config.Config.ExposedPorts != nil { for k := range config.Config.ExposedPorts { - exposedports = append(exposedports, k) + exposedPorts = append(exposedPorts, k) } } @@ -801,8 +1048,14 @@ func convertToContainerExplorerContainer(config ConfigFile) explorers.Container status = "STOPPED" } + var containerName string + if strings.HasPrefix(config.Name, "/") { + containerName = strings.Replace(config.Name, "/", "", 1) + } + return explorers.Container{ - Hostname: config.Config.Hostname, + Name: containerName, + Hostname: containerName, ProcessID: int(config.State.Pid), ContainerType: "docker", Container: containers.Container{ @@ -815,13 +1068,13 @@ func convertToContainerExplorerContainer(config ConfigFile) explorers.Container }, }, Running: config.State.Running, - ExposedPorts: exposedports, + ExposedPorts: exposedPorts, Status: status, } } // readImageContent reads the content of overlay2 image content -func readImageContent(storagename string, storagepath string, digest digest.Digest) (imageContentSummary, error) { +func readImageContent(storageName string, storagePath string, digest digest.Digest) (imageContentSummary, error) { m := strings.Split(string(digest), ":") if len(m) != 2 { return imageContentSummary{}, fmt.Errorf("expecting two colon separated values") @@ -829,42 +1082,42 @@ func readImageContent(storagename string, storagepath string, digest digest.Dige algo := m[0] filename := m[1] - imagecontentfile := filepath.Join(storagepath, "imagedb", "content", algo, filename) + imageContentFile := filepath.Join(storagePath, "imagedb", "content", algo, filename) log.WithFields(log.Fields{ - "filename": imagecontentfile, + "imageContentFile": imageContentFile, }).Debug("reading docker image content file") - data, err := os.ReadFile(imagecontentfile) + data, err := os.ReadFile(imageContentFile) if err != nil { log.WithFields(log.Fields{ - "storage name": storagename, - "algo": algo, - "filename": filename, + "storageName": storageName, + "algo": algo, + "filename": filename, }).Debug("reading docker image content file") return imageContentSummary{}, err } - var imagecontent imageContentSummary - if err := json.Unmarshal(data, &imagecontent); err != nil { + var imageContent imageContentSummary + if err := json.Unmarshal(data, &imageContent); err != nil { return imageContentSummary{}, err } - return imagecontent, nil + return imageContent, nil } // imageBasename returns the base name of an image func imageBasename(name string) string { - imagebase := strings.Replace(name, "\"", "", -1) + imageBase := strings.Replace(name, "\"", "", -1) - if strings.Contains(imagebase, "@") { - imagebase = strings.Split(imagebase, "@")[0] + if strings.Contains(imageBase, "@") { + imageBase = strings.Split(imageBase, "@")[0] } log.WithFields(log.Fields{ - "imagename": name, - "imagebase": imagebase, - }).Debug("extracting imagebase from image") + "imageName": name, + "imageBase": imageBase, + }).Debug("extracting image base from image") - return imagebase + return imageBase } diff --git a/explorers/docker/export.go b/explorers/docker/export.go index 168ca19..91c7ff9 100644 --- a/explorers/docker/export.go +++ b/explorers/docker/export.go @@ -21,8 +21,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" - "strings" "github.com/containerd/containerd/namespaces" "github.com/google/container-explorer/explorers" @@ -31,7 +29,7 @@ import ( ) // ExportContainer exports a container either as a raw image or an archive. -func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOption map[string]bool) error { +func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOptions map[string]bool) error { // Check if the specified containerID exists. containerExists := false @@ -47,7 +45,7 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp if err != nil { log.WithFields(log.Fields{ "namespace": containerNamespace, - "error": err, + "error": err, }).Warnf("error listing containers in namespace") continue @@ -66,7 +64,7 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp if !containerExists { log.WithFields(log.Fields{ "containerID": containerID, - "namespace": containerNamespace, + "namespace": containerNamespace, }).Debug("no container in namespace") continue @@ -74,9 +72,9 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp // Continue the following if a matching containerID is found. log.WithFields(log.Fields{ - "containerID": targetContainer.ID, - "name": targetContainer.Runtime.Name, - "namespace": targetContainer.Namespace, + "containerID": targetContainer.ID, + "name": targetContainer.Runtime.Name, + "namespace": targetContainer.Namespace, "containerType": targetContainer.ContainerType, }).Info("container found") @@ -98,59 +96,59 @@ func (e *explorer) ExportContainer(ctx context.Context, containerID string, outp break } } - log.Infof("Attempting to mount container %s to %s", targetContainer.ID, mountpoint) + log.Infof("attempting to mount container %s to %s", targetContainer.ID, mountpoint) if err := e.MountContainer(ctx, targetContainer.ID, mountpoint); err != nil { // If mountpoint was created, attempt to clean it up. _ = os.Remove(mountpoint) // Best effort removal return fmt.Errorf("failed to mount container %s: %w", targetContainer.ID, err) } - log.Infof("Successfully mounted container %s to %s", targetContainer.ID, mountpoint) + log.Infof("successfully mounted container %s to %s", targetContainer.ID, mountpoint) // Defer unmount and cleanup of the mountpoint defer func() { - log.Infof("Cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) + log.Infof("cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) unmountCmd := exec.Command("umount", mountpoint) unmountCmdOutput, unmountErr := unmountCmd.CombinedOutput() // Run and get output/error if unmountErr != nil { - log.Warnf("Failed to unmount %s: %v. Output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) + log.Warnf("failed to unmount %s: %v; output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) } else { - log.Infof("Successfully unmounted %s. Output: %s", mountpoint, string(unmountCmdOutput)) + log.Infof("successfully unmounted %s; output: %s", mountpoint, string(unmountCmdOutput)) } if rmErr := os.Remove(mountpoint); rmErr != nil { - log.Warnf("Failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) + log.Warnf("failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) } else { - log.Infof("Successfully removed mountpoint directory %s", mountpoint) + log.Infof("successfully removed mountpoint directory %s", mountpoint) } }() - if exportOption["image"] { - log.Infof("Exporting container %s as a raw image to %s", targetContainer.ID, outputDir) - if err := exportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { + if exportOptions["image"] { + log.Infof("exporting container %s as a raw image to %s", targetContainer.ID, outputDir) + if err := utils.ExportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { return fmt.Errorf("failed to export container %s as raw image: %w", targetContainer.ID, err) } - log.Infof("Successfully exported container %s as a raw image.", targetContainer.ID) + log.Infof("successfully exported container %s as a raw image", targetContainer.ID) } - if exportOption["archive"] { - log.Infof("Exporting container %s as an archive to %s", targetContainer.ID, outputDir) - if err := exportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { + if exportOptions["archive"] { + log.Infof("exporting container %s as an archive to %s", targetContainer.ID, outputDir) + if err := utils.ExportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { return fmt.Errorf("failed to export container %s as archive: %w", targetContainer.ID, err) } - log.Infof("Successfully exported container %s as an archive.", targetContainer.ID) + log.Infof("successfully exported container %s as an archive", targetContainer.ID) } } if !containerExists { - log.Infof("Container %s not found in Docker containers", containerID) + log.Infof("container %s not found in docker containers", containerID) } return nil } // ExportAllContainers exports all Docker containers to specified output directory. -func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOption map[string]bool, filter map[string]string, exportSupportContainers bool) error { +func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOptions map[string]bool, filter map[string]string, exportSupportContainers bool) error { containerNamespaces, err := e.ListNamespaces(ctx) if err != nil { return fmt.Errorf("listing namespaces: %w", err) @@ -163,29 +161,29 @@ func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, ex if err != nil { log.WithFields(log.Fields{ "namespace": containerNamespace, - "error": err, + "error": err, }).Warnf("error listing containers in namespace") continue } log.WithFields(log.Fields{ - "namespace": containerNamespace, - "container_count": len(containers), + "namespace": containerNamespace, + "containerCount": len(containers), }).Debug("Docker containers in namespace") for _, container := range containers { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, }).Debug("processing Docker container for export") - if !exportSupportContainers && container.SupportContainer{ + if !exportSupportContainers && container.SupportContainer { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, }).Debug("skipping Kubernetes support containers") continue @@ -193,20 +191,20 @@ func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, ex if utils.IncludeContainer(container, filter) { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, }).Debug("ignoring Docker container for export") - err := e.ExportContainer(ctx, container.ID, outputDir, exportOption) + err := e.ExportContainer(ctx, container.ID, outputDir, exportOptions) if err != nil { log.WithFields(log.Fields{ - "containerID": container.ID, - "name": container.Runtime.Name, - "namespace": container.Namespace, + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, "containerType": container.ContainerType, - "error": err, + "error": err, }).Error("error exporting Docker container") } } @@ -216,211 +214,3 @@ func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, ex // Default return nil } - -// exportContainerImage creates a raw disk image file of a calculated size based on -// the content of the mountpoint, formats it to ext4, and saves it to outputDir. -func exportContainerImage(ctx context.Context, containerID string, mountpoint string, outputDir string) error { - // 1. Calculate the required size for the image. - contentSize, err := utils.CalculateDirectorySize(mountpoint) - if err != nil { - return fmt.Errorf("failed to calculate content size for %s: %w", mountpoint, err) - } - log.Infof("Calculated content size for %s: %d bytes", mountpoint, contentSize) - - // Add overhead for filesystem structures (e.g., 20MB base + 5% of content size for inodes, metadata) - overhead := int64(20*1024*1024) + (contentSize / 20) - imageSize := contentSize + overhead - log.Infof("Target image size for %s: %d bytes (content: %d, overhead: %d)", containerID, imageSize, contentSize, overhead) - - imageFileName := fmt.Sprintf("%s.raw", containerID) - imageFilePath := filepath.Join(outputDir, imageFileName) - - log.WithFields(log.Fields{ - "containerID": containerID, - "imageFilePath": imageFilePath, - "imageSize": imageSize, - }).Info("Preparing to create and format disk image") - - // 2. Create the image file - imgFile, err := os.Create(imageFilePath) - if err != nil { - return fmt.Errorf("failed to create image file %s: %w", imageFilePath, err) - } - - // 3. Set the image file size - if err := imgFile.Truncate(imageSize); err != nil { - imgFile.Close() // Attempt to close before returning - return fmt.Errorf("failed to truncate image file %s to size %d: %w", imageFilePath, imageSize, err) - } - - // 4. Sync and Close the file before formatting - if err := imgFile.Sync(); err != nil { - imgFile.Close() // Attempt to close before returning - log.Warnf("failed to sync image file %s after truncation: %v", imageFilePath, err) - } - if err := imgFile.Close(); err != nil { - return fmt.Errorf("failed to close image file %s before formatting: %w", imageFilePath, err) - } - log.Infof("Successfully created and sized image file: %s", imageFilePath) - - // 5. Format the image file as ext4 - log.WithFields(log.Fields{ - "imageFilePath": imageFilePath, - }).Info("Formatting image as ext4...") - - mkfsCmd := exec.CommandContext(ctx, "mkfs.ext4", "-F", "-q", imageFilePath) - mkfsOutput, err := mkfsCmd.CombinedOutput() - if err != nil { - log.WithFields(log.Fields{ - "command": mkfsCmd.String(), - "output": string(mkfsOutput), - "error": err, - }).Error("mkfs.ext4 command failed") - return fmt.Errorf("mkfs.ext4 failed for %s: %w. Output: %s", imageFilePath, err, string(mkfsOutput)) - } - - log.WithFields(log.Fields{ - "imageFilePath": imageFilePath, - "output": string(mkfsOutput), - }).Info("Successfully formatted image as ext4") - - // 6. Mount the formatted image, copy data, then unmount. - log.Infof("Preparing to copy data from %s to image %s", mountpoint, imageFilePath) - - imageMountDir, err := os.MkdirTemp(outputDir, fmt.Sprintf("%s-img-mount-*.d", containerID)) - if err != nil { - return fmt.Errorf("failed to create temporary mount directory for image %s: %w", imageFilePath, err) - } - log.Infof("Created temporary image mount directory: %s", imageMountDir) - - var loopDevice string - var imageSuccessfullyMounted bool = false - - // Defer cleanup actions in LIFO order (unmount image, detach loop, remove temp dir) - defer func() { - if imageSuccessfullyMounted { - log.Infof("Unmounting image from %s", imageMountDir) - umountCmd := exec.Command("umount", imageMountDir) // Use non-contextual command for cleanup - // Best effort unmount - if umountErr := umountCmd.Run(); umountErr != nil { - umountOutput, _ := umountCmd.CombinedOutput() // Get output for logging - log.Warnf("Failed to unmount image filesystem from %s: %v. Output: %s", imageMountDir, umountErr, string(umountOutput)) - } else { - log.Infof("Successfully unmounted image filesystem from %s", imageMountDir) - } - } - - if loopDevice != "" { - log.Infof("Detaching loop device %s for image %s", loopDevice, imageFilePath) - losetupDetachCmd := exec.Command("losetup", "-d", loopDevice) // Use non-contextual command for cleanup - // Best effort detach - if detachErr := losetupDetachCmd.Run(); detachErr != nil { - detachOutput, _ := losetupDetachCmd.CombinedOutput() // Get output for logging - log.Warnf("Failed to detach loop device %s: %v. Output: %s", loopDevice, detachErr, string(detachOutput)) - } else { - log.Infof("Successfully detached loop device %s", loopDevice) - } - } - - log.Infof("Removing temporary image mount directory %s", imageMountDir) - if err := os.RemoveAll(imageMountDir); err != nil { - log.Warnf("Failed to remove temporary image mount directory %s: %v", imageMountDir, err) - } - }() - - // 6.1. Setup loop device - log.Infof("Setting up loop device for %s", imageFilePath) - losetupCmd := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath) - loopDeviceBytes, err := losetupCmd.Output() // Use Output to capture stdout, which is the loop device path - if err != nil { - // If Output() fails, CombinedOutput() can give more info if stderr was involved - losetupCombinedOutput, _ := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath).CombinedOutput() - log.Errorf("losetup -f --show %s failed: %v. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) - return fmt.Errorf("losetup -f --show %s failed: %w. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) - } - loopDevice = strings.TrimSpace(string(loopDeviceBytes)) - if loopDevice == "" { - log.Errorf("losetup -f --show %s returned an empty loop device path.", imageFilePath) - return fmt.Errorf("losetup -f --show %s returned an empty loop device path", imageFilePath) - } - log.Infof("Image %s associated with loop device %s", imageFilePath, loopDevice) - - // 6.2. Mount the loop device - log.Infof("Mounting loop device %s to %s", loopDevice, imageMountDir) - mountImageCmd := exec.CommandContext(ctx, "mount", loopDevice, imageMountDir) - mountImageOutput, err := mountImageCmd.CombinedOutput() - if err != nil { - log.Errorf("Failed to mount %s to %s: %v. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) - return fmt.Errorf("failed to mount loop device %s to %s: %w. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) - } - imageSuccessfullyMounted = true // Set flag for deferred cleanup - log.Infof("Successfully mounted %s to %s. Output: %s", loopDevice, imageMountDir, string(mountImageOutput)) - - // 6.3. Copy content from container's mountpoint to the image's mountpoint - // Source path: mountpoint + "/." to copy contents of the directory, not the directory itself. - sourcePathFiles, _ := filepath.Glob(filepath.Join(mountpoint, "*")) - - for _, sourcePathForCopy := range sourcePathFiles { - log.Infof("Copying contents from %s to %s using 'cp -a'", sourcePathForCopy, imageMountDir) - - copyCmd := exec.Command("cp", "-a", sourcePathForCopy, imageMountDir) - copyOutput, err := copyCmd.CombinedOutput() - if err != nil { - log.Errorf("Failed to copy data from %s to %s: %v. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) - return fmt.Errorf("failed to copy data from %s to %s: %w. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) - } - log.Infof("Successfully copied data from %s to %s. Output: %s", sourcePathForCopy, imageMountDir, string(copyOutput)) - } - - // 6.4. Sync filesystem buffers to ensure all data is written to the image - log.Info("Syncing filesystem buffers for the image.") - syncCmd := exec.CommandContext(ctx, "sync") - if syncErr := syncCmd.Run(); syncErr != nil { - // This is usually not fatal but good to log. - syncOutput, _ := syncCmd.CombinedOutput() // Get output for logging - log.Warnf("sync command failed after copying to image: %v. Output: %s", syncErr, string(syncOutput)) - } else { - log.Info("Filesystem buffers synced.") - } - - log.Infof("Image %s successfully created, formatted, and populated.", imageFilePath) - - return nil -} - -// exportContainerArchive creates a .tar.gz archive of the content of the mountpoint. -func exportContainerArchive(ctx context.Context, containerID string, mountpoint string, outputDir string) error { - archiveFileName := fmt.Sprintf("%s.tar.gz", containerID) - archiveFilePath := filepath.Join(outputDir, archiveFileName) - - log.WithFields(log.Fields{ - "containerID": containerID, - "mountpoint": mountpoint, - "archiveFilePath": archiveFilePath, - }).Info("Preparing to create container archive") - - // Command: tar -czf -C . - // -c: create - // -z: gzip - // -f: file - // -C : change to directory before processing files - // .: process all files in the current directory (which is due to -C) - tarCmd := exec.CommandContext(ctx, "tar", "-czf", archiveFilePath, "-C", mountpoint, ".") - - tarOutput, err := tarCmd.CombinedOutput() - if err != nil { - log.WithFields(log.Fields{ - "command": tarCmd.String(), - "output": string(tarOutput), - "error": err, - }).Error("tar command failed") - return fmt.Errorf("failed to create archive %s: %w. Output: %s", archiveFilePath, err, string(tarOutput)) - } - - log.WithFields(log.Fields{ - "archiveFilePath": archiveFilePath, - "output": string(tarOutput), - }).Info("Successfully created container archive") - - return nil -} diff --git a/explorers/explorers.go b/explorers/explorers.go index 680c8b0..fe29016 100644 --- a/explorers/explorers.go +++ b/explorers/explorers.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package explorers defines the core interfaces and data structures for exploring container filesystems, +// images, namespaces, snapshots, and metadata across different container runtimes. package explorers import ( @@ -29,13 +31,13 @@ type ContainerExplorer interface { ContainerDrift(ctx context.Context, filter string, skipsupportcontainers bool, containerID string) ([]Drift, error) // ExportAllContainers exports all Docker and containerd containers. - ExportAllContainers(ctx context.Context, outputDir string, exportOption map[string]bool, filter map[string]string, exportSupportContainers bool) error + ExportAllContainers(ctx context.Context, outputDir string, exportOptions map[string]bool, filter map[string]string, exportSupportContainers bool) error // ExportContainer exports a container as an image or archive. - ExportContainer(ctx context.Context, containerID string, outputDir string, exportOption map[string]bool) error + ExportContainer(ctx context.Context, containerID string, outputDir string, exportOptions map[string]bool) error // InfoContainer returns container internal information - InfoContainer(ctx context.Context, containerid string, spec bool) (interface{}, error) + InfoContainer(ctx context.Context, containerID string, spec bool) (any, error) // ListContainers returns all the containers in all the namespaces. // @@ -63,11 +65,17 @@ type ContainerExplorer interface { MountAllContainers(ctx context.Context, mountpoint string, filter string, skipsupportcontainers bool) error // MountContainer mounts a container to the specified path - MountContainer(ctx context.Context, containerid string, mountpoint string) error + MountContainer(ctx context.Context, containerID string, mountpoint string) error // SnapshotRoot returns the directory containing snapshots and snapshot // database i.e. metadata.db // // SnapshotRoot is required for the containers managed using containerd. SnapshotRoot(snapshotter string) string + + // GetContainerByID returns ContainerExplorer for the ID or nil + GetContainerByID(ctx context.Context, containerID string) (*Container, error) + + // Type returns the explorer type (e.g., containerd, docker, podman) + Type() string } diff --git a/explorers/fileinfo.go b/explorers/fileinfo.go index 82ba791..d5b1866 100644 --- a/explorers/fileinfo.go +++ b/explorers/fileinfo.go @@ -22,9 +22,12 @@ import ( "encoding/json" "io" "os" + "path/filepath" "strings" "syscall" "time" + + log "github.com/sirupsen/logrus" ) type FileInfo struct { @@ -35,9 +38,9 @@ type FileInfo struct { FileAccessed time.Time `json:"file_accessed"` FileChanged time.Time `json:"file_changed"` FileBirth time.Time `json:"file_birth"` - FileUid string `json:"file_uid,omitempty"` + FileUID string `json:"file_uid,omitempty"` FileOwner string `json:"file_owner,omitempty"` - FileGid string `json:"file_gid,omitempty"` + FileGID string `json:"file_gid,omitempty"` FileType string `json:"file_type,omitempty"` FileSHA256 string `json:"file_sha256,omitempty"` } @@ -94,3 +97,110 @@ func GetFileInfo(info os.FileInfo, path string, diffDir string) (*FileInfo, erro return &diffFileInfo, nil } + +// ScanDiffDirectory identifies added or modified files in the diff directory +func ScanDiffDirectory(diffDir string) (addedOrModified []FileInfo, inaccessibleFiles []FileInfo, err error) { + log.WithField("path", diffDir).Debug("scanning drift directory") + + // Map to track canonical directory paths and prevent infinite loops/cycles from symlinks + visited := make(map[string]bool) + + var walk func(string) error + walk = func(path string) error { + // 1. Get the Lstat info first to check if the entry itself is a symlink + info, lstatErr := os.Lstat(path) + if lstatErr != nil { + // The file/link is completely unreadable + log.WithFields(log.Fields{ + "upperDir": diffDir, + "error": lstatErr, + }).Debug("reading drift directory") + return nil + } + + // 2. If it is a symlink, resolve it to point to the actual target file/directory + if info.Mode()&os.ModeSymlink != 0 { + targetInfo, err := os.Stat(path) + if err != nil { + // Broken symlink or target permission error; record as an inaccessible file + if fileinfo, err := GetFileInfo(info, path, diffDir); err == nil { + inaccessibleFiles = append(inaccessibleFiles, *fileinfo) + } + return nil + } + // Overwrite info with the target's FileInfo so metadata matches the actual file + info = targetInfo + } + + // 3. Handle Directories (and symlinks pointing to directories) + if info.IsDir() { + // Canonicalize the directory path to detect loops and avoid duplicate traversals + realPath, err := filepath.EvalSymlinks(path) + if err != nil { + log.WithFields(log.Fields{"path": path, "error": err}).Debug("getting real path") + return nil + } + if visited[realPath] { + return nil // Already processed or ancestral loop detected; safely return + } + visited[realPath] = true + + // Read directory contents + entries, err := os.ReadDir(path) + if err != nil { + // Directory exists but contents cannot be read + if fileinfo, err := GetFileInfo(info, path, diffDir); err == nil { + inaccessibleFiles = append(inaccessibleFiles, *fileinfo) + } + return nil + } + + for _, entry := range entries { + nextPath := filepath.Join(path, entry.Name()) + if err := walk(nextPath); err != nil { + log.WithFields(log.Fields{"path": nextPath, "error": err}).Debug("walking drift directory") + return err + } + } + } else { + // 4. Handle regular files, device files, or symlinks resolved to files + fileinfo, err := GetFileInfo(info, path, diffDir) + if err != nil { + log.WithFields(log.Fields{"path": path, "error": err}).Debug("getting file information") + return err + } + + // Ensure the reported FileName matches the logical path name in the tree, + // rather than the base name of the resolved target file. + fileinfo.FileName = filepath.Base(path) + + // Check if the file is a whiteout file + if info.Mode()&os.ModeCharDevice != 0 { + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + rdev := stat.Rdev + + // Extract major and minor device numbers + major := (rdev >> 8) & 0xfff + minor := (rdev & 0xff) | ((rdev >> 12) & 0xfff00) + + if major == 0 && minor == 0 { + inaccessibleFiles = append(inaccessibleFiles, *fileinfo) + return nil + } + } + } + + // Check if the target file has executable permissions + mode := info.Mode().Perm() + if mode&0111 != 0 { + fileinfo.FileType = "executable" + } + + addedOrModified = append(addedOrModified, *fileinfo) + } + return nil + } + + err = walk(diffDir) + return +} diff --git a/explorers/image.go b/explorers/image.go index 135c2fd..2c2a8f0 100644 --- a/explorers/image.go +++ b/explorers/image.go @@ -23,6 +23,7 @@ import ( // Image provides information about a container image. type Image struct { Namespace string + ContainerType string SupportContainerImage bool images.Image } diff --git a/explorers/podman/config.go b/explorers/podman/config.go new file mode 100755 index 0000000..9d77f10 --- /dev/null +++ b/explorers/podman/config.go @@ -0,0 +1,52 @@ +/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package podman + +type containerMetadata struct { + ImageName string `json:"image-name"` + ImageID string `json:"image-id"` + Name string `json:"name"` + CreatedAt int64 `json:"created-at"` +} + +type containerFlags struct { + MountLabel string `json:"MountLabel"` + ProcessLabel string `json:"ProcessLabel"` +} + +type containerConfig struct { + ID string `json:"id"` + Names []string `json:"names"` + Image string `json:"image"` + Layer string `json:"layer"` + Metadata string `json:"metadata"` + Created string `json:"created"` + Flags containerFlags `json:"flags"` +} + +type containerImage struct { + ID string `json:"id"` + Digest string `json:"digest"` + Names []string `json:"names"` + NameHistory []string `json:"name-history"` + Layer string `json:"layer"` + Metadata map[string]any `json:"metadata"` + BigDataNames []string `json:"big-data-names"` + BigDataSizes []string `json:"big-data-sizes"` + BigDataDigests []string `json:"big-data-digests"` + Created string `json:"created"` +} diff --git a/explorers/podman/export.go b/explorers/podman/export.go new file mode 100755 index 0000000..f780673 --- /dev/null +++ b/explorers/podman/export.go @@ -0,0 +1,164 @@ +/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package podman + +import ( + "context" + "fmt" + "os" + "os/exec" + + "github.com/google/container-explorer/utils" + log "github.com/sirupsen/logrus" +) + +// ExportContainer exports a podman container as a raw or as an archive. +func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOptions map[string]bool) error { + targetContainer, err := e.GetContainerByID(ctx, containerID) + if err != nil { + return fmt.Errorf("finding container %s: %w", containerID, err) + } + + // Continue the following if a matching containerID is found. + log.WithFields(log.Fields{ + "containerID": targetContainer.ID, + "name": targetContainer.Name, + "namespace": targetContainer.Namespace, + "containerType": targetContainer.ContainerType, + }).Info("container found") + + // Ensure outputDir exists + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) + } + + // Mount the container + var mountpoint string + for { + mountpoint = utils.GetMountPoint() + exists, err := utils.PathExists(mountpoint) + if err != nil { + return fmt.Errorf("checking if mountpoint %s exists: %w", mountpoint, err) + } + + if !exists { + // Create the mountpoint directory + if err := os.MkdirAll(mountpoint, 0755); err != nil { + return fmt.Errorf("failed to create mountpoint directory %s: %w", mountpoint, err) + } + break + } + } + log.Infof("attempting to mount container %s to %s", targetContainer.ID, mountpoint) + + if err := e.MountContainer(ctx, targetContainer.ID, mountpoint); err != nil { + // If mountpoint was created, attempt to clean it up. + _ = os.Remove(mountpoint) // Best effort removal + return fmt.Errorf("failed to mount container %s: %w", targetContainer.ID, err) + } + log.Infof("successfully mounted container %s to %s", targetContainer.ID, mountpoint) + + // Defer unmount and cleanup of the mountpoint + defer func() { + log.Infof("cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) + unmountCmd := exec.Command("umount", mountpoint) + unmountCmdOutput, unmountErr := unmountCmd.CombinedOutput() // Run and get output/error + if unmountErr != nil { + log.Warnf("failed to unmount %s: %v; output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) + } else { + log.Infof("successfully unmounted %s; output: %s", mountpoint, string(unmountCmdOutput)) + } + + if rmErr := os.Remove(mountpoint); rmErr != nil { + log.Warnf("failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) + } else { + log.Infof("successfully removed mountpoint directory %s", mountpoint) + } + }() + + if exportOptions["image"] { + log.Infof("exporting container %s as a raw image to %s", targetContainer.ID, outputDir) + if err := utils.ExportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { + return fmt.Errorf("failed to export container %s as raw image: %w", targetContainer.ID, err) + } + log.Infof("successfully exported container %s as a raw image", targetContainer.ID) + } + + if exportOptions["archive"] { + log.Infof("exporting container %s as an archive to %s", targetContainer.ID, outputDir) + if err := utils.ExportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { + return fmt.Errorf("failed to export container %s as archive: %w", targetContainer.ID, err) + } + log.Infof("successfully exported container %s as an archive", targetContainer.ID) + } + + return nil +} + +// ExportAllContainers exports all podman container to specific output directory. +func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOptions map[string]bool, filter map[string]string, exportSupportContainers bool) error { + containers, err := e.ListContainers(ctx) + if err != nil { + return fmt.Errorf("listing containers: %w", err) + } + + log.WithFields(log.Fields{ + "container_count": len(containers), + }).Debug("podman containers") + + for _, container := range containers { + log.WithFields(log.Fields{ + "containerID": container.ID, + "name": container.Name, + "namespace": container.Namespace, + "containerType": container.ContainerType, + }).Debug("processing podman container for export") + + if !exportSupportContainers && container.SupportContainer { + log.WithFields(log.Fields{ + "containerID": container.ID, + "name": container.Name, + "namespace": container.Namespace, + "containerType": container.ContainerType, + }).Debug("skipping Kubernetes support containers") + continue + } + + if utils.IncludeContainer(container, filter) { + log.WithFields(log.Fields{ + "containerID": container.ID, + "name": container.Name, + "namespace": container.Namespace, + "containerType": container.ContainerType, + }).Debug("processing podman container for export") + + err := e.ExportContainer(ctx, container.ID, outputDir, exportOptions) + if err != nil { + log.WithFields(log.Fields{ + "containerID": container.ID, + "name": container.Runtime.Name, + "namespace": container.Namespace, + "containerType": container.ContainerType, + "error": err, + }).Error("error exporting podman container") + } + } + } + + // Return no error + return nil +} diff --git a/explorers/podman/podman.go b/explorers/podman/podman.go new file mode 100755 index 0000000..2a0cdf7 --- /dev/null +++ b/explorers/podman/podman.go @@ -0,0 +1,668 @@ +/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package podman implements the ContainerExplorer interface for exploring Podman managed containers. +package podman + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/google/container-explorer/explorers" + "github.com/google/container-explorer/utils" + + "github.com/containerd/containerd/images" + digest "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + spec "github.com/opencontainers/runtime-spec/specs-go" + + "github.com/containerd/containerd/containers" + _ "github.com/mattn/go-sqlite3" + "github.com/BurntSushi/toml" + log "github.com/sirupsen/logrus" + "go.podman.io/podman/v6/libpod" + storageTypes "go.podman.io/storage/types" +) + +type explorer struct { + imageroot string + podmanRootDirs []string +} + +// NewExplorer returns ContainerExplorer interface to explore podman containers. +func NewExplorer(imageroot string) (explorers.ContainerExplorer, error) { + e := &explorer{ + imageroot:imageroot, + } + + rootDirs, err := e.getPodmanRootDirs() + if err != nil { + return nil, fmt.Errorf("no podman root directories: %w", err) + } + + e.podmanRootDirs = rootDirs + return e, nil +} + +// GetContainerByID returns Container for a given container ID or container name. +func (e *explorer) GetContainerByID(ctx context.Context, containerID string) (*explorers.Container, error) { + containers, err := e.ListContainers(ctx) + if err != nil { + return nil, err + } + + for _, container := range containers { + if container.ID == containerID || container.Name == containerID { + return &container, nil + } + } + + return nil, fmt.Errorf("no matching container") +} + +func (e *explorer) Type() string { + return "podman" +} + +// ListNamespaces returns podman namespaces if exist. +func (e *explorer) ListNamespaces(ctx context.Context) ([]string, error) { + // No namespaces in podman returning nil, nil + log.Info("listing namespaces is not supported in podman") + + return nil, nil +} + +// ListSnapshots returns podman containers snapshots. +func (e *explorer) ListSnapshots(ctx context.Context) ([]explorers.SnapshotKeyInfo, error) { + // No snapshots for podman + log.Info("listing snapshots is not implemented in podman") + + return nil, nil +} + +// SnapshotRoot returns snapshot root directory. +func (e *explorer) SnapshotRoot(snapshotter string) string { + // No snapshot root for podman + log.Info("snapshot root concept is not applicable in podman") + + return "" +} + +// ListContainers returns all podman containers. +func (e *explorer) ListContainers(ctx context.Context) ([]explorers.Container, error) { + var podmanContainers []explorers.Container + + for _, podmanRootDir := range e.podmanRootDirs { + configs, err := e.readContainerConfig(podmanRootDir) + if err != nil { + log.WithFields(log.Fields{"podmanRootDir": podmanRootDir, "error": err}).Debug("reading container config") + continue + } + + var metadata containerMetadata + + for _, config := range configs { + if err := json.Unmarshal([]byte(config.Metadata), &metadata); err != nil { + log.WithFields(log.Fields{"containerID": config.ID, "error": err}).Debug("unmarshalling container metadata") + continue + } + + parsedTime, err := time.Parse(time.RFC3339Nano, config.Created) + if err != nil { + log.WithFields(log.Fields{"containerID": config.ID, "error": err}).Debug("parsing container creation time") + } + + podmanContainer := explorers.Container{ + Name: metadata.Name, + Hostname: metadata.Name, + ImageBase: metadata.ImageName, + SupportContainer: false, + ContainerType: "podman", + Container: containers.Container{ + ID: config.ID, + Image: metadata.ImageName, + CreatedAt: parsedTime, + }, + } + podmanContainers = append(podmanContainers, podmanContainer) + } + } + + return podmanContainers, nil +} + +// ListImages returns podman images. +func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { + var ceImages []explorers.Image + + for _, podmanRootDir := range e.podmanRootDirs { + imageConfigFile := filepath.Join(podmanRootDir, "storage", "overlay-images", "images.json") + if ok := utils.PathExistsV2(imageConfigFile); !ok { + log.WithField("imageConfigPath", imageConfigFile).Info("podman image config file not found") + continue + } + + data, err := os.ReadFile(imageConfigFile) + if err != nil { + log.WithError(err).Error("reading image config file") + continue + } + + var pmImages []containerImage + if err := json.Unmarshal(data, &pmImages); err != nil { + log.WithError(err).Error("unmarshalling image config file") + continue + } + + for _, pmImage := range pmImages { + createdAt, err := time.Parse(time.RFC3339Nano, pmImage.Created) + if err != nil { + log.WithFields(log.Fields{"imageID": pmImage.ID, "error": err}).Debug("parsing image creation time") + } + + var imageManifest ocispec.Manifest + + imageManifestFile := filepath.Join(podmanRootDir, "storage", "overlay-images", pmImage.ID, "manifest") + imageManifestData, err := os.ReadFile(imageManifestFile) + if err != nil { + log.WithFields(log.Fields{ + "imageManifestFile": imageManifestFile, + "error": err, + }).Error("reading podman image manifest file") + } else { + if err := json.Unmarshal(imageManifestData, &imageManifest); err != nil { + log.WithFields(log.Fields{"imageID": pmImage.ID, "error": err}).Debug("unmarshalling image manifest") + } + } + + imageName := "" + if len(pmImage.Names) > 0 { + imageName = pmImage.Names[0] + } + + ceImage := explorers.Image{ + ContainerType: "podman", + Image: images.Image{ + Name: imageName, + Target: ocispec.Descriptor{ + Digest: digest.Digest(pmImage.Digest), + MediaType: imageManifest.MediaType, + }, + CreatedAt: createdAt, + }, + SupportContainerImage: false, + } + + ceImages = append(ceImages, ceImage) + } + } + + return ceImages, nil +} + +// ListContent returns container contents. +func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) { + log.Info("listing content is not implemented for podman") + + return nil, nil +} + +// ListTasks returns running tasks. +func (e *explorer) ListTasks(ctx context.Context) ([]explorers.Task, error) { + var containerTasks []explorers.Task + + for _, podmanRootDir := range e.podmanRootDirs { + dbfile := filepath.Join(podmanRootDir, "storage", "db.sql") + if ok := utils.PathExistsV2(dbfile); !ok { + log.WithField("dbfile", dbfile).Debug("podman sqlite database file not found, skipping root directory") + continue + } + + conn, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro", dbfile)) + if err != nil { + return nil, fmt.Errorf("opening sqlite database: %w", err) + } + + err = func() error { + defer conn.Close() + + rows, err := conn.Query("SELECT ID, JSON FROM ContainerState;") + if err != nil { + return fmt.Errorf("query podman container state: %w", err) + } + defer rows.Close() + + for rows.Next() { + var id, stateJSON string + if err := rows.Scan(&id, &stateJSON); err != nil { + return fmt.Errorf("reading container state row: %w", err) + } + + var containerstate libpod.ContainerState + if err := json.Unmarshal([]byte(stateJSON), &containerstate); err != nil { + return fmt.Errorf("unmarshalling podman container state json: %w", err) + } + + containerTask := explorers.Task{ + ContainerType: "podman", + Name: id, + PID: containerstate.PID, + Status: containerstate.State.String(), + } + + containerTasks = append(containerTasks, containerTask) + } + return nil + }() + if err != nil { + return nil, err + } + } + + return containerTasks, nil +} + +// InfoContainer returns container internal information. +func (e *explorer) InfoContainer(ctx context.Context, containerID string, showSpec bool) (any, error) { + var matchedConfig *containerConfig + var matchedRootDir string + + // Find the container config and root directory + for _, podmanRootDir := range e.podmanRootDirs { + configs, err := e.readContainerConfig(podmanRootDir) + if err != nil { + log.WithFields(log.Fields{"podmanRootDir": podmanRootDir, "error": err}).Debug("reading container config") + continue + } + + for _, config := range configs { + if config.ID == containerID || (len(config.Names) > 0 && config.Names[0] == containerID) { + matchedConfig = &config + matchedRootDir = podmanRootDir + break + } + } + if matchedConfig != nil { + break + } + } + + if matchedConfig == nil { + return nil, fmt.Errorf("getting container %s: no matching container", containerID) + } + + // Read OCI spec from userdata/config.json + specFile := filepath.Join(matchedRootDir, "storage", "overlay-containers", matchedConfig.ID, "userdata", "config.json") + if ok := utils.PathExistsV2(specFile); !ok { + return nil, fmt.Errorf("container spec file %s does not exist", specFile) + } + + data, err := os.ReadFile(specFile) + if err != nil { + return nil, fmt.Errorf("reading container spec file %s: %w", specFile, err) + } + + var ociSpec spec.Spec + if err := json.Unmarshal(data, &ociSpec); err != nil { + return nil, fmt.Errorf("unmarshalling container spec: %w", err) + } + + if showSpec { + return ociSpec, nil + } + + var metadata containerMetadata + if err := json.Unmarshal([]byte(matchedConfig.Metadata), &metadata); err != nil { + log.WithFields(log.Fields{"containerID": matchedConfig.ID, "error": err}).Debug("unmarshalling container metadata") + } + + parsedTime, err := time.Parse(time.RFC3339Nano, matchedConfig.Created) + if err != nil { + log.WithFields(log.Fields{"containerID": matchedConfig.ID, "error": err}).Debug("parsing container creation time") + } + + container := containers.Container{ + ID: matchedConfig.ID, + Image: metadata.ImageName, + CreatedAt: parsedTime, + Labels: ociSpec.Annotations, + } + + // Return container and spec info + return struct { + containers.Container + Spec any `json:"Spec,omitempty"` + }{ + Container: container, + Spec: ociSpec, + }, nil +} + +// MountContainer mounts podman container for a given ID or name. +func (e *explorer) MountContainer(ctx context.Context, containerID string, mountpoint string) error { + for _, podmanRootDir := range e.podmanRootDirs { + configs, err := e.readContainerConfig(podmanRootDir) + if err != nil { + log.WithFields(log.Fields{"podmanRootDir": podmanRootDir, "error": err}).Debug("reading containers.json") + continue + } + + for _, config := range configs { + if config.ID == containerID || (len(config.Names) > 0 && config.Names[0] == containerID) { + return e.mountContainer(ctx, podmanRootDir, containerID, config.Layer, mountpoint) + } + } + } + + return fmt.Errorf("no matching container") +} + +// MountAllContainers mounts all podman containers. +func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, filter string, skipsupportcontainers bool) error { + containers, err := e.ListContainers(ctx) + if err != nil { + return fmt.Errorf("listing container: %w", err) + } + + for _, container := range containers { + containerMountPoint := filepath.Join(mountpoint, container.ID) + if err := os.MkdirAll(containerMountPoint, 0755); err != nil { + log.WithFields(log.Fields{ + "containerID": container.ID, + "error": err, + }).Error("creating container mountpoint failed; skipping container mount") + continue + } + + containerName := container.Name + if err := e.MountContainer(ctx, containerName, containerMountPoint); err != nil { + log.WithFields(log.Fields{ + "containerID": container.ID, + "error": err, + }).Error("mounting container failed; skipping container mount") + } + } + + return nil +} + +// ContainerDrift finds the drifted files from containers. +// - skipsupportcontainers are only applicable for containerd containers used in GKE. It is not used in Docker and podman. +// - filter uses labels to filter the containers. `filter` is not used in podman containers. +func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsupportcontainers bool, containerID string) ([]explorers.Drift, error) { + var drifts []explorers.Drift + + for _, podmanRootDir := range e.podmanRootDirs { + configs, err := e.readContainerConfig(podmanRootDir) + if err != nil { + log.WithFields(log.Fields{"podmanRootDir": podmanRootDir, "error": err}).Error("reading container config") + continue + } + + for _, config := range configs { + // If containerID is supplied & doesn't match skip + if containerID != "" && config.ID != containerID && (len(config.Names) == 0 || config.Names[0] != containerID) { + continue + } + + log.WithFields(log.Fields{"containerType": "podman", "containerID": config.ID}).Debug("checking container drift") + + // Get upperdir for Podman container + overlayDir := filepath.Join(podmanRootDir, "storage", "overlay") + layerDir := filepath.Join(overlayDir, config.Layer) + + linkFile := filepath.Join(layerDir, "link") + linkData, err := os.ReadFile(linkFile) + if err != nil { + log.WithFields(log.Fields{"container": config.ID, "error": err}).Error("reading link file") + continue + } + upperDir := filepath.Join(overlayDir, "l", strings.TrimSpace(string(linkData))) + log.WithFields(log.Fields{"containerType": "podman", "containerID": config.ID, "upperdir": upperDir}).Debug("checking upper layer for drift") + + // Scan upperdir + addedOrModified, inaccessibleFiles, err := explorers.ScanDiffDirectory(upperDir) + if err != nil { + log.WithFields(log.Fields{"container": config.ID, "error": err}).Error("failed to scan diff directory") + continue + } + + drift := explorers.Drift{ + ContainerID: config.ID, + ContainerType: "podman", + AddedOrModified: addedOrModified, + InaccessibleFiles: inaccessibleFiles, + } + + log.WithFields(log.Fields{ + "containerType": drift.ContainerType, + "containerID": drift.ContainerID, + "numAddedOrModified": len(drift.AddedOrModified), + "numInaccessibleFiles": len(drift.InaccessibleFiles), + }).Debug("container drift detail") + + drifts = append(drifts, drift) + } + } + + return drifts, nil +} + +// Close closes explorer. +func (e *explorer) Close() error { + // No closing required for podman explorer. + // Returning nil + + return nil +} + +func (e *explorer) getUserHomeDirs() ([]string, error) { + passwdFile := filepath.Join(e.imageroot, "etc", "passwd") + data, err := os.ReadFile(passwdFile) + if err != nil { + return nil, fmt.Errorf("error reading passwd file: %w", err) + } + + var homeDirs []string + + passwdLines := strings.Split(string(data), "\n") + for _, passwdLine := range passwdLines { + if passwdLine == "" { + continue + } + // ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash + parts := strings.Split(passwdLine, ":") + if len(parts) < 7 { + log.WithField("line", "REDACTED").Debug("skipping malformed passwd entry") + continue + } + + shell := parts[6] + if !strings.HasSuffix(shell, "/bash") && !strings.HasSuffix(shell, "/sh") && !strings.HasSuffix(shell, "/zsh") && !strings.HasSuffix(shell, "/fish") { + continue + } + + if parts[5] == "" { + log.WithField("line", "REDACTED").Debug("passwd entry missing home directory") + continue + } + + homeDirs = append(homeDirs, parts[5]) + } + + return homeDirs, nil +} + +// getPodmanRootDirs scans for storage.conf and `graphroot` and returns the based directory +// as the podmanRootDir. +func (e *explorer) getPodmanRootDirs() ([]string, error) { + var podmanRootDirs []string + + // Podman containers in user directories + homedirs, err := e.getUserHomeDirs() + if err != nil { + log.WithError(err).Info("failed to list user home directories") + } else { + for _, homedir := range homedirs { + userStorageGraphRoot, err := e.getUserStorageGraphRoot(homedir) + if err != nil { + log.WithFields(log.Fields{ + "homedir": homedir, + "error": err, + }).Debug("failed to get user storage graphroot") + continue + } + if ok := utils.PathExistsV2(userStorageGraphRoot); !ok { + continue + } + podmanRootDirs = append(podmanRootDirs, filepath.Dir(userStorageGraphRoot)) + } + } + + // Podman containers in system directory + systemStorageGraphRoot, err := e.getSystemStorageGraphRoot() + if err != nil { + log.WithError(err).Debug("failed to get system graphroot") + } else { + if ok := utils.PathExistsV2(systemStorageGraphRoot); ok { + podmanRootDirs = append(podmanRootDirs, filepath.Dir(systemStorageGraphRoot)) + } + } + + return podmanRootDirs, nil +} + +func (e *explorer) getUserStorageGraphRoot(homedir string) (string, error) { + userPodmanStorageConfig := filepath.Join(e.imageroot, strings.TrimPrefix(homedir, "/"), ".config", "containers", "storage.conf") + graphRoot := filepath.Join(homedir, ".local/share/containers/storage") + + if ok := utils.PathExistsV2(userPodmanStorageConfig); ok { + var config struct { + Storage struct { + GraphRoot string `toml:"graphroot"` + RootlessStoragePath string `toml:"rootless_storage_path"` + } `toml:"storage"` + } + if _, err := toml.DecodeFile(userPodmanStorageConfig, &config); err == nil { + if config.Storage.GraphRoot != "" { + graphRoot = config.Storage.GraphRoot + } else if config.Storage.RootlessStoragePath != "" { + graphRoot = config.Storage.RootlessStoragePath + } + } else { + log.WithFields(log.Fields{ + "path": userPodmanStorageConfig, + "error": err, + }).Warn("failed to decode user storage config") + } + } + + if strings.HasPrefix(graphRoot, "~/") { + graphRoot = filepath.Join(homedir, graphRoot[2:]) + } else if strings.HasPrefix(graphRoot, "$HOME/") { + graphRoot = filepath.Join(homedir, graphRoot[6:]) + } + + return filepath.Join(e.imageroot, strings.TrimPrefix(graphRoot, "/")), nil +} + +func (e *explorer) getSystemStorageGraphRoot() (string, error) { + storeOpts, err := storageTypes.LoadStoreOptions(storageTypes.LoadOptions{ + RootForImplicitAbsolutePaths: e.imageroot, + }) + if err != nil { + return "", fmt.Errorf("loading storage options: %w", err) + } + return filepath.Join(e.imageroot, storeOpts.GraphRoot), nil +} + +func (e *explorer) readContainerConfig(podmanRootDir string) ([]containerConfig, error) { + var configs []containerConfig + + containerDir := filepath.Join(podmanRootDir, "storage", "overlay-containers") + configFile := filepath.Join(containerDir, "containers.json") + + configData, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("reading containers.json: %w", err) + } + + if err := json.Unmarshal(configData, &configs); err != nil { + return nil, fmt.Errorf("unmarshalling containers.json: %w", err) + } + + return configs, nil +} + +func (e *explorer) mountContainer(ctx context.Context, podmanRootDir string, containerID string, layer string, mountpoint string) error { + overlayDir := filepath.Join(podmanRootDir, "storage", "overlay") + layerDir := filepath.Join(overlayDir, layer) + + // Upperdir as link + linkFile := filepath.Join(layerDir, "link") + linkData, err := os.ReadFile(linkFile) + if err != nil { + return fmt.Errorf("reading link file: %w", err) + } + upperDir := filepath.Join(overlayDir, "l", strings.TrimSpace(string(linkData))) + + // Lowerdir + lowerFile := filepath.Join(layerDir, "lower") + lowerData, err := os.ReadFile(lowerFile) + if err != nil { + return fmt.Errorf("reading lower file: %w", err) + } + + log.WithFields(log.Fields{ + "containerID": containerID, + "podmanRootDir": podmanRootDir, + "overlayDir": overlayDir, + "lowerdir": strings.TrimSpace(string(lowerData)), + "upperdir": strings.TrimSpace(string(linkData)), + }).Infof("container layers") + + var lowerDirs []string + for _, lowerDir := range strings.Split(strings.TrimSpace(string(lowerData)), ":") { + lowerDirs = append(lowerDirs, filepath.Join(overlayDir, lowerDir)) + } + lowerDir := strings.Join(lowerDirs, ":") + + // Linux mount options + mountOpt := fmt.Sprintf("ro,lowerdir=%s:%s", upperDir, lowerDir) + mountArgs := []string{"-t", "overlay", "overlay", "-o", mountOpt, mountpoint} + + cmd := exec.Command("mount", mountArgs...) + out, err := cmd.CombinedOutput() + if err != nil { + log.Infof("mount command: mount %s", strings.Join(mountArgs, " ")) + if string(out) != "" { + return fmt.Errorf("running mount command: %w, output: %s", err, strings.TrimSpace(string(out))) + } + return fmt.Errorf("running mount command: %w", err) + } + if string(out) != "" { + log.Infof("mount command output: %s", string(out)) + } + + return nil +} diff --git a/explorers/snapshot.go b/explorers/snapshot.go index 15f333f..e5c7d34 100644 --- a/explorers/snapshot.go +++ b/explorers/snapshot.go @@ -27,18 +27,19 @@ import ( // SnapshotKeyInfo contains information found in containerd // metadata (meta.db) and snapshot database (metadata.db). type SnapshotKeyInfo struct { - Namespace string // namespace only used in meta.db - Snapshotter string // only used in meta.db - Key string // snapshot key - ID uint64 // File system ID. Only used in metadata.db - Name string // snapshot name. Only used in meta.db - Parent string // snapshot parent - Kind snapshots.Kind // snapshot kind - Inodes []int64 // Inode numbers. Only in metadata.db - Size uint64 // Only in metadata.db - OverlayPath string // Custom field added by container explorer - Labels map[string]string // mapped labels - Children []string // array of . Only in meta.db - CreatedAt time.Time // created timestamp - UpdatedAt time.Time // updated timestamp + ContainerType string // container type: containerd, docker, podman, etc. + Namespace string // namespace only used in meta.db + Snapshotter string // only used in meta.db + Key string // snapshot key + ID uint64 // File system ID. Only used in metadata.db + Name string // snapshot name. Only used in meta.db + Parent string // snapshot parent + Kind snapshots.Kind // snapshot kind + Inodes []int64 // Inode numbers. Only in metadata.db + Size uint64 // Only in metadata.db + OverlayPath string // Custom field added by container explorer + Labels map[string]string // mapped labels + Children []string // array of . Only in meta.db + CreatedAt time.Time // created timestamp + UpdatedAt time.Time // updated timestamp } diff --git a/explorers/state.go b/explorers/state.go index 6a80af4..7b06286 100644 --- a/explorers/state.go +++ b/explorers/state.go @@ -24,14 +24,14 @@ import "time" // // The State structure only maps the required attributes form state.json. type State struct { - ID string `json:"state,omitempty"` - InitProcessPid int `json:"init_process_pid"` - InitProcessstart int `json:"init_process_start"` - Created time.Time `json:"created"` - Config map[string]interface{} `json:"config"` - Rootless bool `json:"rootless"` - CgroupPaths map[string]string `json:"cgroup_paths"` - NamespacePaths map[string]string `json:"namespace_paths"` - ExternalDescriptors []string `json:"external_descriptors"` - IntelRdtPath string `json:"intel_rdt_path"` + ID string `json:"state,omitempty"` + InitProcessPid int `json:"init_process_pid"` + InitProcessstart int `json:"init_process_start"` + Created time.Time `json:"created"` + Config map[string]any `json:"config"` + Rootless bool `json:"rootless"` + CgroupPaths map[string]string `json:"cgroup_paths"` + NamespacePaths map[string]string `json:"namespace_paths"` + ExternalDescriptors []string `json:"external_descriptors"` + IntelRdtPath string `json:"intel_rdt_path"` } diff --git a/explorers/support_containers.go b/explorers/support_containers.go index 5273222..970135b 100644 --- a/explorers/support_containers.go +++ b/explorers/support_containers.go @@ -40,6 +40,9 @@ type SupportContainer struct { // NewSupportContainer returns the support container instance. func NewSupportContainer(path string) (*SupportContainer, error) { + if path == "" { + return nil, nil + } sc, err := LoadSupportContainerFromFile(path) if err != nil { return nil, err @@ -70,7 +73,7 @@ func LoadSupportContainerFromFile(path string) (SupportContainer, error) { // container image. func (sc *SupportContainer) SupportContainerImage(image string) bool { if sc == nil { - log.WithField("imagebase", image).Debug("support container data not initialized") + log.WithField("imageBase", image).Debug("support container data not initialized") return false } @@ -81,12 +84,12 @@ func (sc *SupportContainer) SupportContainerImage(image string) bool { } */ if strings.Contains(strings.ToLower(image), strings.ToLower(scimage)) { - log.WithField("imagebase", image).Debug("support container image found") + log.WithField("imageBase", image).Debug("support container image found") return true } } // default - log.WithField("imagebase", image).Debug("support container image not found") + log.WithField("imageBase", image).Debug("support container image not found") return false } diff --git a/go.mod b/go.mod index a9a376b..ebfffdc 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,212 @@ module github.com/google/container-explorer -go 1.24.3 +go 1.25.10 require ( - github.com/containerd/containerd v1.7.29 + github.com/BurntSushi/toml v1.6.0 + github.com/containerd/containerd v1.7.32 + github.com/containerd/containerd/v2 v2.2.2 + github.com/docker/docker v28.5.2+incompatible + github.com/mattn/go-sqlite3 v1.14.44 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 - github.com/opencontainers/runtime-spec v1.1.0 - github.com/sirupsen/logrus v1.9.3 - github.com/urfave/cli v1.22.12 - go.etcd.io/bbolt v1.3.10 + github.com/opencontainers/image-spec v1.1.1 + github.com/opencontainers/runtime-spec v1.3.0 + github.com/sirupsen/logrus v1.9.4 + github.com/urfave/cli v1.22.17 + go.etcd.io/bbolt v1.4.3 + go.podman.io/podman/v6 v6.0.0-20260521125140-2d09c79dfe54 + go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844 gopkg.in/yaml.v3 v3.0.1 ) require ( + cyphar.com/go-pathrs v0.2.4 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.11.7 // indirect - github.com/containerd/cgroups v1.1.0 // indirect - github.com/containerd/containerd/api v1.8.0 // indirect - github.com/containerd/continuity v0.4.4 // indirect - github.com/containerd/errdefs v0.3.0 // indirect + github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/checkpoint-restore/checkpointctl v1.5.0 // indirect + github.com/checkpoint-restore/go-criu/v7 v7.2.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/cfssl v1.6.4 // indirect + github.com/containerd/cgroups/v3 v3.1.3 // indirect + github.com/containerd/containerd/api v1.10.0 // indirect + github.com/containerd/continuity v0.4.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect - github.com/containerd/typeurl/v2 v2.1.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/containerd/platforms v1.0.0-rc.4 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect + github.com/containerd/ttrpc v1.2.8 // indirect + github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect + github.com/containers/luksy v0.0.0-20251208191447-ca096313c38f // indirect + github.com/containers/ocicrypt v1.3.0 // indirect + github.com/containers/psgo v1.10.0 // indirect + github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/disiqueira/gotree/v3 v3.0.2 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.7 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fsouza/go-dockerclient v1.13.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/klauspost/compress v1.16.7 // indirect - github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.21.1 // indirect + github.com/google/go-intervals v0.0.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/jmoiron/sqlx v1.3.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-shellwords v1.0.13 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect + github.com/moby/buildkit v0.29.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.1 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/swarmkit/v2 v2.1.2 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/sys/capability v0.4.0 // indirect + github.com/moby/sys/devices v0.1.0 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/opencontainers/cgroups v0.0.6 // indirect + github.com/opencontainers/runc v1.4.2 // indirect + github.com/opencontainers/runtime-tools v0.9.1-0.20260316125833-8a4db579f5c8 // indirect + github.com/opencontainers/selinux v1.14.1 // indirect + github.com/openshift/imagebuilder v1.2.21 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.10 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/proglottis/gpgme v0.1.6 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/rootless-containers/rootlesskit/v2 v2.3.6 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/seccomp/libseccomp-golang v0.11.1 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.4 // indirect + github.com/sigstore/fulcio v1.8.5 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/sigstore v1.10.6 // indirect + github.com/sigstore/sigstore-go v1.1.4 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/smallstep/pkcs7 v0.1.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + github.com/sylabs/sif/v2 v2.24.0 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/vbatts/tar-split v0.12.3 // indirect + github.com/vbauerster/mpb/v8 v8.12.1 // indirect + github.com/vishvananda/netlink v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc // indirect + github.com/zmap/zlint/v3 v3.1.0 // indirect + go.etcd.io/raft/v3 v3.6.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect - google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/contrib/processors/baggagecopy v0.16.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b // indirect + go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844 // indirect + go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect + tags.cncf.io/container-device-interface v1.1.0 // indirect + tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 9c4de73..e68d4e3 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,229 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +code.cloudfoundry.org/clock v1.1.0 h1:XLzC6W3Ah/Y7ht1rmZ6+QfPdt1iGWEAAtIZXgiaj57c= +code.cloudfoundry.org/clock v1.1.0/go.mod h1:yA3fxddT9RINQL2XHS7PS+OXxKCGhfrZmlNUCIM6AKo= +cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY= +cyphar.com/go-pathrs v0.2.4/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= -github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= +github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs= +github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/checkpointctl v1.5.0 h1:Uu+D2cOf/GUyCMk23Y8L69P6YoATTe6pH+Au64O3y28= +github.com/checkpoint-restore/checkpointctl v1.5.0/go.mod h1:y5HRs1ZWQUZGyEuthlTHmTJN9PUMOjlaH6JvVaNq9kE= +github.com/checkpoint-restore/go-criu/v7 v7.2.0 h1:qGiWA4App1gGlEfIJ68WR9jbezV9J7yZdjzglezcqKo= +github.com/checkpoint-restore/go-criu/v7 v7.2.0/go.mod h1:u0LCWLg0w4yqqu14aXhiB4YD3a1qd8EcCEg7vda5dwo= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= +github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= -github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= -github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= -github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= -github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= -github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= +github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= +github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8= +github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= +github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= +github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= +github.com/containerd/containerd/v2 v2.2.2 h1:mjVQdtfryzT7lOqs5EYUFZm8ioPVjOpkSoG1GJPxEMY= +github.com/containerd/containerd/v2 v2.2.2/go.mod h1:5Jhevmv6/2J+Iu/A2xXAdUIdI5Ah/hfyO7okJ4AFIdY= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= -github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= +github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= +github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/luksy v0.0.0-20251208191447-ca096313c38f h1:DWGVgZ9ToKBOMiBMv//ilrKPKEy4tQG752XtTJXYUQ0= +github.com/containers/luksy v0.0.0-20251208191447-ca096313c38f/go.mod h1:bynzkJ2rWsqgt0s31fuAdkHZBB3zrPtD6pV2zHt/qRk= +github.com/containers/ocicrypt v1.3.0 h1:ps3St6ZWNWhOQ/Kqld6K2wPHt01Mj3AqRTNCZLIWOfo= +github.com/containers/ocicrypt v1.3.0/go.mod h1:PmfuGFpBwnGLnbqBm+QIy2nc8noDJ1Wt6B19la7VBFo= +github.com/containers/psgo v1.10.0 h1:r9cEzAMVRtC0sw4ayIPjbd9EgF9pPaTCqKgDHhS0D/8= +github.com/containers/psgo v1.10.0/go.mod h1:e44fw+1A7eJH1y0eWAo3P7sjfftXDlfF4AY498h+svQ= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWhkNRq8= +github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs= +github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA= +github.com/docker/docker-credential-helpers v0.9.7/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 h1:IMfrF5LCzP2Vhw7j4IIH3HxPsCLuZYjDqFAM/C88ulg= +github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fsouza/go-dockerclient v1.13.1 h1:HbkJO8UPUhuQ1wHIgX+ho7AUucBmjtOfzSWYUgmWL/8= +github.com/fsouza/go-dockerclient v1.13.1/go.mod h1:0gx0SIFGV1F+79sM9p5K+UCc8enYKA8DBp7BlwlixpM= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= +github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= +github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -51,101 +232,501 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc= +github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= +github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.2 h1:RBKHOsnSszpU6vxq80LzC2BaQjuuvoyaQbkLTf7V7g8= +github.com/hashicorp/go-memdb v1.3.2/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= +github.com/in-toto/in-toto-golang v0.10.0 h1:+s2eZQSK3WmWfYV85qXVSBfqgawi/5L02MaqA4o/tpM= +github.com/in-toto/in-toto-golang v0.10.0/go.mod h1:wjT4RiyFlLWCmLUJjwB8oZcjaq7HA390aMJcD3xXgmg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2 h1:36qep4gxKs+JgeHGWeQ040RyZdt9kQlLglL1rFVn/oQ= +github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= +github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= +github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4= +github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0= +github.com/moby/buildkit v0.29.0 h1:wxLEFbCOJntEDjSNNN2YWd8zxltZxT5muDQ0LzpbtpU= +github.com/moby/buildkit v0.29.0/go.mod h1:Dmv2FeDe34t75QuzeU87rBoZpAAkcpT5zeu4hXzmASc= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I= +github.com/moby/swarmkit/v2 v2.1.2/go.mod h1:GQ6T0ij2oBbWX10OHwpvK449xfXkQMZ8J+B+eQ4mgp4= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= +github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= +github.com/moby/sys/devices v0.1.0 h1:uaMrDm1U3h0AwUDNWeT5lBV40v0eayt+VuukRbYn5K4= +github.com/moby/sys/devices v0.1.0/go.mod h1:nIV6AO7t0DY2ObAm1GfL4AX9mBRqzxzHwGfvNCR9lfI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag= +github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA= +github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/cgroups v0.0.6 h1:tfZFWTIIGaUUFImTyuTg+Mr5x8XRiSdZESgEBW7UxuI= +github.com/opencontainers/cgroups v0.0.6/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= -github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.4.2 h1:/AEjjXuVH9lTRl9ZyUFQj7oWBM7Xv00qFV6Vx9q5N3o= +github.com/opencontainers/runc v1.4.2/go.mod h1:ufk5PTTsy5pnGBAvTh50e+eqGk01pYH2YcVxh557Qlk= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.9.1-0.20260316125833-8a4db579f5c8 h1:2NAWFjN0PmdIe3XojVL9wf3lJ1//VqAgc7MOSYHQslE= +github.com/opencontainers/runtime-tools v0.9.1-0.20260316125833-8a4db579f5c8/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= +github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= +github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +github.com/openshift/imagebuilder v1.2.21 h1:XX0tZVznWTxzYevvNVZ/0eeTzmgY6cfcT4/xjs5ToyU= +github.com/openshift/imagebuilder v1.2.21/go.mod h1:+L09sXUQ0RPdCU1tmzKrfBhqMlYvZtaA3MHb7aTjVU8= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs= +github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98= +github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rootless-containers/rootlesskit/v2 v2.3.6 h1:m/26nAx0DbHZYaM46+uoQjfpu9G77QLzWj2jz25chO8= +github.com/rootless-containers/rootlesskit/v2 v2.3.6/go.mod h1:pv+RESmjRmeUIOsEWOT1f8560CrdaQrDW0YsF4K5kAY= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= +github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/seccomp/libseccomp-golang v0.11.1 h1:wuk4ZjSx6kyQII4rj6G6fvVzRHQaSiPvccJazDagu4g= +github.com/seccomp/libseccomp-golang v0.11.1/go.mod h1:5m1Lk8E9OwgZTTVz4bBOer7JuazaBa+xTkM895tDiWc= +github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= +github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= +github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ= +github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ= +github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= +github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/sigstore v1.10.6 h1:YWhMQfTrJSK80QB1pbxjYeAwGKx+5UwWPPAY9hrPPZg= +github.com/sigstore/sigstore v1.10.6/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/timestamp-authority/v2 v2.0.3 h1:sRyYNtdED/ttLCMdaYnwpf0zre1A9chvjTnCmWWxN8Y= +github.com/sigstore/timestamp-authority/v2 v2.0.3/go.mod h1:mDaHxkt3HmZYoIlwYj4QWo0RUr7VjYU52aVO5f5Qb3I= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= +github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= -github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w= +github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= +github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= +github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= +github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0= +github.com/vbauerster/mpb/v8 v8.12.1 h1:pyj3yQ2ZGQJgUXm4h17QpR+eERaNz5OQ1ftPSEE/sMM= +github.com/vbauerster/mpb/v8 v8.12.1/go.mod h1:XLXRfStkw/6i5k0aQltijDHT1Z93fD1DVwmIdcFUp6k= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= +github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss= +github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20210123152837-9cf5beac6d91/go.mod h1:R/deQh6+tSWlgI9tb4jNmXxn8nSCabl5ZQsBX9//I/E= +github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw= +github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk= +github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0= +github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/contrib/processors/baggagecopy v0.16.0 h1:P145nWHakUG/Of8vorFJ5pTPM+tlRbnAACpYYddCpgk= +go.opentelemetry.io/contrib/processors/baggagecopy v0.16.0/go.mod h1:VK5EpYdxPlVXEMWpmPDFxRzSepc/nDmYGV9rQb5Wcuk= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b h1:i8ntFzITajbJA3ojnA0ZdpbC+I+ccweZvZaGIhQb4i8= +go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b/go.mod h1:hPvgsjBU09C+15fKoIZJvKvNaxR+c0QvMg/n4NgBS7A= +go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844 h1:Un2Wz6Ni/QmkVC528gXOVvVKAM/vOi/c7kLK8gWvmvI= +go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844/go.mod h1:bhfqGXJ/cMC6CYubcmENInEQlemgClO0ea9+e1Mz/8k= +go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844 h1:wbh4wP38Ba13JjvHtNS2RZrI+L2/n2pIhQY36dUaebw= +go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844/go.mod h1:Jg91tpyyYgFAQLG1YjWbaZs4NHRzLWpDgCZyiOvDZyY= +go.podman.io/podman/v6 v6.0.0-20260521125140-2d09c79dfe54 h1:47cNXSj34kjOPOvQ0CyEO7OMuC4WzikwaNSECmgZdaw= +go.podman.io/podman/v6 v6.0.0-20260521125140-2d09c79dfe54/go.mod h1:aMCD+fMUgOwoHuLUyYYsYGJzVB7mZ08yzrnNclT7vzQ= +go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844 h1:s3bBIQVRqZ4uusgEMaVmUfXXphj4tEsg7BwiQB4COkQ= +go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -154,26 +735,37 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= -google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -183,15 +775,34 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= +tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= +tags.cncf.io/container-device-interface/specs-go v1.1.0 h1:QRZVeAceQM+zTZe12eyfuJuuzp524EKYwhmvLd+h+yQ= +tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= diff --git a/utils/export.go b/utils/export.go new file mode 100644 index 0000000..02d35a0 --- /dev/null +++ b/utils/export.go @@ -0,0 +1,260 @@ +/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package utils implements common utility functions used by Container Explorer +package utils + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" +) + +// ExportContainerImage creates a raw disk image file of a container. +func ExportContainerImage(ctx context.Context, containerID string, mountpoint string, outputDir string) error { + var success bool + imageFileName := fmt.Sprintf("%s.raw", containerID) + imageFilePath := filepath.Join(outputDir, imageFileName) + + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) + } + + defer func() { + if !success { + log.Infof("cleaning up incomplete image file: %s", imageFilePath) + os.Remove(imageFilePath) + } + }() + + // 1. Calculate the required size for the image. + contentSize, err := CalculateDirectorySize(mountpoint) + if err != nil { + return fmt.Errorf("failed to calculate content size for %s: %w", mountpoint, err) + } + log.Infof("calculated the content size of %s as %d bytes", mountpoint, contentSize) + + // Add overhead for filesystem structures (e.g., 20MB base + 5% of content size for inodes, metadata) + overhead := int64(20*1024*1024) + (contentSize / 20) + imageSize := contentSize + overhead + + log.Infof("preparing to create target disk %s of size %d bytes", imageFilePath, imageSize) + + // 2. Create the image file + imgFile, err := os.Create(imageFilePath) + if err != nil { + return fmt.Errorf("failed to create image file %s: %w", imageFilePath, err) + } + + // 3. Set the image file size + if err := imgFile.Truncate(imageSize); err != nil { + imgFile.Close() + return fmt.Errorf("failed to truncate image file %s to size %d: %w", imageFilePath, imageSize, err) + } + + // 4. Sync and Close the file before formatting + if err := imgFile.Sync(); err != nil { + log.Warnf("failed to sync image file %s after truncation: %v", imageFilePath, err) + } + if err := imgFile.Close(); err != nil { + return fmt.Errorf("failed to close image file %s before formatting: %w", imageFilePath, err) + } + log.Infof("successfully created container %s target image file %s", containerID, imageFilePath) + + // 5. Format the image file as ext4 + mkfsCmd := exec.CommandContext(ctx, "mkfs.ext4", "-F", "-q", imageFilePath) + mkfsOutput, err := mkfsCmd.CombinedOutput() + if err != nil { + log.WithFields(log.Fields{ + "command": mkfsCmd.String(), + "output": string(mkfsOutput), + "error": err, + }).Error("formatting target image") + return fmt.Errorf("mkfs.ext4 failed for %s: %w. Output: %s", imageFilePath, err, string(mkfsOutput)) + } + log.Infof("successfully formatted image %s as ext4", imageFilePath) + + // 6. Mount the formatted image, copy data, then unmount. + log.Infof("preparing to copy data from %s to image %s", mountpoint, imageFilePath) + + imageMountDir, err := os.MkdirTemp("", fmt.Sprintf("%s-img-mount-*.d", containerID)) + if err != nil { + return fmt.Errorf("failed to create temporary mount directory for image %s: %w", imageFilePath, err) + } + log.Infof("created temporary image mount directory: %s", imageMountDir) + + var loopDevice string + imageSuccessfullyMounted := false + + // Defer cleanup actions in LIFO order (unmount image, detach loop, remove temp dir) + var unmounted bool + defer func() { + if imageSuccessfullyMounted { + log.Infof("unmounting image from %s", imageMountDir) + umountCmd := exec.Command("umount", imageMountDir) // Use non-contextual command for cleanup + // Best effort unmount + if umountErr := umountCmd.Run(); umountErr == nil { + unmounted = true + log.Infof("successfully unmounted image filesystem from %s", imageMountDir) + } else { + umountOutput, _ := umountCmd.CombinedOutput() // Get output for logging + log.Warnf("failed to unmount image filesystem from %s: %v; output: %s", imageMountDir, umountErr, string(umountOutput)) + // Try lazy unmount + log.Infof("attempting lazy unmount from %s", imageMountDir) + lazyUmountCmd := exec.Command("umount", "-l", imageMountDir) + if lazyErr := lazyUmountCmd.Run(); lazyErr == nil { + unmounted = true + } else { + lazyOutput, _ := lazyUmountCmd.CombinedOutput() + log.Warnf("lazy unmount also failed: %v; output: %s", lazyErr, string(lazyOutput)) + } + } + } + + if loopDevice != "" { + log.Infof("detaching loop device %s for image %s", loopDevice, imageFilePath) + losetupDetachCmd := exec.Command("losetup", "-d", loopDevice) // Use non-contextual command for cleanup + // Best effort detach + if detachErr := losetupDetachCmd.Run(); detachErr != nil { + detachOutput, _ := losetupDetachCmd.CombinedOutput() // Get output for logging + log.Warnf("failed to detach loop device %s: %v; output: %s", loopDevice, detachErr, string(detachOutput)) + } else { + log.Infof("successfully detached loop device %s", loopDevice) + } + } + + if !imageSuccessfullyMounted || unmounted { + log.Infof("removing temporary image mount directory %s", imageMountDir) + if err := os.RemoveAll(imageMountDir); err != nil { + log.Warnf("failed to remove temporary image mount directory %s: %v", imageMountDir, err) + } + } else { + log.Warnf("skipping removal of temporary image mount directory %s because unmount failed", imageMountDir) + } + }() + + // 6.1. Setup loop device + log.Infof("setting up loop device for %s", imageFilePath) + var stdoutBuf, stderrBuf bytes.Buffer + losetupCmd := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath) + losetupCmd.Stdout = &stdoutBuf + losetupCmd.Stderr = &stderrBuf + err = losetupCmd.Run() + if err != nil { + log.Errorf("losetup -f --show %s failed: %v; stderr: %s", imageFilePath, err, stderrBuf.String()) + return fmt.Errorf("losetup -f --show %s failed: %w. Output: %s", imageFilePath, err, stderrBuf.String()) + } + loopDevice = strings.TrimSpace(stdoutBuf.String()) + if loopDevice == "" { + log.Errorf("losetup -f --show %s returned an empty loop device path", imageFilePath) + return fmt.Errorf("losetup -f --show %s returned an empty loop device path", imageFilePath) + } + log.Infof("image %s associated with loop device %s", imageFilePath, loopDevice) + + // 6.2. Mount the loop device + log.Infof("mounting loop device %s to %s", loopDevice, imageMountDir) + mountImageCmd := exec.CommandContext(ctx, "mount", loopDevice, imageMountDir) + mountImageOutput, err := mountImageCmd.CombinedOutput() + if err != nil { + log.Errorf("failed to mount %s to %s: %v; output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) + return fmt.Errorf("failed to mount loop device %s to %s: %w. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) + } + imageSuccessfullyMounted = true // Set flag for deferred cleanup + log.Infof("successfully mounted %s to %s; output: %s", loopDevice, imageMountDir, string(mountImageOutput)) + + // 6.3. Copy content from container's mountpoint to the image's mountpoint + // Source path: mountpoint + "/." to copy contents of the directory, including hidden files. + log.Infof("copying contents from %s to %s using 'cp -a'", mountpoint, imageMountDir) + + copyCmd := exec.CommandContext(ctx, "cp", "-a", filepath.Join(mountpoint, "."), imageMountDir) + copyOutput, err := copyCmd.CombinedOutput() + if err != nil { + log.Errorf("failed to copy data from %s to %s: %v; output: %s", mountpoint, imageMountDir, err, string(copyOutput)) + return fmt.Errorf("failed to copy data from %s to %s: %w. Output: %s", mountpoint, imageMountDir, err, string(copyOutput)) + } + log.Infof("successfully copied data from %s to %s; output: %s", mountpoint, imageMountDir, string(copyOutput)) + + // 6.4. Sync filesystem buffers to ensure all data is written to the image + log.Info("syncing filesystem buffers for the image") + syncCmd := exec.CommandContext(ctx, "sync", "-f", imageMountDir) + if syncErr := syncCmd.Run(); syncErr != nil { + syncOutput, _ := syncCmd.CombinedOutput() // Get output for logging + log.Warnf("sync command failed after copying to image: %v. Output: %s", syncErr, string(syncOutput)) + } else { + log.Info("filesystem buffers synced") + } + + log.Infof("image %s successfully created, formatted, and populated", imageFilePath) + + success = true + return nil +} + +// ExportContainerArchive creates a .tar.gz archive of the content of the mountpoint. +func ExportContainerArchive(ctx context.Context, containerID string, mountpoint string, outputDir string) error { + var success bool + archiveFileName := fmt.Sprintf("%s.tar.gz", containerID) + archiveFilePath := filepath.Join(outputDir, archiveFileName) + + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) + } + + defer func() { + if !success { + log.Infof("cleaning up incomplete archive file: %s", archiveFilePath) + os.Remove(archiveFilePath) + } + }() + + log.WithFields(log.Fields{ + "containerID": containerID, + "mountpoint": mountpoint, + "archiveFilePath": archiveFilePath, + }).Debug("preparing to create container archive") + + // Command: tar -czf -C . + // -c: create + // -z: gzip + // -f: file + // -C : change to directory before processing files + // .: process all files in the current directory (which is due to -C) + tarCmd := exec.CommandContext(ctx, "tar", "-czf", archiveFilePath, "-C", mountpoint, ".") + + tarOutput, err := tarCmd.CombinedOutput() + if err != nil { + log.WithFields(log.Fields{ + "command": tarCmd.String(), + "output": string(tarOutput), + "error": err, + }).Error("tar command failed") + return fmt.Errorf("failed to create archive %s: %w. Output: %s", archiveFilePath, err, string(tarOutput)) + } + + log.WithFields(log.Fields{ + "archiveFilePath": archiveFilePath, + "output": string(tarOutput), + }).Debug("successfully created container archive") + + success = true + return nil +} diff --git a/utils/file.go b/utils/file.go old mode 100644 new mode 100755 index 1592481..008a741 --- a/utils/file.go +++ b/utils/file.go @@ -17,17 +17,19 @@ limitations under the License. package utils import ( + "crypto/rand" "errors" "fmt" + "io" "io/fs" - "math/rand" "os" "path/filepath" - "time" + + log "github.com/sirupsen/logrus" ) -// PathExists returns true of specified file or directory exists. -// If symlink is provided, it returns error. +// PathExists returns true if the specified file or directory exists. +// This function follows symbolic links. func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { @@ -41,25 +43,33 @@ func PathExists(path string) (bool, error) { return false, err } +// PathExistsV2 returns true if the path exists. +// It follows symbolic links and returns false on any error (including permission denied). +func PathExistsV2(path string) bool { + exists, _ := PathExists(path) + return exists +} + const ( - charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) -var seededRand *rand.Rand = rand.New( - rand.NewSource(time.Now().UnixNano())) - -// GenerateRandomString creates a random string of a fixed length (6 characters). +// GenerateRandomString creates a random string of a fixed length. +// It is thread-safe and uses crypto/rand for high-quality randomness. func GenerateRandomString(stringLength int) string { b := make([]byte, stringLength) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "" + } for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] + b[i] = charset[int(b[i])%len(charset)] } return string(b) } func GetMountPoint() string { mountSuffix := GenerateRandomString(6) - mountPoint := filepath.Join("/", "mnt", mountSuffix) + mountPoint := filepath.Join(os.TempDir(), "mnt", mountSuffix) return mountPoint } @@ -85,10 +95,6 @@ func CalculateDirectorySize(rootPath string) (int64, error) { // If rootPath itself is a symlink to a directory, WalkDir will follow it once. walkErr := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error { if err != nil { - // This error is from WalkDir itself (e.g., permission denied reading a directory). - // We'll return it to stop the walk. - // You could choose to log this error and return nil to try to continue, - // or return filepath.SkipDir if d is a directory. return fmt.Errorf("error accessing path %s: %w", path, err) } @@ -102,12 +108,11 @@ func CalculateDirectorySize(rootPath string) (int64, error) { // d.Info() would give info about the symlink itself, not its target. fileInfo, statErr := os.Stat(path) if statErr != nil { - // If it's a broken symlink (os.IsNotExist error and entry is a symlink), skip it. + // If it's a broken symlink, skip it. if os.IsNotExist(statErr) && (d.Type()&fs.ModeSymlink != 0) { - fmt.Fprintf(os.Stderr, "Warning: skipping broken symlink %s\n", path) + log.Debugf("skipping broken symlink %s", path) return nil // Continue walking } - // For other stat errors, stop the walk. return fmt.Errorf("failed to stat %s: %w", path, statErr) } @@ -124,4 +129,4 @@ func CalculateDirectorySize(rootPath string) (int64, error) { } return totalSize, nil -} \ No newline at end of file +}