From 788fcd383407818ab64ef53a70bda0bec695de8e Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 8 Apr 2025 12:19:18 -0400 Subject: [PATCH] chore: refactor command structure (#587) * refactor cli harness Signed-off-by: Alex Goodman * use single configuration for ui Signed-off-by: Alex Goodman * remove remaining viper rules * add basic CLI tests Signed-off-by: Alex Goodman * separate cmd and api concerns for keybindings * plumb through context for future logging * port to using internal logger * internalize non-analysis path * encapsulate ui with clio * merge runtime into cmd + add adapter package * support legacy config shapes * improve testing around formatting * fix log-ui interactions * fix linting and update test snapshots * fix initialization of tree viewmodel * indent files in report * fix build * setup qemu and buildx in release workflow * show formatted output in CI * add cli tests for source flag * add default ci config cli test --------- Signed-off-by: Alex Goodman --- .dockerignore | 2 +- .github/workflows/release.yaml | 6 + .gitignore | 5 + .goreleaser.yaml | 11 +- Taskfile.yaml | 15 +- cmd/analyze.go | 73 - cmd/build.go | 38 - cmd/ci.go | 37 - cmd/dive/cli/cli.go | 53 + cmd/dive/cli/cli_build_test.go | 91 + cmd/dive/cli/cli_ci_test.go | 51 + cmd/dive/cli/cli_config_test.go | 17 + cmd/dive/cli/cli_json_test.go | 29 + cmd/dive/cli/cli_load_test.go | 105 + cmd/dive/cli/cli_test.go | 175 + .../cli/internal/command/adapter/analyzer.go | 68 + .../cli/internal/command/adapter/evaluator.go | 48 + .../cli/internal/command/adapter/exporter.go | 63 + .../cli/internal/command/adapter/resolver.go | 91 + cmd/dive/cli/internal/command/build.go | 46 + cmd/dive/cli/internal/command/ci/evaluator.go | 308 ++ .../cli/internal/command/ci/evaluator_test.go | 198 + cmd/dive/cli/internal/command/ci/rule.go | 47 + cmd/dive/cli/internal/command/ci/rules.go | 196 + .../cli/internal/command/export/export.go | 86 + .../internal/command/export/export_test.go | 19 + .../cli/internal/command/export/main_test.go | 68 + .../testdata/snapshots/export_test.snap | 4665 ++++++++++++++++ cmd/dive/cli/internal/command/root.go | 99 + cmd/dive/cli/internal/options/analysis.go | 75 + cmd/dive/cli/internal/options/application.go | 32 + cmd/dive/cli/internal/options/ci.go | 105 + cmd/dive/cli/internal/options/ci_rules.go | 73 + cmd/dive/cli/internal/options/export.go | 40 + cmd/dive/cli/internal/options/ui.go | 18 + cmd/dive/cli/internal/options/ui_diff.go | 39 + cmd/dive/cli/internal/options/ui_filetree.go | 43 + .../cli/internal/options/ui_keybindings.go | 178 + cmd/dive/cli/internal/options/ui_layers.go | 20 + cmd/dive/cli/internal/ui/no_ui.go | 30 + cmd/dive/cli/internal/ui/v1.go | 176 + cmd/dive/cli/internal/ui/v1/app/app.go | 136 + .../cli/internal/ui/v1/app}/controller.go | 113 +- .../internal/ui/v1/app}/job_control_other.go | 2 +- .../internal/ui/v1/app}/job_control_unix.go | 2 +- cmd/dive/cli/internal/ui/v1/config.go | 67 + .../dive/cli/internal/ui/v1}/format/format.go | 0 .../dive/cli/internal/ui/v1}/key/binding.go | 50 +- cmd/dive/cli/internal/ui/v1/key/config.go | 100 + .../dive/cli/internal/ui/v1}/layout/area.go | 0 .../layout/compound/layer_details_column.go | 32 +- .../dive/cli/internal/ui/v1}/layout/layout.go | 0 .../cli/internal/ui/v1}/layout/location.go | 0 .../cli/internal/ui/v1}/layout/manager.go | 11 +- .../internal/ui/v1}/layout/manager_test.go | 0 .../dive/cli/internal/ui/v1}/view/cursor.go | 0 .../dive/cli/internal/ui/v1}/view/debug.go | 32 +- .../dive/cli/internal/ui/v1}/view/filetree.go | 121 +- .../dive/cli/internal/ui/v1}/view/filter.go | 50 +- .../cli/internal/ui/v1}/view/image_details.go | 53 +- .../dive/cli/internal/ui/v1}/view/layer.go | 70 +- .../ui/v1/view/layer_change_listener.go | 7 + .../cli/internal/ui/v1}/view/layer_details.go | 32 +- .../dive/cli/internal/ui/v1}/view/renderer.go | 0 .../dive/cli/internal/ui/v1}/view/status.go | 43 +- cmd/dive/cli/internal/ui/v1/view/views.go | 67 + .../cli/internal/ui/v1/viewmodel/config.go | 1 + .../cli/internal/ui/v1}/viewmodel/filetree.go | 70 +- .../ui/v1}/viewmodel/filetree_test.go | 136 +- .../ui/v1}/viewmodel/layer_compare.go | 0 .../ui/v1}/viewmodel/layer_selection.go | 0 .../ui/v1}/viewmodel/layer_set_state.go | 0 .../ui/v1}/viewmodel/layer_set_state_test.go | 0 .../testdata/TestFileShowAggregateChanges.txt | 0 .../testdata/TestFileTreeDirCollapse.txt | 0 .../testdata/TestFileTreeDirCollapseAll.txt | 0 .../testdata/TestFileTreeDirCursorRight.txt | 0 .../testdata/TestFileTreeFilterTree.txt | 0 .../viewmodel/testdata/TestFileTreeGoCase.txt | 0 .../TestFileTreeHideAddedRemovedModified.txt | 0 .../TestFileTreeHideTypeWithFilter.txt | 0 .../testdata/TestFileTreeHideUnmodified.txt | 0 .../testdata/TestFileTreeNoAttributes.txt | 0 .../testdata/TestFileTreePageDown.txt | 0 .../viewmodel/testdata/TestFileTreePageUp.txt | 0 .../testdata/TestFileTreeRestrictedHeight.txt | 0 .../testdata/TestFileTreeSelectLayer.txt | 0 .../cli/testdata/config/dive-ci-legacy.yaml | 4 + .../cli/testdata/default-ci-config/.dive-ci | 9 + cmd/dive/cli/testdata/dive-enable-ci.yaml | 10 + .../Containerfile | 13 + .../dive-pass.yaml | 10 + .../example.md | 3 + .../overwrite.md | 3 + .../image-multi-layer-dockerfile/Dockerfile | 13 + .../dive-fail.yaml | 3 + .../dive-pass.yaml | 5 + .../image-multi-layer-dockerfile/example.md | 3 + .../image-multi-layer-dockerfile/overwrite.md | 3 + cmd/dive/cli/testdata/invalid/Dockerfile | 2 + .../testdata/snapshots/cli_build_test.snap | 137 + .../cli/testdata/snapshots/cli_ci_test.snap | 78 + .../testdata/snapshots/cli_config_test.snap | 127 + .../cli/testdata/snapshots/cli_json_test.snap | 4432 ++++++++++++++++ .../cli/testdata/snapshots/cli_load_test.snap | 144 + main.go => cmd/dive/main.go | 38 +- cmd/root.go | 220 - cmd/version.go | 34 - dive/filetree/comparer.go | 5 +- dive/filetree/efficiency.go | 14 +- dive/filetree/file_info.go | 10 +- dive/filetree/file_node.go | 4 +- dive/filetree/file_tree.go | 9 +- dive/filetree/file_tree_test.go | 5 +- dive/image/analysis.go | 47 + dive/image/analyzer.go | 20 - dive/image/docker/archive_resolver.go | 9 +- dive/image/docker/cli.go | 9 +- dive/image/docker/config.go | 5 +- dive/image/docker/engine_resolver.go | 33 +- dive/image/docker/image_archive.go | 7 +- dive/image/docker/manifest.go | 5 +- dive/image/docker/testing.go | 26 +- dive/image/image.go | 33 +- dive/image/podman/build.go | 1 - dive/image/podman/cli.go | 15 +- dive/image/podman/resolver.go | 16 +- dive/image/podman/resolver_unsupported.go | 7 +- dive/image/resolver.go | 12 +- go.mod | 61 +- go.sum | 117 +- internal/bus/bus.go | 19 + internal/bus/event/event.go | 21 + internal/bus/event/parser/parsers.go | 106 + internal/bus/event/payload/explore.go | 8 + internal/bus/event/payload/generic.go | 48 + internal/bus/helpers.go | 63 + internal/log/log.go | 79 + {utils => internal/utils}/format.go | 6 - {utils => internal/utils}/view.go | 9 +- runtime/ci/evaluator.go | 186 - runtime/ci/evaluator_test.go | 59 - runtime/ci/reference_file.go | 7 - runtime/ci/rule.go | 168 - runtime/event.go | 31 - runtime/export/export.go | 66 - runtime/export/export_test.go | 4690 ----------------- runtime/export/file_reference.go | 7 - runtime/export/image.go | 8 - runtime/export/layer.go | 14 - runtime/options.go | 17 - runtime/run.go | 160 - runtime/run_test.go | 314 -- runtime/ui/app.go | 165 - runtime/ui/view/layer_change_listener.go | 5 - runtime/ui/view/views.go | 85 - 156 files changed, 13845 insertions(+), 6991 deletions(-) delete mode 100644 cmd/analyze.go delete mode 100644 cmd/build.go delete mode 100644 cmd/ci.go create mode 100644 cmd/dive/cli/cli.go create mode 100644 cmd/dive/cli/cli_build_test.go create mode 100644 cmd/dive/cli/cli_ci_test.go create mode 100644 cmd/dive/cli/cli_config_test.go create mode 100644 cmd/dive/cli/cli_json_test.go create mode 100644 cmd/dive/cli/cli_load_test.go create mode 100644 cmd/dive/cli/cli_test.go create mode 100644 cmd/dive/cli/internal/command/adapter/analyzer.go create mode 100644 cmd/dive/cli/internal/command/adapter/evaluator.go create mode 100644 cmd/dive/cli/internal/command/adapter/exporter.go create mode 100644 cmd/dive/cli/internal/command/adapter/resolver.go create mode 100644 cmd/dive/cli/internal/command/build.go create mode 100644 cmd/dive/cli/internal/command/ci/evaluator.go create mode 100644 cmd/dive/cli/internal/command/ci/evaluator_test.go create mode 100644 cmd/dive/cli/internal/command/ci/rule.go create mode 100644 cmd/dive/cli/internal/command/ci/rules.go create mode 100644 cmd/dive/cli/internal/command/export/export.go create mode 100644 cmd/dive/cli/internal/command/export/export_test.go create mode 100644 cmd/dive/cli/internal/command/export/main_test.go create mode 100755 cmd/dive/cli/internal/command/export/testdata/snapshots/export_test.snap create mode 100644 cmd/dive/cli/internal/command/root.go create mode 100644 cmd/dive/cli/internal/options/analysis.go create mode 100644 cmd/dive/cli/internal/options/application.go create mode 100644 cmd/dive/cli/internal/options/ci.go create mode 100644 cmd/dive/cli/internal/options/ci_rules.go create mode 100644 cmd/dive/cli/internal/options/export.go create mode 100644 cmd/dive/cli/internal/options/ui.go create mode 100644 cmd/dive/cli/internal/options/ui_diff.go create mode 100644 cmd/dive/cli/internal/options/ui_filetree.go create mode 100644 cmd/dive/cli/internal/options/ui_keybindings.go create mode 100644 cmd/dive/cli/internal/options/ui_layers.go create mode 100644 cmd/dive/cli/internal/ui/no_ui.go create mode 100644 cmd/dive/cli/internal/ui/v1.go create mode 100644 cmd/dive/cli/internal/ui/v1/app/app.go rename {runtime/ui => cmd/dive/cli/internal/ui/v1/app}/controller.go (61%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1/app}/job_control_other.go (94%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1/app}/job_control_unix.go (96%) create mode 100644 cmd/dive/cli/internal/ui/v1/config.go rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/format/format.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/key/binding.go (54%) create mode 100644 cmd/dive/cli/internal/ui/v1/key/config.go rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/layout/area.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/layout/compound/layer_details_column.go (74%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/layout/layout.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/layout/location.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/layout/manager.go (96%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/layout/manager_test.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/cursor.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/debug.go (79%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/filetree.go (79%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/filter.go (80%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/image_details.go (75%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/layer.go (85%) create mode 100644 cmd/dive/cli/internal/ui/v1/view/layer_change_listener.go rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/layer_details.go (79%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/renderer.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/view/status.go (74%) create mode 100644 cmd/dive/cli/internal/ui/v1/view/views.go create mode 100644 cmd/dive/cli/internal/ui/v1/viewmodel/config.go rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/filetree.go (85%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/filetree_test.go (77%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/layer_compare.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/layer_selection.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/layer_set_state.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/layer_set_state_test.go (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileShowAggregateChanges.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeDirCollapse.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeDirCollapseAll.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeDirCursorRight.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeFilterTree.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeGoCase.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeHideUnmodified.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeNoAttributes.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreePageDown.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreePageUp.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeRestrictedHeight.txt (100%) rename {runtime/ui => cmd/dive/cli/internal/ui/v1}/viewmodel/testdata/TestFileTreeSelectLayer.txt (100%) create mode 100644 cmd/dive/cli/testdata/config/dive-ci-legacy.yaml create mode 100644 cmd/dive/cli/testdata/default-ci-config/.dive-ci create mode 100644 cmd/dive/cli/testdata/dive-enable-ci.yaml create mode 100644 cmd/dive/cli/testdata/image-multi-layer-containerfile/Containerfile create mode 100644 cmd/dive/cli/testdata/image-multi-layer-containerfile/dive-pass.yaml create mode 100644 cmd/dive/cli/testdata/image-multi-layer-containerfile/example.md create mode 100644 cmd/dive/cli/testdata/image-multi-layer-containerfile/overwrite.md create mode 100644 cmd/dive/cli/testdata/image-multi-layer-dockerfile/Dockerfile create mode 100644 cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-fail.yaml create mode 100644 cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-pass.yaml create mode 100644 cmd/dive/cli/testdata/image-multi-layer-dockerfile/example.md create mode 100644 cmd/dive/cli/testdata/image-multi-layer-dockerfile/overwrite.md create mode 100644 cmd/dive/cli/testdata/invalid/Dockerfile create mode 100755 cmd/dive/cli/testdata/snapshots/cli_build_test.snap create mode 100755 cmd/dive/cli/testdata/snapshots/cli_ci_test.snap create mode 100755 cmd/dive/cli/testdata/snapshots/cli_config_test.snap create mode 100755 cmd/dive/cli/testdata/snapshots/cli_json_test.snap create mode 100755 cmd/dive/cli/testdata/snapshots/cli_load_test.snap rename main.go => cmd/dive/main.go (61%) delete mode 100644 cmd/root.go delete mode 100644 cmd/version.go create mode 100644 dive/image/analysis.go delete mode 100644 dive/image/analyzer.go create mode 100644 internal/bus/bus.go create mode 100644 internal/bus/event/event.go create mode 100644 internal/bus/event/parser/parsers.go create mode 100644 internal/bus/event/payload/explore.go create mode 100644 internal/bus/event/payload/generic.go create mode 100644 internal/bus/helpers.go create mode 100644 internal/log/log.go rename {utils => internal/utils}/format.go (70%) rename {utils => internal/utils}/view.go (51%) delete mode 100644 runtime/ci/evaluator.go delete mode 100644 runtime/ci/evaluator_test.go delete mode 100644 runtime/ci/reference_file.go delete mode 100644 runtime/ci/rule.go delete mode 100644 runtime/event.go delete mode 100644 runtime/export/export.go delete mode 100644 runtime/export/export_test.go delete mode 100644 runtime/export/file_reference.go delete mode 100644 runtime/export/image.go delete mode 100644 runtime/export/layer.go delete mode 100644 runtime/options.go delete mode 100644 runtime/run.go delete mode 100644 runtime/run_test.go delete mode 100644 runtime/ui/app.go delete mode 100644 runtime/ui/view/layer_change_listener.go delete mode 100644 runtime/ui/view/views.go diff --git a/.dockerignore b/.dockerignore index 1021b60..37212b5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,7 @@ /dist !/dist/dive_linux_amd64 /ui -/utils +/internal/utils /image /cmd /build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8174581..d7d5066 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -105,6 +105,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Tag release run: | git tag ${{ github.event.inputs.version }} diff --git a/.gitignore b/.gitignore index e80299d..1e177e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# app configs +.dive.yaml + # misc /.image *.log @@ -15,6 +18,8 @@ VERSION /.tool /.mise.toml /.task +/go.work +/go.work.sum # builds /dist diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2b12c2d..4ab778b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -15,6 +15,7 @@ env: builds: - binary: dive + dir: ./cmd/dive env: - CGO_ENABLED=0 goos: @@ -25,7 +26,14 @@ builds: - amd64 - arm64 - ppc64le - ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.buildTime={{.Date}} + ldflags: + -w + -s + -extldflags '-static' + -X main.version={{.Version}} + -X main.gitCommit={{.Commit}} + -X main.buildDate={{.Date}} + -X main.gitDescription={{.Summary}} brews: - repository: @@ -59,6 +67,7 @@ dockers: use: buildx goarch: amd64 image_templates: + - docker.io/wagoodman/dive:latest - docker.io/wagoodman/dive:v{{.Version}}-amd64 build_flag_templates: - "--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}" diff --git a/Taskfile.yaml b/Taskfile.yaml index 19c9f3f..e515ed7 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -61,6 +61,7 @@ tasks: desc: Run all levels of test cmds: - task: unit + - task: cli ## Bootstrap tasks ################################# @@ -172,15 +173,19 @@ tasks: - tmpdir vars: TEST_PKGS: - sh: "go list ./... | tr '\n' ' '" + sh: "go list ./... | grep -v '^github.com/wagoodman/dive/cmd/dive/cli$' | tr '\n' ' '" # unit test coverage threshold (in % coverage) - COVERAGE_THRESHOLD: 30 + COVERAGE_THRESHOLD: 25 cmds: - "go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}" - cmd: ".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt" silent: true + cli: + desc: Run CLI tests + cmds: + - "go test github.com/wagoodman/dive/cmd/dive/cli -v" ## Acceptance tests ################################# @@ -198,6 +203,7 @@ tasks: docker run \ --rm \ -t \ + --env CLICOLOR_FORCE=true \ -v /var/run/docker.sock:/var/run/docker.sock \ 'docker.io/wagoodman/dive:latest' \ '{{ .TEST_IMAGE }}' \ @@ -208,9 +214,11 @@ tasks: cmds: - | docker run \ + --platform linux/amd64 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /${PWD}:/src \ -w /src \ + --env CLICOLOR_FORCE=true \ ubuntu:latest \ /bin/bash -x -c "\ apt update && \ @@ -229,9 +237,11 @@ tasks: cmds: - | docker run \ + --platform linux/amd64 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /${PWD}:/src \ -w /src \ + --env CLICOLOR_FORCE=true \ fedora:latest \ /bin/bash -x -c "\ curl -L 'https://download.docker.com/linux/static/stable/x86_64/docker-{{ .DOCKER_CLI_VERSION }}.tgz' | \ @@ -321,6 +331,7 @@ tasks: deps: [tools, tmpdir] sources: - "**/*.go" + - ".goreleaser.yaml" method: checksum cmds: - silent: true diff --git a/cmd/analyze.go b/cmd/analyze.go deleted file mode 100644 index 31a56f8..0000000 --- a/cmd/analyze.go +++ /dev/null @@ -1,73 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive" - "github.com/wagoodman/dive/runtime" -) - -// doAnalyzeCmd takes a docker image tag, digest, or id and displays the -// image analysis to the screen -func doAnalyzeCmd(cmd *cobra.Command, args []string) { - if len(args) == 0 { - printVersionFlag, err := cmd.PersistentFlags().GetBool("version") - if err == nil && printVersionFlag { - printVersion(cmd, args) - return - } - - fmt.Println("No image argument given") - os.Exit(1) - } - - userImage := args[0] - if userImage == "" { - fmt.Println("No image argument given") - os.Exit(1) - } - - initLogging() - - isCi, ciConfig, err := configureCi() - - if err != nil { - fmt.Printf("ci configuration error: %v\n", err) - os.Exit(1) - } - - var sourceType dive.ImageSource - var imageStr string - - sourceType, imageStr = dive.DeriveImageSource(userImage) - - if sourceType == dive.SourceUnknown { - sourceStr := viper.GetString("source") - sourceType = dive.ParseImageSource(sourceStr) - if sourceType == dive.SourceUnknown { - fmt.Printf("unable to determine image source: %v\n", sourceStr) - os.Exit(1) - } - - imageStr = userImage - } - - ignoreErrors, err := cmd.PersistentFlags().GetBool("ignore-errors") - if err != nil { - logrus.Error("unable to get 'ignore-errors' option:", err) - } - - runtime.Run(runtime.Options{ - Ci: isCi, - Source: sourceType, - Image: imageStr, - ExportFile: exportFile, - CiConfig: ciConfig, - IgnoreErrors: viper.GetBool("ignore-errors") || ignoreErrors, - }) -} diff --git a/cmd/build.go b/cmd/build.go deleted file mode 100644 index 84b029e..0000000 --- a/cmd/build.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive" - "github.com/wagoodman/dive/runtime" -) - -// buildCmd represents the build command -var buildCmd = &cobra.Command{ - Use: "build [any valid `docker build` arguments]", - Short: "Builds and analyzes a docker image from a Dockerfile (this is a thin wrapper for the `docker build` command).", - DisableFlagParsing: true, - Run: doBuildCmd, -} - -func init() { - rootCmd.AddCommand(buildCmd) -} - -// doBuildCmd implements the steps taken for the build command -func doBuildCmd(cmd *cobra.Command, args []string) { - initLogging() - - // there is no cli options allowed, only config can be supplied - // todo: allow for an engine flag to be passed to dive but not the container engine - engine := viper.GetString("container-engine") - - runtime.Run(runtime.Options{ - Ci: isCi, - Source: dive.ParseImageSource(engine), - BuildArgs: args, - ExportFile: exportFile, - CiConfig: ciConfig, - }) -} diff --git a/cmd/ci.go b/cmd/ci.go deleted file mode 100644 index bf87a04..0000000 --- a/cmd/ci.go +++ /dev/null @@ -1,37 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - "os" - "strconv" - - "github.com/spf13/viper" -) - -func configureCi() (bool, *viper.Viper, error) { - isCiFromEnv, _ := strconv.ParseBool(os.Getenv("CI")) - isCi = isCi || isCiFromEnv - - if isCi { - ciConfig.SetConfigType("yaml") - - if _, err := os.Stat(ciConfigFile); !os.IsNotExist(err) { - fmt.Printf(" Using CI config: %s\n", ciConfigFile) - - fileBytes, err := os.ReadFile(ciConfigFile) - if err != nil { - return isCi, nil, err - } - - err = ciConfig.ReadConfig(bytes.NewBuffer(fileBytes)) - if err != nil { - return isCi, nil, err - } - } else { - fmt.Println(" Using default CI config") - } - } - - return isCi, ciConfig, nil -} diff --git a/cmd/dive/cli/cli.go b/cmd/dive/cli/cli.go new file mode 100644 index 0000000..a2f3d36 --- /dev/null +++ b/cmd/dive/cli/cli.go @@ -0,0 +1,53 @@ +package cli + +import ( + "github.com/anchore/clio" + "github.com/spf13/cobra" + "github.com/wagoodman/dive/cmd/dive/cli/internal/command" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui" + "github.com/wagoodman/dive/internal/bus" + "github.com/wagoodman/dive/internal/log" +) + +func Application(id clio.Identification) clio.Application { + app, _ := create(id) + return app +} + +func Command(id clio.Identification) *cobra.Command { + _, cmd := create(id) + return cmd +} + +func create(id clio.Identification) (clio.Application, *cobra.Command) { + clioCfg := clio.NewSetupConfig(id). + WithGlobalConfigFlag(). // add persistent -c for reading an application config from + WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config + WithConfigInRootHelp(). // --help on the root command renders the full application config in the help text + WithUI(ui.None()). + WithInitializers( + func(state *clio.State) error { + bus.Set(state.Bus) + log.Set(state.Logger) + + //stereoscope.SetBus(state.Bus) + //stereoscope.SetLogger(state.Logger) + return nil + }, + ) + //WithPostRuns(func(_ *clio.State, _ error) { + // stereoscope.Cleanup() + //}) + + app := clio.New(*clioCfg) + + rootCmd := command.Root(app) + + rootCmd.AddCommand( + clio.VersionCommand(id), + clio.ConfigCommand(app, nil), + command.Build(app), + ) + + return app, rootCmd +} diff --git a/cmd/dive/cli/cli_build_test.go b/cmd/dive/cli/cli_build_test.go new file mode 100644 index 0000000..feabe91 --- /dev/null +++ b/cmd/dive/cli/cli_build_test.go @@ -0,0 +1,91 @@ +package cli + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "regexp" + "testing" +) + +func Test_Build_Dockerfile(t *testing.T) { + t.Setenv("DIVE_CONFIG", "./testdata/image-multi-layer-dockerfile/dive-pass.yaml") + + t.Run("implicit dockerfile", func(t *testing.T) { + rootCmd := getTestCommand(t, "build testdata/image-multi-layer-dockerfile") + stdout := Capture().WithStdout().WithSuppress().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + snaps.MatchSnapshot(t, stdout) + }) + + t.Run("explicit file flag", func(t *testing.T) { + rootCmd := getTestCommand(t, "build testdata/image-multi-layer-dockerfile -f testdata/image-multi-layer-dockerfile/Dockerfile") + stdout := Capture().WithStdout().WithSuppress().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + snaps.MatchSnapshot(t, stdout) + }) +} + +func Test_Build_Containerfile(t *testing.T) { + t.Setenv("DIVE_CONFIG", "./testdata/image-multi-layer-containerfile/dive-pass.yaml") + + t.Run("implicit containerfile", func(t *testing.T) { + rootCmd := getTestCommand(t, "build testdata/image-multi-layer-containerfile") + stdout := Capture().WithStdout().WithSuppress().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + snaps.MatchSnapshot(t, stdout) + }) + + t.Run("explicit file flag", func(t *testing.T) { + rootCmd := getTestCommand(t, "build testdata/image-multi-layer-containerfile -f testdata/image-multi-layer-containerfile/Containerfile") + stdout := Capture().WithStdout().WithSuppress().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + snaps.MatchSnapshot(t, stdout) + }) +} + +func Test_Build_CI_gate_fail(t *testing.T) { + t.Setenv("DIVE_CONFIG", "./testdata/image-multi-layer-dockerfile/dive-fail.yaml") + + rootCmd := getTestCommand(t, "build testdata/image-multi-layer-dockerfile") + stdout := Capture().WithStdout().WithSuppress().Run(t, func() { + // failing gate should result in a non-zero exit code + require.Error(t, rootCmd.Execute()) + }) + snaps.MatchSnapshot(t, stdout) + +} + +func Test_BuildFailure(t *testing.T) { + + t.Run("nonexistent directory", func(t *testing.T) { + rootCmd := getTestCommand(t, "build ./path/does/not/exist") + combined := Capture().WithStdout().WithStderr().Run(t, func() { + require.ErrorContains(t, rootCmd.Execute(), "could not find Containerfile or Dockerfile") + }) + + assert.Contains(t, combined, "Building image") + + snaps.MatchSnapshot(t, combined) + }) + + t.Run("invalid dockerfile", func(t *testing.T) { + rootCmd := getTestCommand(t, "build ./testdata/invalid") + combined := Capture().WithStdout().WithStderr().WithSuppress().Run(t, func() { + + require.ErrorContains(t, rootCmd.Execute(), "cannot build image: exit status 1") + }) + + assert.Contains(t, combined, "Building image") + // ensure we're passing through docker feedback + assert.Contains(t, combined, "unknown instruction: INVALID") + + // replace anything starting with "docker-desktop://", like "docker-desktop://dashboard/build/desktop-linux/desktop-linux/ujdmhgkwo0sqqpopsnum3xakd" + combined = regexp.MustCompile("docker-desktop://[^ ]+").ReplaceAllString(combined, "docker-desktop://") + + snaps.MatchSnapshot(t, combined) + }) +} diff --git a/cmd/dive/cli/cli_ci_test.go b/cmd/dive/cli/cli_ci_test.go new file mode 100644 index 0000000..fdc806e --- /dev/null +++ b/cmd/dive/cli/cli_ci_test.go @@ -0,0 +1,51 @@ +package cli + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_CI_DefaultCIConfig(t *testing.T) { + // this lets the test harness to unset any DIVE_CONFIG env var + t.Setenv("DIVE_CONFIG", "-") + + rootCmd := getTestCommand(t, repoPath(t, ".data/test-docker-image.tar")+" -vv") + cd(t, "testdata/default-ci-config") + combined := Capture().WithStdout().WithStderr().Run(t, func() { + // failing gate should result in a non-zero exit code + require.Error(t, rootCmd.Execute()) + }) + + assert.Contains(t, combined, "lowest-efficiency: \"0.96\"", "missing lowest-efficiency rule") + assert.Contains(t, combined, "highest-wasted-bytes: 19Mb", "missing highest-wasted-bytes rule") + assert.Contains(t, combined, "highest-user-wasted-percent: \"0.6\"", "missing highest-user-wasted-percent rule") + + snaps.MatchSnapshot(t, combined) +} + +func Test_CI_Fail(t *testing.T) { + t.Setenv("DIVE_CONFIG", "./testdata/image-multi-layer-dockerfile/dive-fail.yaml") + + rootCmd := getTestCommand(t, "build testdata/image-multi-layer-dockerfile") + stdout := Capture().WithStdout().WithSuppress().Run(t, func() { + // failing gate should result in a non-zero exit code + require.Error(t, rootCmd.Execute()) + }) + snaps.MatchSnapshot(t, stdout) + +} + +func Test_CI_LegacyRules(t *testing.T) { + t.Setenv("DIVE_CONFIG", "./testdata/config/dive-ci-legacy.yaml") + + rootCmd := getTestCommand(t, "config --load") + all := Capture().All().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + + // this proves that we can load the legacy rules and map them to the standard rules + assert.Contains(t, all, "lowest-efficiency: '0.95'", "missing lowest-efficiency legacy rule") + assert.Contains(t, all, "highest-wasted-bytes: '20MB'", "missing highest-wasted-bytes legacy rule") + assert.Contains(t, all, "highest-user-wasted-percent: '0.2'", "missing highest-user-wasted-percent legacy rule") +} diff --git a/cmd/dive/cli/cli_config_test.go b/cmd/dive/cli/cli_config_test.go new file mode 100644 index 0000000..04f595b --- /dev/null +++ b/cmd/dive/cli/cli_config_test.go @@ -0,0 +1,17 @@ +package cli + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func Test_Config(t *testing.T) { + t.Setenv("DIVE_CONFIG", "./testdata/image-multi-layer-dockerfile/dive-pass.yaml") + + rootCmd := getTestCommand(t, "config --load") + all := Capture().All().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + + snaps.MatchSnapshot(t, all) +} diff --git a/cmd/dive/cli/cli_json_test.go b/cmd/dive/cli/cli_json_test.go new file mode 100644 index 0000000..1934398 --- /dev/null +++ b/cmd/dive/cli/cli_json_test.go @@ -0,0 +1,29 @@ +package cli + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func Test_JsonOutput(t *testing.T) { + + t.Run("json output", func(t *testing.T) { + dest := t.TempDir() + file := filepath.Join(dest, "output.json") + rootCmd := getTestCommand(t, "busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f --json "+file) + combined := Capture().WithStdout().WithStderr().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + + assert.Contains(t, combined, "Exporting details") + assert.Contains(t, combined, "file") + + contents, err := os.ReadFile(file) + require.NoError(t, err) + + snaps.MatchJSON(t, contents) + }) +} diff --git a/cmd/dive/cli/cli_load_test.go b/cmd/dive/cli/cli_load_test.go new file mode 100644 index 0000000..e51bcf9 --- /dev/null +++ b/cmd/dive/cli/cli_load_test.go @@ -0,0 +1,105 @@ +package cli + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "os/exec" + "testing" +) + +func Test_LoadImage(t *testing.T) { + image := "busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f" + archive := repoPath(t, ".data/test-docker-image.tar") + + t.Run("from docker engine", func(t *testing.T) { + runWithCombinedOutput(t, fmt.Sprintf("docker://%s", image)) + }) + + t.Run("from docker engine (flag)", func(t *testing.T) { + + runWithCombinedOutput(t, fmt.Sprintf("--source docker %s", image)) + }) + + t.Run("from podman engine", func(t *testing.T) { + if _, err := exec.LookPath("podman"); err != nil { + t.Skip("podman not installed, skipping test") + } + // pull the image from podman first + require.NoError(t, exec.Command("podman", "pull", image).Run()) + + runWithCombinedOutput(t, fmt.Sprintf("podman://%s", image)) + }) + + t.Run("from podman engine (flag)", func(t *testing.T) { + if _, err := exec.LookPath("podman"); err != nil { + t.Skip("podman not installed, skipping test") + } + + // pull the image from podman first + require.NoError(t, exec.Command("podman", "pull", image).Run()) + + runWithCombinedOutput(t, fmt.Sprintf("--source podman %s", image)) + }) + + t.Run("from archive", func(t *testing.T) { + runWithCombinedOutput(t, fmt.Sprintf("docker-archive://%s", archive)) + }) + + t.Run("from archive (flag)", func(t *testing.T) { + runWithCombinedOutput(t, fmt.Sprintf("--source docker-archive %s", archive)) + }) +} + +func runWithCombinedOutput(t testing.TB, cmd string) { + t.Helper() + rootCmd := getTestCommand(t, cmd) + combined := Capture().WithStdout().WithStderr().Run(t, func() { + require.NoError(t, rootCmd.Execute()) + }) + + assertLoadOutput(t, combined) +} + +func assertLoadOutput(t testing.TB, combined string) { + t.Helper() + assert.Contains(t, combined, "Loading image") + assert.Contains(t, combined, "Analyzing image") + assert.Contains(t, combined, "Evaluating image") + snaps.MatchSnapshot(t, combined) +} + +func Test_FetchFailure(t *testing.T) { + t.Run("nonexistent image", func(t *testing.T) { + rootCmd := getTestCommand(t, "docker:wagoodman/nonexistent/image:tag") + combined := Capture().WithStdout().WithStderr().Run(t, func() { + require.ErrorContains(t, rootCmd.Execute(), "cannot load image: Error response from daemon: invalid reference format") + }) + + assert.Contains(t, combined, "Loading image") + + snaps.MatchSnapshot(t, combined) + }) + + t.Run("invalid image name", func(t *testing.T) { + rootCmd := getTestCommand(t, "docker:///wagoodman/invalid:image:format") + combined := Capture().WithStdout().WithStderr().Run(t, func() { + require.ErrorContains(t, rootCmd.Execute(), "cannot load image: Error response from daemon: invalid reference format") + }) + + assert.Contains(t, combined, "Loading image") + + snaps.MatchSnapshot(t, combined) + }) +} + +func cd(t testing.TB, to string) { + t.Helper() + from, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(to)) + t.Cleanup(func() { + require.NoError(t, os.Chdir(from)) + }) +} diff --git a/cmd/dive/cli/cli_test.go b/cmd/dive/cli/cli_test.go new file mode 100644 index 0000000..5b2e2a5 --- /dev/null +++ b/cmd/dive/cli/cli_test.go @@ -0,0 +1,175 @@ +package cli + +import ( + "bytes" + "flag" + "github.com/anchore/clio" + "github.com/charmbracelet/lipgloss" + snapsPkg "github.com/gkampitakis/go-snaps/snaps" + "github.com/google/shlex" + "github.com/muesli/termenv" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +var ( + updateSnapshot = flag.Bool("update", false, "update any test snapshots") + snaps *snapsPkg.Config + repoRootCache atomic.String +) + +func TestMain(m *testing.M) { + // flags are not parsed until after test.Main is called... + flag.Parse() + + os.Unsetenv("DIVE_CONFIG") + + // disable colors + lipgloss.SetColorProfile(termenv.Ascii) + + snaps = snapsPkg.WithConfig( + snapsPkg.Update(*updateSnapshot), + snapsPkg.Dir("testdata/snapshots"), + ) + + v := m.Run() + + snapsPkg.Clean(m) + + os.Exit(v) +} + +func TestUpdateSnapshotDisabled(t *testing.T) { + require.False(t, *updateSnapshot, "update snapshot flag should be disabled") +} + +func repoPath(t testing.TB, path string) string { + t.Helper() + root := repoRoot(t) + return filepath.Join(root, path) +} + +func repoRoot(t testing.TB) string { + val := repoRootCache.Load() + if val != "" { + return val + } + t.Helper() + // use git to find the root of the repo + out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + t.Fatalf("failed to get repo root: %v", err) + } + val = strings.TrimSpace(string(out)) + repoRootCache.Store(val) + return val +} + +func getTestCommand(t testing.TB, cmd string) *cobra.Command { + switch os.Getenv("DIVE_CONFIG") { + case "": + t.Setenv("DIVE_CONFIG", "./testdata/dive-enable-ci.yaml") + case "-": + t.Setenv("DIVE_CONFIG", "") + } + + // need basic output to logger for testing... + //l, err := logrus.New(logrus.DefaultConfig()) + //require.NoError(t, err) + //log.Set(l) + + // get the root command + c := Command(clio.Identification{ + Name: "dive", + Version: "testing", + }) + + args, err := shlex.Split(cmd) + require.NoError(t, err, "failed to parse command line %q", cmd) + + c.SetArgs(args) + + return c +} + +type capturer struct { + stdout bool + stderr bool + suppress bool +} + +func Capture() *capturer { + return &capturer{} +} + +func (c *capturer) WithSuppress() *capturer { + c.suppress = true + return c +} + +func (c *capturer) All() *capturer { + c.stdout = true + c.stderr = true + return c +} + +func (c *capturer) WithStdout() *capturer { + c.stdout = true + return c +} + +func (c *capturer) WithStderr() *capturer { + c.stderr = true + return c +} + +func (c *capturer) Run(t testing.TB, f func()) string { + t.Helper() + + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + panic(err) + } + defer devNull.Close() + + oldStdout := os.Stdout + oldStderr := os.Stderr + + if c.stdout { + os.Stdout = w + } else if c.suppress { + os.Stdout = devNull + } + + if c.stderr { + os.Stderr = w + } else if c.suppress { + os.Stderr = devNull + } + + defer func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + }() + + f() + require.NoError(t, w.Close()) + + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err) + + return buf.String() +} diff --git a/cmd/dive/cli/internal/command/adapter/analyzer.go b/cmd/dive/cli/internal/command/adapter/analyzer.go new file mode 100644 index 0000000..5a90566 --- /dev/null +++ b/cmd/dive/cli/internal/command/adapter/analyzer.go @@ -0,0 +1,68 @@ +package adapter + +import ( + "context" + "fmt" + "github.com/dustin/go-humanize" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/dive/internal/log" +) + +type Analyzer interface { + Analyze(ctx context.Context, img *image.Image) (*image.Analysis, error) +} + +type analysisActionObserver struct { + Analyzer func(context.Context, *image.Image) (*image.Analysis, error) +} + +func NewAnalyzer() Analyzer { + return analysisActionObserver{ + Analyzer: image.Analyze, + } +} + +func (a analysisActionObserver) Analyze(ctx context.Context, img *image.Image) (*image.Analysis, error) { + log.WithFields("image", img.Request).Infof("analyzing") + + layers := len(img.Layers) + var files int + var fileSize uint64 + for _, layer := range img.Layers { + files += layer.Tree.Size + fileSize += layer.Tree.FileSize + } + fileSizeStr := humanize.Bytes(fileSize) + filesStr := humanize.Comma(int64(files)) + + log.Debugf("├── layers: %d", layers) + log.Debugf("├── files: %s", filesStr) + log.Debugf("└── file size: %s", fileSizeStr) + + mon := bus.StartTask(payload.GenericTask{ + Title: payload.Title{ + Default: "Analyzing image", + WhileRunning: "Analyzing image", + OnSuccess: "Analyzed image", + }, + HideOnSuccess: false, + HideStageOnSuccess: false, + ID: img.Request, + Context: fmt.Sprintf("[layers:%d files:%s size:%s]", layers, filesStr, fileSizeStr), + }) + + analysis, err := a.Analyzer(ctx, img) + if err != nil { + mon.SetError(err) + } else { + mon.SetCompleted() + } + + if err == nil && analysis == nil { + err = fmt.Errorf("no results returned") + } + + return analysis, err +} diff --git a/cmd/dive/cli/internal/command/adapter/evaluator.go b/cmd/dive/cli/internal/command/adapter/evaluator.go new file mode 100644 index 0000000..0309d61 --- /dev/null +++ b/cmd/dive/cli/internal/command/adapter/evaluator.go @@ -0,0 +1,48 @@ +package adapter + +import ( + "context" + "fmt" + "github.com/wagoodman/dive/cmd/dive/cli/internal/command/ci" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/dive/internal/log" +) + +type Evaluator interface { + Evaluate(ctx context.Context, analysis *image.Analysis) ci.Evaluation +} + +type evaluationActionObserver struct { + ci.Evaluator +} + +func NewEvaluator(rules []ci.Rule) Evaluator { + return evaluationActionObserver{ + Evaluator: ci.NewEvaluator(rules), + } +} + +func (c evaluationActionObserver) Evaluate(ctx context.Context, analysis *image.Analysis) ci.Evaluation { + log.WithFields("image", analysis.Image).Infof("evaluating image") + mon := bus.StartTask(payload.GenericTask{ + Title: payload.Title{ + Default: "Evaluating image", + WhileRunning: "Evaluating image", + OnSuccess: "Evaluated image", + }, + HideOnSuccess: false, + HideStageOnSuccess: false, + ID: analysis.Image, + Context: fmt.Sprintf("[rules: %d]", len(c.Rules)), + }) + eval := c.Evaluator.Evaluate(ctx, analysis) + if eval.Pass { + mon.SetCompleted() + } else { + mon.SetError(fmt.Errorf("failed evaluation")) + } + bus.Report(eval.Report) + return eval +} diff --git a/cmd/dive/cli/internal/command/adapter/exporter.go b/cmd/dive/cli/internal/command/adapter/exporter.go new file mode 100644 index 0000000..4fff67b --- /dev/null +++ b/cmd/dive/cli/internal/command/adapter/exporter.go @@ -0,0 +1,63 @@ +package adapter + +import ( + "context" + "fmt" + "github.com/spf13/afero" + "github.com/wagoodman/dive/cmd/dive/cli/internal/command/export" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/dive/internal/log" + "os" +) + +type Exporter interface { + ExportTo(ctx context.Context, img *image.Analysis, path string) error +} + +type jsonExporter struct { + filesystem afero.Fs +} + +func NewExporter(fs afero.Fs) Exporter { + return &jsonExporter{ + filesystem: fs, + } +} + +func (e *jsonExporter) ExportTo(ctx context.Context, analysis *image.Analysis, path string) error { + log.WithFields("path", path).Infof("exporting analysis") + + mon := bus.StartTask(payload.GenericTask{ + Title: payload.Title{ + Default: "Exporting details", + WhileRunning: "Exporting details", + OnSuccess: "Exported details", + }, + HideOnSuccess: false, + HideStageOnSuccess: false, + ID: analysis.Image, + Context: fmt.Sprintf("[file: %s]", path), + }) + + bytes, err := export.NewExport(analysis).Marshal() + if err != nil { + mon.SetError(err) + return fmt.Errorf("cannot marshal export payload: %w", err) + } else { + mon.SetCompleted() + } + + file, err := e.filesystem.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("cannot open export file: %w", err) + } + defer file.Close() + + _, err = file.Write(bytes) + if err != nil { + return fmt.Errorf("cannot write to export file: %w", err) + } + return nil +} diff --git a/cmd/dive/cli/internal/command/adapter/resolver.go b/cmd/dive/cli/internal/command/adapter/resolver.go new file mode 100644 index 0000000..3d69a99 --- /dev/null +++ b/cmd/dive/cli/internal/command/adapter/resolver.go @@ -0,0 +1,91 @@ +package adapter + +import ( + "context" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/dive/internal/log" + "strings" + "time" +) + +type imageActionObserver struct { + image.Resolver +} + +func ImageResolver(resolver image.Resolver) image.Resolver { + return imageActionObserver{ + Resolver: resolver, + } +} + +func (i imageActionObserver) Build(ctx context.Context, options []string) (*image.Image, error) { + log.Info("building image") + log.Debugf("└── %s", strings.Join(options, " ")) + + mon := bus.StartTask(payload.GenericTask{ + Title: payload.Title{ + Default: "Building image", + WhileRunning: "Building image", + OnSuccess: "Built image", + }, + HideOnSuccess: false, + HideStageOnSuccess: false, + Context: "... " + strings.Join(options, " "), + }) + + ctx = payload.SetGenericProgressToContext(ctx, mon) + + img, err := i.Resolver.Build(ctx, options) + if err != nil { + mon.SetError(err) + } else { + mon.SetCompleted() + } + return img, err +} + +func (i imageActionObserver) Fetch(ctx context.Context, id string) (*image.Image, error) { + log.WithFields("image", id).Info("fetching") + log.Debugf("└── resolver: %s", i.Resolver.Name()) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + mon := bus.StartTask(payload.GenericTask{ + Title: payload.Title{ + Default: "Loading image", + WhileRunning: "Loading image", + OnSuccess: "Fetched image", + }, + HideOnSuccess: false, + HideStageOnSuccess: false, + ID: id, + Context: id, + }) + + ctx = payload.SetGenericProgressToContext(ctx, mon) + + go func() { + // in 5 seconds if the context is not cancelled, log the message + select { // nolint:gosimple + case <-time.After(3 * time.Second): + if ctx.Err() == nil { + bus.Notify(" • this can take a while for large images...") + mon.AtomicStage.Set("(this can take a while for large images)") + + // TODO: default level should be error for this to work when using the UI + //log.Warn("this can take a while for large images") + } + } + }() + + img, err := i.Resolver.Fetch(ctx, id) + if err != nil { + mon.SetError(err) + } else { + mon.SetCompleted() + } + return img, err +} diff --git a/cmd/dive/cli/internal/command/build.go b/cmd/dive/cli/internal/command/build.go new file mode 100644 index 0000000..6826824 --- /dev/null +++ b/cmd/dive/cli/internal/command/build.go @@ -0,0 +1,46 @@ +package command + +import ( + "fmt" + "github.com/anchore/clio" + "github.com/spf13/cobra" + "github.com/wagoodman/dive/cmd/dive/cli/internal/command/adapter" + "github.com/wagoodman/dive/cmd/dive/cli/internal/options" + "github.com/wagoodman/dive/dive" +) + +type buildOptions struct { + options.Application `yaml:",inline" mapstructure:",squash"` + + // reserved for future use of build-only flags +} + +func Build(app clio.Application) *cobra.Command { + opts := &buildOptions{ + Application: options.DefaultApplication(), + } + return app.SetupCommand(&cobra.Command{ + Use: "build [any valid `docker build` arguments]", + Short: "Builds and analyzes a docker image from a Dockerfile (this is a thin wrapper for the `docker build` command).", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + if err := setUI(app, opts.Application); err != nil { + return fmt.Errorf("failed to set UI: %w", err) + } + + resolver, err := dive.GetImageResolver(opts.Analysis.Source) + if err != nil { + return fmt.Errorf("cannot determine image provider for build: %w", err) + } + + ctx := cmd.Context() + + img, err := adapter.ImageResolver(resolver).Build(ctx, args) + if err != nil { + return fmt.Errorf("cannot build image: %w", err) + } + + return run(cmd.Context(), opts.Application, img, resolver) + }, + }, opts) +} diff --git a/cmd/dive/cli/internal/command/ci/evaluator.go b/cmd/dive/cli/internal/command/ci/evaluator.go new file mode 100644 index 0000000..ee4fea1 --- /dev/null +++ b/cmd/dive/cli/internal/command/ci/evaluator.go @@ -0,0 +1,308 @@ +package ci + +import ( + "fmt" + "github.com/charmbracelet/lipgloss" + "golang.org/x/net/context" + "sort" + "strconv" + "strings" + + "github.com/dustin/go-humanize" + "github.com/wagoodman/dive/dive/image" +) + +type Evaluation struct { + Report string + Pass bool +} + +type Evaluator struct { + Rules []Rule + Results map[string]RuleResult + Tally ResultTally + Pass bool + Misconfigured bool + InefficientFiles []ReferenceFile + format format +} + +type format struct { + Title lipgloss.Style + Success lipgloss.Style + Warning lipgloss.Style + Disabled lipgloss.Style + Failure lipgloss.Style + TableHeader lipgloss.Style + Label lipgloss.Style + Aux lipgloss.Style + Value lipgloss.Style +} + +type ResultTally struct { + Pass int + Fail int + Skip int + Warn int + Total int +} + +type ReferenceFile struct { + References int `json:"count"` + SizeBytes uint64 `json:"sizeBytes"` + Path string `json:"file"` +} + +func NewEvaluator(rules []Rule) Evaluator { + return Evaluator{ + Rules: rules, + Results: make(map[string]RuleResult), + Pass: true, + format: format{ + Title: lipgloss.NewStyle().Bold(true), + Success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")), + Warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), + Disabled: lipgloss.NewStyle().Faint(true), + Failure: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true), + TableHeader: lipgloss.NewStyle().Bold(true), + Label: lipgloss.NewStyle().Width(18), + Aux: lipgloss.NewStyle().Faint(true), + Value: lipgloss.NewStyle(), + }, + } +} + +func (e Evaluator) isRuleEnabled(rule Rule) bool { + return rule.Configuration() != "disabled" +} + +func (e Evaluator) Evaluate(ctx context.Context, analysis *image.Analysis) Evaluation { + for _, rule := range e.Rules { + if !e.isRuleEnabled(rule) { + e.Results[rule.Key()] = RuleResult{ + status: RuleConfigured, + message: "rule disabled", + } + continue + } + + e.Results[rule.Key()] = RuleResult{ + status: RuleConfigured, + message: "test", + } + } + + // capture inefficient files + for idx := 0; idx < len(analysis.Inefficiencies); idx++ { + fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx] + + e.InefficientFiles = append(e.InefficientFiles, ReferenceFile{ + References: len(fileData.Nodes), + SizeBytes: uint64(fileData.CumulativeSize), + Path: fileData.Path, + }) + } + + // evaluate results against the configured CI rules + for _, rule := range e.Rules { + if !e.isRuleEnabled(rule) { + e.Results[rule.Key()] = RuleResult{ + status: RuleDisabled, + message: "disabled", + } + continue + } + + status, message := rule.Evaluate(analysis) + + if value, exists := e.Results[rule.Key()]; exists && value.status != RuleConfigured && value.status != RuleMisconfigured { + panic(fmt.Errorf("CI rule result recorded twice: %s", rule.Key())) + } + + if status == RuleFailed { + e.Pass = false + } + + if message == "" { + message = rule.Configuration() + } + + e.Results[rule.Key()] = RuleResult{ + status: status, + message: message, + } + } + + e.Tally.Total = len(e.Results) + for rule, result := range e.Results { + switch result.status { + case RulePassed: + e.Tally.Pass++ + case RuleFailed: + e.Tally.Fail++ + case RuleWarning: + e.Tally.Warn++ + case RuleDisabled: + e.Tally.Skip++ + default: + panic(fmt.Errorf("unknown test status (rule='%v'): %v", rule, result.status)) + } + } + + return Evaluation{ + Report: e.report(analysis), + Pass: e.Pass, + } +} + +func (e Evaluator) report(analysis *image.Analysis) string { + sections := []string{ + e.renderAnalysisSection(analysis), + e.renderInefficientFilesSection(analysis), + e.renderEvaluationSection(), + } + + return strings.Join(sections, "\n\n") +} + +func (e Evaluator) renderAnalysisSection(analysis *image.Analysis) string { + wastedByteStr := "" + userWastedPercent := "0 %" + + if analysis.WastedBytes > 0 { + wastedByteStr = fmt.Sprintf("(%s)", humanize.Bytes(analysis.WastedBytes)) + userWastedPercent = fmt.Sprintf("%.2f %%", analysis.WastedUserPercent*100) + } + + title := e.format.Title.Render("Analysis:") + + rows := []string{ + formatKeyValue(e.format, "efficiency", fmt.Sprintf("%.2f %%", analysis.Efficiency*100)), + formatKeyValue(e.format, "wastedBytes", fmt.Sprintf("%d bytes %s", analysis.WastedBytes, wastedByteStr)), + formatKeyValue(e.format, "userWastedPercent", userWastedPercent), + } + + return title + "\n" + strings.Join(rows, "\n") +} + +func (e Evaluator) renderInefficientFilesSection(analysis *image.Analysis) string { + title := e.format.Title.Render("Inefficient Files:") + + if len(analysis.Inefficiencies) == 0 { + return title + " (None)" + } + + header := e.format.TableHeader.Render( + fmt.Sprintf(" %-5s %-12s %-s", "Count", "Wasted Space", "File Path"), + ) + + rows := []string{header} + for _, file := range e.InefficientFiles { + row := fmt.Sprintf(" %-5s %-12s %-s", + strconv.Itoa(file.References), + humanize.Bytes(file.SizeBytes), + file.Path, + ) + rows = append(rows, row) + } + + return title + "\n" + strings.Join(rows, "\n") +} + +func (e Evaluator) renderEvaluationSection() string { + title := e.format.Title.Render("Evaluation:") + + // sort rules by name for consistent output + rules := make([]string, 0, len(e.Results)) + for name := range e.Results { + rules = append(rules, name) + } + sort.Strings(rules) + + ruleResults := []string{} + for _, rule := range rules { + result := e.Results[rule] + ruleResult := e.formatRuleResult(rule, result) + ruleResults = append(ruleResults, ruleResult) + } + + status := e.renderStatusSummary() + + return title + "\n" + strings.Join(ruleResults, "\n") + "\n\n" + status +} + +func (e Evaluator) formatRuleResult(ruleName string, result RuleResult) string { + var style lipgloss.Style + textStyle := lipgloss.NewStyle() + switch result.status { + case RulePassed: + style = e.format.Success + case RuleFailed: + style = e.format.Failure + case RuleWarning, RuleMisconfigured: + style = e.format.Warning + case RuleDisabled: + style = e.format.Disabled + textStyle = e.format.Disabled + default: + style = lipgloss.NewStyle() + } + + statusStr := style.Render(result.status.String(e.format)) + + if result.message != "" { + return fmt.Sprintf(" %s %s", statusStr, textStyle.Render(ruleName+" ("+result.message+")")) + } + + return fmt.Sprintf(" %s %s", statusStr, textStyle.Render(ruleName)) +} + +func (e Evaluator) renderStatusSummary() string { + if e.Misconfigured { + return e.format.Failure.Render("CI Misconfigured") + } + + status := "PASS" + if e.Tally.Fail > 0 { + status = "FAIL" + } + + parts := []string{} + + type tallyItem struct { + name string + value int + } + + items := []tallyItem{ + //{"total", e.Tally.Total}, + {"pass", e.Tally.Pass}, + {"fail", e.Tally.Fail}, + {"warn", e.Tally.Warn}, + {"skip", e.Tally.Skip}, + } + + for _, item := range items { + if item.value > 0 { + parts = append(parts, fmt.Sprintf("%s:%d", item.name, item.value)) + } + } + + auxSummary := e.format.Aux.Render(" [" + strings.Join(parts, " ") + "]") + + var style lipgloss.Style + switch { + case e.Pass && e.Tally.Warn == 0: + style = e.format.Success + case e.Pass && e.Tally.Warn > 0: + style = e.format.Warning + default: + style = e.format.Failure + } + return style.Render(status) + auxSummary +} + +func formatKeyValue(f format, key, value string) string { + formattedKey := f.Label.Render(key + ":") + return fmt.Sprintf(" %s %s", formattedKey, value) +} diff --git a/cmd/dive/cli/internal/command/ci/evaluator_test.go b/cmd/dive/cli/internal/command/ci/evaluator_test.go new file mode 100644 index 0000000..21682d3 --- /dev/null +++ b/cmd/dive/cli/internal/command/ci/evaluator_test.go @@ -0,0 +1,198 @@ +package ci + +import ( + "context" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/wagoodman/dive/dive/image/docker" +) + +var repoRootCache atomic.String + +func Test_Evaluator(t *testing.T) { + result := docker.TestAnalysisFromArchive(t, repoPath(t, ".data/test-docker-image.tar")) + + validTests := []struct { + name string + efficiency string + wastedBytes string + wastedPercent string + expectedPass bool + expectedResult map[string]RuleStatus + }{ + { + name: "allFail", + efficiency: "0.99", + wastedBytes: "1B", + wastedPercent: "0.01", + expectedPass: false, + expectedResult: map[string]RuleStatus{ + "lowestEfficiency": RuleFailed, + "highestWastedBytes": RuleFailed, + "highestUserWastedPercent": RuleFailed, + }, + }, + { + name: "allPass", + efficiency: "0.9", + wastedBytes: "50kB", + wastedPercent: "0.5", + expectedPass: true, + expectedResult: map[string]RuleStatus{ + "lowestEfficiency": RulePassed, + "highestWastedBytes": RulePassed, + "highestUserWastedPercent": RulePassed, + }, + }, + { + name: "allDisabled", + efficiency: "disabled", + wastedBytes: "disabled", + wastedPercent: "disabled", + expectedPass: true, + expectedResult: map[string]RuleStatus{ + "lowestEfficiency": RuleDisabled, + "highestWastedBytes": RuleDisabled, + "highestUserWastedPercent": RuleDisabled, + }, + }, + { + name: "mixedResults", + efficiency: "0.9", + wastedBytes: "1B", + wastedPercent: "0.5", + expectedPass: false, + expectedResult: map[string]RuleStatus{ + "lowestEfficiency": RulePassed, + "highestWastedBytes": RuleFailed, + "highestUserWastedPercent": RulePassed, + }, + }, + } + + for _, test := range validTests { + t.Run(test.name, func(t *testing.T) { + // Create rules - these should not error + rules, err := Rules(test.efficiency, test.wastedBytes, test.wastedPercent) + require.NoError(t, err) + + evaluator := NewEvaluator(rules) + eval := evaluator.Evaluate(context.TODO(), result) + + if test.expectedPass != eval.Pass { + t.Errorf("expected pass=%v, got %v", test.expectedPass, eval.Pass) + } + + if len(test.expectedResult) != len(evaluator.Results) { + t.Errorf("expected %v results, got %v", len(test.expectedResult), len(evaluator.Results)) + } + + for rule, actualResult := range evaluator.Results { + expectedStatus := test.expectedResult[rule] + if expectedStatus != actualResult.status { + t.Errorf("%v: expected %v rule status, got %v: %v", + rule, expectedStatus, actualResult.status, actualResult) + } + } + }) + } + +} + +func Test_Evaluator_Misconfigurations(t *testing.T) { + invalidTests := []struct { + name string + efficiency string + wastedBytes string + wastedPercent string + expectError bool + }{ + { + name: "invalid_efficiency_too_high", + efficiency: "1.1", // fail! + wastedBytes: "50kB", + wastedPercent: "0.5", + expectError: true, + }, + { + name: "invalid_efficiency_too_low", + efficiency: "-0.1", // fail! + wastedBytes: "50kB", + wastedPercent: "0.5", + expectError: true, + }, + { + name: "invalid_efficiency_format", + efficiency: "not_a_number", // fail! + wastedBytes: "50kB", + wastedPercent: "0.5", + expectError: true, + }, + { + name: "invalid_wasted_bytes_format", + efficiency: "0.9", + wastedBytes: "not_a_size", // fail! + wastedPercent: "0.5", + expectError: true, + }, + { + name: "invalid_wasted_percent_high", + efficiency: "0.9", + wastedBytes: "50kB", + wastedPercent: "1.1", // fail! + expectError: true, + }, + { + name: "invalid_wasted_percent_low", + efficiency: "0.9", + wastedBytes: "50kB", + wastedPercent: "-0.1", // fail! + expectError: true, + }, + { + name: "invalid_wasted_percent_format", + efficiency: "0.9", + wastedBytes: "50kB", + wastedPercent: "not_a_number", // fail! + expectError: true, + }, + } + + for _, test := range invalidTests { + t.Run(test.name, func(t *testing.T) { + _, err := Rules(test.efficiency, test.wastedBytes, test.wastedPercent) + if test.expectError { + require.Error(t, err, "Expected an error for invalid configuration") + } else { + require.NoError(t, err, "Expected no error for valid configuration") + } + }) + } +} + +func repoPath(t testing.TB, path string) string { + t.Helper() + root := repoRoot(t) + return filepath.Join(root, path) +} + +func repoRoot(t testing.TB) string { + val := repoRootCache.Load() + if val != "" { + return val + } + t.Helper() + // use git to find the root of the repo + out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + t.Fatalf("failed to get repo root: %v", err) + } + val = strings.TrimSpace(string(out)) + repoRootCache.Store(val) + return val +} diff --git a/cmd/dive/cli/internal/command/ci/rule.go b/cmd/dive/cli/internal/command/ci/rule.go new file mode 100644 index 0000000..c1930ef --- /dev/null +++ b/cmd/dive/cli/internal/command/ci/rule.go @@ -0,0 +1,47 @@ +package ci + +import ( + "github.com/wagoodman/dive/dive/image" +) + +const ( + RuleUnknown = iota + RulePassed + RuleFailed + RuleWarning + RuleDisabled + RuleMisconfigured + RuleConfigured +) + +type Rule interface { + Key() string + Configuration() string + Evaluate(result *image.Analysis) (RuleStatus, string) +} + +type RuleStatus int + +type RuleResult struct { + status RuleStatus + message string +} + +func (status RuleStatus) String(f format) string { + switch status { + case RulePassed: + return f.Success.Render("PASS") + case RuleFailed: + return f.Failure.Render("FAIL") + case RuleWarning: + return f.Warning.Render("WARN") + case RuleDisabled: + return f.Disabled.Render("SKIP") + case RuleMisconfigured: + return f.Warning.Render("MISCONFIGURED") + case RuleConfigured: + return "CONFIGURED " + default: + return f.Warning.Render("Unknown") + } +} diff --git a/cmd/dive/cli/internal/command/ci/rules.go b/cmd/dive/cli/internal/command/ci/rules.go new file mode 100644 index 0000000..84da482 --- /dev/null +++ b/cmd/dive/cli/internal/command/ci/rules.go @@ -0,0 +1,196 @@ +package ci + +import ( + "errors" + "fmt" + "github.com/dustin/go-humanize" + "github.com/wagoodman/dive/dive/image" + "strconv" + "strings" +) + +const ( + ciKeyLowestEfficiencyThreshold = "lowestEfficiency" + ciKeyHighestWastedBytes = "highestWastedBytes" + ciKeyHighestUserWastedPercent = "highestUserWastedPercent" +) + +func Rules(lowerEfficiency, highestWastedBytes, highestUserWastedPercent string) ([]Rule, error) { + var rules []Rule + var errs []error + + lowestEfficiencyRule, err := NewLowestEfficiencyRule(lowerEfficiency) + if err != nil { + errs = append(errs, err) + } + rules = append(rules, lowestEfficiencyRule) + + highestWastedBytesRule, err := NewHighestWastedBytesRule(highestWastedBytes) + if err != nil { + errs = append(errs, err) + } + rules = append(rules, highestWastedBytesRule) + + highestUserWastedPercentRule, err := NewHighestUserWastedPercentRule(highestUserWastedPercent) + if err != nil { + errs = append(errs, err) + } + rules = append(rules, highestUserWastedPercentRule) + + return rules, errors.Join(errs...) + +} + +func DisabledRule(key string) Rule { + return &BaseRule{ + key: key, + configValue: "disabled", + evaluator: func(_ *image.Analysis) (RuleStatus, string) { + return RuleDisabled, "rule disabled" + }, + } +} + +type BaseRule struct { + key string + configValue string + status RuleStatus + evaluator func(*image.Analysis) (RuleStatus, string) +} + +func (rule *BaseRule) Key() string { + return rule.key +} + +func (rule *BaseRule) Configuration() string { + return rule.configValue +} + +func (rule *BaseRule) Evaluate(result *image.Analysis) (RuleStatus, string) { + if rule.status != RuleUnknown { + return rule.status, "" + } + return rule.evaluator(result) +} + +// LowestEfficiencyRule checks if image efficiency is above threshold +type LowestEfficiencyRule struct { + BaseRule + threshold float64 +} + +// HighestWastedBytesRule checks if wasted bytes are below threshold +type HighestWastedBytesRule struct { + BaseRule + threshold uint64 +} + +// HighestUserWastedPercentRule checks if percentage of wasted bytes is below threshold +type HighestUserWastedPercentRule struct { + BaseRule + threshold float64 +} + +func NewLowestEfficiencyRule(configValue string) (Rule, error) { + if isRuleDisabled(configValue) { + return DisabledRule(ciKeyLowestEfficiencyThreshold), nil + } + + threshold, err := strconv.ParseFloat(configValue, 64) + if err != nil { + return nil, fmt.Errorf("invalid %s config value, given %q: %v", + ciKeyLowestEfficiencyThreshold, configValue, err) + } + + if threshold < 0 || threshold > 1 { + return nil, fmt.Errorf("%s config value is outside allowed range (0-1), given '%f'", + ciKeyLowestEfficiencyThreshold, threshold) + } + + return &LowestEfficiencyRule{ + BaseRule: BaseRule{ + key: ciKeyLowestEfficiencyThreshold, + configValue: configValue, + }, + threshold: threshold, + }, nil +} + +func (r *LowestEfficiencyRule) Evaluate(analysis *image.Analysis) (RuleStatus, string) { + if r.threshold > analysis.Efficiency { + return RuleFailed, fmt.Sprintf( + "image efficiency is too low (efficiency=%2.2f < threshold=%v)", + analysis.Efficiency, r.threshold) + } + return RulePassed, "" +} + +// NewHighestWastedBytesRule creates a new rule to check wasted bytes +func NewHighestWastedBytesRule(configValue string) (Rule, error) { + if isRuleDisabled(configValue) { + return DisabledRule(ciKeyHighestWastedBytes), nil + } + + threshold, err := humanize.ParseBytes(configValue) + if err != nil { + return nil, fmt.Errorf("invalid highestWastedBytes config value, given %q: %v", + configValue, err) + } + + return &HighestWastedBytesRule{ + BaseRule: BaseRule{ + key: ciKeyHighestWastedBytes, + configValue: configValue, + }, + threshold: threshold, + }, nil +} + +func (r *HighestWastedBytesRule) Evaluate(analysis *image.Analysis) (RuleStatus, string) { + if analysis.WastedBytes > r.threshold { + return RuleFailed, fmt.Sprintf( + "too many bytes wasted (wasted-bytes=%d > threshold=%v)", + analysis.WastedBytes, r.threshold) + } + return RulePassed, "" +} + +// NewHighestUserWastedPercentRule creates a new rule to check percentage of wasted bytes +func NewHighestUserWastedPercentRule(configValue string) (Rule, error) { + if isRuleDisabled(configValue) { + return DisabledRule(ciKeyHighestUserWastedPercent), nil + } + + threshold, err := strconv.ParseFloat(configValue, 64) + if err != nil { + return nil, fmt.Errorf("invalid highestUserWastedPercent config value, given %q: %v", + configValue, err) + } + + if threshold < 0 || threshold > 1 { + return nil, fmt.Errorf("highestUserWastedPercent config value is outside allowed range (0-1), given '%f'", + threshold) + } + + return &HighestUserWastedPercentRule{ + BaseRule: BaseRule{ + key: ciKeyHighestUserWastedPercent, + configValue: configValue, + }, + threshold: threshold, + }, nil +} + +func (r *HighestUserWastedPercentRule) Evaluate(analysis *image.Analysis) (RuleStatus, string) { + if analysis.WastedUserPercent > r.threshold { + return RuleFailed, fmt.Sprintf( + "too many bytes wasted, relative to the user bytes added (%%-user-wasted-bytes=%2.2f > threshold=%v)", + analysis.WastedUserPercent, r.threshold) + } + return RulePassed, "" +} + +func isRuleDisabled(value string) bool { + value = strings.TrimSpace(strings.ToLower(value)) + return value == "" || value == "disabled" || value == "off" || value == "false" +} diff --git a/cmd/dive/cli/internal/command/export/export.go b/cmd/dive/cli/internal/command/export/export.go new file mode 100644 index 0000000..94d5590 --- /dev/null +++ b/cmd/dive/cli/internal/command/export/export.go @@ -0,0 +1,86 @@ +package export + +import ( + "encoding/json" + "github.com/wagoodman/dive/dive/filetree" + diveImage "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/log" +) + +type Export struct { + Layer []Layer `json:"layer"` + Image Image `json:"image"` +} + +type Layer struct { + Index int `json:"index"` + ID string `json:"id"` + DigestID string `json:"digestId"` + SizeBytes uint64 `json:"sizeBytes"` + Command string `json:"command"` + FileList []filetree.FileInfo `json:"fileList"` +} + +type Image struct { + SizeBytes uint64 `json:"sizeBytes"` + InefficientBytes uint64 `json:"inefficientBytes"` + EfficiencyScore float64 `json:"efficiencyScore"` + InefficientFiles []FileReference `json:"fileReference"` +} + +type FileReference struct { + References int `json:"count"` + SizeBytes uint64 `json:"sizeBytes"` + Path string `json:"file"` +} + +// NewExport exports the analysis to a JSON +func NewExport(analysis *diveImage.Analysis) *Export { + data := Export{ + Layer: make([]Layer, len(analysis.Layers)), + Image: Image{ + InefficientFiles: make([]FileReference, len(analysis.Inefficiencies)), + SizeBytes: analysis.SizeBytes, + EfficiencyScore: analysis.Efficiency, + InefficientBytes: analysis.WastedBytes, + }, + } + + // export layers in order + for idx, curLayer := range analysis.Layers { + layerFileList := make([]filetree.FileInfo, 0) + visitor := func(node *filetree.FileNode) error { + layerFileList = append(layerFileList, node.Data.FileInfo) + return nil + } + err := curLayer.Tree.VisitDepthChildFirst(visitor, nil) + if err != nil { + log.WithFields("layer", curLayer.Id, "error", err).Debug("unable to propagate layer tree") + } + data.Layer[idx] = Layer{ + Index: curLayer.Index, + ID: curLayer.Id, + DigestID: curLayer.Digest, + SizeBytes: curLayer.Size, + Command: curLayer.Command, + FileList: layerFileList, + } + } + + // add file references + for idx := 0; idx < len(analysis.Inefficiencies); idx++ { + fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx] + + data.Image.InefficientFiles[idx] = FileReference{ + References: len(fileData.Nodes), + SizeBytes: uint64(fileData.CumulativeSize), + Path: fileData.Path, + } + } + + return &data +} + +func (exp *Export) Marshal() ([]byte, error) { + return json.MarshalIndent(&exp, "", " ") +} diff --git a/cmd/dive/cli/internal/command/export/export_test.go b/cmd/dive/cli/internal/command/export/export_test.go new file mode 100644 index 0000000..2d1465f --- /dev/null +++ b/cmd/dive/cli/internal/command/export/export_test.go @@ -0,0 +1,19 @@ +package export + +import ( + "testing" + + "github.com/wagoodman/dive/dive/image/docker" +) + +func Test_Export(t *testing.T) { + result := docker.TestAnalysisFromArchive(t, repoPath(t, ".data/test-docker-image.tar")) + + export := NewExport(result) + payload, err := export.Marshal() + if err != nil { + t.Errorf("Test_Export: unable to export analysis: %v", err) + } + + snaps.MatchJSON(t, payload) +} diff --git a/cmd/dive/cli/internal/command/export/main_test.go b/cmd/dive/cli/internal/command/export/main_test.go new file mode 100644 index 0000000..f41cd83 --- /dev/null +++ b/cmd/dive/cli/internal/command/export/main_test.go @@ -0,0 +1,68 @@ +package export + +import ( + "flag" + "github.com/charmbracelet/lipgloss" + snapsPkg "github.com/gkampitakis/go-snaps/snaps" + "github.com/muesli/termenv" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +var ( + updateSnapshot = flag.Bool("update", false, "update any test snapshots") + snaps *snapsPkg.Config + repoRootCache atomic.String +) + +func TestMain(m *testing.M) { + // flags are not parsed until after test.Main is called... + flag.Parse() + + os.Unsetenv("DIVE_CONFIG") + + // disable colors + lipgloss.SetColorProfile(termenv.Ascii) + + snaps = snapsPkg.WithConfig( + snapsPkg.Update(*updateSnapshot), + snapsPkg.Dir("testdata/snapshots"), + ) + + v := m.Run() + + snapsPkg.Clean(m) + + os.Exit(v) +} + +func TestUpdateSnapshotDisabled(t *testing.T) { + require.False(t, *updateSnapshot, "update snapshot flag should be disabled") +} + +func repoPath(t testing.TB, path string) string { + t.Helper() + root := repoRoot(t) + return filepath.Join(root, path) +} + +func repoRoot(t testing.TB) string { + val := repoRootCache.Load() + if val != "" { + return val + } + t.Helper() + // use git to find the root of the repo + out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + t.Fatalf("failed to get repo root: %v", err) + } + val = strings.TrimSpace(string(out)) + repoRootCache.Store(val) + return val +} diff --git a/cmd/dive/cli/internal/command/export/testdata/snapshots/export_test.snap b/cmd/dive/cli/internal/command/export/testdata/snapshots/export_test.snap new file mode 100755 index 0000000..454c34a --- /dev/null +++ b/cmd/dive/cli/internal/command/export/testdata/snapshots/export_test.snap @@ -0,0 +1,4665 @@ + +[Test_Export - 1] +{ + "image": { + "efficiencyScore": 0.9844212134184309, + "fileReference": [ + { + "count": 2, + "file": "/root/saved.txt", + "sizeBytes": 12810 + }, + { + "count": 2, + "file": "/root/example/somefile1.txt", + "sizeBytes": 12810 + }, + { + "count": 2, + "file": "/root/example/somefile3.txt", + "sizeBytes": 6405 + } + ], + "inefficientBytes": 32025, + "sizeBytes": 1220598 + }, + "layer": [ + { + "command": "#(nop) ADD file:ce026b62356eec3ad1214f92be2c9dc063fe205bd5e600be3492c4dfb17148bd in / ", + "digestId": "sha256:23bc2b70b2014dec0ac22f27bb93e9babd08cdd6f1115d0c955b9ff22b382f5a", + "fileList": [ + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "bin/[", + "size": 1075464, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/[[", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/acpid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/add-shell", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/addgroup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/adduser", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/adjtimex", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ar", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/arch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/arp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/arping", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ash", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/awk", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/base64", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/basename", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/beep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/blkdiscard", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/blkid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/blockdev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bootchartd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/brctl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bunzip2", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/busybox", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bzcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bzip2", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cal", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chgrp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chown", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chpasswd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chpst", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chroot", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chrt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chvt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cksum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/clear", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cmp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/comm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/conspy", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cpio", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/crond", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/crontab", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cryptpw", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cttyhack", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cut", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/date", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/deallocvt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/delgroup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/deluser", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/depmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/devmem", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/df", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dhcprelay", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/diff", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dirname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dmesg", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dnsd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dnsdomainname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dos2unix", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dpkg", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dpkg-deb", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/du", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dumpkmap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dumpleases", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/echo", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ed", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/egrep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/eject", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/env", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/envdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/envuidgid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ether-wake", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/expand", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/expr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/factor", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fakeidentd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fallocate", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/false", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fatattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fbset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fbsplash", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fdflush", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fdformat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fdisk", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fgconsole", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fgrep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/find", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/findfs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/flock", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fold", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/free", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/freeramdisk", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsck", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsck.minix", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsfreeze", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fstrim", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsync", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ftpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ftpget", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ftpput", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fuser", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "bin/getconf", + "size": 77880, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/getopt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/getty", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/grep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/groups", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/gunzip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/gzip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/halt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hdparm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/head", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hexdump", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hexedit", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hostid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hostname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/httpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hush", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hwclock", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cdetect", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cdump", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cget", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/id", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifconfig", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifdown", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifenslave", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifplugd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/inetd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/init", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/insmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/install", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ionice", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iostat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipaddr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipcalc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipcrm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipcs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iplink", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipneigh", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iproute", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iprule", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iptunnel", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/kbd_mode", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/kill", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/killall", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/killall5", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/klogd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/last", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/less", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/link", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/linux32", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/linux64", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/linuxrc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ln", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/loadfont", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/loadkmap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/logger", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/login", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/logname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/logread", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/losetup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lpq", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lpr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ls", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsof", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lspci", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsscsi", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsusb", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lzcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lzma", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lzop", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/makedevs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/makemime", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/man", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/md5sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mdev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mesg", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/microcom", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkdosfs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mke2fs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfifo", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfs.ext2", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfs.minix", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfs.vfat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mknod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkpasswd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkswap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mktemp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/modinfo", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/modprobe", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/more", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mount", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mountpoint", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mpstat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nameif", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nanddump", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nandwrite", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nbd-client", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/netstat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nice", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nmeter", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nohup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nproc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nsenter", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nslookup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ntpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nuke", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/od", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/openvt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/partprobe", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/passwd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/paste", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/patch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pgrep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pidof", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ping", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ping6", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pipe_progress", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pivot_root", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pkill", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pmap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/popmaildir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/poweroff", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/powertop", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/printenv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/printf", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ps", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pscan", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pstree", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pwd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pwdx", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/raidautorun", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rdate", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rdev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/readahead", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/readlink", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/readprofile", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/realpath", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/reboot", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/reformime", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/remove-shell", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/renice", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/reset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/resize", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/resume", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rmdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rmmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/route", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rpm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rpm2cpio", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rtcwake", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/run-init", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/run-parts", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/runlevel", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/runsv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/runsvdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rx", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/script", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/scriptreplay", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sed", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sendmail", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/seq", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setarch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setconsole", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setfattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setfont", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setkeycodes", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setlogcons", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setpriv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setserial", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setsid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setuidgid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sh", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha1sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha256sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha3sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha512sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/showkey", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/shred", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/shuf", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/slattach", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sleep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/smemcap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/softlimit", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sort", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/split", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ssl_client", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/start-stop-daemon", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/stat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/strings", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/stty", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/su", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sulogin", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/svc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/svlogd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/svok", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/swapoff", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/swapon", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/switch_root", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sync", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sysctl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/syslogd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tac", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tail", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tar", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/taskset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tcpsvd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tee", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/telnet", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/telnetd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/test", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tftp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tftpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/time", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/timeout", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/top", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/touch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/traceroute", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/traceroute6", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/true", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/truncate", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tty", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ttysize", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tunctl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubiattach", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubidetach", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubimkvol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubirename", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubirmvol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubirsvol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubiupdatevol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udhcpc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udhcpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udpsvd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uevent", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/umount", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unexpand", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uniq", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unix2dos", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unlink", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unlzma", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unshare", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unxz", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unzip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uptime", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/users", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/usleep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uudecode", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uuencode", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/vconfig", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/vi", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/vlock", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/volname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/w", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/wall", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/watch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/watchdog", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/wc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/wget", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/which", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/who", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/whoami", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/whois", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xargs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xxd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xz", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xzcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/yes", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/zcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/zcip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "bin", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "dev", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 436, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/group", + "size": 307, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/localtime", + "size": 127, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-down.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-post-down.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-pre-up.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-up.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/passwd", + "size": 340, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 384, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/shadow", + "size": 243, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 65534, + "isDir": true, + "linkName": "", + "path": "home", + "size": 0, + "typeFlag": 53, + "uid": 65534 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2148532735, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "tmp", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 1, + "isDir": true, + "linkName": "", + "path": "usr/sbin", + "size": 0, + "typeFlag": 53, + "uid": 1 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "usr", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 8, + "isDir": true, + "linkName": "", + "path": "var/spool/mail", + "size": 0, + "typeFlag": 53, + "uid": 8 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "var/spool", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "var/www", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "var", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694", + "index": 0, + "sizeBytes": 1154361 + }, + { + "command": "#(nop) ADD file:139c3708fb6261126453e34483abd8bf7b26ed16d952fd976994d68e72d93be2 in /somefile.txt ", + "digestId": "sha256:a65b7d7ac139a0e4337bc3c73ce511f937d6140ef61a0108f7d4b8aab8d67274", + "fileList": [ + { + "fileMode": 436, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "somefile.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + } + ], + "id": "1871059774abe6914075e4a919b778fa1561f577d620ae52438a9635e6241936", + "index": 1, + "sizeBytes": 6405 + }, + { + "command": "mkdir -p /root/example/really/nested", + "digestId": "sha256:93e208d471756ffbac88cf9c25feb442007f221d3bd73231e27b747a0a68927c", + "fileList": [ + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example/really/nested", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example/really", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "49fe2a475548bfa4d493fc796fce41f30704e3d4cbff3e45dd3e06f463236d1d", + "index": 2, + "sizeBytes": 0 + }, + { + "command": "cp /somefile.txt /root/example/somefile1.txt", + "digestId": "sha256:4abad3abe3cb99ad7a492a9d9f6b3d66287c1646843c74128bbbec4f7be5aa9e", + "fileList": [ + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/example/somefile1.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "80cd2ca1ffc89962b9349c80280c2bc551acbd11e09b16badb0669f8e2369020", + "index": 3, + "sizeBytes": 6405 + }, + { + "command": "chmod 444 /root/example/somefile1.txt", + "digestId": "sha256:14c9a6ffcb6a0f32d1035f97373b19608e2d307961d8be156321c3f1c1504cbf", + "fileList": [ + { + "fileMode": 292, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/example/somefile1.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "c99e2f8d3f6282668f0d30dc1db5e67a51d7a1dcd7ff6ddfa0f90760836778ec", + "index": 4, + "sizeBytes": 6405 + }, + { + "command": "cp /somefile.txt /root/example/somefile2.txt", + "digestId": "sha256:778fb5770ef466f314e79cc9dc418eba76bfc0a64491ce7b167b76aa52c736c4", + "fileList": [ + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/example/somefile2.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "5eca617bdc3bc06134fe957a30da4c57adb7c340a6d749c8edc4c15861c928d7", + "index": 5, + "sizeBytes": 6405 + }, + { + "command": "cp /somefile.txt /root/example/somefile3.txt", + "digestId": "sha256:f275b8a31a71deb521cc048e6021e2ff6fa52bedb25c9b7bbe129a0195ddca5f", + "fileList": [ + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/example/somefile3.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "f07c3eb887572395408f8e11a07af945e4da5f02b3188bb06b93fad713ca0b99", + "index": 6, + "sizeBytes": 6405 + }, + { + "command": "mv /root/example/somefile3.txt /root/saved.txt", + "digestId": "sha256:dd1effc5eb19894c3e9b57411c98dd1cf30fa1de4253c7fae53c9cea67267d83", + "fileList": [ + { + "fileMode": 0, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/example/.wh.somefile3.txt", + "size": 0, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/example", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/saved.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "461885fc22589158dee3c5b9f01cc41c87805439f58b4399d733b51aa305cbf9", + "index": 7, + "sizeBytes": 6405 + }, + { + "command": "cp /root/saved.txt /root/.saved.txt", + "digestId": "sha256:8d1869a0a066cdd12e48d648222866e77b5e2814f773bb3bd8774ab4052f0f1d", + "fileList": [ + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/.saved.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "a10327f68ffed4afcba78919052809a8f774978a6b87fc117d39c53c4842f72c", + "index": 8, + "sizeBytes": 6405 + }, + { + "command": "rm -rf /root/example/", + "digestId": "sha256:bc2e36423fa31a97223fd421f22c35466220fa160769abf697b8eb58c896b468", + "fileList": [ + { + "fileMode": 0, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/.wh.example", + "size": 0, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "f2fc54e25cb7966dc9732ec671a77a1c5c104e732bd15ad44a2dc1ac42368f84", + "index": 9, + "sizeBytes": 0 + }, + { + "command": "#(nop) ADD dir:7ec14b81316baa1a31c38c97686a8f030c98cba2035c968412749e33e0c4427e in /root/.data/ ", + "digestId": "sha256:7f648d45ee7b6de2292162fba498b66cbaaf181da9004fcceef824c72dbae445", + "fileList": [ + { + "fileMode": 509, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/.data/tag.sh", + "size": 917, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/.data/test.sh", + "size": 1270, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/.data", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "aad36d0b05e71c7e6d4dfe0ca9ed6be89e2e0d8995dafe83438299a314e91071", + "index": 10, + "sizeBytes": 2187 + }, + { + "command": "cp /root/saved.txt /tmp/saved.again1.txt", + "digestId": "sha256:a4b8f95f266d5c063c9a9473c45f2f85ddc183e37941b5e6b6b9d3c00e8e0457", + "fileList": [ + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "tmp/saved.again1.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2148532735, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "tmp", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "3d4ad907517a021d86a4102d2764ad2161e4818bbd144e41d019bfc955434181", + "index": 11, + "sizeBytes": 6405 + }, + { + "command": "cp /root/saved.txt /root/.data/saved.again2.txt", + "digestId": "sha256:22a44d45780a541e593a8862d80f3e14cb80b6bf76aa42ce68dc207a35bf3a4a", + "fileList": [ + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/.data/saved.again2.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root/.data", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "81b1b002d4b4c1325a9cad9990b5277e7f29f79e0f24582344c0891178f95905", + "index": 12, + "sizeBytes": 6405 + }, + { + "command": "chmod +x /root/saved.txt", + "digestId": "sha256:ba689cac6a98c92d121fa5c9716a1bab526b8bb1fd6d43625c575b79e97300c5", + "fileList": [ + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "root/saved.txt", + "size": 6405, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "cfb35bb5c127d848739be5ca726057e6e2c77b2849f588e7aebb642c0d3d4b7b", + "index": 13, + "sizeBytes": 6405 + } + ] +} +--- diff --git a/cmd/dive/cli/internal/command/root.go b/cmd/dive/cli/internal/command/root.go new file mode 100644 index 0000000..e26db45 --- /dev/null +++ b/cmd/dive/cli/internal/command/root.go @@ -0,0 +1,99 @@ +package command + +import ( + "context" + "errors" + "fmt" + "github.com/anchore/clio" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/wagoodman/dive/cmd/dive/cli/internal/command/adapter" + "github.com/wagoodman/dive/cmd/dive/cli/internal/options" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui" + "github.com/wagoodman/dive/dive" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus" + "os" +) + +type rootOptions struct { + options.Application `yaml:",inline" mapstructure:",squash"` + + // reserved for future use of root-only flags +} + +func Root(app clio.Application) *cobra.Command { + opts := &rootOptions{ + Application: options.DefaultApplication(), + } + return app.SetupRootCommand(&cobra.Command{ + Use: "dive [IMAGE]", + Short: "Docker Image Visualizer & Explorer", + Long: `This tool provides a way to discover and explore the contents of a docker image. Additionally the tool estimates +the amount of wasted space and identifies the offending files from the image.`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("exactly one argument is required") + } + opts.Analysis.Image = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := setUI(app, opts.Application); err != nil { + return fmt.Errorf("failed to set UI: %w", err) + } + + resolver, err := dive.GetImageResolver(opts.Analysis.Source) + if err != nil { + return fmt.Errorf("cannot determine image provider to fetch from: %w", err) + } + + ctx := cmd.Context() + + img, err := adapter.ImageResolver(resolver).Fetch(ctx, opts.Analysis.Image) + if err != nil { + return fmt.Errorf("cannot load image: %w", err) + } + + return run(ctx, opts.Application, img, resolver) + }, + }, opts) +} + +func setUI(app clio.Application, opts options.Application) error { + type Stater interface { + State() *clio.State + } + + state := app.(Stater).State() + + ux := ui.NewV1UI(opts.V1Preferences(), os.Stdout, state.Config.Log.Quiet, state.Config.Log.Verbosity) + return state.UI.Replace(ux) +} + +func run(ctx context.Context, opts options.Application, img *image.Image, content image.ContentReader) error { + analysis, err := adapter.NewAnalyzer().Analyze(ctx, img) + if err != nil { + return fmt.Errorf("cannot analyze image: %w", err) + } + + if opts.Export.JsonPath != "" { + if err := adapter.NewExporter(afero.NewOsFs()).ExportTo(ctx, analysis, opts.Export.JsonPath); err != nil { + return fmt.Errorf("cannot export analysis: %w", err) + } + return nil + } + + if opts.CI.Enabled { + eval := adapter.NewEvaluator(opts.CI.Rules.List).Evaluate(ctx, analysis) + + if !eval.Pass { + return errors.New("evaluation failed") + } + return nil + } + + bus.ExploreAnalysis(*analysis, content) + + return nil +} diff --git a/cmd/dive/cli/internal/options/analysis.go b/cmd/dive/cli/internal/options/analysis.go new file mode 100644 index 0000000..502623d --- /dev/null +++ b/cmd/dive/cli/internal/options/analysis.go @@ -0,0 +1,75 @@ +package options + +import ( + "fmt" + "github.com/anchore/clio" + "github.com/scylladb/go-set/strset" + "github.com/wagoodman/dive/dive" + "github.com/wagoodman/dive/internal/log" + "strings" +) + +const defaultContainerEngine = "docker" + +var _ interface { + clio.PostLoader + clio.FieldDescriber +} = (*Analysis)(nil) + +// Analysis provides configuration for the image analysis behavior +type Analysis struct { + Image string `yaml:"image" mapstructure:"-"` + ContainerEngine string `yaml:"container-engine" mapstructure:"container-engine"` + Source dive.ImageSource `yaml:"-" mapstructure:"-"` + IgnoreErrors bool `yaml:"ignore-errors" mapstructure:"ignore-errors"` + AvailableContainerEngines []string `yaml:"-" mapstructure:"-"` +} + +func DefaultAnalysis() Analysis { + return Analysis{ + ContainerEngine: defaultContainerEngine, + IgnoreErrors: false, + AvailableContainerEngines: dive.ImageSources, + } +} + +func (c *Analysis) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&c.ContainerEngine, "container engine to use for image analysis (supported options: 'docker' and 'podman')") + descriptions.Add(&c.IgnoreErrors, "continue with analysis even if there are errors parsing the image archive") +} + +func (c *Analysis) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&c.ContainerEngine, "source", "", + fmt.Sprintf("The container engine to fetch the image from. Allowed values: %s", strings.Join(c.AvailableContainerEngines, ", "))) + + flags.BoolVarP(&c.IgnoreErrors, "ignore-errors", "i", "ignore image parsing errors and run the analysis anyway") +} + +func (c *Analysis) PostLoad() error { + validEngines := strset.New(c.AvailableContainerEngines...) + if !validEngines.Has(c.ContainerEngine) { + log.Warnf("invalid container engine: %s (valid options: %s), using default %q", c.ContainerEngine, strings.Join(c.AvailableContainerEngines, ", "), defaultContainerEngine) + c.ContainerEngine = "docker" + } + + if c.Image != "" { + sourceType, imageStr := dive.DeriveImageSource(c.Image) + + if sourceType == dive.SourceUnknown { + sourceType = dive.ParseImageSource(c.ContainerEngine) + if sourceType == dive.SourceUnknown { + return fmt.Errorf("unable to determine image source from %q: %v\n", c.Image, c.ContainerEngine) + } + + // use exactly what the user provided + imageStr = c.Image + } + + c.Image = imageStr + c.Source = sourceType + } else { + c.Source = dive.ParseImageSource(c.ContainerEngine) + } + + return nil +} diff --git a/cmd/dive/cli/internal/options/application.go b/cmd/dive/cli/internal/options/application.go new file mode 100644 index 0000000..e03e038 --- /dev/null +++ b/cmd/dive/cli/internal/options/application.go @@ -0,0 +1,32 @@ +package options + +import ( + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" +) + +type Application struct { + Analysis Analysis `yaml:",inline" mapstructure:",squash"` + CI CI `yaml:",inline" mapstructure:",squash"` + Export Export `yaml:",inline" mapstructure:",squash"` + UI UI `yaml:",inline" mapstructure:",squash"` +} + +func DefaultApplication() Application { + return Application{ + Analysis: DefaultAnalysis(), + CI: DefaultCI(), + Export: DefaultExport(), + UI: DefaultUI(), + } +} + +func (c Application) V1Preferences() v1.Preferences { + return v1.Preferences{ + KeyBindings: c.UI.Keybinding.Config, + ShowFiletreeAttributes: c.UI.Filetree.ShowAttributes, + ShowAggregatedLayerChanges: c.UI.Layer.ShowAggregatedChanges, + CollapseFiletreeDirectory: c.UI.Filetree.CollapseDir, + FiletreePaneWidth: c.UI.Filetree.PaneWidth, + FiletreeDiffHide: nil, + } +} diff --git a/cmd/dive/cli/internal/options/ci.go b/cmd/dive/cli/internal/options/ci.go new file mode 100644 index 0000000..702670f --- /dev/null +++ b/cmd/dive/cli/internal/options/ci.go @@ -0,0 +1,105 @@ +package options + +import ( + "fmt" + "github.com/anchore/clio" + "gopkg.in/yaml.v3" + "os" +) + +var _ interface { + clio.PostLoader + clio.FieldDescriber + clio.FlagAdder +} = (*CI)(nil) + +const defaultCIConfigPath = ".dive-ci" + +type CI struct { + Enabled bool `yaml:"ci" mapstructure:"ci"` + ConfigPath string `yaml:"ci-config" mapstructure:"ci-config"` + Rules CIRules `yaml:"rules" mapstructure:"rules"` +} + +func DefaultCI() CI { + return CI{ + Enabled: false, + ConfigPath: defaultCIConfigPath, + Rules: DefaultCIRules(), + } +} + +func (c *CI) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&c.Enabled, "enable CI mode") + descriptions.Add(&c.ConfigPath, "path to the CI config file") +} + +func (c *CI) AddFlags(flags clio.FlagSet) { + flags.BoolVarP(&c.Enabled, "ci", "", "skip the interactive TUI and validate against CI rules (same as env var CI=true)") + flags.StringVarP(&c.ConfigPath, "ci-config", "", "if CI=true in the environment, use the given yaml to drive validation rules.") +} + +func (c *CI) PostLoad() error { + enabledFromEnv := truthy(os.Getenv("CI")) + if !c.Enabled && enabledFromEnv { + c.Enabled = true + } + + if c.ConfigPath != "" { + if fileExists(c.ConfigPath) { + // if a config file is provided, load it and override any values provided in the application config. + // If we're hitting this case we should pretend that only the config file was provided and applied + // on top of the default config values. + yamlFile, err := os.ReadFile(c.ConfigPath) + if err != nil { + return fmt.Errorf("failed to read CI config file %s: %w", c.ConfigPath, err) + } + def := DefaultCIRules() + r := legacyRuleFile{ + LowestEfficiencyThresholdString: def.LowestEfficiencyThresholdString, + HighestWastedBytesString: def.HighestWastedBytesString, + HighestUserWastedPercentString: def.HighestUserWastedPercentString, + } + wrapper := struct { + Rules *legacyRuleFile `yaml:"rules"` + }{ + Rules: &r, + } + if err := yaml.Unmarshal(yamlFile, &wrapper); err != nil { + return fmt.Errorf("failed to unmarshal CI config file %s: %w", c.ConfigPath, err) + } + // TODO: should this be a deprecated use warning in the future? + c.Rules = CIRules{ + LowestEfficiencyThresholdString: r.LowestEfficiencyThresholdString, + HighestWastedBytesString: r.HighestWastedBytesString, + HighestUserWastedPercentString: r.HighestUserWastedPercentString, + } + } + } + + return nil +} + +type legacyRuleFile struct { + LowestEfficiencyThresholdString string `yaml:"lowestEfficiency"` + HighestWastedBytesString string `yaml:"highestWastedBytes"` + HighestUserWastedPercentString string `yaml:"highestUserWastedPercent"` +} + +func fileExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} + +func truthy(value string) bool { + switch value { + case "true", "1", "yes": + return true + case "false", "0", "no": + return false + default: + return false + } +} diff --git a/cmd/dive/cli/internal/options/ci_rules.go b/cmd/dive/cli/internal/options/ci_rules.go new file mode 100644 index 0000000..710c9b8 --- /dev/null +++ b/cmd/dive/cli/internal/options/ci_rules.go @@ -0,0 +1,73 @@ +package options + +import ( + "github.com/anchore/clio" + "github.com/wagoodman/dive/cmd/dive/cli/internal/command/ci" + "github.com/wagoodman/dive/internal/log" +) + +type CIRules struct { + LowestEfficiencyThresholdString string `yaml:"lowest-efficiency" mapstructure:"lowest-efficiency"` + LegacyLowestEfficiencyThresholdString string `yaml:"-" mapstructure:"lowestEfficiency"` + + HighestWastedBytesString string `yaml:"highest-wasted-bytes" mapstructure:"highest-wasted-bytes"` + LegacyHighestWastedBytesString string `yaml:"-" mapstructure:"highestWastedBytes"` + + HighestUserWastedPercentString string `yaml:"highest-user-wasted-percent" mapstructure:"highest-user-wasted-percent"` + LegacyHighestUserWastedPercentString string `yaml:"-" mapstructure:"highestUserWastedPercent"` + + List []ci.Rule `yaml:"-" mapstructure:"-"` +} + +func DefaultCIRules() CIRules { + return CIRules{ + LowestEfficiencyThresholdString: "0.9", + HighestWastedBytesString: "disabled", + HighestUserWastedPercentString: "0.1", + } +} + +func (c *CIRules) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&c.LowestEfficiencyThresholdString, "lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail.") + descriptions.Add(&c.HighestWastedBytesString, "highest allowable bytes wasted, otherwise CI validation will fail.") + descriptions.Add(&c.HighestUserWastedPercentString, "highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail.") +} + +func (c *CIRules) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&c.LowestEfficiencyThresholdString, "lowestEfficiency", "", "(only valid with --ci given) lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail.") + flags.StringVarP(&c.HighestWastedBytesString, "highestWastedBytes", "", "(only valid with --ci given) highest allowable bytes wasted, otherwise CI validation will fail.") + flags.StringVarP(&c.HighestUserWastedPercentString, "highestUserWastedPercent", "", "(only valid with --ci given) highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail.") +} + +func (c CIRules) hasLegacyOptionsInUse() bool { + return c.LegacyLowestEfficiencyThresholdString != "" || c.LegacyHighestWastedBytesString != "" || c.LegacyHighestUserWastedPercentString != "" +} + +func (c *CIRules) PostLoad() error { + // protect against repeated calls + c.List = nil + + if c.hasLegacyOptionsInUse() { + log.Warnf("please specify ci rules in snake-case (the legacy camelCase format is deprecated)") + } + + if c.LegacyLowestEfficiencyThresholdString != "" { + c.LowestEfficiencyThresholdString = c.LegacyLowestEfficiencyThresholdString + } + + if c.LegacyHighestWastedBytesString != "" { + c.HighestWastedBytesString = c.LegacyHighestWastedBytesString + } + + if c.LegacyHighestUserWastedPercentString != "" { + c.HighestUserWastedPercentString = c.LegacyHighestUserWastedPercentString + } + + rules, err := ci.Rules(c.LowestEfficiencyThresholdString, c.HighestWastedBytesString, c.HighestUserWastedPercentString) + if err != nil { + return err + } + c.List = append(c.List, rules...) + + return nil +} diff --git a/cmd/dive/cli/internal/options/export.go b/cmd/dive/cli/internal/options/export.go new file mode 100644 index 0000000..3a007cf --- /dev/null +++ b/cmd/dive/cli/internal/options/export.go @@ -0,0 +1,40 @@ +package options + +import ( + "fmt" + "os" + "path" + + "github.com/anchore/clio" +) + +var _ interface { + clio.FlagAdder + clio.PostLoader +} = (*Export)(nil) + +// Export provides configuration for data export functionality +type Export struct { + // Path to export analysis results as JSON (empty string = disabled) + JsonPath string `yaml:"json-path" json:"json-path" mapstructure:"json-path"` +} + +func DefaultExport() Export { + return Export{} +} + +func (o *Export) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.JsonPath, "json", "j", "Skip the interactive TUI and write the layer analysis statistics to a given file.") +} + +func (o *Export) PostLoad() error { + + if o.JsonPath != "" { + dir := path.Dir(o.JsonPath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + return fmt.Errorf("directory for JSON export does not exist: %s", dir) + } + } + + return nil +} diff --git a/cmd/dive/cli/internal/options/ui.go b/cmd/dive/cli/internal/options/ui.go new file mode 100644 index 0000000..01e619b --- /dev/null +++ b/cmd/dive/cli/internal/options/ui.go @@ -0,0 +1,18 @@ +package options + +// UI combines all UI configuration elements +type UI struct { + Keybinding UIKeybindings `yaml:"keybinding" mapstructure:"keybinding"` + Diff UIDiff `yaml:"diff" mapstructure:"diff"` + Filetree UIFiletree `yaml:"filetree" mapstructure:"filetree"` + Layer UILayers `yaml:"layer" mapstructure:"layer"` +} + +func DefaultUI() UI { + return UI{ + Keybinding: DefaultUIKeybinding(), + Diff: DefaultUIDiff(), + Filetree: DefaultUIFiletree(), + Layer: DefaultUILayers(), + } +} diff --git a/cmd/dive/cli/internal/options/ui_diff.go b/cmd/dive/cli/internal/options/ui_diff.go new file mode 100644 index 0000000..e451f84 --- /dev/null +++ b/cmd/dive/cli/internal/options/ui_diff.go @@ -0,0 +1,39 @@ +package options + +import ( + "github.com/anchore/clio" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/internal/log" +) + +var _ interface { + clio.PostLoader + clio.FieldDescriber +} = (*UIDiff)(nil) + +// UIDiff provides configuration for how differences are displayed +type UIDiff struct { + Hide []string `yaml:"hide" mapstructure:"hide"` +} + +func DefaultUIDiff() UIDiff { + prefs := v1.DefaultPreferences() + return UIDiff{ + Hide: prefs.FiletreeDiffHide, + } +} + +func (c *UIDiff) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&c.Hide, "types of file differences to hide (added, removed, modified, unmodified)") +} + +func (c *UIDiff) PostLoad() error { + validHideValues := map[string]bool{"added": true, "removed": true, "modified": true, "unmodified": true} + for _, value := range c.Hide { + if _, ok := validHideValues[value]; !ok { + log.Warnf("invalid diff hide value: %s (valid values: added, removed, modified, unmodified)", value) + } + } + + return nil +} diff --git a/cmd/dive/cli/internal/options/ui_filetree.go b/cmd/dive/cli/internal/options/ui_filetree.go new file mode 100644 index 0000000..44cc506 --- /dev/null +++ b/cmd/dive/cli/internal/options/ui_filetree.go @@ -0,0 +1,43 @@ +package options + +import ( + "github.com/anchore/clio" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/internal/log" +) + +var _ interface { + clio.PostLoader + clio.FieldDescriber +} = (*UIFiletree)(nil) + +// UIFiletree provides configuration for the file tree display +type UIFiletree struct { + CollapseDir bool `yaml:"collapse-dir" mapstructure:"collapse-dir"` + PaneWidth float64 `yaml:"pane-width" mapstructure:"pane-width"` + ShowAttributes bool `yaml:"show-attributes" mapstructure:"show-attributes"` +} + +func DefaultUIFiletree() UIFiletree { + prefs := v1.DefaultPreferences() + return UIFiletree{ + CollapseDir: prefs.CollapseFiletreeDirectory, + PaneWidth: prefs.FiletreePaneWidth, + ShowAttributes: prefs.ShowFiletreeAttributes, + } +} + +func (c *UIFiletree) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&c.CollapseDir, "collapse directories by default in the filetree") + descriptions.Add(&c.PaneWidth, "percentage of screen width for the filetree pane (must be >0 and <1)") + descriptions.Add(&c.ShowAttributes, "show file attributes in the filetree view") +} + +func (c *UIFiletree) PostLoad() error { + // Validate pane width is between 0 and 1 + if c.PaneWidth <= 0 || c.PaneWidth >= 1 { + log.Warnf("filetree pane-width must be >0 and <1, got %v, resetting to default 0.5", c.PaneWidth) + c.PaneWidth = 0.5 + } + return nil +} diff --git a/cmd/dive/cli/internal/options/ui_keybindings.go b/cmd/dive/cli/internal/options/ui_keybindings.go new file mode 100644 index 0000000..7a6ad8c --- /dev/null +++ b/cmd/dive/cli/internal/options/ui_keybindings.go @@ -0,0 +1,178 @@ +package options + +import ( + "github.com/anchore/clio" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "reflect" +) + +var _ interface { + clio.FieldDescriber +} = (*UIKeybindings)(nil) + +// UIKeybindings provides configuration for all keyboard shortcuts +type UIKeybindings struct { + Global GlobalBindings `yaml:",inline" mapstructure:",squash"` + Navigation NavigationBindings `yaml:",inline" mapstructure:",squash"` + Layer LayerBindings `yaml:",inline" mapstructure:",squash"` + Filetree FiletreeBindings `yaml:",inline" mapstructure:",squash"` + + Config key.Bindings `yaml:"-" mapstructure:"-"` +} + +type GlobalBindings struct { + Quit string `yaml:"quit" mapstructure:"quit"` + ToggleView string `yaml:"toggle-view" mapstructure:"toggle-view"` + FilterFiles string `yaml:"filter-files" mapstructure:"filter-files"` + CloseFilterFiles string `yaml:"close-filter-files" mapstructure:"close-filter-files"` +} + +type NavigationBindings struct { + Up string `yaml:"up" mapstructure:"up"` + Down string `yaml:"down" mapstructure:"down"` + Left string `yaml:"left" mapstructure:"left"` + Right string `yaml:"right" mapstructure:"right"` + PageUp string `yaml:"page-up" mapstructure:"page-up"` + PageDown string `yaml:"page-down" mapstructure:"page-down"` +} + +type LayerBindings struct { + CompareAll string `yaml:"compare-all" mapstructure:"compare-all"` + CompareLayer string `yaml:"compare-layer" mapstructure:"compare-layer"` +} + +type FiletreeBindings struct { + ToggleCollapseDir string `yaml:"toggle-collapse-dir" mapstructure:"toggle-collapse-dir"` + ToggleCollapseAllDir string `yaml:"toggle-collapse-all-dir" mapstructure:"toggle-collapse-all-dir"` + ToggleAddedFiles string `yaml:"toggle-added-files" mapstructure:"toggle-added-files"` + ToggleRemovedFiles string `yaml:"toggle-removed-files" mapstructure:"toggle-removed-files"` + ToggleModifiedFiles string `yaml:"toggle-modified-files" mapstructure:"toggle-modified-files"` + ToggleUnmodifiedFiles string `yaml:"toggle-unmodified-files" mapstructure:"toggle-unmodified-files"` + ToggleTreeAttributes string `yaml:"toggle-filetree-attributes" mapstructure:"toggle-filetree-attributes"` + ToggleSortOrder string `yaml:"toggle-sort-order" mapstructure:"toggle-sort-order"` + ToggleWrapTree string `yaml:"toggle-wrap-tree" mapstructure:"toggle-wrap-tree"` + ExtractFile string `yaml:"extract-file" mapstructure:"extract-file"` +} + +func DefaultUIKeybinding() UIKeybindings { + var result UIKeybindings + defaults := key.DefaultBindings() + + // converts from key.Bindings to UIKeybindings + getUIBindingValues(reflect.ValueOf(defaults), reflect.ValueOf(&result).Elem()) + + return result +} + +func getUIBindingValues(src, dst reflect.Value) { + switch src.Kind() { + case reflect.Struct: + for i := 0; i < src.NumField(); i++ { + srcField := src.Field(i) + srcType := src.Type().Field(i) + + if !srcField.CanInterface() { + continue + } + + dstField := dst.FieldByName(srcType.Name) + if !dstField.IsValid() || !dstField.CanSet() { + continue + } + + if srcType.Type.Name() == "Config" { + inputField := srcField.FieldByName("Input") + if inputField.IsValid() && dstField.Kind() == reflect.String { + dstField.SetString(inputField.String()) + } + continue + } + getUIBindingValues(srcField, dstField) + } + } +} + +func (c *UIKeybindings) PostLoad() error { + cfg := key.Bindings{} + + // convert UIKeybindings to key.Bindings + err := createKeyBindings(reflect.ValueOf(c).Elem(), reflect.ValueOf(&cfg).Elem()) + if err != nil { + return err + } + + c.Config = cfg + return nil +} + +func createKeyBindings(src, dst reflect.Value) error { + switch dst.Kind() { + case reflect.Struct: + for i := 0; i < dst.NumField(); i++ { + dstField := dst.Field(i) + dstType := dst.Type().Field(i) + + if !dstField.CanSet() { + continue + } + + srcField := src.FieldByName(dstType.Name) + if !srcField.IsValid() { + continue + } + + if dstType.Type.Name() == "Config" { + inputField := dstField.FieldByName("Input") + if inputField.IsValid() && inputField.CanSet() && srcField.Kind() == reflect.String { + inputField.SetString(srcField.String()) + + // call the Setup method if it exists + setupMethod := dstField.Addr().MethodByName("Setup") + if setupMethod.IsValid() { + result := setupMethod.Call([]reflect.Value{}) + if !result[0].IsNil() { + return result[0].Interface().(error) + } + } + } + continue + } + err := createKeyBindings(srcField, dstField) + if err != nil { + return err + } + } + } + + return nil +} +func (c *UIKeybindings) DescribeFields(descriptions clio.FieldDescriptionSet) { + // global keybindings + descriptions.Add(&c.Global.Quit, "quit the application (global)") + descriptions.Add(&c.Global.ToggleView, "toggle between different views (global)") + descriptions.Add(&c.Global.FilterFiles, "filter files by name (global)") + descriptions.Add(&c.Global.CloseFilterFiles, "close file filtering (global)") + + // navigation keybindings + descriptions.Add(&c.Navigation.Up, "move cursor up (global)") + descriptions.Add(&c.Navigation.Down, "move cursor down (global)") + descriptions.Add(&c.Navigation.Left, "move cursor left (global)") + descriptions.Add(&c.Navigation.Right, "move cursor right (global)") + descriptions.Add(&c.Navigation.PageUp, "scroll page up (file view)") + descriptions.Add(&c.Navigation.PageDown, "scroll page down (file view)") + + // layer view keybindings + descriptions.Add(&c.Layer.CompareAll, "compare all layers (layer view)") + descriptions.Add(&c.Layer.CompareLayer, "compare specific layer (layer view)") + + // file view keybindings + descriptions.Add(&c.Filetree.ToggleCollapseDir, "toggle directory collapse (file view)") + descriptions.Add(&c.Filetree.ToggleCollapseAllDir, "toggle collapse all directories (file view)") + descriptions.Add(&c.Filetree.ToggleAddedFiles, "toggle visibility of added files (file view)") + descriptions.Add(&c.Filetree.ToggleRemovedFiles, "toggle visibility of removed files (file view)") + descriptions.Add(&c.Filetree.ToggleModifiedFiles, "toggle visibility of modified files (file view)") + descriptions.Add(&c.Filetree.ToggleUnmodifiedFiles, "toggle visibility of unmodified files (file view)") + descriptions.Add(&c.Filetree.ToggleTreeAttributes, "toggle display of file attributes (file view)") + descriptions.Add(&c.Filetree.ToggleSortOrder, "toggle sort order (file view)") + descriptions.Add(&c.Filetree.ExtractFile, "extract file contents (file view)") +} diff --git a/cmd/dive/cli/internal/options/ui_layers.go b/cmd/dive/cli/internal/options/ui_layers.go new file mode 100644 index 0000000..ddec927 --- /dev/null +++ b/cmd/dive/cli/internal/options/ui_layers.go @@ -0,0 +1,20 @@ +package options + +import "github.com/anchore/clio" + +var _ clio.FieldDescriber = (*UILayers)(nil) + +// UILayers provides configuration for layer display behavior +type UILayers struct { + ShowAggregatedChanges bool `yaml:"show-aggregated-changes" mapstructure:"show-aggregated-changes"` +} + +func DefaultUILayers() UILayers { + return UILayers{ + ShowAggregatedChanges: false, + } +} + +func (c *UILayers) DescribeFields(descriptions clio.FieldDescriptionSet) { + descriptions.Add(&c.ShowAggregatedChanges, "show aggregated changes across all previous layers") +} diff --git a/cmd/dive/cli/internal/ui/no_ui.go b/cmd/dive/cli/internal/ui/no_ui.go new file mode 100644 index 0000000..b25e9e7 --- /dev/null +++ b/cmd/dive/cli/internal/ui/no_ui.go @@ -0,0 +1,30 @@ +package ui + +import ( + "github.com/wagoodman/go-partybus" + + "github.com/anchore/clio" +) + +var _ clio.UI = (*NoUI)(nil) + +type NoUI struct { + subscription partybus.Unsubscribable +} + +func None() *NoUI { + return &NoUI{} +} + +func (n *NoUI) Setup(subscription partybus.Unsubscribable) error { + n.subscription = subscription + return nil +} + +func (n *NoUI) Handle(_ partybus.Event) error { + return nil +} + +func (n NoUI) Teardown(_ bool) error { + return nil +} diff --git a/cmd/dive/cli/internal/ui/v1.go b/cmd/dive/cli/internal/ui/v1.go new file mode 100644 index 0000000..ecefa8e --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1.go @@ -0,0 +1,176 @@ +package ui + +import ( + "context" + "fmt" + "github.com/anchore/clio" + "github.com/anchore/go-logger/adapter/discard" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" + v1 "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/app" + "github.com/wagoodman/dive/internal/bus/event" + "github.com/wagoodman/dive/internal/bus/event/parser" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/go-partybus" + "io" + "os" + "strings" +) + +var _ clio.UI = (*V1UI)(nil) + +type V1UI struct { + cfg v1.Preferences + out io.Writer + err io.Writer + + subscription partybus.Unsubscribable + quiet bool + verbosity int + format format +} + +type format struct { + Title lipgloss.Style + Aux lipgloss.Style + Line lipgloss.Style + Notification lipgloss.Style +} + +func NewV1UI(cfg v1.Preferences, out io.Writer, quiet bool, verbosity int) *V1UI { + return &V1UI{ + cfg: cfg, + out: out, + err: os.Stderr, + quiet: quiet, + verbosity: verbosity, + format: format{ + Title: lipgloss.NewStyle().Bold(true).Width(30), + Aux: lipgloss.NewStyle().Faint(true), + Notification: lipgloss.NewStyle().Foreground(lipgloss.Color("#A77BCA")), + }, + } +} + +func (n *V1UI) Setup(subscription partybus.Unsubscribable) error { + if n.verbosity == 0 || n.quiet { + // we still use the UI, but we want to suppress responding to events that would print out what is already + // being logged. + log.Set(discard.New()) + } + + // remove CI var from consideration when determining if we should use the UI + lipgloss.SetDefaultRenderer(lipgloss.NewRenderer(n.out, termenv.WithEnvironment(environWithoutCI{}))) + + n.subscription = subscription + return nil +} + +var _ termenv.Environ = (*environWithoutCI)(nil) + +type environWithoutCI struct { +} + +func (e environWithoutCI) Environ() []string { + var out []string + for _, s := range os.Environ() { + if strings.HasPrefix(s, "CI=") { + continue + } + out = append(out, s) + } + return out +} + +func (e environWithoutCI) Getenv(s string) string { + if s == "CI" { + return "" + } + return os.Getenv(s) +} + +func (n *V1UI) Handle(e partybus.Event) error { + switch e.Type { + case event.TaskStarted: + if n.quiet { + return nil + } + prog, task, err := parser.ParseTaskStarted(e) + if err != nil { + log.WithFields("error", err, "event", fmt.Sprintf("%#v", e)).Warn("failed to parse event") + } + + var aux string + stage := prog.Stage() + switch { + case task.Context != "": + aux = task.Context + case stage != "": + aux = stage + } + + if aux != "" { + aux = n.format.Aux.Render(aux) + } + + n.writeToStderr(n.format.Title.Render(task.Title.Default) + aux) + case event.Notification: + if n.quiet { + return nil + } + _, text, err := parser.ParseNotification(e) + if err != nil { + log.WithFields("error", err, "event", fmt.Sprintf("%#v", e)).Warn("failed to parse event") + } + + n.writeToStderr(n.format.Notification.Render(text)) + case event.Report: + if n.quiet { + return nil + } + _, text, err := parser.ParseReport(e) + if err != nil { + log.WithFields("error", err, "event", fmt.Sprintf("%#v", e)).Warn("failed to parse event") + } + + n.writeToStderr("") + n.writeToStdout(text) + case event.ExploreAnalysis: + analysis, content, err := parser.ParseExploreAnalysis(e) + if err != nil { + log.WithFields("error", err, "event", fmt.Sprintf("%#v", e)).Warn("failed to parse event") + } + + // ensure the logger will not interfere with the UI + log.Set(discard.New()) + + return app.Run( + // TODO: this is not plumbed through from the command object... + context.Background(), + v1.Config{ + Content: content, + Analysis: analysis, + Preferences: n.cfg, + }, + ) + } + return nil +} + +func (n *V1UI) writeToStdout(s string) { + fmt.Fprintln(n.out, s) +} + +func (n *V1UI) writeToStderr(s string) { + if n.quiet || n.verbosity > 0 { + // we've been told to not report anything or that we're in verbose mode thus the logger should report all info. + // This only applies to status like info on stderr, not to primary reports on stdout. + return + } + fmt.Fprintln(n.err, s) +} + +func (n V1UI) Teardown(_ bool) error { + return nil +} diff --git a/cmd/dive/cli/internal/ui/v1/app/app.go b/cmd/dive/cli/internal/ui/v1/app/app.go new file mode 100644 index 0000000..a996222 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1/app/app.go @@ -0,0 +1,136 @@ +package app + +import ( + "errors" + "github.com/awesome-gocui/gocui" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/layout" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/layout/compound" + "golang.org/x/net/context" + "time" +) + +const debug = false + +type app struct { + gui *gocui.Gui + controller *controller + layout *layout.Manager +} + +// Run is the UI entrypoint. +func Run(ctx context.Context, c v1.Config) error { + var err error + + // it appears there is a race condition where termbox.Init() will + // block nearly indefinitely when running as the first process in + // a Docker container when started within ~25ms of container startup. + // I can't seem to determine the exact root cause, however, a large + // enough sleep will prevent this behavior (todo: remove this hack) + time.Sleep(100 * time.Millisecond) + + g, err := gocui.NewGui(gocui.OutputNormal, true) + if err != nil { + return err + } + defer g.Close() + + _, err = newApp(ctx, g, c) + if err != nil { + return err + } + + k, mod := gocui.MustParse("Ctrl+Z") + if err := g.SetKeybinding("", k, mod, handle_ctrl_z); err != nil { + return err + } + + if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) { + return err + } + return nil +} + +func newApp(ctx context.Context, gui *gocui.Gui, cfg v1.Config) (*app, error) { + var err error + var c *controller + var globalHelpKeys []*key.Binding + + c, err = newController(ctx, gui, cfg) + if err != nil { + return nil, err + } + + // note: order matters when adding elements to the layout + lm := layout.NewManager() + lm.Add(c.views.Status, layout.LocationFooter) + lm.Add(c.views.Filter, layout.LocationFooter) + lm.Add(compound.NewLayerDetailsCompoundLayout(c.views.Layer, c.views.LayerDetails, c.views.ImageDetails), layout.LocationColumn) + lm.Add(c.views.Tree, layout.LocationColumn) + + // todo: access this more programmatically + if debug { + lm.Add(c.views.Debug, layout.LocationColumn) + } + gui.Cursor = false + // g.Mouse = true + gui.SetManagerFunc(lm.Layout) + + a := &app{ + gui: gui, + controller: c, + layout: lm, + } + + var infos = []key.BindingInfo{ + { + Config: cfg.Preferences.KeyBindings.Global.Quit, + OnAction: a.quit, + Display: "Quit", + }, + { + Config: cfg.Preferences.KeyBindings.Global.ToggleView, + OnAction: c.ToggleView, + Display: "Switch view", + }, + { + Config: cfg.Preferences.KeyBindings.Navigation.Right, + OnAction: c.NextPane, + }, + { + Config: cfg.Preferences.KeyBindings.Navigation.Left, + OnAction: c.PrevPane, + }, + { + Config: cfg.Preferences.KeyBindings.Global.FilterFiles, + OnAction: c.ToggleFilterView, + IsSelected: c.views.Filter.IsVisible, + Display: "Filter", + }, + { + Config: cfg.Preferences.KeyBindings.Global.CloseFilterFiles, + OnAction: c.CloseFilterView, + }, + } + + globalHelpKeys, err = key.GenerateBindings(gui, "", infos) + if err != nil { + return nil, err + } + + c.views.Status.AddHelpKeys(globalHelpKeys...) + + // perform the first update and render now that all resources have been loaded + err = c.UpdateAndRender() + if err != nil { + return nil, err + } + + return a, err +} + +// quit is the gocui callback invoked when the user hits Ctrl+C +func (a *app) quit() error { + return gocui.ErrQuit +} diff --git a/runtime/ui/controller.go b/cmd/dive/cli/internal/ui/v1/app/controller.go similarity index 61% rename from runtime/ui/controller.go rename to cmd/dive/cli/internal/ui/v1/app/controller.go index fd40398..eb2afbc 100644 --- a/runtime/ui/controller.go +++ b/cmd/dive/cli/internal/ui/v1/app/controller.go @@ -1,52 +1,51 @@ -package ui +package app import ( + "fmt" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/view" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" + "golang.org/x/net/context" "regexp" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/view" - "github.com/wagoodman/dive/runtime/ui/viewmodel" ) -type Controller struct { - gui *gocui.Gui - views *view.Views - resolver image.Resolver - imageName string +type controller struct { + gui *gocui.Gui + views *view.Views + config v1.Config + ctx context.Context // TODO: storing context in the controller is not ideal } -func NewCollection(g *gocui.Gui, imageName string, resolver image.Resolver, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) { - views, err := view.NewViews(g, imageName, analysis, cache) +func newController(ctx context.Context, g *gocui.Gui, cfg v1.Config) (*controller, error) { + views, err := view.NewViews(g, cfg) if err != nil { return nil, err } - controller := &Controller{ - gui: g, - views: views, - resolver: resolver, - imageName: imageName, + c := &controller{ + gui: g, + views: views, + config: cfg, + ctx: ctx, } // layer view cursor down event should trigger an update in the file tree - controller.views.Layer.AddLayerChangeListener(controller.onLayerChange) + c.views.Layer.AddLayerChangeListener(c.onLayerChange) // update the status pane when a filetree option is changed by the user - controller.views.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange) + c.views.Tree.AddViewOptionChangeListener(c.onFileTreeViewOptionChange) // update the status pane when a filetree option is changed by the user - controller.views.Tree.AddViewExtractListener(controller.onFileTreeViewExtract) + c.views.Tree.AddViewExtractListener(c.onFileTreeViewExtract) // update the tree view while the user types into the filter view - controller.views.Filter.AddFilterEditListener(controller.onFilterEdit) + c.views.Filter.AddFilterEditListener(c.onFilterEdit) // propagate initial conditions to necessary views - err = controller.onLayerChange(viewmodel.LayerSelection{ - Layer: controller.views.Layer.CurrentLayer(), + err = c.onLayerChange(viewmodel.LayerSelection{ + Layer: c.views.Layer.CurrentLayer(), BottomTreeStart: 0, BottomTreeStop: 0, TopTreeStart: 0, @@ -57,14 +56,14 @@ func NewCollection(g *gocui.Gui, imageName string, resolver image.Resolver, anal return nil, err } - return controller, nil + return c, nil } -func (c *Controller) onFileTreeViewExtract(p string) error { - return c.resolver.Extract(c.imageName, c.views.LayerDetails.CurrentLayer.Id, p) +func (c *controller) onFileTreeViewExtract(p string) error { + return c.config.Content.Extract(c.ctx, c.config.Analysis.Image, c.views.LayerDetails.CurrentLayer.Id, p) } -func (c *Controller) onFileTreeViewOptionChange() error { +func (c *controller) onFileTreeViewOptionChange() error { err := c.views.Status.Update() if err != nil { return err @@ -72,7 +71,7 @@ func (c *Controller) onFileTreeViewOptionChange() error { return c.views.Status.Render() } -func (c *Controller) onFilterEdit(filter string) error { +func (c *controller) onFilterEdit(filter string) error { var filterRegex *regexp.Regexp var err error @@ -93,7 +92,7 @@ func (c *Controller) onFilterEdit(filter string) error { return c.views.Tree.Render() } -func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error { +func (c *controller) onLayerChange(selection viewmodel.LayerSelection) error { // update the details c.views.LayerDetails.CurrentLayer = selection.Layer @@ -113,39 +112,36 @@ func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error { return c.UpdateAndRender() } -func (c *Controller) UpdateAndRender() error { +func (c *controller) UpdateAndRender() error { err := c.Update() if err != nil { - logrus.Debug("failed update: ", err) - return err + return fmt.Errorf("controller failed update: %w", err) } err = c.Render() if err != nil { - logrus.Debug("failed render: ", err) - return err + return fmt.Errorf("controller failed render: %w", err) } return nil } // Update refreshes the state objects for future rendering. -func (c *Controller) Update() error { - for _, controller := range c.views.All() { - err := controller.Update() +func (c *controller) Update() error { + for _, v := range c.views.Renderers() { + err := v.Update() if err != nil { - logrus.Debug("unable to update controller: ") - return err + return fmt.Errorf("controller unable to update view: %w", err) } } return nil } // Render flushes the state objects to the screen. -func (c *Controller) Render() error { - for _, controller := range c.views.All() { - if controller.IsVisible() { - err := controller.Render() +func (c *controller) Render() error { + for _, v := range c.views.Renderers() { + if v.IsVisible() { + err := v.Render() if err != nil { return err } @@ -155,10 +151,10 @@ func (c *Controller) Render() error { } //nolint:dupl -func (c *Controller) NextPane() (err error) { +func (c *controller) NextPane() (err error) { v := c.gui.CurrentView() if v == nil { - panic("Current view is nil") + panic("CurrentView is nil") } if v.Name() == c.views.Layer.Name() { _, err = c.gui.SetCurrentView(c.views.LayerDetails.Name()) @@ -172,15 +168,14 @@ func (c *Controller) NextPane() (err error) { } if err != nil { - logrus.Error("unable to toggle view: ", err) - return err + return fmt.Errorf("controller unable to switch to next pane: %w", err) } return c.UpdateAndRender() } //nolint:dupl -func (c *Controller) PrevPane() (err error) { +func (c *controller) PrevPane() (err error) { v := c.gui.CurrentView() if v == nil { panic("Current view is nil") @@ -197,15 +192,14 @@ func (c *Controller) PrevPane() (err error) { } if err != nil { - logrus.Error("unable to toggle view: ", err) - return err + return fmt.Errorf("controller unable to switch to previous pane: %w", err) } return c.UpdateAndRender() } // ToggleView switches between the file view and the layer view and re-renders the screen. -func (c *Controller) ToggleView() (err error) { +func (c *controller) ToggleView() (err error) { v := c.gui.CurrentView() if v == nil || v.Name() == c.views.Layer.Name() { _, err = c.gui.SetCurrentView(c.views.Tree.Name()) @@ -216,14 +210,13 @@ func (c *Controller) ToggleView() (err error) { } if err != nil { - logrus.Error("unable to toggle view: ", err) - return err + return fmt.Errorf("controller unable to toggle view: %w", err) } return c.UpdateAndRender() } -func (c *Controller) CloseFilterView() error { +func (c *controller) CloseFilterView() error { // filter view needs to be visible if c.views.Filter.IsVisible() { // toggle filter view @@ -232,12 +225,11 @@ func (c *Controller) CloseFilterView() error { return nil } -func (c *Controller) ToggleFilterView() error { +func (c *controller) ToggleFilterView() error { // delete all user input from the tree view err := c.views.Filter.ToggleVisible() if err != nil { - logrus.Error("unable to toggle filter visibility: ", err) - return err + return fmt.Errorf("unable to toggle filter visibility: %w", err) } // we have just hidden the filter view... @@ -248,8 +240,7 @@ func (c *Controller) ToggleFilterView() error { // ...adjust focus to a valid (visible) view err = c.ToggleView() if err != nil { - logrus.Error("unable to toggle filter view (back): ", err) - return err + return fmt.Errorf("unable to toggle filter view (back): %w", err) } } diff --git a/runtime/ui/job_control_other.go b/cmd/dive/cli/internal/ui/v1/app/job_control_other.go similarity index 94% rename from runtime/ui/job_control_other.go rename to cmd/dive/cli/internal/ui/v1/app/job_control_other.go index 420ad1e..9550954 100644 --- a/runtime/ui/job_control_other.go +++ b/cmd/dive/cli/internal/ui/v1/app/job_control_other.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package ui +package app import ( "github.com/awesome-gocui/gocui" diff --git a/runtime/ui/job_control_unix.go b/cmd/dive/cli/internal/ui/v1/app/job_control_unix.go similarity index 96% rename from runtime/ui/job_control_unix.go rename to cmd/dive/cli/internal/ui/v1/app/job_control_unix.go index f3953c8..a082a2c 100644 --- a/runtime/ui/job_control_unix.go +++ b/cmd/dive/cli/internal/ui/v1/app/job_control_unix.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package ui +package app import ( "syscall" diff --git a/cmd/dive/cli/internal/ui/v1/config.go b/cmd/dive/cli/internal/ui/v1/config.go new file mode 100644 index 0000000..87db171 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1/config.go @@ -0,0 +1,67 @@ +package v1 + +import ( + "errors" + "fmt" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/dive/image" + "golang.org/x/net/context" + "sync" +) + +type Config struct { + // required input + Analysis image.Analysis + Content ContentReader + Preferences Preferences + + stack filetree.Comparer + stackErrs error + do *sync.Once +} + +type Preferences struct { + KeyBindings key.Bindings + IgnoreErrors bool + ShowFiletreeAttributes bool + ShowAggregatedLayerChanges bool + CollapseFiletreeDirectory bool + FiletreePaneWidth float64 + FiletreeDiffHide []string +} + +func DefaultPreferences() Preferences { + return Preferences{ + KeyBindings: key.DefaultBindings(), + ShowFiletreeAttributes: true, + ShowAggregatedLayerChanges: true, + CollapseFiletreeDirectory: false, // don't start with collapsed directories + FiletreePaneWidth: 0.5, + FiletreeDiffHide: []string{}, // empty slice means show all + } +} + +func (c *Config) TreeComparer() (filetree.Comparer, error) { + if c.do == nil { + c.do = &sync.Once{} + } + c.do.Do(func() { + treeStack := filetree.NewComparer(c.Analysis.RefTrees) + errs := treeStack.BuildCache() + if errs != nil { + if !c.Preferences.IgnoreErrors { + errs = append(errs, fmt.Errorf("file tree has path errors (use '--ignore-errors' to attempt to continue)")) + c.stackErrs = errors.Join(errs...) + return + } + } + c.stack = treeStack + }) + + return c.stack, c.stackErrs +} + +type ContentReader interface { + Extract(ctx context.Context, id string, layer string, path string) error +} diff --git a/runtime/ui/format/format.go b/cmd/dive/cli/internal/ui/v1/format/format.go similarity index 100% rename from runtime/ui/format/format.go rename to cmd/dive/cli/internal/ui/v1/format/format.go diff --git a/runtime/ui/key/binding.go b/cmd/dive/cli/internal/ui/v1/key/binding.go similarity index 54% rename from runtime/ui/key/binding.go rename to cmd/dive/cli/internal/ui/v1/key/binding.go index ff7c1a7..8dd18d2 100644 --- a/runtime/ui/key/binding.go +++ b/cmd/dive/cli/internal/ui/v1/key/binding.go @@ -2,19 +2,15 @@ package key import ( "fmt" - "github.com/awesome-gocui/gocui" "github.com/awesome-gocui/keybinding" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" ) type BindingInfo struct { Key gocui.Key Modifier gocui.Modifier - ConfigKeys []string + Config Config OnAction func() error IsSelected func() bool Display string @@ -30,15 +26,12 @@ type Binding struct { func GenerateBindings(gui *gocui.Gui, influence string, infos []BindingInfo) ([]*Binding, error) { var result = make([]*Binding, 0) for _, info := range infos { - var err error - var binding *Binding - - if len(info.ConfigKeys) > 0 { - binding, err = NewBindingFromConfig(gui, influence, info.ConfigKeys, info.Display, info.OnAction) - } else { - binding, err = NewBinding(gui, influence, info.Key, info.Modifier, info.Display, info.OnAction) + if len(info.Config.Keys) == 0 { + return nil, fmt.Errorf("no keybinding configured for '%s'", info.Display) } + binding, err := newBinding(gui, influence, info.Config.Keys, info.Display, info.OnAction) + if err != nil { return nil, err } @@ -53,37 +46,6 @@ func GenerateBindings(gui *gocui.Gui, influence string, infos []BindingInfo) ([] return result, nil } -func NewBinding(gui *gocui.Gui, influence string, key gocui.Key, mod gocui.Modifier, displayName string, actionFn func() error) (*Binding, error) { - return newBinding(gui, influence, []keybinding.Key{{Value: key, Modifier: mod}}, displayName, actionFn) -} - -func NewBindingFromConfig(gui *gocui.Gui, influence string, configKeys []string, displayName string, actionFn func() error) (*Binding, error) { - var parsedKeys []keybinding.Key - for _, configKey := range configKeys { - bindStr := viper.GetString(configKey) - if bindStr == "" { - logrus.Debugf("skipping keybinding '%s' (no value given)", configKey) - continue - } - logrus.Debugf("parsing keybinding '%s' --> '%s'", configKey, bindStr) - - keys, err := keybinding.ParseAll(bindStr) - if err != nil { - return nil, err - } - if len(keys) > 0 { - parsedKeys = keys - break - } - } - - if parsedKeys == nil { - return nil, fmt.Errorf("could not find configured keybindings for: %+v", configKeys) - } - - return newBinding(gui, influence, parsedKeys, displayName, actionFn) -} - func newBinding(gui *gocui.Gui, influence string, keys []keybinding.Key, displayName string, actionFn func() error) (*Binding, error) { binding := &Binding{ key: keys, diff --git a/cmd/dive/cli/internal/ui/v1/key/config.go b/cmd/dive/cli/internal/ui/v1/key/config.go new file mode 100644 index 0000000..ec7ca46 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1/key/config.go @@ -0,0 +1,100 @@ +package key + +import ( + "fmt" + "github.com/awesome-gocui/keybinding" +) + +type Config struct { + Input string + Keys []keybinding.Key `yaml:"-" mapstructure:"-"` +} + +func (c *Config) Setup() error { + if len(c.Input) == 0 { + return nil + } + + parsed, err := keybinding.ParseAll(c.Input) + if err != nil { + return fmt.Errorf("failed to parse key %q: %w", c.Input, err) + } + c.Keys = parsed + return nil +} + +type Bindings struct { + Global GlobalBindings `yaml:",inline" mapstructure:",squash"` + Navigation NavigationBindings `yaml:",inline" mapstructure:",squash"` + Layer LayerBindings `yaml:",inline" mapstructure:",squash"` + Filetree FiletreeBindings `yaml:",inline" mapstructure:",squash"` +} + +type GlobalBindings struct { + Quit Config `yaml:"quit" mapstructure:"quit"` + ToggleView Config `yaml:"toggle-view" mapstructure:"toggle-view"` + FilterFiles Config `yaml:"filter-files" mapstructure:"filter-files"` + CloseFilterFiles Config `yaml:"close-filter-files" mapstructure:"close-filter-files"` +} + +type NavigationBindings struct { + Up Config `yaml:"up" mapstructure:"up"` + Down Config `yaml:"down" mapstructure:"down"` + Left Config `yaml:"left" mapstructure:"left"` + Right Config `yaml:"right" mapstructure:"right"` + PageUp Config `yaml:"page-up" mapstructure:"page-up"` + PageDown Config `yaml:"page-down" mapstructure:"page-down"` +} + +type LayerBindings struct { + CompareAll Config `yaml:"compare-all" mapstructure:"compare-all"` + CompareLayer Config `yaml:"compare-layer" mapstructure:"compare-layer"` +} + +type FiletreeBindings struct { + ToggleCollapseDir Config `yaml:"toggle-collapse-dir" mapstructure:"toggle-collapse-dir"` + ToggleCollapseAllDir Config `yaml:"toggle-collapse-all-dir" mapstructure:"toggle-collapse-all-dir"` + ToggleAddedFiles Config `yaml:"toggle-added-files" mapstructure:"toggle-added-files"` + ToggleRemovedFiles Config `yaml:"toggle-removed-files" mapstructure:"toggle-removed-files"` + ToggleModifiedFiles Config `yaml:"toggle-modified-files" mapstructure:"toggle-modified-files"` + ToggleUnmodifiedFiles Config `yaml:"toggle-unmodified-files" mapstructure:"toggle-unmodified-files"` + ToggleTreeAttributes Config `yaml:"toggle-filetree-attributes" mapstructure:"toggle-filetree-attributes"` + ToggleSortOrder Config `yaml:"toggle-sort-order" mapstructure:"toggle-sort-order"` + ToggleWrapTree Config `yaml:"toggle-wrap-tree" mapstructure:"toggle-wrap-tree"` + ExtractFile Config `yaml:"extract-file" mapstructure:"extract-file"` +} + +func DefaultBindings() Bindings { + return Bindings{ + Global: GlobalBindings{ + Quit: Config{Input: "ctrl+c"}, + ToggleView: Config{Input: "tab"}, + FilterFiles: Config{Input: "ctrl+f, ctrl+slash"}, + CloseFilterFiles: Config{Input: "esc"}, + }, + Navigation: NavigationBindings{ + Up: Config{Input: "up,k"}, + Down: Config{Input: "down,j"}, + Left: Config{Input: "left,h"}, + Right: Config{Input: "right,l"}, + PageUp: Config{Input: "pgup,u"}, + PageDown: Config{Input: "pgdn,d"}, + }, + Layer: LayerBindings{ + CompareAll: Config{Input: "ctrl+a"}, + CompareLayer: Config{Input: "ctrl+l"}, + }, + Filetree: FiletreeBindings{ + ToggleCollapseDir: Config{Input: "space"}, + ToggleCollapseAllDir: Config{Input: "ctrl+space"}, + ToggleAddedFiles: Config{Input: "ctrl+a"}, + ToggleRemovedFiles: Config{Input: "ctrl+r"}, + ToggleModifiedFiles: Config{Input: "ctrl+m"}, + ToggleUnmodifiedFiles: Config{Input: "ctrl+u"}, + ToggleTreeAttributes: Config{Input: "ctrl+b"}, + ToggleWrapTree: Config{Input: "ctrl+p"}, + ToggleSortOrder: Config{Input: "ctrl+o"}, + ExtractFile: Config{Input: "ctrl+e"}, + }, + } +} diff --git a/runtime/ui/layout/area.go b/cmd/dive/cli/internal/ui/v1/layout/area.go similarity index 100% rename from runtime/ui/layout/area.go rename to cmd/dive/cli/internal/ui/v1/layout/area.go diff --git a/runtime/ui/layout/compound/layer_details_column.go b/cmd/dive/cli/internal/ui/v1/layout/compound/layer_details_column.go similarity index 74% rename from runtime/ui/layout/compound/layer_details_column.go rename to cmd/dive/cli/internal/ui/v1/layout/compound/layer_details_column.go index 92363fb..3f297a4 100644 --- a/runtime/ui/layout/compound/layer_details_column.go +++ b/cmd/dive/cli/internal/ui/v1/layout/compound/layer_details_column.go @@ -1,11 +1,11 @@ package compound import ( + "fmt" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/runtime/ui/view" - "github.com/wagoodman/dive/utils" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/view" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" ) type LayerDetailsCompoundLayout struct { @@ -31,26 +31,23 @@ func (cl *LayerDetailsCompoundLayout) Name() string { func (cl *LayerDetailsCompoundLayout) OnLayoutChange() error { err := cl.layer.OnLayoutChange() if err != nil { - logrus.Error("unable to setup layer controller onLayoutChange", err) - return err + return fmt.Errorf("unable to setup layer controller onLayoutChange: %w", err) } err = cl.layerDetails.OnLayoutChange() if err != nil { - logrus.Error("unable to setup layer details controller onLayoutChange", err) - return err + return fmt.Errorf("unable to setup layer details controller onLayoutChange: %w", err) } err = cl.imageDetails.OnLayoutChange() if err != nil { - logrus.Error("unable to setup image details controller onLayoutChange", err) - return err + return fmt.Errorf("unable to setup image details controller onLayoutChange: %w", err) } return nil } func (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX, maxY int, viewName string, setup func(*gocui.View, *gocui.View) error) error { - logrus.Tracef("layoutRow(g, minX: %d, minY: %d, maxX: %d, maxY: %d, viewName: %s, )", minX, minY, maxX, maxY, viewName) + log.WithFields("ui", cl.Name()).Tracef("layoutRow(g, minX: %d, minY: %d, maxX: %d, maxY: %d, viewName: %s, )", minX, minY, maxX, maxY, viewName) // header + border headerHeight := 2 @@ -64,17 +61,16 @@ func (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX, if utils.IsNewView(bodyErr, headerErr) { err := setup(bodyView, headerView) if err != nil { - logrus.Debug("unable to setup row layout for ", viewName, err) - return err + return fmt.Errorf("unable to setup row layout for %s: %w", viewName, err) } } return nil } func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("LayerDetailsCompoundLayout.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name()) + log.WithFields("ui", cl.Name()).Tracef("layout(minX: %d, minY: %d, maxX: %d, maxY: %d)", minX, minY, maxX, maxY) - layouts := []view.IView{ + layouts := []view.View{ cl.layer, cl.layerDetails, cl.imageDetails, @@ -83,15 +79,13 @@ func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, max rowHeight := maxY / 3 for i := 0; i < 3; i++ { if err := cl.layoutRow(g, minX, i*rowHeight, maxX, (i+1)*rowHeight, layouts[i].Name(), layouts[i].Setup); err != nil { - logrus.Debug("Laying out layers view errored!") - return err + return fmt.Errorf("unable to layout %q: %w", layouts[i].Name(), err) } } if g.CurrentView() == nil { if _, err := g.SetCurrentView(cl.layer.Name()); err != nil { - logrus.Error("unable to set view to layer", err) - return err + return fmt.Errorf("unable to set view to layer %q: %w", cl.layer.Name(), err) } } return nil diff --git a/runtime/ui/layout/layout.go b/cmd/dive/cli/internal/ui/v1/layout/layout.go similarity index 100% rename from runtime/ui/layout/layout.go rename to cmd/dive/cli/internal/ui/v1/layout/layout.go diff --git a/runtime/ui/layout/location.go b/cmd/dive/cli/internal/ui/v1/layout/location.go similarity index 100% rename from runtime/ui/layout/location.go rename to cmd/dive/cli/internal/ui/v1/layout/location.go diff --git a/runtime/ui/layout/manager.go b/cmd/dive/cli/internal/ui/v1/layout/manager.go similarity index 96% rename from runtime/ui/layout/manager.go rename to cmd/dive/cli/internal/ui/v1/layout/manager.go index 91ce7c6..cd8f1eb 100644 --- a/runtime/ui/layout/manager.go +++ b/cmd/dive/cli/internal/ui/v1/layout/manager.go @@ -1,8 +1,9 @@ package layout import ( + "fmt" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/internal/log" ) type Constraint func(int) int @@ -45,7 +46,7 @@ func (lm *Manager) planAndLayoutHeaders(g *gocui.Gui, area Area) (Area, error) { // layout the header within the allocated space err := element.Layout(g, area.minX, area.minY, area.maxX, area.minY+height) if err != nil { - logrus.Errorf("failed to layout '%s' header: %+v", element.Name(), err) + log.WithFields("element", element.Name(), "error", err).Error("failed to layout header") return area, err } @@ -134,8 +135,7 @@ func (lm *Manager) planAndLayoutColumns(g *gocui.Gui, area Area) (Area, error) { // layout the column within the allocated space err := element.Layout(g, area.minX, area.minY, area.minX+width, area.maxY) if err != nil { - logrus.Errorf("failed to layout '%s' column: %+v", element.Name(), err) - return area, err + return area, fmt.Errorf("failed to layout '%s' column: %w", element.Name(), err) } // move left to right, scratching off real estate as it is taken @@ -164,8 +164,7 @@ func (lm *Manager) layoutFooters(g *gocui.Gui, area Area, footerHeights []int) e // do the same vertically, thus a -1 is needed for a starting Y err := element.Layout(g, area.minX, topY, area.maxX, bottomY) if err != nil { - logrus.Errorf("failed to layout '%s' footer: %+v", element.Name(), err) - return err + return fmt.Errorf("failed to layout %q footer: %w", element.Name(), err) } } } diff --git a/runtime/ui/layout/manager_test.go b/cmd/dive/cli/internal/ui/v1/layout/manager_test.go similarity index 100% rename from runtime/ui/layout/manager_test.go rename to cmd/dive/cli/internal/ui/v1/layout/manager_test.go diff --git a/runtime/ui/view/cursor.go b/cmd/dive/cli/internal/ui/v1/view/cursor.go similarity index 100% rename from runtime/ui/view/cursor.go rename to cmd/dive/cli/internal/ui/v1/view/cursor.go diff --git a/runtime/ui/view/debug.go b/cmd/dive/cli/internal/ui/v1/view/debug.go similarity index 79% rename from runtime/ui/view/debug.go rename to cmd/dive/cli/internal/ui/v1/view/debug.go index 3478631..24b8557 100644 --- a/runtime/ui/view/debug.go +++ b/cmd/dive/cli/internal/ui/v1/view/debug.go @@ -2,12 +2,11 @@ package view import ( "fmt" - + "github.com/anchore/go-logger" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/utils" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" ) // Debug is just for me :) @@ -16,19 +15,21 @@ type Debug struct { gui *gocui.Gui view *gocui.View header *gocui.View + logger logger.Logger selectedView Helper } // newDebugView creates a new view object attached the global [gocui] screen object. -func newDebugView(gui *gocui.Gui) (controller *Debug) { - controller = new(Debug) +func newDebugView(gui *gocui.Gui) *Debug { + c := new(Debug) // populate main fields - controller.name = "debug" - controller.gui = gui + c.name = "debug" + c.gui = gui + c.logger = log.Nested("ui", "debug") - return controller + return c } func (v *Debug) SetCurrentView(r Helper) { @@ -41,7 +42,7 @@ func (v *Debug) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Debug) Setup(view *gocui.View, header *gocui.View) error { - logrus.Tracef("view.Setup() %s", v.Name()) + v.logger.Trace("setup()") // set controller options v.view = view @@ -78,7 +79,7 @@ func (v *Debug) OnLayoutChange() error { // Render flushes the state objects to the screen. func (v *Debug) Render() error { - logrus.Tracef("view.Render() %s", v.Name()) + v.logger.Trace("render()") v.gui.Update(func(g *gocui.Gui) error { // update header... @@ -91,7 +92,7 @@ func (v *Debug) Render() error { v.view.Clear() _, err := fmt.Fprintln(v.view, "blerg") if err != nil { - logrus.Debug("unable to write to buffer: ", err) + v.logger.WithFields("error", err).Debug("unable to write to buffer") } return nil @@ -100,7 +101,7 @@ func (v *Debug) Render() error { } func (v *Debug) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + v.logger.Tracef("layout(minX: %d, minY: %d, maxX: %d, maxY: %d)", minX, minY, maxX, maxY) // header headerSize := 1 @@ -112,8 +113,7 @@ func (v *Debug) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { if utils.IsNewView(viewErr, headerErr) { err := v.Setup(view, header) if err != nil { - logrus.Error("unable to setup debug controller", err) - return err + return fmt.Errorf("unable to setup debug controller: %w", err) } } return nil diff --git a/runtime/ui/view/filetree.go b/cmd/dive/cli/internal/ui/v1/view/filetree.go similarity index 79% rename from runtime/ui/view/filetree.go rename to cmd/dive/cli/internal/ui/v1/view/filetree.go index 923f7a9..f106cb1 100644 --- a/runtime/ui/view/filetree.go +++ b/cmd/dive/cli/internal/ui/v1/view/filetree.go @@ -2,24 +2,24 @@ package view import ( "fmt" + "github.com/anchore/go-logger" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" "regexp" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" - "github.com/wagoodman/dive/runtime/ui/viewmodel" - "github.com/wagoodman/dive/utils" ) type ViewOptionChangeListener func() error type ViewExtractListener func(string) error -// FileTree holds the UI objects and data models for populating the right pane. Specifically the pane that +// FileTree holds the UI objects and data models for populating the right pane. Specifically, the pane that // shows selected layer or aggregate file ASCII tree. type FileTree struct { name string @@ -28,6 +28,8 @@ type FileTree struct { header *gocui.View vm *viewmodel.FileTreeViewModel title string + kb key.Bindings + logger logger.Logger filterRegex *regexp.Regexp listeners []ViewOptionChangeListener @@ -37,26 +39,29 @@ type FileTree struct { } // newFileTreeView creates a new view object attached the global [gocui] screen object. -func newFileTreeView(gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (controller *FileTree, err error) { - controller = new(FileTree) - controller.listeners = make([]ViewOptionChangeListener, 0) +func newFileTreeView(gui *gocui.Gui, cfg v1.Config, initial int) (v *FileTree, err error) { + v = new(FileTree) + v.logger = log.Nested("ui", "filetree") + v.listeners = make([]ViewOptionChangeListener, 0) // populate main fields - controller.name = "filetree" - controller.gui = gui - controller.vm, err = viewmodel.NewFileTreeViewModel(tree, refTrees, cache) + v.name = "filetree" + v.gui = gui + v.kb = cfg.Preferences.KeyBindings + v.vm, err = viewmodel.NewFileTreeViewModel(cfg, initial) if err != nil { return nil, err } - requestedWidthRatio := viper.GetFloat64("filetree.pane-width") + requestedWidthRatio := cfg.Preferences.FiletreePaneWidth if requestedWidthRatio >= 1 || requestedWidthRatio <= 0 { - logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", requestedWidthRatio) + v.logger.Warnf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", requestedWidthRatio) + requestedWidthRatio = 0.5 } - controller.requestedWidthRatio = requestedWidthRatio + v.requestedWidthRatio = requestedWidthRatio - return controller, err + return v, err } func (v *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) { @@ -81,7 +86,7 @@ func (v *FileTree) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *FileTree) Setup(view, header *gocui.View) error { - logrus.Tracef("view.Setup() %s", v.Name()) + log.Trace("setup()") // set controller options v.view = view @@ -96,88 +101,88 @@ func (v *FileTree) Setup(view, header *gocui.View) error { var infos = []key.BindingInfo{ { - ConfigKeys: []string{"keybinding.toggle-collapse-dir"}, - OnAction: v.toggleCollapse, - Display: "Collapse dir", + Config: v.kb.Filetree.ToggleCollapseDir, + OnAction: v.toggleCollapse, + Display: "Collapse dir", }, { - ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"}, - OnAction: v.toggleCollapseAll, - Display: "Collapse all dir", + Config: v.kb.Filetree.ToggleCollapseAllDir, + OnAction: v.toggleCollapseAll, + Display: "Collapse all dir", }, { - ConfigKeys: []string{"keybinding.toggle-sort-order"}, - OnAction: v.toggleSortOrder, - Display: "Toggle sort order", + Config: v.kb.Filetree.ToggleSortOrder, + OnAction: v.toggleSortOrder, + Display: "Toggle sort order", }, { - ConfigKeys: []string{"keybinding.extract-file"}, - OnAction: v.extractFile, - Display: "Extract File", + Config: v.kb.Filetree.ExtractFile, + OnAction: v.extractFile, + Display: "Extract File", }, { - ConfigKeys: []string{"keybinding.toggle-added-files"}, + Config: v.kb.Filetree.ToggleAddedFiles, OnAction: func() error { return v.toggleShowDiffType(filetree.Added) }, IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Added] }, Display: "Added", }, { - ConfigKeys: []string{"keybinding.toggle-removed-files"}, + Config: v.kb.Filetree.ToggleRemovedFiles, OnAction: func() error { return v.toggleShowDiffType(filetree.Removed) }, IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Removed] }, Display: "Removed", }, { - ConfigKeys: []string{"keybinding.toggle-modified-files"}, + Config: v.kb.Filetree.ToggleModifiedFiles, OnAction: func() error { return v.toggleShowDiffType(filetree.Modified) }, IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Modified] }, Display: "Modified", }, { - ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"}, + Config: v.kb.Filetree.ToggleUnmodifiedFiles, OnAction: func() error { return v.toggleShowDiffType(filetree.Unmodified) }, IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Unmodified] }, Display: "Unmodified", }, { - ConfigKeys: []string{"keybinding.toggle-filetree-attributes"}, + Config: v.kb.Filetree.ToggleTreeAttributes, OnAction: v.toggleAttributes, IsSelected: func() bool { return v.vm.ShowAttributes }, Display: "Attributes", }, { - ConfigKeys: []string{"keybinding.toggle-wrap-tree"}, + Config: v.kb.Filetree.ToggleWrapTree, OnAction: v.toggleWrapTree, IsSelected: func() bool { return v.view.Wrap }, Display: "Wrap", }, { - ConfigKeys: []string{"keybinding.page-up"}, - OnAction: v.PageUp, + Config: v.kb.Navigation.PageUp, + OnAction: v.PageUp, }, { - ConfigKeys: []string{"keybinding.page-down"}, - OnAction: v.PageDown, + Config: v.kb.Navigation.PageDown, + OnAction: v.PageDown, }, { - ConfigKeys: []string{"keybinding.down"}, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, + Config: v.kb.Navigation.Down, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, }, { - ConfigKeys: []string{"keybinding.up"}, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, + Config: v.kb.Navigation.Up, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, }, { - ConfigKeys: []string{"keybinding.left"}, - Modifier: gocui.ModNone, - OnAction: v.CursorLeft, + Config: v.kb.Navigation.Left, + Modifier: gocui.ModNone, + OnAction: v.CursorLeft, }, { - ConfigKeys: []string{"keybinding.right"}, - Modifier: gocui.ModNone, - OnAction: v.CursorRight, + Config: v.kb.Navigation.Right, + Modifier: gocui.ModNone, + OnAction: v.CursorRight, }, } @@ -347,8 +352,7 @@ func (v *FileTree) notifyOnViewOptionChangeListeners() error { for _, listener := range v.listeners { err := listener() if err != nil { - logrus.Errorf("notifyOnViewOptionChangeListeners error: %+v", err) - return err + return fmt.Errorf("notifyOnViewOptionChangeListeners error: %w", err) } } return nil @@ -416,7 +420,7 @@ func (v *FileTree) Update() error { // Render flushes the state objects (file tree) to the pane. func (v *FileTree) Render() error { - logrus.Tracef("view.Render() %s", v.Name()) + v.logger.Trace("render()") title := v.title isSelected := v.gui.CurrentView() == v.view @@ -454,7 +458,7 @@ func (v *FileTree) KeyHelp() string { } func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + v.logger.Tracef("layout(minX: %d, minY: %d, maxX: %d, maxY: %d)", minX, minY, maxX, maxY) attributeRowSize := 0 // make the layout responsive to the available realestate. Make more room for the main content by hiding auxiliary @@ -479,8 +483,7 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { if utils.IsNewView(viewErr, headerErr) { err := v.Setup(view, header) if err != nil { - logrus.Error("unable to setup tree controller", err) - return err + return fmt.Errorf("unable to setup tree controller: %w", err) } } return nil diff --git a/runtime/ui/view/filter.go b/cmd/dive/cli/internal/ui/v1/view/filter.go similarity index 80% rename from runtime/ui/view/filter.go rename to cmd/dive/cli/internal/ui/v1/view/filter.go index ccbff27..f936b8e 100644 --- a/runtime/ui/view/filter.go +++ b/cmd/dive/cli/internal/ui/v1/view/filter.go @@ -2,13 +2,13 @@ package view import ( "fmt" + "github.com/anchore/go-logger" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" "strings" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/utils" ) type FilterEditListener func(string) error @@ -16,9 +16,11 @@ type FilterEditListener func(string) error // Filter holds the UI objects and data models for populating the bottom row. Specifically the pane that // allows the user to filter the file tree by path. type Filter struct { - gui *gocui.Gui - view *gocui.View - header *gocui.View + gui *gocui.Gui + view *gocui.View + header *gocui.View + logger logger.Logger + labelStr string maxLength int hidden bool @@ -28,19 +30,20 @@ type Filter struct { } // newFilterView creates a new view object attached the global [gocui] screen object. -func newFilterView(gui *gocui.Gui) (controller *Filter) { - controller = new(Filter) +func newFilterView(gui *gocui.Gui) *Filter { + c := new(Filter) + c.logger = log.Nested("ui", "filter") - controller.filterEditListeners = make([]FilterEditListener, 0) + c.filterEditListeners = make([]FilterEditListener, 0) // populate main fields - controller.gui = gui - controller.labelStr = "Path Filter: " - controller.hidden = true + c.gui = gui + c.labelStr = "Path Filter: " + c.hidden = true - controller.requestedHeight = 1 + c.requestedHeight = 1 - return controller + return c } func (v *Filter) AddFilterEditListener(listener ...FilterEditListener) { @@ -53,7 +56,7 @@ func (v *Filter) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Filter) Setup(view, header *gocui.View) error { - logrus.Tracef("view.Setup() %s", v.Name()) + log.Trace("Setup()") // set controller options v.view = view @@ -83,8 +86,7 @@ func (v *Filter) ToggleVisible() error { if !v.hidden { _, err := v.gui.SetCurrentView(v.Name()) if err != nil { - logrus.Error("unable to toggle filter view: ", err) - return err + return fmt.Errorf("unable to toggle filter view: %w", err) } return nil } @@ -131,7 +133,7 @@ func (v *Filter) notifyFilterEditListeners() { err := listener(currentValue) if err != nil { // note: cannot propagate error from here since this is from the main gogui thread - logrus.Errorf("notifyFilterEditListeners: %+v", err) + v.logger.WithFields("error", err).Debug("unable to notify filter edit listeners") } } } @@ -143,13 +145,10 @@ func (v *Filter) Update() error { // Render flushes the state objects to the screen. Currently this is the users path filter input. func (v *Filter) Render() error { - logrus.Tracef("view.Render() %s", v.Name()) + v.logger.Trace("render()") v.gui.Update(func(g *gocui.Gui) error { _, err := fmt.Fprintln(v.header, format.Header(v.labelStr)) - if err != nil { - logrus.Error("unable to write to buffer: ", err) - } return err }) return nil @@ -170,7 +169,7 @@ func (v *Filter) OnLayoutChange() error { } func (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + v.logger.Tracef("layout(minX: %d, minY: %d, maxX: %d, maxY: %d)", minX, minY, maxX, maxY) label, labelErr := g.SetView(v.Name()+"label", minX, minY, len(v.labelStr), maxY, 0) view, viewErr := g.SetView(v.Name(), minX+(len(v.labelStr)-1), minY, maxX, maxY, 0) @@ -178,8 +177,7 @@ func (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { if utils.IsNewView(viewErr, labelErr) { err := v.Setup(view, label) if err != nil { - logrus.Error("unable to setup status controller", err) - return err + return fmt.Errorf("unable to setup filter controller: %w", err) } } return nil diff --git a/runtime/ui/view/image_details.go b/cmd/dive/cli/internal/ui/v1/view/image_details.go similarity index 75% rename from runtime/ui/view/image_details.go rename to cmd/dive/cli/internal/ui/v1/view/image_details.go index 977a72c..a119ba7 100644 --- a/runtime/ui/view/image_details.go +++ b/cmd/dive/cli/internal/ui/v1/view/image_details.go @@ -2,26 +2,29 @@ package view import ( "fmt" + "github.com/anchore/go-logger" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/internal/log" "strconv" "strings" "github.com/awesome-gocui/gocui" "github.com/dustin/go-humanize" - "github.com/sirupsen/logrus" - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" ) type ImageDetails struct { - gui *gocui.Gui - body *gocui.View - header *gocui.View + gui *gocui.Gui + body *gocui.View + header *gocui.View + logger logger.Logger + imageName string imageSize uint64 efficiency float64 inefficiencies filetree.EfficiencySlice + kb key.Bindings } func (v *ImageDetails) Name() string { @@ -29,7 +32,9 @@ func (v *ImageDetails) Name() string { } func (v *ImageDetails) Setup(body, header *gocui.View) error { - logrus.Tracef("ImageDetails setup()") + v.logger = log.Nested("ui", "imageDetails") + v.logger.Trace("Setup()") + v.body = body v.body.Editable = false v.body.Wrap = true @@ -44,22 +49,22 @@ func (v *ImageDetails) Setup(body, header *gocui.View) error { var infos = []key.BindingInfo{ { - ConfigKeys: []string{"keybinding.down"}, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, + Config: v.kb.Navigation.Down, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, }, { - ConfigKeys: []string{"keybinding.up"}, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, + Config: v.kb.Navigation.Up, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, }, { - ConfigKeys: []string{"keybinding.page-up"}, - OnAction: v.PageUp, + Config: v.kb.Navigation.PageUp, + OnAction: v.PageUp, }, { - ConfigKeys: []string{"keybinding.page-down"}, - OnAction: v.PageDown, + Config: v.kb.Navigation.PageDown, + OnAction: v.PageDown, }, } @@ -99,7 +104,7 @@ func (v *ImageDetails) Render() error { v.header.Clear() _, err := fmt.Fprintln(v.header, imageHeaderStr) if err != nil { - logrus.Debug("unable to write to buffer: ", err) + log.WithFields("error", err).Debug("unable to write to buffer") } var lines = []string{ @@ -114,7 +119,7 @@ func (v *ImageDetails) Render() error { v.body.Clear() _, err = fmt.Fprintln(v.body, strings.Join(lines, "\n")) if err != nil { - logrus.Debug("unable to write to buffer: ", err) + log.WithFields("error", err).Debug("unable to write to buffer") } return err }) @@ -137,7 +142,7 @@ func (v *ImageDetails) IsVisible() bool { func (v *ImageDetails) PageUp() error { _, height := v.body.Size() if err := CursorStep(v.gui, v.body, -height); err != nil { - logrus.Debugf("Couldn't move the cursor up by %d steps", height) + v.logger.WithFields("error", err).Debugf("couldn't move the cursor up by %d steps", height) } return nil } @@ -145,21 +150,21 @@ func (v *ImageDetails) PageUp() error { func (v *ImageDetails) PageDown() error { _, height := v.body.Size() if err := CursorStep(v.gui, v.body, height); err != nil { - logrus.Debugf("Couldn't move the cursor down by %d steps", height) + v.logger.WithFields("error", err).Debugf("couldn't move the cursor down by %d steps", height) } return nil } func (v *ImageDetails) CursorUp() error { if err := CursorUp(v.gui, v.body); err != nil { - logrus.Debug("Couldn't move the cursor up") + v.logger.WithFields("error", err).Debug("couldn't move the cursor up") } return nil } func (v *ImageDetails) CursorDown() error { if err := CursorDown(v.gui, v.body); err != nil { - logrus.Debug("Couldn't move the cursor down") + v.logger.WithFields("error", err).Debug("couldn't move the cursor down") } return nil } diff --git a/runtime/ui/view/layer.go b/cmd/dive/cli/internal/ui/v1/view/layer.go similarity index 85% rename from runtime/ui/view/layer.go rename to cmd/dive/cli/internal/ui/v1/view/layer.go index c4e197c..b1adc1c 100644 --- a/runtime/ui/view/layer.go +++ b/cmd/dive/cli/internal/ui/v1/view/layer.go @@ -2,14 +2,14 @@ package view import ( "fmt" + "github.com/anchore/go-logger" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" - "github.com/wagoodman/dive/runtime/ui/viewmodel" + "github.com/wagoodman/dive/internal/log" ) // Layer holds the UI objects and data models for populating the lower-left pane. @@ -20,6 +20,8 @@ type Layer struct { body *gocui.View header *gocui.View vm *viewmodel.LayerSetState + kb key.Bindings + logger logger.Logger constrainedRealEstate bool listeners []LayerChangeListener @@ -28,18 +30,20 @@ type Layer struct { } // newLayerView creates a new view object attached the global [gocui] screen object. -func newLayerView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { - controller = new(Layer) +func newLayerView(gui *gocui.Gui, cfg v1.Config) (c *Layer, err error) { + c = new(Layer) - controller.listeners = make([]LayerChangeListener, 0) + c.logger = log.Nested("ui", "layer") + c.listeners = make([]LayerChangeListener, 0) // populate main fields - controller.name = "layer" - controller.gui = gui + c.name = "layer" + c.gui = gui + c.kb = cfg.Preferences.KeyBindings var compareMode viewmodel.LayerCompareMode - switch mode := viper.GetBool("layer.show-aggregated-changes"); mode { + switch mode := cfg.Preferences.ShowAggregatedLayerChanges; mode { case true: compareMode = viewmodel.CompareAllLayers case false: @@ -48,9 +52,9 @@ func newLayerView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode) } - controller.vm = viewmodel.NewLayerSetState(layers, compareMode) + c.vm = viewmodel.NewLayerSetState(cfg.Analysis.Layers, compareMode) - return controller, err + return c, err } func (v *Layer) AddLayerChangeListener(listener ...LayerChangeListener) { @@ -69,14 +73,13 @@ func (v *Layer) notifyLayerChangeListeners() error { for _, listener := range v.listeners { err := listener(selection) if err != nil { - logrus.Errorf("notifyLayerChangeListeners error: %+v", err) - return err + return fmt.Errorf("error notifying layer change listeners: %w", err) } } // this is hacky, and I do not like it if layerDetails, err := v.gui.View("layerDetails"); err == nil { if err := layerDetails.SetCursor(0, 0); err != nil { - logrus.Debug("Couldn't set cursor to 0,0 for layerDetails") + v.logger.Debug("Couldn't set cursor to 0,0 for layerDetails") } } return nil @@ -88,7 +91,7 @@ func (v *Layer) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Layer) Setup(body *gocui.View, header *gocui.View) error { - logrus.Tracef("view.Setup() %s", v.Name()) + v.logger.Trace("Setup()") // set controller options v.body = body @@ -103,34 +106,34 @@ func (v *Layer) Setup(body *gocui.View, header *gocui.View) error { var infos = []key.BindingInfo{ { - ConfigKeys: []string{"keybinding.compare-layer"}, + Config: v.kb.Layer.CompareLayer, OnAction: func() error { return v.setCompareMode(viewmodel.CompareSingleLayer) }, IsSelected: func() bool { return v.vm.CompareMode == viewmodel.CompareSingleLayer }, Display: "Show layer changes", }, { - ConfigKeys: []string{"keybinding.compare-all"}, + Config: v.kb.Layer.CompareAll, OnAction: func() error { return v.setCompareMode(viewmodel.CompareAllLayers) }, IsSelected: func() bool { return v.vm.CompareMode == viewmodel.CompareAllLayers }, Display: "Show aggregated changes", }, { - ConfigKeys: []string{"keybinding.down"}, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, + Config: v.kb.Navigation.Down, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, }, { - ConfigKeys: []string{"keybinding.up"}, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, + Config: v.kb.Navigation.Up, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, }, { - ConfigKeys: []string{"keybinding.page-up"}, - OnAction: v.PageUp, + Config: v.kb.Navigation.PageUp, + OnAction: v.PageUp, }, { - ConfigKeys: []string{"keybinding.page-down"}, - OnAction: v.PageDown, + Config: v.kb.Navigation.PageDown, + OnAction: v.PageDown, }, } @@ -267,14 +270,14 @@ func (v *Layer) renderCompareBar(layerIdx int) string { func (v *Layer) ConstrainLayout() { if !v.constrainedRealEstate { - logrus.Debugf("constraining layer layout") + v.logger.Debug("constraining layout") v.constrainedRealEstate = true } } func (v *Layer) ExpandLayout() { if v.constrainedRealEstate { - logrus.Debugf("expanding layer layout") + v.logger.Debug("expanding layout") v.constrainedRealEstate = false } } @@ -297,7 +300,7 @@ func (v *Layer) Update() error { // 1. the layers of the image + metadata // 2. the current selected image func (v *Layer) Render() error { - logrus.Tracef("view.Render() %s", v.Name()) + v.logger.Trace("render()") // indicate when selected title := "Layers" @@ -343,7 +346,6 @@ func (v *Layer) Render() error { } if err != nil { - logrus.Debug("unable to write to buffer: ", err) return err } } diff --git a/cmd/dive/cli/internal/ui/v1/view/layer_change_listener.go b/cmd/dive/cli/internal/ui/v1/view/layer_change_listener.go new file mode 100644 index 0000000..ed1abf1 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1/view/layer_change_listener.go @@ -0,0 +1,7 @@ +package view + +import ( + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" +) + +type LayerChangeListener func(viewmodel.LayerSelection) error diff --git a/runtime/ui/view/layer_details.go b/cmd/dive/cli/internal/ui/v1/view/layer_details.go similarity index 79% rename from runtime/ui/view/layer_details.go rename to cmd/dive/cli/internal/ui/v1/view/layer_details.go index 0334fb2..fcc0019 100644 --- a/runtime/ui/view/layer_details.go +++ b/cmd/dive/cli/internal/ui/v1/view/layer_details.go @@ -2,15 +2,15 @@ package view import ( "fmt" + "github.com/anchore/go-logger" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/internal/log" "strings" "github.com/awesome-gocui/gocui" "github.com/dustin/go-humanize" - "github.com/sirupsen/logrus" - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" ) type LayerDetails struct { @@ -18,6 +18,8 @@ type LayerDetails struct { header *gocui.View body *gocui.View CurrentLayer *image.Layer + kb key.Bindings + logger logger.Logger } func (v *LayerDetails) Name() string { @@ -25,7 +27,9 @@ func (v *LayerDetails) Name() string { } func (v *LayerDetails) Setup(body, header *gocui.View) error { - logrus.Tracef("LayerDetails setup()") + v.logger = log.Nested("ui", "layerDetails") + v.logger.Trace("setup()") + v.body = body v.body.Editable = false v.body.Wrap = true @@ -40,14 +44,14 @@ func (v *LayerDetails) Setup(body, header *gocui.View) error { var infos = []key.BindingInfo{ { - ConfigKeys: []string{"keybinding.down"}, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, + Config: v.kb.Navigation.Down, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, }, { - ConfigKeys: []string{"keybinding.up"}, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, + Config: v.kb.Navigation.Up, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, }, } @@ -95,7 +99,7 @@ func (v *LayerDetails) Render() error { v.body.Clear() if _, err = fmt.Fprintln(v.body, strings.Join(lines, "\n")); err != nil { - logrus.Debug("unable to write to buffer: ", err) + log.WithFields("layer", v.CurrentLayer.Id, "error", err).Debug("unable to write to buffer") } return nil }) @@ -117,7 +121,7 @@ func (v *LayerDetails) IsVisible() bool { // CursorUp moves the cursor up in the details pane func (v *LayerDetails) CursorUp() error { if err := CursorUp(v.gui, v.body); err != nil { - logrus.Debug("Couldn't move the cursor up") + v.logger.WithFields("error", err).Debug("couldn't move the cursor up") } return nil } @@ -125,7 +129,7 @@ func (v *LayerDetails) CursorUp() error { // CursorDown moves the cursor up in the details pane func (v *LayerDetails) CursorDown() error { if err := CursorDown(v.gui, v.body); err != nil { - logrus.Debug("Couldn't move the cursor down") + v.logger.WithFields("error", err).Debug("couldn't move the cursor down") } return nil } diff --git a/runtime/ui/view/renderer.go b/cmd/dive/cli/internal/ui/v1/view/renderer.go similarity index 100% rename from runtime/ui/view/renderer.go rename to cmd/dive/cli/internal/ui/v1/view/renderer.go diff --git a/runtime/ui/view/status.go b/cmd/dive/cli/internal/ui/v1/view/status.go similarity index 74% rename from runtime/ui/view/status.go rename to cmd/dive/cli/internal/ui/v1/view/status.go index 648ce2c..d4fbbf8 100644 --- a/runtime/ui/view/status.go +++ b/cmd/dive/cli/internal/ui/v1/view/status.go @@ -2,22 +2,23 @@ package view import ( "fmt" + "github.com/anchore/go-logger" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" "strings" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" - "github.com/wagoodman/dive/utils" ) // Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel // shows the user a set of possible actions to take in the window and currently selected pane. type Status struct { - name string - gui *gocui.Gui - view *gocui.View + name string + gui *gocui.Gui + view *gocui.View + logger logger.Logger selectedView Helper requestedHeight int @@ -26,16 +27,17 @@ type Status struct { } // newStatusView creates a new view object attached the global [gocui] screen object. -func newStatusView(gui *gocui.Gui) (controller *Status) { - controller = new(Status) +func newStatusView(gui *gocui.Gui) *Status { + c := new(Status) // populate main fields - controller.name = "status" - controller.gui = gui - controller.helpKeys = make([]*key.Binding, 0) - controller.requestedHeight = 1 + c.name = "status" + c.gui = gui + c.helpKeys = make([]*key.Binding, 0) + c.requestedHeight = 1 + c.logger = log.Nested("ui", "status") - return controller + return c } func (v *Status) SetCurrentView(r Helper) { @@ -52,7 +54,7 @@ func (v *Status) AddHelpKeys(keys ...*key.Binding) { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Status) Setup(view *gocui.View) error { - logrus.Tracef("view.Setup() %s", v.Name()) + v.logger.Trace("setup()") // set controller options v.view = view @@ -82,7 +84,7 @@ func (v *Status) OnLayoutChange() error { // Render flushes the state objects to the screen. func (v *Status) Render() error { - logrus.Tracef("view.Render() %s", v.Name()) + v.logger.Trace("render()") v.gui.Update(func(g *gocui.Gui) error { v.view.Clear() @@ -94,7 +96,7 @@ func (v *Status) Render() error { _, err := fmt.Fprintln(v.view, v.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) if err != nil { - logrus.Debug("unable to write to buffer: ", err) + v.logger.WithFields("error", err).Debug("unable to write to buffer") } return err @@ -112,14 +114,13 @@ func (v *Status) KeyHelp() string { } func (v *Status) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + v.logger.Tracef("layout(minX: %d, minY: %d, maxX: %d, maxY: %d)", minX, minY, maxX, maxY) view, viewErr := g.SetView(v.Name(), minX, minY, maxX, maxY, 0) if utils.IsNewView(viewErr) { err := v.Setup(view) if err != nil { - logrus.Error("unable to setup status controller", err) - return err + return fmt.Errorf("unable to setup status controller: %w", err) } } return nil diff --git a/cmd/dive/cli/internal/ui/v1/view/views.go b/cmd/dive/cli/internal/ui/v1/view/views.go new file mode 100644 index 0000000..ad856c9 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1/view/views.go @@ -0,0 +1,67 @@ +package view + +import ( + "github.com/awesome-gocui/gocui" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" +) + +type View interface { + Setup(*gocui.View, *gocui.View) error + Name() string + IsVisible() bool +} + +type Views struct { + Tree *FileTree + Layer *Layer + Status *Status + Filter *Filter + LayerDetails *LayerDetails + ImageDetails *ImageDetails + Debug *Debug +} + +func NewViews(g *gocui.Gui, cfg v1.Config) (*Views, error) { + layer, err := newLayerView(g, cfg) + if err != nil { + return nil, err + } + + tree, err := newFileTreeView(g, cfg, 0) + if err != nil { + return nil, err + } + + status := newStatusView(g) + + // set the layer view as the first selected view + status.SetCurrentView(layer) + + return &Views{ + Tree: tree, + Layer: layer, + Status: status, + Filter: newFilterView(g), + ImageDetails: &ImageDetails{ + gui: g, + imageName: cfg.Analysis.Image, + imageSize: cfg.Analysis.SizeBytes, + efficiency: cfg.Analysis.Efficiency, + inefficiencies: cfg.Analysis.Inefficiencies, + kb: cfg.Preferences.KeyBindings, + }, + LayerDetails: &LayerDetails{gui: g, kb: cfg.Preferences.KeyBindings}, + Debug: newDebugView(g), + }, nil +} + +func (views *Views) Renderers() []Renderer { + return []Renderer{ + views.Tree, + views.Layer, + views.Status, + views.Filter, + views.LayerDetails, + views.ImageDetails, + } +} diff --git a/cmd/dive/cli/internal/ui/v1/viewmodel/config.go b/cmd/dive/cli/internal/ui/v1/viewmodel/config.go new file mode 100644 index 0000000..789cfc0 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v1/viewmodel/config.go @@ -0,0 +1 @@ +package viewmodel diff --git a/runtime/ui/viewmodel/filetree.go b/cmd/dive/cli/internal/ui/v1/viewmodel/filetree.go similarity index 85% rename from runtime/ui/viewmodel/filetree.go rename to cmd/dive/cli/internal/ui/v1/viewmodel/filetree.go index a5f87d0..48332d7 100644 --- a/runtime/ui/viewmodel/filetree.go +++ b/cmd/dive/cli/internal/ui/v1/viewmodel/filetree.go @@ -3,24 +3,23 @@ package viewmodel import ( "bytes" "fmt" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format" + "github.com/wagoodman/dive/internal/log" "regexp" "strings" "github.com/lunixbochs/vtclean" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/runtime/ui/format" ) -// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that +// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically, the pane that // shows selected layer or aggregate file ASCII tree. type FileTreeViewModel struct { ModelTree *filetree.FileTree ViewTree *filetree.FileTree RefTrees []*filetree.FileTree - cache filetree.Comparer + comparer filetree.Comparer constrainedRealEstate bool @@ -39,19 +38,24 @@ type FileTreeViewModel struct { } // NewFileTreeViewModel creates a new view object attached the global [gocui] screen object. -func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (treeViewModel *FileTreeViewModel, err error) { +func NewFileTreeViewModel(cfg v1.Config, initialLayer int) (treeViewModel *FileTreeViewModel, err error) { treeViewModel = new(FileTreeViewModel) + comparer, err := cfg.TreeComparer() + if err != nil { + return nil, err + } + // populate main fields - treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes") + treeViewModel.ShowAttributes = cfg.Preferences.ShowFiletreeAttributes treeViewModel.unconstrainedShowAttributes = treeViewModel.ShowAttributes - treeViewModel.CollapseAll = viper.GetBool("filetree.collapse-dir") - treeViewModel.ModelTree = tree - treeViewModel.RefTrees = refTrees - treeViewModel.cache = cache + treeViewModel.CollapseAll = cfg.Preferences.CollapseFiletreeDirectory + treeViewModel.ModelTree = cfg.Analysis.RefTrees[initialLayer] + treeViewModel.RefTrees = cfg.Analysis.RefTrees + treeViewModel.comparer = comparer treeViewModel.HiddenDiffTypes = make([]bool, 4) - hiddenTypes := viper.GetStringSlice("diff.hide") + hiddenTypes := cfg.Preferences.FiletreeDiffHide for _, hType := range hiddenTypes { switch t := strings.ToLower(hType); t { case "added": @@ -67,7 +71,7 @@ func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree } } - return treeViewModel, nil + return treeViewModel, treeViewModel.SetTreeByLayer(0, 0, initialLayer, initialLayer) } // Setup initializes the UI concerns within the context of a global [gocui] view object. @@ -106,10 +110,9 @@ func (vm *FileTreeViewModel) SetTreeByLayer(bottomTreeStart, bottomTreeStop, top if topTreeStop > len(vm.RefTrees)-1 { return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(vm.RefTrees)-1) } - newTree, err := vm.cache.GetTree(filetree.NewTreeIndexKey(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)) + newTree, err := vm.comparer.GetTree(filetree.NewTreeIndexKey(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)) if err != nil { - logrus.Errorf("unable to fetch layer tree from cache: %+v", err) - return err + return fmt.Errorf("unable to fetch layer tree from cache: %w", err) } // preserve vm state on copy @@ -122,15 +125,14 @@ func (vm *FileTreeViewModel) SetTreeByLayer(bottomTreeStart, bottomTreeStop, top } err = vm.ModelTree.VisitDepthChildFirst(visitor, nil) if err != nil { - logrus.Errorf("unable to propagate layer tree: %+v", err) - return err + return fmt.Errorf("unable to propagate layer tree: %w", err) } vm.ModelTree = newTree return nil } -// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer. +// CursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer. func (vm *FileTreeViewModel) CursorUp() bool { if vm.TreeIndex <= 0 { return false @@ -145,7 +147,7 @@ func (vm *FileTreeViewModel) CursorUp() bool { return true } -// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer. +// CursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer. func (vm *FileTreeViewModel) CursorDown() bool { if vm.TreeIndex >= vm.ModelTree.VisibleSize() { return false @@ -161,7 +163,6 @@ func (vm *FileTreeViewModel) CursorDown() bool { return true } -// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree func (vm *FileTreeViewModel) CurrentNode(filterRegex *regexp.Regexp) *filetree.FileNode { return vm.getAbsPositionNode(filterRegex) } @@ -198,8 +199,7 @@ func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { err := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator) if err != nil { - logrus.Errorf("could not propagate tree on cursorLeft: %+v", err) - return err + return fmt.Errorf("unable to propagate tree on cursorLeft: %w", err) } vm.TreeIndex = newIndex @@ -320,12 +320,16 @@ func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (nod match := filterRegex.Find([]byte(curNode.Path())) regexMatch = match != nil } - return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch + parentCollapsed := false + if curNode.Parent != nil { + parentCollapsed = curNode.Parent.Data.ViewInfo.Collapsed + } + return !parentCollapsed && !curNode.Data.ViewInfo.Hidden && regexMatch } err := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator) if err != nil { - logrus.Errorf("unable to get node position: %+v", err) + log.WithFields("error", err).Debug("unable to propagate tree on getAbsPositionNode") } return node @@ -355,7 +359,7 @@ func (vm *FileTreeViewModel) ToggleCollapseAll() error { err := vm.ModelTree.VisitDepthChildFirst(visitor, evaluator) if err != nil { - logrus.Errorf("unable to propagate tree on ToggleCollapseAll: %+v", err) + log.WithFields("error", err).Debug("unable to propagate tree on ToggleCollapseAll") } return nil @@ -370,7 +374,6 @@ func (vm *FileTreeViewModel) ToggleSortOrder() error { func (vm *FileTreeViewModel) ConstrainLayout() { if !vm.constrainedRealEstate { - logrus.Debugf("constraining filetree layout") vm.constrainedRealEstate = true vm.unconstrainedShowAttributes = vm.ShowAttributes vm.ShowAttributes = false @@ -379,13 +382,12 @@ func (vm *FileTreeViewModel) ConstrainLayout() { func (vm *FileTreeViewModel) ExpandLayout() { if vm.constrainedRealEstate { - logrus.Debugf("expanding filetree layout") vm.ShowAttributes = vm.unconstrainedShowAttributes vm.constrainedRealEstate = false } } -// ToggleCollapse will collapse/expand the selected FileNode. +// ToggleAttributes will hi func (vm *FileTreeViewModel) ToggleAttributes() error { // ignore any attempt to show the attributes when the layout is constrained if vm.constrainedRealEstate { @@ -424,8 +426,7 @@ func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height in }, nil) if err != nil { - logrus.Errorf("unable to propagate vm model tree: %+v", err) - return err + return fmt.Errorf("unable to propagate vm model tree: %w", err) } // make a new tree with only visible nodes @@ -441,8 +442,7 @@ func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height in }, nil) if err != nil { - logrus.Errorf("unable to propagate vm view tree: %+v", err) - return err + return fmt.Errorf("unable to propagate vm view tree: %w", err) } return nil @@ -459,13 +459,11 @@ func (vm *FileTreeViewModel) Render() error { if idx == vm.bufferIndex { _, err := fmt.Fprintln(&vm.Buffer, format.Selected(vtclean.Clean(line, false))) if err != nil { - logrus.Debug("unable to write to buffer: ", err) return err } } else { _, err := fmt.Fprintln(&vm.Buffer, line) if err != nil { - logrus.Debug("unable to write to buffer: ", err) return err } } diff --git a/runtime/ui/viewmodel/filetree_test.go b/cmd/dive/cli/internal/ui/v1/viewmodel/filetree_test.go similarity index 77% rename from runtime/ui/viewmodel/filetree_test.go rename to cmd/dive/cli/internal/ui/v1/viewmodel/filetree_test.go index 3d53b0f..dd406ec 100644 --- a/runtime/ui/viewmodel/filetree_test.go +++ b/cmd/dive/cli/internal/ui/v1/viewmodel/filetree_test.go @@ -1,21 +1,30 @@ package viewmodel import ( - "bytes" + "flag" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" + "go.uber.org/atomic" "os" + "os/exec" "path/filepath" "regexp" + "strings" "testing" - "github.com/fatih/color" - "github.com/sergi/go-diff/diffmatchpatch" - "github.com/wagoodman/dive/dive/filetree" "github.com/wagoodman/dive/dive/image/docker" - "github.com/wagoodman/dive/runtime/ui/format" ) -const allowTestDataCapture = false +var repoRootCache atomic.String + +var updateSnapshot = flag.Bool("update", false, "update any test snapshots") + +func TestUpdateSnapshotDisabled(t *testing.T) { + require.False(t, *updateSnapshot, "update snapshot flag should be disabled") +} func fileExists(filename string) bool { info, err := os.Stat(filename) @@ -30,6 +39,7 @@ func testCaseDataFilePath(name string) string { } func helperLoadBytes(t *testing.T) []byte { + t.Helper() path := testCaseDataFilePath(t.Name()) theBytes, err := os.ReadFile(path) if err != nil { @@ -39,7 +49,9 @@ func helperLoadBytes(t *testing.T) []byte { } func helperCaptureBytes(t *testing.T, data []byte) { - if !allowTestDataCapture { + // TODO: switch to https://github.com/gkampitakis/go-snaps + t.Helper() + if *updateSnapshot { t.Fatalf("cannot capture data in test mode: %s", t.Name()) } @@ -51,54 +63,23 @@ func helperCaptureBytes(t *testing.T, data []byte) { } } -func helperCheckDiff(t *testing.T, expected, actual []byte) { - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("%s", dmp.DiffPrettyText(diffs)) - t.Errorf("%s: bytes mismatch", t.Name()) - } -} - -func assertTestData(t *testing.T, actualBytes []byte) { - path := testCaseDataFilePath(t.Name()) - if !fileExists(path) { - if allowTestDataCapture { - helperCaptureBytes(t, actualBytes) - } else { - t.Fatalf("missing test data: %s", path) - } - } - expectedBytes := helperLoadBytes(t) - helperCheckDiff(t, expectedBytes, actualBytes) -} - func initializeTestViewModel(t *testing.T) *FileTreeViewModel { - result := docker.TestAnalysisFromArchive(t, "../../../.data/test-docker-image.tar") + t.Helper() - cache := filetree.NewComparer(result.RefTrees) - errors := cache.BuildCache() - if len(errors) > 0 { - t.Fatalf("%s: unable to build cache: %d errors", t.Name(), len(errors)) - } + result := docker.TestAnalysisFromArchive(t, repoPath(t, ".data/test-docker-image.tar")) + require.NotNil(t, result, "unable to load test data") - format.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc() + vm, err := NewFileTreeViewModel(v1.Config{ + Analysis: *result, + Preferences: v1.DefaultPreferences(), + }, 0) - treeStack, failedPaths, err := filetree.StackTreeRange(result.RefTrees, 0, 0) - if len(failedPaths) > 0 { - t.Errorf("expected no filepath errors, got %d", len(failedPaths)) - } - if err != nil { - t.Fatalf("%s: unable to stack trees: %v", t.Name(), err) - } - vm, err := NewFileTreeViewModel(treeStack, result.RefTrees, cache) - if err != nil { - t.Fatalf("%s: unable to create tree ViewModel: %+v", t.Name(), err) - } + require.NoError(t, err, "unable to create viewmodel") return vm } func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) { + t.Helper() err := vm.Update(filterRegex, width, height) if err != nil { t.Errorf("failed to update viewmodel: %v", err) @@ -109,7 +90,19 @@ func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterR t.Errorf("failed to render viewmodel: %v", err) } - assertTestData(t, vm.Buffer.Bytes()) + actualBytes := vm.Buffer.Bytes() + path := testCaseDataFilePath(t.Name()) + if !fileExists(path) { + if *updateSnapshot { + helperCaptureBytes(t, actualBytes) + } else { + t.Fatalf("missing test data: %s", path) + } + } + expectedBytes := helperLoadBytes(t) + if d := cmp.Diff(string(expectedBytes), string(actualBytes)); d != "" { + t.Errorf("bytes mismatch (-want +got):\n%s", d) + } } func checkError(t *testing.T, err error, message string) { @@ -155,27 +148,36 @@ func TestFileTreeDirCollapse(t *testing.T) { vm.Setup(0, height) vm.ShowAttributes = true + assertPath(t, vm, "/bin", "before toggle of bin") + // collapse /bin err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") + assertPath(t, vm, "/bin", "after toggle of bin") - moved := vm.CursorDown() - if !moved { - t.Error("unable to cursor down") - } + moved := vm.CursorDown() // select /dev + require.True(t, moved, "unable to cursor down") + assertPath(t, vm, "/dev", "down to dev") - moved = vm.CursorDown() - if !moved { - t.Error("unable to cursor down") - } + moved = vm.CursorDown() // select /etc + require.True(t, moved, "unable to cursor down") + assertPath(t, vm, "/etc", "down to etc") // collapse /etc err = vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /etc") + assertPath(t, vm, "/etc", "after toggle of etc") runTestCase(t, vm, width, height, nil) } +func assertPath(t *testing.T, vm *FileTreeViewModel, expected string, msg string) { + t.Helper() + n := vm.CurrentNode(nil) + require.NotNil(t, n, "unable to get current node") + assert.Equal(t, expected, n.Path(), msg) +} + func TestFileTreeDirCollapseAll(t *testing.T) { vm := initializeTestViewModel(t) @@ -396,3 +398,25 @@ func TestFileTreeHideTypeWithFilter(t *testing.T) { runTestCase(t, vm, width, height, regex) } + +func repoPath(t testing.TB, path string) string { + t.Helper() + root := repoRoot(t) + return filepath.Join(root, path) +} + +func repoRoot(t testing.TB) string { + val := repoRootCache.Load() + if val != "" { + return val + } + t.Helper() + // use git to find the root of the repo + out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + t.Fatalf("failed to get repo root: %v", err) + } + val = strings.TrimSpace(string(out)) + repoRootCache.Store(val) + return val +} diff --git a/runtime/ui/viewmodel/layer_compare.go b/cmd/dive/cli/internal/ui/v1/viewmodel/layer_compare.go similarity index 100% rename from runtime/ui/viewmodel/layer_compare.go rename to cmd/dive/cli/internal/ui/v1/viewmodel/layer_compare.go diff --git a/runtime/ui/viewmodel/layer_selection.go b/cmd/dive/cli/internal/ui/v1/viewmodel/layer_selection.go similarity index 100% rename from runtime/ui/viewmodel/layer_selection.go rename to cmd/dive/cli/internal/ui/v1/viewmodel/layer_selection.go diff --git a/runtime/ui/viewmodel/layer_set_state.go b/cmd/dive/cli/internal/ui/v1/viewmodel/layer_set_state.go similarity index 100% rename from runtime/ui/viewmodel/layer_set_state.go rename to cmd/dive/cli/internal/ui/v1/viewmodel/layer_set_state.go diff --git a/runtime/ui/viewmodel/layer_set_state_test.go b/cmd/dive/cli/internal/ui/v1/viewmodel/layer_set_state_test.go similarity index 100% rename from runtime/ui/viewmodel/layer_set_state_test.go rename to cmd/dive/cli/internal/ui/v1/viewmodel/layer_set_state_test.go diff --git a/runtime/ui/viewmodel/testdata/TestFileShowAggregateChanges.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileShowAggregateChanges.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileShowAggregateChanges.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileShowAggregateChanges.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeDirCollapse.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCollapse.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeDirCollapse.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCollapse.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeDirCollapseAll.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCollapseAll.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeDirCollapseAll.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCollapseAll.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeDirCursorRight.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCursorRight.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeDirCursorRight.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCursorRight.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeFilterTree.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeFilterTree.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeFilterTree.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeFilterTree.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeGoCase.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeGoCase.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeGoCase.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeGoCase.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeHideUnmodified.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideUnmodified.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeHideUnmodified.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideUnmodified.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeNoAttributes.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeNoAttributes.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeNoAttributes.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeNoAttributes.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreePageDown.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreePageDown.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreePageDown.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreePageDown.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreePageUp.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreePageUp.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreePageUp.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreePageUp.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeRestrictedHeight.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeRestrictedHeight.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeRestrictedHeight.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeRestrictedHeight.txt diff --git a/runtime/ui/viewmodel/testdata/TestFileTreeSelectLayer.txt b/cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeSelectLayer.txt similarity index 100% rename from runtime/ui/viewmodel/testdata/TestFileTreeSelectLayer.txt rename to cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeSelectLayer.txt diff --git a/cmd/dive/cli/testdata/config/dive-ci-legacy.yaml b/cmd/dive/cli/testdata/config/dive-ci-legacy.yaml new file mode 100644 index 0000000..44dcbbf --- /dev/null +++ b/cmd/dive/cli/testdata/config/dive-ci-legacy.yaml @@ -0,0 +1,4 @@ +rules: + lowestEfficiency: 0.95 + highestWastedBytes: 20MB + highestUserWastedPercent: 0.20 diff --git a/cmd/dive/cli/testdata/default-ci-config/.dive-ci b/cmd/dive/cli/testdata/default-ci-config/.dive-ci new file mode 100644 index 0000000..9521ab5 --- /dev/null +++ b/cmd/dive/cli/testdata/default-ci-config/.dive-ci @@ -0,0 +1,9 @@ +rules: + # If the efficiency is measured below X%, mark as failed. (expressed as a percentage between 0-1) + lowestEfficiency: 0.96 + + # If the amount of wasted space is at least X or larger than X, mark as failed. (expressed in B, KB, MB, and GB) + highestWastedBytes: 19Mb + + # If the amount of wasted space makes up for X% of the image, mark as failed. (fail if the threshold is met or crossed; expressed as a percentage between 0-1) + highestUserWastedPercent: 0.6 diff --git a/cmd/dive/cli/testdata/dive-enable-ci.yaml b/cmd/dive/cli/testdata/dive-enable-ci.yaml new file mode 100644 index 0000000..51e1947 --- /dev/null +++ b/cmd/dive/cli/testdata/dive-enable-ci.yaml @@ -0,0 +1,10 @@ +ci: true +rules: + # lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_LOWEST_EFFICIENCY_THRESHOLD) + lowest-efficiency-threshold: '0.10' + + # highest allowable bytes wasted, otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_WASTED_BYTES) + highest-wasted-bytes: '20MB' + + # highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_USER_WASTED_PERCENT) + highest-user-wasted-percent: '0.90' diff --git a/cmd/dive/cli/testdata/image-multi-layer-containerfile/Containerfile b/cmd/dive/cli/testdata/image-multi-layer-containerfile/Containerfile new file mode 100644 index 0000000..89af4a3 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-containerfile/Containerfile @@ -0,0 +1,13 @@ +FROM busybox:1.37.0@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f +ADD example.md /somefile.txt +RUN mkdir -p /root/example/really/nested +RUN cp /somefile.txt /root/example/somefile1.txt +RUN chmod 444 /root/example/somefile1.txt +RUN cp /somefile.txt /root/example/somefile2.txt +RUN cp /somefile.txt /root/example/somefile3.txt +RUN mv /root/example/somefile3.txt /root/saved.txt +RUN cp /root/saved.txt /root/.saved.txt +RUN chmod +x /root/saved.txt +RUN chmod 421 /root +RUN rm -rf /root/example/ +ADD overwrite.md /root/saved.txt diff --git a/cmd/dive/cli/testdata/image-multi-layer-containerfile/dive-pass.yaml b/cmd/dive/cli/testdata/image-multi-layer-containerfile/dive-pass.yaml new file mode 100644 index 0000000..51e1947 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-containerfile/dive-pass.yaml @@ -0,0 +1,10 @@ +ci: true +rules: + # lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_LOWEST_EFFICIENCY_THRESHOLD) + lowest-efficiency-threshold: '0.10' + + # highest allowable bytes wasted, otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_WASTED_BYTES) + highest-wasted-bytes: '20MB' + + # highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_USER_WASTED_PERCENT) + highest-user-wasted-percent: '0.90' diff --git a/cmd/dive/cli/testdata/image-multi-layer-containerfile/example.md b/cmd/dive/cli/testdata/image-multi-layer-containerfile/example.md new file mode 100644 index 0000000..d347ea2 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-containerfile/example.md @@ -0,0 +1,3 @@ +# exmaple! + +woot! \ No newline at end of file diff --git a/cmd/dive/cli/testdata/image-multi-layer-containerfile/overwrite.md b/cmd/dive/cli/testdata/image-multi-layer-containerfile/overwrite.md new file mode 100644 index 0000000..1d671e6 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-containerfile/overwrite.md @@ -0,0 +1,3 @@ +# evil! + +this will overwrite the other file... \ No newline at end of file diff --git a/cmd/dive/cli/testdata/image-multi-layer-dockerfile/Dockerfile b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/Dockerfile new file mode 100644 index 0000000..89af4a3 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/Dockerfile @@ -0,0 +1,13 @@ +FROM busybox:1.37.0@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f +ADD example.md /somefile.txt +RUN mkdir -p /root/example/really/nested +RUN cp /somefile.txt /root/example/somefile1.txt +RUN chmod 444 /root/example/somefile1.txt +RUN cp /somefile.txt /root/example/somefile2.txt +RUN cp /somefile.txt /root/example/somefile3.txt +RUN mv /root/example/somefile3.txt /root/saved.txt +RUN cp /root/saved.txt /root/.saved.txt +RUN chmod +x /root/saved.txt +RUN chmod 421 /root +RUN rm -rf /root/example/ +ADD overwrite.md /root/saved.txt diff --git a/cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-fail.yaml b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-fail.yaml new file mode 100644 index 0000000..2377858 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-fail.yaml @@ -0,0 +1,3 @@ +ci: true +rules: + lowest-efficiency-threshold: '0.9' diff --git a/cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-pass.yaml b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-pass.yaml new file mode 100644 index 0000000..904ffe5 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-pass.yaml @@ -0,0 +1,5 @@ +ci: true +rules: + lowest-efficiency-threshold: '0.10' + highest-wasted-bytes: '20MB' + highest-user-wasted-percent: '0.90' diff --git a/cmd/dive/cli/testdata/image-multi-layer-dockerfile/example.md b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/example.md new file mode 100644 index 0000000..d347ea2 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/example.md @@ -0,0 +1,3 @@ +# exmaple! + +woot! \ No newline at end of file diff --git a/cmd/dive/cli/testdata/image-multi-layer-dockerfile/overwrite.md b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/overwrite.md new file mode 100644 index 0000000..1d671e6 --- /dev/null +++ b/cmd/dive/cli/testdata/image-multi-layer-dockerfile/overwrite.md @@ -0,0 +1,3 @@ +# evil! + +this will overwrite the other file... \ No newline at end of file diff --git a/cmd/dive/cli/testdata/invalid/Dockerfile b/cmd/dive/cli/testdata/invalid/Dockerfile new file mode 100644 index 0000000..ac06488 --- /dev/null +++ b/cmd/dive/cli/testdata/invalid/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +INVALID woops \ No newline at end of file diff --git a/cmd/dive/cli/testdata/snapshots/cli_build_test.snap b/cmd/dive/cli/testdata/snapshots/cli_build_test.snap new file mode 100755 index 0000000..cafccd5 --- /dev/null +++ b/cmd/dive/cli/testdata/snapshots/cli_build_test.snap @@ -0,0 +1,137 @@ + +[Test_Build_Dockerfile/implicit_dockerfile - 1] +Analysis: + efficiency: 100.00 % + wastedBytes: 131 bytes (131 B) + userWastedPercent: 71.98 % + +Inefficient Files: + Count Wasted Space File Path + 3 80 B /root/saved.txt + 2 34 B /root/example/somefile1.txt + 2 17 B /root/example/somefile3.txt + 2 0 B /root + 10 0 B /etc + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_Build_Dockerfile/explicit_file_flag - 1] +Analysis: + efficiency: 100.00 % + wastedBytes: 131 bytes (131 B) + userWastedPercent: 71.98 % + +Inefficient Files: + Count Wasted Space File Path + 3 80 B /root/saved.txt + 2 34 B /root/example/somefile1.txt + 2 17 B /root/example/somefile3.txt + 2 0 B /root + 10 0 B /etc + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_Build_Containerfile/implicit_containerfile - 1] +Analysis: + efficiency: 100.00 % + wastedBytes: 131 bytes (131 B) + userWastedPercent: 71.98 % + +Inefficient Files: + Count Wasted Space File Path + 3 80 B /root/saved.txt + 2 34 B /root/example/somefile1.txt + 2 17 B /root/example/somefile3.txt + 2 0 B /root + 10 0 B /etc + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_Build_Containerfile/explicit_file_flag - 1] +Analysis: + efficiency: 100.00 % + wastedBytes: 131 bytes (131 B) + userWastedPercent: 71.98 % + +Inefficient Files: + Count Wasted Space File Path + 3 80 B /root/saved.txt + 2 34 B /root/example/somefile1.txt + 2 17 B /root/example/somefile3.txt + 2 0 B /root + 10 0 B /etc + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_BuildFailure/nonexistent_directory - 1] +Building image ... ./path/does/not/exist + +--- + +[Test_BuildFailure/invalid_dockerfile - 1] +Building image ... ./testdata/invalid +#0 building with "desktop-linux" instance using docker driver + +#1 [internal] load build definition from Dockerfile +#1 transferring dockerfile: 100B done +#1 DONE 0.0s +Dockerfile:2 +-------------------- + 1 | FROM scratch + 2 | >>> INVALID woops +-------------------- +ERROR: failed to solve: dockerfile parse error on line 2: unknown instruction: INVALID + +View build details: docker-desktop:// +--- + +[Test_Build_CI_gate_fail - 1] +Analysis: + efficiency: 100.00 % + wastedBytes: 131 bytes (131 B) + userWastedPercent: 71.98 % + +Inefficient Files: + Count Wasted Space File Path + 3 80 B /root/saved.txt + 2 34 B /root/example/somefile1.txt + 2 17 B /root/example/somefile3.txt + 2 0 B /root + 10 0 B /etc + +Evaluation: + FAIL highestUserWastedPercent (too many bytes wasted, relative to the user bytes added (%-user-wasted-bytes=0.72 > threshold=0.1)) + SKIP highestWastedBytes (disabled) + PASS lowestEfficiency (0.9) + +FAIL [pass:1 fail:1 skip:1] + +--- diff --git a/cmd/dive/cli/testdata/snapshots/cli_ci_test.snap b/cmd/dive/cli/testdata/snapshots/cli_ci_test.snap new file mode 100755 index 0000000..db9c699 --- /dev/null +++ b/cmd/dive/cli/testdata/snapshots/cli_ci_test.snap @@ -0,0 +1,78 @@ + +[Test_CI_Fail - 1] +Analysis: + efficiency: 100.00 % + wastedBytes: 131 bytes (131 B) + userWastedPercent: 71.98 % + +Inefficient Files: + Count Wasted Space File Path + 3 80 B /root/saved.txt + 2 34 B /root/example/somefile1.txt + 2 17 B /root/example/somefile3.txt + 2 0 B /root + 10 0 B /etc + +Evaluation: + FAIL highestUserWastedPercent (too many bytes wasted, relative to the user bytes added (%-user-wasted-bytes=0.72 > threshold=0.1)) + SKIP highestWastedBytes (disabled) + PASS lowestEfficiency (0.9) + +FAIL [pass:1 fail:1 skip:1] + +--- + +[Test_CI_DefaultCIConfig - 1] +[0001] INFO dive version: testing +[0001] DEBUG config: + log: + quiet: false + level: debug + file: "" + dev: + profile: none + image: /Users/wagoodman/code/dive/.data/test-docker-image.tar + container-engine: docker + ignore-errors: false + ci: false + ci-config: .dive-ci + rules: + lowest-efficiency: "0.96" + highest-wasted-bytes: 19Mb + highest-user-wasted-percent: "0.6" + json-path: "" + keybinding: + quit: ctrl+c + toggle-view: tab + filter-files: ctrl+f, ctrl+slash + close-filter-files: esc + up: up,k + down: down,j + left: left,h + right: right,l + page-up: pgup,u + page-down: pgdn,d + compare-all: ctrl+a + compare-layer: ctrl+l + toggle-collapse-dir: space + toggle-collapse-all-dir: ctrl+space + toggle-added-files: ctrl+a + toggle-removed-files: ctrl+r + toggle-modified-files: ctrl+m + toggle-unmodified-files: ctrl+u + toggle-filetree-attributes: ctrl+b + toggle-sort-order: ctrl+o + toggle-wrap-tree: ctrl+p + extract-file: ctrl+e + diff: + hide: [] + filetree: + collapse-dir: false + pane-width: 0.5 + show-attributes: true + layer: + show-aggregated-changes: false +[0001] INFO fetching image=/Users/wagoodman/code/dive/.data/test-docker-image.tar +[0001] DEBUG └── resolver: docker-engine + +--- diff --git a/cmd/dive/cli/testdata/snapshots/cli_config_test.snap b/cmd/dive/cli/testdata/snapshots/cli_config_test.snap new file mode 100755 index 0000000..b789068 --- /dev/null +++ b/cmd/dive/cli/testdata/snapshots/cli_config_test.snap @@ -0,0 +1,127 @@ + +[Test_Config - 1] +log: + # suppress all logging output (env: DIVE_LOG_QUIET) + quiet: false + + # explicitly set the logging level (available: [error warn info debug trace]) (env: DIVE_LOG_LEVEL) + level: 'warn' + + # file path to write logs to (env: DIVE_LOG_FILE) + file: '' + +dev: + # capture resource profiling data (available: [cpu, mem]) (env: DIVE_DEV_PROFILE) + profile: 'none' + +# container engine to use for image analysis (supported options: 'docker' and 'podman') (env: DIVE_CONTAINER_ENGINE) +container-engine: 'docker' + +# continue with analysis even if there are errors parsing the image archive (env: DIVE_IGNORE_ERRORS) +ignore-errors: false + +# enable CI mode (env: DIVE_CI) +ci: true + +# path to the CI config file (env: DIVE_CI_CONFIG) +ci-config: '.dive-ci' + +rules: + # lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_LOWEST_EFFICIENCY) + lowest-efficiency: '0.9' + + # highest allowable bytes wasted, otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_WASTED_BYTES) + highest-wasted-bytes: '20MB' + + # highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_USER_WASTED_PERCENT) + highest-user-wasted-percent: '0.90' + +# Skip the interactive TUI and write the layer analysis statistics to a given file. (env: DIVE_JSON_PATH) +json-path: '' + +keybinding: + # quit the application (global) (env: DIVE_KEYBINDING_QUIT) + quit: 'ctrl+c' + + # toggle between different views (global) (env: DIVE_KEYBINDING_TOGGLE_VIEW) + toggle-view: 'tab' + + # filter files by name (global) (env: DIVE_KEYBINDING_FILTER_FILES) + filter-files: 'ctrl+f, ctrl+slash' + + # close file filtering (global) (env: DIVE_KEYBINDING_CLOSE_FILTER_FILES) + close-filter-files: 'esc' + + # move cursor up (global) (env: DIVE_KEYBINDING_UP) + up: 'up,k' + + # move cursor down (global) (env: DIVE_KEYBINDING_DOWN) + down: 'down,j' + + # move cursor left (global) (env: DIVE_KEYBINDING_LEFT) + left: 'left,h' + + # move cursor right (global) (env: DIVE_KEYBINDING_RIGHT) + right: 'right,l' + + # scroll page up (file view) (env: DIVE_KEYBINDING_PAGE_UP) + page-up: 'pgup,u' + + # scroll page down (file view) (env: DIVE_KEYBINDING_PAGE_DOWN) + page-down: 'pgdn,d' + + # compare all layers (layer view) (env: DIVE_KEYBINDING_COMPARE_ALL) + compare-all: 'ctrl+a' + + # compare specific layer (layer view) (env: DIVE_KEYBINDING_COMPARE_LAYER) + compare-layer: 'ctrl+l' + + # toggle directory collapse (file view) (env: DIVE_KEYBINDING_TOGGLE_COLLAPSE_DIR) + toggle-collapse-dir: 'space' + + # toggle collapse all directories (file view) (env: DIVE_KEYBINDING_TOGGLE_COLLAPSE_ALL_DIR) + toggle-collapse-all-dir: 'ctrl+space' + + # toggle visibility of added files (file view) (env: DIVE_KEYBINDING_TOGGLE_ADDED_FILES) + toggle-added-files: 'ctrl+a' + + # toggle visibility of removed files (file view) (env: DIVE_KEYBINDING_TOGGLE_REMOVED_FILES) + toggle-removed-files: 'ctrl+r' + + # toggle visibility of modified files (file view) (env: DIVE_KEYBINDING_TOGGLE_MODIFIED_FILES) + toggle-modified-files: 'ctrl+m' + + # toggle visibility of unmodified files (file view) (env: DIVE_KEYBINDING_TOGGLE_UNMODIFIED_FILES) + toggle-unmodified-files: 'ctrl+u' + + # toggle display of file attributes (file view) (env: DIVE_KEYBINDING_TOGGLE_FILETREE_ATTRIBUTES) + toggle-filetree-attributes: 'ctrl+b' + + # toggle sort order (file view) (env: DIVE_KEYBINDING_TOGGLE_SORT_ORDER) + toggle-sort-order: 'ctrl+o' + + # (env: DIVE_KEYBINDING_TOGGLE_WRAP_TREE) + toggle-wrap-tree: 'ctrl+p' + + # extract file contents (file view) (env: DIVE_KEYBINDING_EXTRACT_FILE) + extract-file: 'ctrl+e' + +diff: + # types of file differences to hide (added, removed, modified, unmodified) (env: DIVE_DIFF_HIDE) + hide: [] + +filetree: + # collapse directories by default in the filetree (env: DIVE_FILETREE_COLLAPSE_DIR) + collapse-dir: false + + # percentage of screen width for the filetree pane (must be >0 and <1) (env: DIVE_FILETREE_PANE_WIDTH) + pane-width: 0.5 + + # show file attributes in the filetree view (env: DIVE_FILETREE_SHOW_ATTRIBUTES) + show-attributes: true + +layer: + # show aggregated changes across all previous layers (env: DIVE_LAYER_SHOW_AGGREGATED_CHANGES) + show-aggregated-changes: false + +--- diff --git a/cmd/dive/cli/testdata/snapshots/cli_json_test.snap b/cmd/dive/cli/testdata/snapshots/cli_json_test.snap new file mode 100755 index 0000000..d5b2a80 --- /dev/null +++ b/cmd/dive/cli/testdata/snapshots/cli_json_test.snap @@ -0,0 +1,4432 @@ + +[Test_JsonOutput/json_output - 1] +{ + "image": { + "efficiencyScore": 1, + "fileReference": [], + "inefficientBytes": 0, + "sizeBytes": 4277894 + }, + "layer": [ + { + "command": "BusyBox 1.37.0 (glibc), Debian 12", + "digestId": "sha256:068f50152bbc6e10c9d223150c9fbd30d11bcfd7789c432152aa0a99703bd03a", + "fileList": [ + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "bin/[", + "size": 1029688, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/[[", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/acpid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/add-shell", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/addgroup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/adduser", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/adjtimex", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ar", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/arch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/arp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/arping", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ascii", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ash", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/awk", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/base32", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/base64", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/basename", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/beep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/blkdiscard", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/blkid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/blockdev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bootchartd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/brctl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bunzip2", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/busybox", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bzcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/bzip2", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cal", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chgrp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chown", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chpasswd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chpst", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chroot", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chrt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/chvt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cksum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/clear", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cmp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/comm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/conspy", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cpio", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/crc32", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/crond", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/crontab", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cryptpw", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cttyhack", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/cut", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/date", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/deallocvt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/delgroup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/deluser", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/depmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/devmem", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/df", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dhcprelay", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/diff", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dirname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dmesg", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dnsd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dnsdomainname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dos2unix", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dpkg", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dpkg-deb", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/du", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dumpkmap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/dumpleases", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/echo", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ed", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/egrep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/eject", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/env", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/envdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/envuidgid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ether-wake", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/expand", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/expr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/factor", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fakeidentd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fallocate", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/false", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fatattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fbset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fbsplash", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fdflush", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fdformat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fdisk", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fgconsole", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fgrep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/find", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/findfs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/flock", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fold", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/free", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/freeramdisk", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsck", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsck.minix", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsfreeze", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fstrim", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fsync", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ftpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ftpget", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ftpput", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/fuser", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "bin/getconf", + "size": 27136, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/getfattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/getopt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/getty", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/grep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/groups", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/gunzip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/gzip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/halt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hdparm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/head", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hexdump", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hexedit", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hostid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hostname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/httpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hush", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/hwclock", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cdetect", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cdump", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cget", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2cset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/i2ctransfer", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/id", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifconfig", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifdown", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifenslave", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifplugd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ifup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/inetd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/init", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/insmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/install", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ionice", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iostat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipaddr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipcalc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipcrm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipcs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iplink", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ipneigh", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iproute", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iprule", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/iptunnel", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/kbd_mode", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/kill", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/killall", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/killall5", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/klogd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/last", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/less", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/link", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/linux32", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/linux64", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/linuxrc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ln", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/loadfont", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/loadkmap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/logger", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/login", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/logname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/logread", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/losetup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lpq", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lpr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ls", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsof", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lspci", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsscsi", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lsusb", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lzcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lzma", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/lzop", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/makedevs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/makemime", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/man", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/md5sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mdev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mesg", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/microcom", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mim", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkdosfs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mke2fs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfifo", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfs.ext2", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfs.minix", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkfs.vfat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mknod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkpasswd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mkswap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mktemp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/modinfo", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/modprobe", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/more", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mount", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mountpoint", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mpstat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/mv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nameif", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nanddump", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nandwrite", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nbd-client", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/netstat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nice", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nmeter", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nohup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nologin", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nproc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nsenter", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/nslookup", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ntpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/od", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/openvt", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/partprobe", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/passwd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/paste", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/patch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pgrep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pidof", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ping", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ping6", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pipe_progress", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pivot_root", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pkill", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pmap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/popmaildir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/poweroff", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/powertop", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/printenv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/printf", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ps", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pscan", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pstree", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pwd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/pwdx", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/raidautorun", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rdate", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rdev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/readahead", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/readlink", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/readprofile", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/realpath", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/reboot", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/reformime", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/remove-shell", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/renice", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/reset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/resize", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/resume", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rev", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rmdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rmmod", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/route", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rpm", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rpm2cpio", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rtcwake", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/run-init", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/run-parts", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/runlevel", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/runsv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/runsvdir", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/rx", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/script", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/scriptreplay", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sed", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/seedrng", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sendmail", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/seq", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setarch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setconsole", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setfattr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setfont", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setkeycodes", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setlogcons", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setpriv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setserial", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setsid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/setuidgid", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sh", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha1sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha256sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha3sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sha512sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/showkey", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/shred", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/shuf", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/slattach", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sleep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/smemcap", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/softlimit", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sort", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/split", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ssl_client", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/start-stop-daemon", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/stat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/strings", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/stty", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/su", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sulogin", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sum", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sv", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/svc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/svlogd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/svok", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/swapoff", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/swapon", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/switch_root", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sync", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/sysctl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/syslogd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tac", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tail", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tar", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/taskset", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tcpsvd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tee", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/telnet", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/telnetd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/test", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tftp", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tftpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/time", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/timeout", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/top", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/touch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tr", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/traceroute", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/traceroute6", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tree", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/true", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/truncate", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ts", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tsort", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tty", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ttysize", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/tunctl", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubiattach", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubidetach", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubimkvol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubirename", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubirmvol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubirsvol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/ubiupdatevol", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udhcpc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udhcpc6", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udhcpd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/udpsvd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uevent", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/umount", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unexpand", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uniq", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unix2dos", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unlink", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unlzma", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unshare", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unxz", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/unzip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uptime", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/users", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/usleep", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uudecode", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/uuencode", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/vconfig", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/vi", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/vlock", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/volname", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/w", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/wall", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/watch", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/watchdog", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/wc", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/wget", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/which", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/who", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/whoami", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/whois", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xargs", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xxd", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xz", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/xzcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/yes", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/zcat", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "bin/[", + "path": "bin/zcip", + "size": 0, + "typeFlag": 49, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "bin", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "dev", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/group", + "size": 306, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/localtime", + "size": 114, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-down.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-post-down.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-pre-up.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network/if-up.d", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc/network", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/nsswitch.conf", + "size": 494, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/passwd", + "size": 340, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 384, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "etc/shadow", + "size": 136, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "etc", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 65534, + "isDir": true, + "linkName": "", + "path": "home", + "size": 0, + "typeFlag": 53, + "uid": 65534 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/ld-linux-x86-64.so.2", + "size": 215000, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 493, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libc.so.6", + "size": 1922136, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libm.so.6", + "size": 911904, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libnss_compat.so.2", + "size": 39896, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libnss_dns.so.2", + "size": 14400, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libnss_files.so.2", + "size": 14400, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libnss_hesiod.so.2", + "size": 27136, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libpthread.so.0", + "size": 14480, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 420, + "gid": 0, + "isDir": false, + "linkName": "", + "path": "lib/libresolv.so.2", + "size": 60328, + "typeFlag": 48, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "lib", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 134218239, + "gid": 0, + "isDir": false, + "linkName": "lib", + "path": "lib64", + "size": 0, + "typeFlag": 50, + "uid": 0 + }, + { + "fileMode": 2147484096, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "root", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2148532735, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "tmp", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 134218239, + "gid": 0, + "isDir": false, + "linkName": "../../bin/env", + "path": "usr/bin/env", + "size": 0, + "typeFlag": 50, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "usr/bin", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 1, + "isDir": true, + "linkName": "", + "path": "usr/sbin", + "size": 0, + "typeFlag": 53, + "uid": 1 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "usr", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 8, + "isDir": true, + "linkName": "", + "path": "var/spool/mail", + "size": 0, + "typeFlag": 53, + "uid": 8 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "var/spool", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "var/www", + "size": 0, + "typeFlag": 53, + "uid": 0 + }, + { + "fileMode": 2147484141, + "gid": 0, + "isDir": true, + "linkName": "", + "path": "var", + "size": 0, + "typeFlag": 53, + "uid": 0 + } + ], + "id": "blobs", + "index": 0, + "sizeBytes": 4277894 + } + ] +} +--- diff --git a/cmd/dive/cli/testdata/snapshots/cli_load_test.snap b/cmd/dive/cli/testdata/snapshots/cli_load_test.snap new file mode 100755 index 0000000..c0182fd --- /dev/null +++ b/cmd/dive/cli/testdata/snapshots/cli_load_test.snap @@ -0,0 +1,144 @@ + +[Test_LoadImage/from_docker_engine - 1] +Loading image busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f +Analyzing image [layers:1 files:441 size:4.3 MB] +Evaluating image [rules: 3] + +Analysis: + efficiency: 100.00 % + wastedBytes: 0 bytes + userWastedPercent: 0 % + +Inefficient Files: (None) + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_LoadImage/from_docker_engine_(flag) - 1] +Loading image busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f +Analyzing image [layers:1 files:441 size:4.3 MB] +Evaluating image [rules: 3] + +Analysis: + efficiency: 100.00 % + wastedBytes: 0 bytes + userWastedPercent: 0 % + +Inefficient Files: (None) + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_LoadImage/from_podman_engine - 1] +Loading image busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f +Analyzing image [layers:1 files:441 size:4.3 MB] +Evaluating image [rules: 3] + +Analysis: + efficiency: 100.00 % + wastedBytes: 0 bytes + userWastedPercent: 0 % + +Inefficient Files: (None) + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_LoadImage/from_podman_engine_(flag) - 1] +Loading image busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f +Analyzing image [layers:1 files:441 size:4.3 MB] +Evaluating image [rules: 3] + +Analysis: + efficiency: 100.00 % + wastedBytes: 0 bytes + userWastedPercent: 0 % + +Inefficient Files: (None) + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_LoadImage/from_archive - 1] +Loading image /Users/wagoodman/code/dive/.data/test-docker-image.tar +Analyzing image [layers:14 files:451 size:1.2 MB] +Evaluating image [rules: 3] + +Analysis: + efficiency: 98.44 % + wastedBytes: 32025 bytes (32 kB) + userWastedPercent: 48.35 % + +Inefficient Files: + Count Wasted Space File Path + 2 13 kB /root/saved.txt + 2 13 kB /root/example/somefile1.txt + 2 6.4 kB /root/example/somefile3.txt + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_LoadImage/from_archive_(flag) - 1] +Loading image /Users/wagoodman/code/dive/.data/test-docker-image.tar +Analyzing image [layers:14 files:451 size:1.2 MB] +Evaluating image [rules: 3] + +Analysis: + efficiency: 98.44 % + wastedBytes: 32025 bytes (32 kB) + userWastedPercent: 48.35 % + +Inefficient Files: + Count Wasted Space File Path + 2 13 kB /root/saved.txt + 2 13 kB /root/example/somefile1.txt + 2 6.4 kB /root/example/somefile3.txt + +Evaluation: + PASS highestUserWastedPercent (0.90) + PASS highestWastedBytes (20MB) + PASS lowestEfficiency (0.9) + +PASS [pass:3] + +--- + +[Test_FetchFailure/nonexistent_image - 1] +Loading image docker:wagoodman/nonexistent/image:tag + +--- + +[Test_FetchFailure/invalid_image_name - 1] +Loading image /wagoodman/invalid:image:format + +--- diff --git a/main.go b/cmd/dive/main.go similarity index 61% rename from main.go rename to cmd/dive/main.go index ba3b90c..9cafad1 100644 --- a/main.go +++ b/cmd/dive/main.go @@ -1,3 +1,5 @@ +package main + // Copyright © 2018 Alex Goodman // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,24 +20,36 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -package main - import ( - "github.com/wagoodman/dive/cmd" + "github.com/anchore/clio" + "github.com/wagoodman/dive/cmd/dive/cli" ) +// applicationName is the non-capitalized name of the application (do not change this) +const ( + applicationName = "dive" + notProvided = "[not provided]" +) + +// TODO: these need to be wired up to the build flags +// all variables here are provided as build-time arguments, with clear default values var ( - version = "No version provided" - commit = "No commit provided" - buildTime = "No build timestamp provided" + version = notProvided + buildDate = notProvided + gitCommit = notProvided + gitDescription = notProvided ) func main() { - cmd.SetVersion(&cmd.Version{ - Version: version, - Commit: commit, - BuildTime: buildTime, - }) + app := cli.Application( + clio.Identification{ + Name: applicationName, + Version: version, + BuildDate: buildDate, + GitCommit: gitCommit, + GitDescription: gitDescription, + }, + ) - cmd.Execute() + app.Run() } diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index ad9f10b..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,220 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "path" - "strings" - - "github.com/mitchellh/go-homedir" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive" - "github.com/wagoodman/dive/dive/filetree" -) - -var cfgFile string -var exportFile string -var ciConfigFile string -var ciConfig = viper.New() -var isCi bool - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "dive [IMAGE]", - Short: "Docker Image Visualizer & Explorer", - Long: `This tool provides a way to discover and explore the contents of a docker image. Additionally the tool estimates -the amount of wasted space and identifies the offending files from the image.`, - Args: cobra.MaximumNArgs(1), - Run: doAnalyzeCmd, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - initCli() - cobra.OnInitialize(initConfig) -} - -func initCli() { - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)") - rootCmd.PersistentFlags().String("source", "docker", "The container engine to fetch the image from. Allowed values: "+strings.Join(dive.ImageSources, ", ")) - rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number") - rootCmd.PersistentFlags().BoolP("ignore-errors", "i", false, "ignore image parsing errors and run the analysis anyway") - rootCmd.Flags().BoolVar(&isCi, "ci", false, "Skip the interactive TUI and validate against CI rules (same as env var CI=true)") - rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.") - rootCmd.Flags().StringVar(&ciConfigFile, "ci-config", ".dive-ci", "If CI=true in the environment, use the given yaml to drive validation rules.") - - rootCmd.Flags().String("lowestEfficiency", "0.9", "(only valid with --ci given) lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail.") - rootCmd.Flags().String("highestWastedBytes", "disabled", "(only valid with --ci given) highest allowable bytes wasted, otherwise CI validation will fail.") - rootCmd.Flags().String("highestUserWastedPercent", "0.1", "(only valid with --ci given) highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail.") - - for _, key := range []string{"lowestEfficiency", "highestWastedBytes", "highestUserWastedPercent"} { - if err := ciConfig.BindPFlag(fmt.Sprintf("rules.%s", key), rootCmd.Flags().Lookup(key)); err != nil { - log.Fatalf("Unable to bind '%s' flag: %v", key, err) - } - } - - if err := ciConfig.BindPFlag("ignore-errors", rootCmd.PersistentFlags().Lookup("ignore-errors")); err != nil { - log.Fatalf("Unable to bind 'ignore-errors' flag: %v", err) - } -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - var err error - - viper.SetDefault("log.level", log.InfoLevel.String()) - viper.SetDefault("log.path", "./dive.log") - viper.SetDefault("log.enabled", false) - // keybindings: status view / global - viper.SetDefault("keybinding.quit", "ctrl+c,q") - viper.SetDefault("keybinding.toggle-view", "tab") - viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash") - viper.SetDefault("keybinding.close-filter-files", "esc") - // keybindings: layer view - viper.SetDefault("keybinding.compare-all", "ctrl+a") - viper.SetDefault("keybinding.compare-layer", "ctrl+l") - // keybindings: filetree view - viper.SetDefault("keybinding.toggle-collapse-dir", "space") - viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space") - viper.SetDefault("keybinding.toggle-sort-order", "ctrl+o") - viper.SetDefault("keybinding.toggle-filetree-attributes", "ctrl+b") - viper.SetDefault("keybinding.toggle-added-files", "ctrl+a") - viper.SetDefault("keybinding.toggle-removed-files", "ctrl+r") - viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m") - viper.SetDefault("keybinding.toggle-unmodified-files", "ctrl+u") - viper.SetDefault("keybinding.toggle-wrap-tree", "ctrl+p") - viper.SetDefault("keybinding.extract-file", "ctrl+e") - viper.SetDefault("keybinding.page-up", "pgup,u") - viper.SetDefault("keybinding.page-down", "pgdn,d") - viper.SetDefault("keybinding.up", "up,k") - viper.SetDefault("keybinding.down", "down,j") - viper.SetDefault("keybinding.left", "left,h") - viper.SetDefault("keybinding.right", "right,l") - - viper.SetDefault("diff.hide", "") - - viper.SetDefault("layer.show-aggregated-changes", false) - - viper.SetDefault("filetree.collapse-dir", false) - viper.SetDefault("filetree.pane-width", 0.5) - viper.SetDefault("filetree.show-attributes", true) - - viper.SetDefault("container-engine", "docker") - viper.SetDefault("ignore-errors", false) - - err = viper.BindPFlag("source", rootCmd.PersistentFlags().Lookup("source")) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - viper.SetEnvPrefix("DIVE") - // replace all - with _ when looking for matching environment variables - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - viper.AutomaticEnv() - - // if config files are present, load them - if cfgFile == "" { - // default configs are ignored if not found - filepathToCfg := getDefaultCfgFile() - viper.SetConfigFile(filepathToCfg) - } else { - viper.SetConfigFile(cfgFile) - } - err = viper.ReadInConfig() - if err == nil { - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } else if cfgFile != "" { - fmt.Println(err) - os.Exit(0) - } - - // set global defaults (for performance) - filetree.GlobalFileTreeCollapse = viper.GetBool("filetree.collapse-dir") -} - -// initLogging sets up the logging object with a formatter and location -func initLogging() { - var logFileObj *os.File - var err error - - if viper.GetBool("log.enabled") { - logFileObj, err = os.OpenFile(viper.GetString("log.path"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) - log.SetOutput(logFileObj) - } else { - log.SetOutput(io.Discard) - } - - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - - Formatter := new(log.TextFormatter) - Formatter.DisableTimestamp = true - log.SetFormatter(Formatter) - - level, err := log.ParseLevel(viper.GetString("log.level")) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - - log.SetLevel(level) - log.Debug("Starting Dive...") - log.Debugf("config filepath: %s", viper.ConfigFileUsed()) - for k, v := range viper.AllSettings() { - log.Debug("config value: ", k, " : ", v) - } -} - -// getDefaultCfgFile checks for config file in paths from xdg specs -// and in $HOME/.config/dive/ directory -// defaults to $HOME/.dive.yaml -func getDefaultCfgFile() string { - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(0) - } - - xdgHome := os.Getenv("XDG_CONFIG_HOME") - xdgDirs := os.Getenv("XDG_CONFIG_DIRS") - xdgPaths := append([]string{xdgHome}, strings.Split(xdgDirs, ":")...) - allDirs := append(xdgPaths, path.Join(home, ".config")) - - for _, val := range allDirs { - file := findInPath(val) - if len(file) > 0 { - return file - } - } - return path.Join(home, ".dive.yaml") -} - -// findInPath returns first "*.yaml" file in path's subdirectory "dive" -// if not found returns empty string -func findInPath(pathTo string) string { - directory := path.Join(pathTo, "dive") - files, err := os.ReadDir(directory) - if err != nil { - return "" - } - - for _, file := range files { - filename := file.Name() - if path.Ext(filename) == ".yaml" || path.Ext(filename) == ".yml" { - return path.Join(directory, filename) - } - } - return "" -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 1889d11..0000000 --- a/cmd/version.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -type Version struct { - Version string - Commit string - BuildTime string -} - -var version *Version - -// versionCmd represents the version command -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print the version number and exit (also --version)", - Run: printVersion, -} - -func init() { - rootCmd.AddCommand(versionCmd) -} - -func SetVersion(v *Version) { - version = v -} - -func printVersion(cmd *cobra.Command, args []string) { - fmt.Printf("dive %s\n", version.Version) -} diff --git a/dive/filetree/comparer.go b/dive/filetree/comparer.go index 3e0ea25..a3a5674 100644 --- a/dive/filetree/comparer.go +++ b/dive/filetree/comparer.go @@ -2,8 +2,6 @@ package filetree import ( "fmt" - - "github.com/sirupsen/logrus" ) type TreeIndexKey struct { @@ -78,8 +76,7 @@ func (cmp *Comparer) get(key TreeIndexKey) (*FileTree, []PathError, error) { markPathErrors, err := newTree.CompareAndMark(cmp.refTrees[idx]) pathErrors = append(pathErrors, markPathErrors...) if err != nil { - logrus.Errorf("error while building tree: %+v", err) - return nil, nil, err + return nil, nil, fmt.Errorf("failed to build tree: %w", err) } } return newTree, pathErrors, nil diff --git a/dive/filetree/efficiency.go b/dive/filetree/efficiency.go index 7713b1e..02035a6 100644 --- a/dive/filetree/efficiency.go +++ b/dive/filetree/efficiency.go @@ -1,9 +1,9 @@ package filetree import ( + "fmt" + "github.com/wagoodman/dive/internal/log" "sort" - - "github.com/sirupsen/logrus" ) // EfficiencyData represents the storage and reference statistics for a given file tree path. @@ -65,12 +65,11 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) { stackedTree, failedPaths, err := StackTreeRange(trees, 0, currentTree-1) if len(failedPaths) > 0 { for _, path := range failedPaths { - logrus.Errorf("%s", path.String()) + log.WithFields("path", path.String()).Debug("unable to include path in stacked tree") } } if err != nil { - logrus.Errorf("unable to stack tree range: %+v", err) - return err + return fmt.Errorf("unable to stack tree range: %w", err) } previousTreeNode, err := stackedTree.GetNode(node.Path()) @@ -81,8 +80,7 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) { if previousTreeNode.Data.FileInfo.IsDir { err = previousTreeNode.VisitDepthChildFirst(sizer, nil, nil) if err != nil { - logrus.Errorf("unable to propagate whiteout dir: %+v", err) - return err + return fmt.Errorf("unable to propagate whiteout dir: %w", err) } } } else { @@ -108,7 +106,7 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) { currentTree = idx err := tree.VisitDepthChildFirst(visitor, visitEvaluator) if err != nil { - logrus.Errorf("unable to propagate ref tree: %+v", err) + log.WithFields("layer", tree.Id, "error", err).Debug("unable to propagate layer tree") } } diff --git a/dive/filetree/file_info.go b/dive/filetree/file_info.go index 3fc8786..bd3e563 100644 --- a/dive/filetree/file_info.go +++ b/dive/filetree/file_info.go @@ -2,11 +2,11 @@ package filetree import ( "archive/tar" + "fmt" "io" "os" "github.com/cespare/xxhash/v2" - "github.com/sirupsen/logrus" ) // FileInfo contains tar metadata for a specific FileNode @@ -55,7 +55,7 @@ func NewFileInfo(realPath, path string, info os.FileInfo) FileInfo { linkName, err = os.Readlink(realPath) if err != nil { - logrus.Panic("unable to read link:", realPath, err) + panic(fmt.Errorf("unable to read symlink %q: %s", realPath, err)) } } else if info.IsDir() { fileType = tar.TypeDir @@ -69,7 +69,7 @@ func NewFileInfo(realPath, path string, info os.FileInfo) FileInfo { if fileType != tar.TypeDir { file, err := os.Open(realPath) if err != nil { - logrus.Panic("unable to read file:", realPath) + panic(fmt.Errorf("unable to open file %q: %s", realPath, err)) } defer file.Close() hash = getHashFromReader(file) @@ -127,7 +127,7 @@ func getHashFromReader(reader io.Reader) uint64 { for { n, err := reader.Read(buf) if err != nil && err != io.EOF { - logrus.Panic(err) + panic(fmt.Errorf("unable to read file: %w", err)) } if n == 0 { break @@ -135,7 +135,7 @@ func getHashFromReader(reader io.Reader) uint64 { _, err = h.Write(buf[:n]) if err != nil { - logrus.Panic(err) + panic(fmt.Errorf("unable to write to hash: %w", err)) } } diff --git a/dive/filetree/file_node.go b/dive/filetree/file_node.go index 88ff29d..619fcca 100644 --- a/dive/filetree/file_node.go +++ b/dive/filetree/file_node.go @@ -3,12 +3,12 @@ package filetree import ( "archive/tar" "fmt" + "github.com/wagoodman/dive/internal/log" "strings" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/phayes/permbits" - "github.com/sirupsen/logrus" ) const ( @@ -199,7 +199,7 @@ func (node *FileNode) GetSize() int64 { } err := node.VisitDepthChildFirst(sizer, nil, nil) if err != nil { - logrus.Errorf("unable to propagate node for metadata: %+v", err) + log.WithFields("error", err).Debug("unable to propagate tree to get file size") } } node.Size = sizeBytes diff --git a/dive/filetree/file_tree.go b/dive/filetree/file_tree.go index d74a350..b90d5d9 100644 --- a/dive/filetree/file_tree.go +++ b/dive/filetree/file_tree.go @@ -2,11 +2,11 @@ package filetree import ( "fmt" + "github.com/wagoodman/dive/internal/log" "path" "strings" "github.com/google/uuid" - "github.com/sirupsen/logrus" ) const ( @@ -146,7 +146,7 @@ func (tree *FileTree) VisibleSize() int { } err := tree.VisitDepthParentFirst(visitor, visitEvaluator) if err != nil { - logrus.Errorf("unable to determine visible tree size: %+v", err) + log.WithFields("error", err).Debug("unable to determine visible tree size") } // don't include root @@ -180,7 +180,7 @@ func (tree *FileTree) Copy() *FileTree { }, nil) if err != nil { - logrus.Errorf("unable to propagate tree on copy(): %+v", err) + log.WithFields("error", err).Debug("unable to propagate tree on copy") } return newTree @@ -383,8 +383,7 @@ func StackTreeRange(trees []*FileTree, start, stop int) (*FileTree, []PathError, errors = append(errors, failedPaths...) } if err != nil { - logrus.Errorf("could not stack tree range: %v", err) - return nil, nil, err + return nil, nil, fmt.Errorf("could not stack tree range: %w", err) } } return tree, errors, nil diff --git a/dive/filetree/file_tree_test.go b/dive/filetree/file_tree_test.go index c1bc37c..d4fc4a0 100644 --- a/dive/filetree/file_tree_test.go +++ b/dive/filetree/file_tree_test.go @@ -2,6 +2,7 @@ package filetree import ( "fmt" + "github.com/stretchr/testify/assert" "testing" ) @@ -798,9 +799,7 @@ func TestStackRange(t *testing.T) { if len(failedPaths) > 0 { t.Errorf("expected no filepath errors, got %d", len(failedPaths)) } - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) } func TestRemoveOnIterate(t *testing.T) { diff --git a/dive/image/analysis.go b/dive/image/analysis.go new file mode 100644 index 0000000..d2b0f04 --- /dev/null +++ b/dive/image/analysis.go @@ -0,0 +1,47 @@ +package image + +import ( + "context" + "github.com/wagoodman/dive/dive/filetree" +) + +type Analysis struct { + Image string + Layers []*Layer + RefTrees []*filetree.FileTree + Efficiency float64 + SizeBytes uint64 + UserSizeByes uint64 // this is all bytes except for the base image + WastedUserPercent float64 // = wasted-bytes/user-size-bytes + WastedBytes uint64 + Inefficiencies filetree.EfficiencySlice +} + +func Analyze(ctx context.Context, img *Image) (*Analysis, error) { + efficiency, inefficiencies := filetree.Efficiency(img.Trees) + var sizeBytes, userSizeBytes uint64 + + for i, v := range img.Layers { + sizeBytes += v.Size + if i != 0 { + userSizeBytes += v.Size + } + } + + var wastedBytes uint64 + for _, file := range inefficiencies { + wastedBytes += uint64(file.CumulativeSize) + } + + return &Analysis{ + Image: img.Request, + Layers: img.Layers, + RefTrees: img.Trees, + Efficiency: efficiency, + UserSizeByes: userSizeBytes, + SizeBytes: sizeBytes, + WastedBytes: wastedBytes, + WastedUserPercent: float64(wastedBytes) / float64(userSizeBytes), + Inefficiencies: inefficiencies, + }, nil +} diff --git a/dive/image/analyzer.go b/dive/image/analyzer.go deleted file mode 100644 index 58b8da8..0000000 --- a/dive/image/analyzer.go +++ /dev/null @@ -1,20 +0,0 @@ -package image - -import ( - "github.com/wagoodman/dive/dive/filetree" -) - -type Analyzer interface { - Analyze() (*AnalysisResult, error) -} - -type AnalysisResult struct { - Layers []*Layer - RefTrees []*filetree.FileTree - Efficiency float64 - SizeBytes uint64 - UserSizeByes uint64 // this is all bytes except for the base image - WastedUserPercent float64 // = wasted-bytes/user-size-bytes - WastedBytes uint64 - Inefficiencies filetree.EfficiencySlice -} diff --git a/dive/image/docker/archive_resolver.go b/dive/image/docker/archive_resolver.go index 5404837..41f9a58 100644 --- a/dive/image/docker/archive_resolver.go +++ b/dive/image/docker/archive_resolver.go @@ -1,6 +1,7 @@ package docker import ( + "context" "fmt" "os" @@ -18,7 +19,7 @@ func (r *archiveResolver) Name() string { return "docker-archive" } -func (r *archiveResolver) Fetch(path string) (*image.Image, error) { +func (r *archiveResolver) Fetch(ctx context.Context, path string) (*image.Image, error) { reader, err := os.Open(path) if err != nil { return nil, err @@ -29,13 +30,13 @@ func (r *archiveResolver) Fetch(path string) (*image.Image, error) { if err != nil { return nil, err } - return img.ToImage() + return img.ToImage(path) } -func (r *archiveResolver) Build(args []string) (*image.Image, error) { +func (r *archiveResolver) Build(ctx context.Context, args []string) (*image.Image, error) { return nil, fmt.Errorf("build option not supported for docker archive resolver") } -func (r *archiveResolver) Extract(id string, l string, p string) error { +func (r *archiveResolver) Extract(ctx context.Context, id string, l string, p string) error { return fmt.Errorf("not implemented") } diff --git a/dive/image/docker/cli.go b/dive/image/docker/cli.go index a1d3689..f84b971 100644 --- a/dive/image/docker/cli.go +++ b/dive/image/docker/cli.go @@ -2,20 +2,25 @@ package docker import ( "fmt" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" "os" "os/exec" - - "github.com/wagoodman/dive/utils" + "strings" ) // runDockerCmd runs a given Docker command in the current tty func runDockerCmd(cmdStr string, args ...string) error { + if !isDockerClientBinaryAvailable() { return fmt.Errorf("cannot find docker client executable") } allArgs := utils.CleanArgs(append([]string{cmdStr}, args...)) + fullCmd := strings.Join(append([]string{"docker"}, allArgs...), " ") + log.WithFields("cmd", fullCmd).Trace("executing") + cmd := exec.Command("docker", allArgs...) cmd.Env = os.Environ() diff --git a/dive/image/docker/config.go b/dive/image/docker/config.go index 6167a21..b4d13e0 100644 --- a/dive/image/docker/config.go +++ b/dive/image/docker/config.go @@ -2,8 +2,7 @@ package docker import ( "encoding/json" - - "github.com/sirupsen/logrus" + "fmt" ) type config struct { @@ -29,7 +28,7 @@ func newConfig(configBytes []byte) config { var imageConfig config err := json.Unmarshal(configBytes, &imageConfig) if err != nil { - logrus.Panic(err) + panic(fmt.Errorf("failed to unmarshal docker config: %w", err)) } layerIdx := 0 diff --git a/dive/image/docker/engine_resolver.go b/dive/image/docker/engine_resolver.go index 11f7c0d..0068d09 100644 --- a/dive/image/docker/engine_resolver.go +++ b/dive/image/docker/engine_resolver.go @@ -3,6 +3,8 @@ package docker import ( "fmt" "github.com/spf13/afero" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/dive/internal/log" "io" "net/http" "os" @@ -29,8 +31,8 @@ func (r *engineResolver) Name() string { return "docker-engine" } -func (r *engineResolver) Fetch(id string) (*image.Image, error) { - reader, err := r.fetchArchive(id) +func (r *engineResolver) Fetch(ctx context.Context, id string) (*image.Image, error) { + reader, err := r.fetchArchive(ctx, id) if err != nil { return nil, err } @@ -40,19 +42,19 @@ func (r *engineResolver) Fetch(id string) (*image.Image, error) { if err != nil { return nil, err } - return img.ToImage() + return img.ToImage(id) } -func (r *engineResolver) Build(args []string) (*image.Image, error) { +func (r *engineResolver) Build(ctx context.Context, args []string) (*image.Image, error) { id, err := buildImageFromCli(afero.NewOsFs(), args) if err != nil { return nil, err } - return r.Fetch(id) + return r.Fetch(ctx, id) } -func (r *engineResolver) Extract(id string, l string, p string) error { - reader, err := r.fetchArchive(id) +func (r *engineResolver) Extract(ctx context.Context, id string, l string, p string) error { + reader, err := r.fetchArchive(ctx, id) if err != nil { return err } @@ -64,16 +66,13 @@ func (r *engineResolver) Extract(id string, l string, p string) error { return fmt.Errorf("unable to extract from image '%s': %+v", id, err) } -func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) { +func (r *engineResolver) fetchArchive(ctx context.Context, id string) (io.ReadCloser, error) { var err error var dockerClient *client.Client - // pull the engineResolver if it does not exist - ctx := context.Background() - host, err := determineDockerHost() if err != nil { - fmt.Printf("> could not determine docker host: %v\n", err) + return nil, fmt.Errorf("could not determine docker host: %v", err) } clientOpts := []client.Opt{client.FromEnv} clientOpts = append(clientOpts, client.WithHost(host)) @@ -82,7 +81,7 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) { case "ssh": helper, err := connhelper.GetConnectionHelper(host) if err != nil { - fmt.Println("docker host", err) + return nil, fmt.Errorf("failed to get docker connection helper: %w", err) } clientOpts = append(clientOpts, func(c *client.Client) error { httpClient := &http.Client{ @@ -112,7 +111,13 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) { if err != nil { // check if the error is due to the image not existing locally if client.IsErrNotFound(err) { - fmt.Println("The image is not available locally. Trying to pull '" + id + "'...") + mon := payload.GetGenericProgressFromContext(ctx) + if mon != nil { + mon.AtomicStage.Set("attempting to pull") + log.Debugf("the image is not available locally, pulling %q", id) + } else { + log.Infof("the image is not available locally, pulling %q", id) + } err = runDockerCmd("pull", id) if err != nil { return nil, err diff --git a/dive/image/docker/image_archive.go b/dive/image/docker/image_archive.go index 4abdb2c..51d992e 100644 --- a/dive/image/docker/image_archive.go +++ b/dive/image/docker/image_archive.go @@ -247,7 +247,7 @@ func getFileList(tarReader *tar.Reader) ([]filetree.FileInfo, error) { return files, nil } -func (img *ImageArchive) ToImage() (*image.Image, error) { +func (img *ImageArchive) ToImage(id string) (*image.Image, error) { trees := make([]*filetree.FileTree, 0) // build the content tree @@ -294,8 +294,9 @@ func (img *ImageArchive) ToImage() (*image.Image, error) { } return &image.Image{ - Trees: trees, - Layers: layers, + Request: id, + Trees: trees, + Layers: layers, }, nil } diff --git a/dive/image/docker/manifest.go b/dive/image/docker/manifest.go index 3a4e9af..6ad18ef 100644 --- a/dive/image/docker/manifest.go +++ b/dive/image/docker/manifest.go @@ -2,8 +2,7 @@ package docker import ( "encoding/json" - - "github.com/sirupsen/logrus" + "fmt" ) type manifest struct { @@ -16,7 +15,7 @@ func newManifest(manifestBytes []byte) manifest { var manifest []manifest err := json.Unmarshal(manifestBytes, &manifest) if err != nil { - logrus.Panic(err) + panic(fmt.Errorf("failed to unmarshal manifest: %w", err)) } return manifest[0] } diff --git a/dive/image/docker/testing.go b/dive/image/docker/testing.go index f8d8817..12808b2 100644 --- a/dive/image/docker/testing.go +++ b/dive/image/docker/testing.go @@ -1,13 +1,16 @@ package docker import ( + "github.com/stretchr/testify/require" + "golang.org/x/net/context" "os" "testing" "github.com/wagoodman/dive/dive/image" ) -func TestLoadArchive(tarPath string) (*ImageArchive, error) { +func TestLoadArchive(t testing.TB, tarPath string) (*ImageArchive, error) { + t.Helper() f, err := os.Open(tarPath) if err != nil { return nil, err @@ -17,20 +20,15 @@ func TestLoadArchive(tarPath string) (*ImageArchive, error) { return NewImageArchive(f) } -func TestAnalysisFromArchive(t *testing.T, path string) *image.AnalysisResult { - archive, err := TestLoadArchive(path) - if err != nil { - t.Fatalf("unable to fetch archive: %v", err) - } +func TestAnalysisFromArchive(t testing.TB, path string) *image.Analysis { + t.Helper() + archive, err := TestLoadArchive(t, path) + require.NoError(t, err, "unable to load archive") - img, err := archive.ToImage() - if err != nil { - t.Fatalf("unable to convert to image: %v", err) - } + img, err := archive.ToImage(path) + require.NoError(t, err, "unable to convert archive to image") - result, err := img.Analyze() - if err != nil { - t.Fatalf("unable to analyze: %v", err) - } + result, err := image.Analyze(context.Background(), img) + require.NoError(t, err, "unable to analyze image") return result } diff --git a/dive/image/image.go b/dive/image/image.go index 04a1be2..82eddd4 100644 --- a/dive/image/image.go +++ b/dive/image/image.go @@ -5,34 +5,7 @@ import ( ) type Image struct { - Trees []*filetree.FileTree - Layers []*Layer -} - -func (img *Image) Analyze() (*AnalysisResult, error) { - efficiency, inefficiencies := filetree.Efficiency(img.Trees) - var sizeBytes, userSizeBytes uint64 - - for i, v := range img.Layers { - sizeBytes += v.Size - if i != 0 { - userSizeBytes += v.Size - } - } - - var wastedBytes uint64 - for _, file := range inefficiencies { - wastedBytes += uint64(file.CumulativeSize) - } - - return &AnalysisResult{ - Layers: img.Layers, - RefTrees: img.Trees, - Efficiency: efficiency, - UserSizeByes: userSizeBytes, - SizeBytes: sizeBytes, - WastedBytes: wastedBytes, - WastedUserPercent: float64(wastedBytes) / float64(userSizeBytes), - Inefficiencies: inefficiencies, - }, nil + Request string + Trees []*filetree.FileTree + Layers []*Layer } diff --git a/dive/image/podman/build.go b/dive/image/podman/build.go index 17c188a..fac996c 100644 --- a/dive/image/podman/build.go +++ b/dive/image/podman/build.go @@ -1,5 +1,4 @@ //go:build linux || darwin -// +build linux darwin package podman diff --git a/dive/image/podman/cli.go b/dive/image/podman/cli.go index 73d1e0a..aff8679 100644 --- a/dive/image/podman/cli.go +++ b/dive/image/podman/cli.go @@ -1,15 +1,15 @@ //go:build linux || darwin -// +build linux darwin package podman import ( "fmt" + "github.com/wagoodman/dive/internal/log" + "github.com/wagoodman/dive/internal/utils" "io" "os" "os/exec" - - "github.com/wagoodman/dive/utils" + "strings" ) // runPodmanCmd runs a given Podman command in the current tty @@ -20,6 +20,9 @@ func runPodmanCmd(cmdStr string, args ...string) error { allArgs := utils.CleanArgs(append([]string{cmdStr}, args...)) + fullCmd := strings.Join(append([]string{"docker"}, allArgs...), " ") + log.WithFields("cmd", fullCmd).Trace("executing") + cmd := exec.Command("podman", allArgs...) cmd.Env = os.Environ() @@ -35,7 +38,11 @@ func streamPodmanCmd(args ...string) (error, io.Reader) { return fmt.Errorf("cannot find podman client executable"), nil } - cmd := exec.Command("podman", utils.CleanArgs(args)...) + allArgs := utils.CleanArgs(args) + fullCmd := strings.Join(append([]string{"docker"}, allArgs...), " ") + log.WithFields("cmd", fullCmd).Trace("executing (streaming)") + + cmd := exec.Command("podman", allArgs...) cmd.Env = os.Environ() reader, writer, err := os.Pipe() diff --git a/dive/image/podman/resolver.go b/dive/image/podman/resolver.go index 80852a6..7bdf23f 100644 --- a/dive/image/podman/resolver.go +++ b/dive/image/podman/resolver.go @@ -1,9 +1,9 @@ //go:build linux || darwin -// +build linux darwin package podman import ( + "context" "fmt" "io" @@ -22,15 +22,15 @@ func (r *resolver) Name() string { return "podman" } -func (r *resolver) Build(args []string) (*image.Image, error) { +func (r *resolver) Build(ctx context.Context, args []string) (*image.Image, error) { id, err := buildImageFromCli(args) if err != nil { return nil, err } - return r.Fetch(id) + return r.Fetch(ctx, id) } -func (r *resolver) Fetch(id string) (*image.Image, error) { +func (r *resolver) Fetch(ctx context.Context, id string) (*image.Image, error) { // todo: add podman fetch attempt via varlink first... img, err := r.resolveFromDockerArchive(id) @@ -38,10 +38,10 @@ func (r *resolver) Fetch(id string) (*image.Image, error) { return img, err } - return nil, fmt.Errorf("unable to resolve image '%s': %+v", id, err) + return nil, fmt.Errorf("unable to resolve image %q: %+v", id, err) } -func (r *resolver) Extract(id string, l string, p string) error { +func (r *resolver) Extract(ctx context.Context, id string, l string, p string) error { // todo: add podman fetch attempt via varlink first... err, reader := streamPodmanCmd("image", "save", id) @@ -53,7 +53,7 @@ func (r *resolver) Extract(id string, l string, p string) error { return nil } - return fmt.Errorf("unable to extract from image '%s': %+v", id, err) + return fmt.Errorf("unable to extract from image %q: %+v", id, err) } func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) { @@ -66,5 +66,5 @@ func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) { if err != nil { return nil, err } - return img.ToImage() + return img.ToImage(id) } diff --git a/dive/image/podman/resolver_unsupported.go b/dive/image/podman/resolver_unsupported.go index 120f62e..36afd59 100644 --- a/dive/image/podman/resolver_unsupported.go +++ b/dive/image/podman/resolver_unsupported.go @@ -4,6 +4,7 @@ package podman import ( + "context" "fmt" "github.com/wagoodman/dive/dive/image" @@ -19,14 +20,14 @@ func NewResolverFromEngine() *resolver { func (r *resolver) Name() string { return "podman" } -func (r *resolver) Build(args []string) (*image.Image, error) { +func (r *resolver) Build(ctx context.Context, args []string) (*image.Image, error) { return nil, fmt.Errorf("unsupported platform") } -func (r *resolver) Fetch(id string) (*image.Image, error) { +func (r *resolver) Fetch(ctx context.Context, id string) (*image.Image, error) { return nil, fmt.Errorf("unsupported platform") } -func (r *resolver) Extract(id string, l string, p string) error { +func (r *resolver) Extract(ctx context.Context, id string, l string, p string) error { return fmt.Errorf("unsupported platform") } diff --git a/dive/image/resolver.go b/dive/image/resolver.go index f3999b9..70ee5ce 100644 --- a/dive/image/resolver.go +++ b/dive/image/resolver.go @@ -1,8 +1,14 @@ package image +import "golang.org/x/net/context" + type Resolver interface { Name() string - Fetch(id string) (*Image, error) - Build(options []string) (*Image, error) - Extract(id string, layer string, path string) error + Fetch(ctx context.Context, id string) (*Image, error) + Build(ctx context.Context, options []string) (*Image, error) + ContentReader +} + +type ContentReader interface { + Extract(ctx context.Context, id string, layer string, path string) error } diff --git a/go.mod b/go.mod index 40cc911..79f8fb9 100644 --- a/go.mod +++ b/go.mod @@ -3,77 +3,114 @@ module github.com/wagoodman/dive go 1.24 require ( + github.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872 + github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 github.com/awesome-gocui/gocui v1.1.0 github.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f github.com/cespare/xxhash/v2 v2.3.0 + github.com/charmbracelet/lipgloss v1.1.0 github.com/docker/cli v28.0.4+incompatible github.com/docker/docker v28.0.4+incompatible github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.18.0 + github.com/gkampitakis/go-snaps v0.5.11 + github.com/google/go-cmp v0.7.0 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.0 - github.com/logrusorgru/aurora/v4 v4.0.0 github.com/lunixbochs/vtclean v1.0.0 - github.com/mitchellh/go-homedir v1.1.0 + github.com/muesli/termenv v0.16.0 github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee github.com/scylladb/go-set v1.0.2 - github.com/sergi/go-diff v1.3.1 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.14.0 github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 + github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 + github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 + go.uber.org/atomic v1.9.0 golang.org/x/net v0.38.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe // indirect + github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.4.0 // indirect + github.com/gkampitakis/ciinfo v0.3.1 // indirect + github.com/gkampitakis/go-diff v1.3.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-yaml v1.15.13 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.0.3 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/maruel/natural v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.1.0 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.20.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.31.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect go.opentelemetry.io/otel/sdk v1.31.0 // indirect go.opentelemetry.io/otel/trace v1.31.0 // indirect - go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.11.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.0 // indirect ) diff --git a/go.sum b/go.sum index 126c060..c350321 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,53 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872 h1:iEF0xhHUuh3J8FrlPsZAQVaMpTa2j4lvLRI5XrXzge4= +github.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872/go.mod h1:Utb9i4kwiCWvqAIxZaJeMIXFO9uOgQXlvH2BfbfO/zI= +github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe h1:qv/xxpjF5RdKPqZjx8RM0aBi3HUCAO0DhRBMs2xhY1I= +github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe/go.mod h1:vrcYMDps9YXwwx2a9AsvipM6Fi5H9//9bymGb8G8BIQ= +github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= +github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= +github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= +github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/awesome-gocui/gocui v0.5.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII= github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= github.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f h1:u5xQfLwWC98BFToYDifqEcgK2ht2FFlbvRlzRnMb0cQ= github.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag= github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/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 v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= @@ -36,6 +66,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -48,6 +80,12 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo= +github.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.11 h1:LFG0ggUKR+KEiiaOvFCmLgJ5NO2zf93AxxddkBn3LdQ= +github.com/gkampitakis/go-snaps v0.5.11/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -56,14 +94,30 @@ 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-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg= +github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -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/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -78,50 +132,64 @@ 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/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= -github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= -github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 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.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= +github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/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/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE= github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -141,14 +209,35 @@ github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 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.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -174,6 +263,8 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV 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/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -191,6 +282,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -225,6 +317,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/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= diff --git a/internal/bus/bus.go b/internal/bus/bus.go new file mode 100644 index 0000000..fe455f3 --- /dev/null +++ b/internal/bus/bus.go @@ -0,0 +1,19 @@ +package bus + +import "github.com/wagoodman/go-partybus" + +var publisher partybus.Publisher + +func Set(p partybus.Publisher) { + publisher = p +} + +func Get() partybus.Publisher { + return publisher +} + +func Publish(e partybus.Event) { + if publisher != nil { + publisher.Publish(e) + } +} diff --git a/internal/bus/event/event.go b/internal/bus/event/event.go new file mode 100644 index 0000000..75d8a97 --- /dev/null +++ b/internal/bus/event/event.go @@ -0,0 +1,21 @@ +package event + +import ( + "github.com/wagoodman/go-partybus" +) + +const ( + typePrefix = "dive-cli" + + // TaskStarted encompasses all events that are related to the analysis of a docker image (build, fetch, analyze) + TaskStarted partybus.EventType = typePrefix + "-task-started" + + // ExploreAnalysis is a partybus event that occurs when an analysis result is ready for presentation to stdout + ExploreAnalysis partybus.EventType = typePrefix + "-analysis" + + // Report is a partybus event that occurs when an analysis result is ready for final presentation to stdout + Report partybus.EventType = typePrefix + "-report" + + // Notification is a partybus event that occurs when auxiliary information is ready for presentation to stderr + Notification partybus.EventType = typePrefix + "-notification" +) diff --git a/internal/bus/event/parser/parsers.go b/internal/bus/event/parser/parsers.go new file mode 100644 index 0000000..d876ed4 --- /dev/null +++ b/internal/bus/event/parser/parsers.go @@ -0,0 +1,106 @@ +package parser + +import ( + "fmt" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus/event" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" +) + +type ErrBadPayload struct { + Type partybus.EventType + Field string + Value interface{} +} + +func (e *ErrBadPayload) Error() string { + return fmt.Sprintf("event='%s' has bad event payload field=%q: %q", string(e.Type), e.Field, e.Value) +} + +func newPayloadErr(t partybus.EventType, field string, value interface{}) error { + return &ErrBadPayload{ + Type: t, + Field: field, + Value: value, + } +} + +func checkEventType(actual, expected partybus.EventType) error { + if actual != expected { + return newPayloadErr(expected, "Type", actual) + } + return nil +} + +func ParseTaskStarted(e partybus.Event) (progress.StagedProgressable, *payload.GenericTask, error) { + if err := checkEventType(e.Type, event.TaskStarted); err != nil { + return nil, nil, err + } + + var mon progress.StagedProgressable + + source, ok := e.Source.(payload.GenericTask) + if !ok { + return nil, nil, newPayloadErr(e.Type, "Source", e.Source) + } + + mon, ok = e.Value.(progress.StagedProgressable) + if !ok { + mon = nil + } + + return mon, &source, nil +} + +func ParseExploreAnalysis(e partybus.Event) (image.Analysis, image.ContentReader, error) { + if err := checkEventType(e.Type, event.ExploreAnalysis); err != nil { + return image.Analysis{}, nil, err + } + + ex, ok := e.Value.(payload.Explore) + if !ok { + return image.Analysis{}, nil, newPayloadErr(e.Type, "Value", e.Value) + } + + return ex.Analysis, ex.Content, nil +} + +func ParseReport(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.Report); err != nil { + return "", "", err + } + + context, ok := e.Source.(string) + if !ok { + // this is optional + context = "" + } + + report, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) + } + + return context, report, nil +} + +func ParseNotification(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.Notification); err != nil { + return "", "", err + } + + context, ok := e.Source.(string) + if !ok { + // this is optional + context = "" + } + + notification, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) + } + + return context, notification, nil +} diff --git a/internal/bus/event/payload/explore.go b/internal/bus/event/payload/explore.go new file mode 100644 index 0000000..e606466 --- /dev/null +++ b/internal/bus/event/payload/explore.go @@ -0,0 +1,8 @@ +package payload + +import "github.com/wagoodman/dive/dive/image" + +type Explore struct { + Analysis image.Analysis + Content image.ContentReader +} diff --git a/internal/bus/event/payload/generic.go b/internal/bus/event/payload/generic.go new file mode 100644 index 0000000..12b1388 --- /dev/null +++ b/internal/bus/event/payload/generic.go @@ -0,0 +1,48 @@ +package payload + +import ( + "context" + "github.com/wagoodman/go-progress" +) + +type genericProgressKey struct{} + +func SetGenericProgressToContext(ctx context.Context, mon *GenericProgress) context.Context { + return context.WithValue(ctx, genericProgressKey{}, mon) +} + +func GetGenericProgressFromContext(ctx context.Context) *GenericProgress { + mon, ok := ctx.Value(genericProgressKey{}).(*GenericProgress) + if !ok { + return nil + } + return mon +} + +type GenericTask struct { + // required fields + + Title Title + + // optional format fields + + HideOnSuccess bool + HideStageOnSuccess bool + + // optional fields + + ID string + ParentID string + Context string +} + +type GenericProgress struct { + *progress.AtomicStage + *progress.Manual +} + +type Title struct { + Default string + WhileRunning string + OnSuccess string +} diff --git a/internal/bus/helpers.go b/internal/bus/helpers.go new file mode 100644 index 0000000..b54d77f --- /dev/null +++ b/internal/bus/helpers.go @@ -0,0 +1,63 @@ +package bus + +import ( + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/bus/event" + "github.com/wagoodman/dive/internal/bus/event/payload" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" +) + +func Report(report string) { + if len(report) == 0 { + return + } + Publish(partybus.Event{ + Type: event.Report, + Value: report, + }) +} + +func Notify(message string) { + Publish(partybus.Event{ + Type: event.Notification, + Value: message, + }) +} + +func StartTask(info payload.GenericTask) *payload.GenericProgress { + t := &payload.GenericProgress{ + AtomicStage: progress.NewAtomicStage(""), + Manual: progress.NewManual(-1), + } + + Publish(partybus.Event{ + Type: event.TaskStarted, + Source: info, + Value: progress.StagedProgressable(t), + }) + + return t +} + +func StartSizedTask(info payload.GenericTask, size int64, initialStage string) *payload.GenericProgress { + t := &payload.GenericProgress{ + AtomicStage: progress.NewAtomicStage(initialStage), + Manual: progress.NewManual(size), + } + + Publish(partybus.Event{ + Type: event.TaskStarted, + Source: info, + Value: progress.StagedProgressable(t), + }) + + return t +} + +func ExploreAnalysis(analysis image.Analysis, reader image.ContentReader) { + Publish(partybus.Event{ + Type: event.ExploreAnalysis, + Value: payload.Explore{Analysis: analysis, Content: reader}, + }) +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..086f985 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,79 @@ +package log + +import ( + "github.com/anchore/go-logger" + "github.com/anchore/go-logger/adapter/discard" +) + +// log is the singleton used to facilitate logging internally within +var log = discard.New() + +// Set replaces the default logger with the provided logger. +func Set(l logger.Logger) { + log = l +} + +// Get returns the current logger instance. +func Get() logger.Logger { + return log +} + +// Errorf takes a formatted template string and template arguments for the error logging level. +func Errorf(format string, args ...interface{}) { + log.Errorf(format, args...) +} + +// Error logs the given arguments at the error logging level. +func Error(args ...interface{}) { + log.Error(args...) +} + +// Warnf takes a formatted template string and template arguments for the warning logging level. +func Warnf(format string, args ...interface{}) { + log.Warnf(format, args...) +} + +// Warn logs the given arguments at the warning logging level. +func Warn(args ...interface{}) { + log.Warn(args...) +} + +// Infof takes a formatted template string and template arguments for the info logging level. +func Infof(format string, args ...interface{}) { + log.Infof(format, args...) +} + +// Info logs the given arguments at the info logging level. +func Info(args ...interface{}) { + log.Info(args...) +} + +// Debugf takes a formatted template string and template arguments for the debug logging level. +func Debugf(format string, args ...interface{}) { + log.Debugf(format, args...) +} + +// Debug logs the given arguments at the debug logging level. +func Debug(args ...interface{}) { + log.Debug(args...) +} + +// Tracef takes a formatted template string and template arguments for the trace logging level. +func Tracef(format string, args ...interface{}) { + log.Tracef(format, args...) +} + +// Trace logs the given arguments at the trace logging level. +func Trace(args ...interface{}) { + log.Trace(args...) +} + +// WithFields returns a message logger with multiple key-value fields. +func WithFields(fields ...interface{}) logger.MessageLogger { + return log.WithFields(fields...) +} + +// Nested returns a new logger with hard coded key-value pairs +func Nested(fields ...interface{}) logger.Logger { + return log.Nested(fields...) +} diff --git a/utils/format.go b/internal/utils/format.go similarity index 70% rename from utils/format.go rename to internal/utils/format.go index 4b1fd31..d4b49bc 100644 --- a/utils/format.go +++ b/internal/utils/format.go @@ -2,14 +2,8 @@ package utils import ( "strings" - - "github.com/logrusorgru/aurora/v4" ) -func TitleFormat(s string) string { - return aurora.Bold(s).String() -} - // CleanArgs trims the whitespace from the given set of strings. func CleanArgs(s []string) []string { var r []string diff --git a/utils/view.go b/internal/utils/view.go similarity index 51% rename from utils/view.go rename to internal/utils/view.go index a65ce79..54df011 100644 --- a/utils/view.go +++ b/internal/utils/view.go @@ -1,18 +1,19 @@ package utils import ( + "errors" "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/internal/log" ) -// isNewView determines if a view has already been created based on the set of errors given (a bit hokie) +// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie) func IsNewView(errs ...error) bool { for _, err := range errs { if err == nil { return false } - if err != gocui.ErrUnknownView { - logrus.Errorf("IsNewView() unexpected error: %+v", err) + if !errors.Is(err, gocui.ErrUnknownView) { + log.WithFields("error", err).Error("IsNewView() unexpected error") return true } } diff --git a/runtime/ci/evaluator.go b/runtime/ci/evaluator.go deleted file mode 100644 index c48f6e6..0000000 --- a/runtime/ci/evaluator.go +++ /dev/null @@ -1,186 +0,0 @@ -package ci - -import ( - "fmt" - "sort" - "strconv" - "strings" - - "github.com/dustin/go-humanize" - "github.com/logrusorgru/aurora/v4" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/utils" -) - -type CiEvaluator struct { - Rules []CiRule - Results map[string]RuleResult - Tally ResultTally - Pass bool - Misconfigured bool - InefficientFiles []ReferenceFile -} - -type ResultTally struct { - Pass int - Fail int - Skip int - Warn int - Total int -} - -func NewCiEvaluator(config *viper.Viper) *CiEvaluator { - return &CiEvaluator{ - Rules: loadCiRules(config), - Results: make(map[string]RuleResult), - Pass: true, - } -} - -func (ci *CiEvaluator) isRuleEnabled(rule CiRule) bool { - return rule.Configuration() != "disabled" -} - -func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool { - canEvaluate := true - for _, rule := range ci.Rules { - if !ci.isRuleEnabled(rule) { - ci.Results[rule.Key()] = RuleResult{ - status: RuleConfigured, - message: "rule disabled", - } - continue - } - - err := rule.Validate() - if err != nil { - ci.Results[rule.Key()] = RuleResult{ - status: RuleMisconfigured, - message: err.Error(), - } - canEvaluate = false - } else { - ci.Results[rule.Key()] = RuleResult{ - status: RuleConfigured, - message: "test", - } - } - } - - if !canEvaluate { - ci.Pass = false - ci.Misconfigured = true - return ci.Pass - } - - // capture inefficient files - for idx := 0; idx < len(analysis.Inefficiencies); idx++ { - fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx] - - ci.InefficientFiles = append(ci.InefficientFiles, ReferenceFile{ - References: len(fileData.Nodes), - SizeBytes: uint64(fileData.CumulativeSize), - Path: fileData.Path, - }) - } - - // evaluate results against the configured CI rules - for _, rule := range ci.Rules { - if !ci.isRuleEnabled(rule) { - ci.Results[rule.Key()] = RuleResult{ - status: RuleDisabled, - message: "rule disabled", - } - continue - } - - status, message := rule.Evaluate(analysis) - - if value, exists := ci.Results[rule.Key()]; exists && value.status != RuleConfigured && value.status != RuleMisconfigured { - panic(fmt.Errorf("CI rule result recorded twice: %s", rule.Key())) - } - - if status == RuleFailed { - ci.Pass = false - } - - ci.Results[rule.Key()] = RuleResult{ - status: status, - message: message, - } - } - - ci.Tally.Total = len(ci.Results) - for rule, result := range ci.Results { - switch result.status { - case RulePassed: - ci.Tally.Pass++ - case RuleFailed: - ci.Tally.Fail++ - case RuleWarning: - ci.Tally.Warn++ - case RuleDisabled: - ci.Tally.Skip++ - default: - panic(fmt.Errorf("unknown test status (rule='%v'): %v", rule, result.status)) - } - } - - return ci.Pass -} - -func (ci *CiEvaluator) Report() string { - var sb strings.Builder - fmt.Fprintln(&sb, utils.TitleFormat("Inefficient Files:")) - - template := "%5s %12s %-s\n" - fmt.Fprintf(&sb, template, "Count", "Wasted Space", "File Path") - - if len(ci.InefficientFiles) == 0 { - fmt.Fprintln(&sb, "None") - } else { - for _, file := range ci.InefficientFiles { - fmt.Fprintf(&sb, template, strconv.Itoa(file.References), humanize.Bytes(file.SizeBytes), file.Path) - } - } - - fmt.Fprintln(&sb, utils.TitleFormat("Results:")) - - status := "PASS" - - rules := make([]string, 0, len(ci.Results)) - for name := range ci.Results { - rules = append(rules, name) - } - sort.Strings(rules) - - if ci.Tally.Fail > 0 { - status = "FAIL" - } - - for _, rule := range rules { - result := ci.Results[rule] - name := strings.TrimPrefix(rule, "rules.") - if result.message != "" { - fmt.Fprintf(&sb, " %s: %s: %s\n", result.status.String(), name, result.message) - } else { - fmt.Fprintf(&sb, " %s: %s\n", result.status.String(), name) - } - } - - if ci.Misconfigured { - fmt.Fprintln(&sb, aurora.Red("CI Misconfigured")) - } else { - summary := fmt.Sprintf("Result:%s [Total:%d] [Passed:%d] [Failed:%d] [Warn:%d] [Skipped:%d]", status, ci.Tally.Total, ci.Tally.Pass, ci.Tally.Fail, ci.Tally.Warn, ci.Tally.Skip) - if ci.Pass { - fmt.Fprintln(&sb, aurora.Green(summary)) - } else if ci.Pass && ci.Tally.Warn > 0 { - fmt.Fprintln(&sb, aurora.Blue(summary)) - } else { - fmt.Fprintln(&sb, aurora.Red(summary)) - } - } - return sb.String() -} diff --git a/runtime/ci/evaluator_test.go b/runtime/ci/evaluator_test.go deleted file mode 100644 index c513adf..0000000 --- a/runtime/ci/evaluator_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package ci - -import ( - "strings" - "testing" - - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive/image/docker" -) - -func Test_Evaluator(t *testing.T) { - - result := docker.TestAnalysisFromArchive(t, "../../.data/test-docker-image.tar") - - table := map[string]struct { - efficiency string - wastedBytes string - wastedPercent string - expectedPass bool - expectedResult map[string]RuleStatus - }{ - "allFail": {"0.99", "1B", "0.01", false, map[string]RuleStatus{"lowestEfficiency": RuleFailed, "highestWastedBytes": RuleFailed, "highestUserWastedPercent": RuleFailed}}, - "allPass": {"0.9", "50kB", "0.5", true, map[string]RuleStatus{"lowestEfficiency": RulePassed, "highestWastedBytes": RulePassed, "highestUserWastedPercent": RulePassed}}, - "allDisabled": {"disabled", "disabled", "disabled", true, map[string]RuleStatus{"lowestEfficiency": RuleDisabled, "highestWastedBytes": RuleDisabled, "highestUserWastedPercent": RuleDisabled}}, - "misconfiguredHigh": {"1.1", "1BB", "10", false, map[string]RuleStatus{"lowestEfficiency": RuleMisconfigured, "highestWastedBytes": RuleMisconfigured, "highestUserWastedPercent": RuleMisconfigured}}, - "misconfiguredLow": {"-9", "-1BB", "-0.1", false, map[string]RuleStatus{"lowestEfficiency": RuleMisconfigured, "highestWastedBytes": RuleMisconfigured, "highestUserWastedPercent": RuleMisconfigured}}, - } - - for name, test := range table { - ciConfig := viper.New() - ciConfig.SetDefault("rules.lowestEfficiency", test.efficiency) - ciConfig.SetDefault("rules.highestWastedBytes", test.wastedBytes) - ciConfig.SetDefault("rules.highestUserWastedPercent", test.wastedPercent) - - evaluator := NewCiEvaluator(ciConfig) - - pass := evaluator.Evaluate(result) - - if test.expectedPass != pass { - t.Logf("Test: %s", name) - t.Errorf("Test_Evaluator: expected pass=%v, got %v", test.expectedPass, pass) - } - - if len(test.expectedResult) != len(evaluator.Results) { - t.Logf("Test: %s", name) - t.Errorf("Test_Evaluator: expected %v results, got %v", len(test.expectedResult), len(evaluator.Results)) - } - - for rule, actualResult := range evaluator.Results { - expectedStatus := test.expectedResult[strings.TrimPrefix(rule, "rules.")] - if expectedStatus != actualResult.status { - t.Errorf(" %v: expected %v rule failures, got %v: %v", rule, expectedStatus, actualResult.status, actualResult) - } - } - - } - -} diff --git a/runtime/ci/reference_file.go b/runtime/ci/reference_file.go deleted file mode 100644 index c5891c7..0000000 --- a/runtime/ci/reference_file.go +++ /dev/null @@ -1,7 +0,0 @@ -package ci - -type ReferenceFile struct { - References int `json:"count"` - SizeBytes uint64 `json:"sizeBytes"` - Path string `json:"file"` -} diff --git a/runtime/ci/rule.go b/runtime/ci/rule.go deleted file mode 100644 index f9e348d..0000000 --- a/runtime/ci/rule.go +++ /dev/null @@ -1,168 +0,0 @@ -package ci - -import ( - "fmt" - "strconv" - - "github.com/dustin/go-humanize" - "github.com/logrusorgru/aurora/v4" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive/image" -) - -const ( - RuleUnknown = iota - RulePassed - RuleFailed - RuleWarning - RuleDisabled - RuleMisconfigured - RuleConfigured -) - -type CiRule interface { - Key() string - Configuration() string - Validate() error - Evaluate(result *image.AnalysisResult) (RuleStatus, string) -} - -type GenericCiRule struct { - key string - configValue string - configValidator func(string) error - evaluator func(*image.AnalysisResult, string) (RuleStatus, string) -} - -type RuleStatus int - -type RuleResult struct { - status RuleStatus - message string -} - -func newGenericCiRule(key string, configValue string, validator func(string) error, evaluator func(*image.AnalysisResult, string) (RuleStatus, string)) *GenericCiRule { - return &GenericCiRule{ - key: key, - configValue: configValue, - configValidator: validator, - evaluator: evaluator, - } -} - -func (rule *GenericCiRule) Key() string { - return rule.key -} - -func (rule *GenericCiRule) Configuration() string { - return rule.configValue -} - -func (rule *GenericCiRule) Validate() error { - return rule.configValidator(rule.configValue) -} - -func (rule *GenericCiRule) Evaluate(result *image.AnalysisResult) (RuleStatus, string) { - return rule.evaluator(result, rule.configValue) -} - -func (status RuleStatus) String() string { - switch status { - case RulePassed: - return "PASS" - case RuleFailed: - return aurora.Bold(aurora.Inverse(aurora.Red("FAIL"))).String() - case RuleWarning: - return aurora.Blue("WARN").String() - case RuleDisabled: - return aurora.Blue("SKIP").String() - case RuleMisconfigured: - return aurora.Bold(aurora.Inverse(aurora.Red("MISCONFIGURED"))).String() - case RuleConfigured: - return "CONFIGURED " - default: - return aurora.Inverse("Unknown").String() - } -} - -func loadCiRules(config *viper.Viper) []CiRule { - var rules = make([]CiRule, 0) - var ruleKey = "lowestEfficiency" - rules = append(rules, newGenericCiRule( - ruleKey, - config.GetString(fmt.Sprintf("rules.%s", ruleKey)), - func(value string) error { - lowestEfficiency, err := strconv.ParseFloat(value, 64) - if err != nil { - return fmt.Errorf("invalid config value ('%v'): %v", value, err) - } - if lowestEfficiency < 0 || lowestEfficiency > 1 { - return fmt.Errorf("lowestEfficiency config value is outside allowed range (0-1), given '%s'", value) - } - return nil - }, - func(analysis *image.AnalysisResult, value string) (RuleStatus, string) { - lowestEfficiency, err := strconv.ParseFloat(value, 64) - if err != nil { - return RuleFailed, fmt.Sprintf("invalid config value ('%v'): %v", value, err) - } - if lowestEfficiency > analysis.Efficiency { - return RuleFailed, fmt.Sprintf("image efficiency is too low (efficiency=%v < threshold=%v)", analysis.Efficiency, lowestEfficiency) - } - return RulePassed, "" - }, - )) - - ruleKey = "highestWastedBytes" - rules = append(rules, newGenericCiRule( - ruleKey, - config.GetString(fmt.Sprintf("rules.%s", ruleKey)), - func(value string) error { - _, err := humanize.ParseBytes(value) - if err != nil { - return fmt.Errorf("invalid config value ('%v'): %v", value, err) - } - return nil - }, - func(analysis *image.AnalysisResult, value string) (RuleStatus, string) { - highestWastedBytes, err := humanize.ParseBytes(value) - if err != nil { - return RuleFailed, fmt.Sprintf("invalid config value ('%v'): %v", value, err) - } - if analysis.WastedBytes > highestWastedBytes { - return RuleFailed, fmt.Sprintf("too many bytes wasted (wasted-bytes=%v > threshold=%v)", analysis.WastedBytes, highestWastedBytes) - } - return RulePassed, "" - }, - )) - - ruleKey = "highestUserWastedPercent" - rules = append(rules, newGenericCiRule( - ruleKey, - config.GetString(fmt.Sprintf("rules.%s", ruleKey)), - func(value string) error { - highestUserWastedPercent, err := strconv.ParseFloat(value, 64) - if err != nil { - return fmt.Errorf("invalid config value ('%v'): %v", value, err) - } - if highestUserWastedPercent < 0 || highestUserWastedPercent > 1 { - return fmt.Errorf("highestUserWastedPercent config value is outside allowed range (0-1), given '%s'", value) - } - return nil - }, - func(analysis *image.AnalysisResult, value string) (RuleStatus, string) { - highestUserWastedPercent, err := strconv.ParseFloat(value, 64) - if err != nil { - return RuleFailed, fmt.Sprintf("invalid config value ('%v'): %v", value, err) - } - if highestUserWastedPercent < analysis.WastedUserPercent { - return RuleFailed, fmt.Sprintf("too many bytes wasted, relative to the user bytes added (%%-user-wasted-bytes=%v > threshold=%v)", analysis.WastedUserPercent, highestUserWastedPercent) - } - - return RulePassed, "" - }, - )) - - return rules -} diff --git a/runtime/event.go b/runtime/event.go deleted file mode 100644 index b9ab931..0000000 --- a/runtime/event.go +++ /dev/null @@ -1,31 +0,0 @@ -package runtime - -type eventChannel chan event - -type event struct { - stdout string - stderr string - err error - errorOnExit bool -} - -func (ec eventChannel) message(msg string) { - ec <- event{ - stdout: msg, - } -} - -func (ec eventChannel) exitWithError(err error) { - ec <- event{ - err: err, - errorOnExit: true, - } -} - -func (ec eventChannel) exitWithErrorMessage(msg string, err error) { - ec <- event{ - stderr: msg, - err: err, - errorOnExit: true, - } -} diff --git a/runtime/export/export.go b/runtime/export/export.go deleted file mode 100644 index a9a3660..0000000 --- a/runtime/export/export.go +++ /dev/null @@ -1,66 +0,0 @@ -package export - -import ( - "encoding/json" - - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/dive/filetree" - diveImage "github.com/wagoodman/dive/dive/image" -) - -type export struct { - Layer []layer `json:"layer"` - Image image `json:"image"` -} - -// NewExport exports the analysis to a JSON -func NewExport(analysis *diveImage.AnalysisResult) *export { - data := export{ - Layer: make([]layer, len(analysis.Layers)), - Image: image{ - InefficientFiles: make([]fileReference, len(analysis.Inefficiencies)), - SizeBytes: analysis.SizeBytes, - EfficiencyScore: analysis.Efficiency, - InefficientBytes: analysis.WastedBytes, - }, - } - - // export layers in order - for idx, curLayer := range analysis.Layers { - layerFileList := make([]filetree.FileInfo, 0) - visitor := func(node *filetree.FileNode) error { - layerFileList = append(layerFileList, node.Data.FileInfo) - return nil - } - err := curLayer.Tree.VisitDepthChildFirst(visitor, nil) - if err != nil { - logrus.Errorf("Unable to propagate layer tree: %+v", err) - } - data.Layer[idx] = layer{ - Index: curLayer.Index, - ID: curLayer.Id, - DigestID: curLayer.Digest, - SizeBytes: curLayer.Size, - Command: curLayer.Command, - FileList: layerFileList, - } - } - - // add file references - for idx := 0; idx < len(analysis.Inefficiencies); idx++ { - fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx] - - data.Image.InefficientFiles[idx] = fileReference{ - References: len(fileData.Nodes), - SizeBytes: uint64(fileData.CumulativeSize), - Path: fileData.Path, - } - } - - return &data -} - -func (exp *export) Marshal() ([]byte, error) { - return json.MarshalIndent(&exp, "", " ") -} diff --git a/runtime/export/export_test.go b/runtime/export/export_test.go deleted file mode 100644 index d32e574..0000000 --- a/runtime/export/export_test.go +++ /dev/null @@ -1,4690 +0,0 @@ -package export - -import ( - "testing" - - "github.com/sergi/go-diff/diffmatchpatch" - - "github.com/wagoodman/dive/dive/image/docker" -) - -func Test_Export(t *testing.T) { - result := docker.TestAnalysisFromArchive(t, "../../.data/test-docker-image.tar") - - export := NewExport(result) - payload, err := export.Marshal() - if err != nil { - t.Errorf("Test_Export: unable to export analysis: %v", err) - } - - expectedResult := `{ - "layer": [ - { - "index": 0, - "id": "28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694", - "digestId": "sha256:23bc2b70b2014dec0ac22f27bb93e9babd08cdd6f1115d0c955b9ff22b382f5a", - "sizeBytes": 1154361, - "command": "#(nop) ADD file:ce026b62356eec3ad1214f92be2c9dc063fe205bd5e600be3492c4dfb17148bd in / ", - "fileList": [ - { - "path": "bin/[", - "typeFlag": 48, - "linkName": "", - "size": 1075464, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/[[", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/acpid", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/add-shell", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/addgroup", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/adduser", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/adjtimex", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ar", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/arch", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/arp", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/arping", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ash", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/awk", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/base64", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/basename", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/beep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/blkdiscard", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/blkid", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/blockdev", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/bootchartd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/brctl", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/bunzip2", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/busybox", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/bzcat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/bzip2", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cal", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chattr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chgrp", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chmod", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chown", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chpasswd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chpst", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chroot", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chrt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/chvt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cksum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/clear", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cmp", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/comm", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/conspy", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cp", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cpio", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/crond", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/crontab", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cryptpw", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cttyhack", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/cut", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/date", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/deallocvt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/delgroup", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/deluser", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/depmod", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/devmem", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/df", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dhcprelay", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/diff", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dirname", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dmesg", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dnsd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dnsdomainname", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dos2unix", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dpkg", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dpkg-deb", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/du", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dumpkmap", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/dumpleases", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/echo", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ed", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/egrep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/eject", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/env", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/envdir", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/envuidgid", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ether-wake", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/expand", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/expr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/factor", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fakeidentd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fallocate", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/false", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fatattr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fbset", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fbsplash", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fdflush", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fdformat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fdisk", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fgconsole", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fgrep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/find", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/findfs", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/flock", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fold", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/free", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/freeramdisk", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fsck", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fsck.minix", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fsfreeze", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fstrim", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fsync", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ftpd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ftpget", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ftpput", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/fuser", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/getconf", - "typeFlag": 48, - "linkName": "", - "size": 77880, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/getopt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/getty", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/grep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/groups", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/gunzip", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/gzip", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/halt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hdparm", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/head", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hexdump", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hexedit", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hostid", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hostname", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/httpd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hush", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/hwclock", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/i2cdetect", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/i2cdump", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/i2cget", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/i2cset", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/id", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ifconfig", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ifdown", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ifenslave", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ifplugd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ifup", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/inetd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/init", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/insmod", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/install", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ionice", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/iostat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ip", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ipaddr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ipcalc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ipcrm", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ipcs", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/iplink", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ipneigh", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/iproute", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/iprule", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/iptunnel", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/kbd_mode", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/kill", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/killall", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/killall5", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/klogd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/last", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/less", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/link", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/linux32", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/linux64", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/linuxrc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ln", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/loadfont", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/loadkmap", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/logger", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/login", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/logname", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/logread", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/losetup", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lpd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lpq", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lpr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ls", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lsattr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lsmod", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lsof", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lspci", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lsscsi", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lsusb", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lzcat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lzma", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/lzop", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/makedevs", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/makemime", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/man", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/md5sum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mdev", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mesg", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/microcom", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkdir", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkdosfs", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mke2fs", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkfifo", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkfs.ext2", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkfs.minix", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkfs.vfat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mknod", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkpasswd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mkswap", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mktemp", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/modinfo", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/modprobe", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/more", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mount", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mountpoint", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mpstat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/mv", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nameif", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nanddump", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nandwrite", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nbd-client", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/netstat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nice", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nl", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nmeter", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nohup", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nproc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nsenter", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nslookup", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ntpd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/nuke", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/od", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/openvt", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/partprobe", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/passwd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/paste", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/patch", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pgrep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pidof", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ping", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ping6", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pipe_progress", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pivot_root", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pkill", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pmap", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/popmaildir", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/poweroff", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/powertop", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/printenv", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/printf", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ps", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pscan", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pstree", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pwd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/pwdx", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/raidautorun", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rdate", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rdev", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/readahead", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/readlink", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/readprofile", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/realpath", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/reboot", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/reformime", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/remove-shell", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/renice", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/reset", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/resize", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/resume", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rev", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rm", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rmdir", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rmmod", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/route", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rpm", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rpm2cpio", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rtcwake", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/run-init", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/run-parts", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/runlevel", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/runsv", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/runsvdir", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/rx", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/script", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/scriptreplay", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sed", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sendmail", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/seq", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setarch", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setconsole", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setfattr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setfont", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setkeycodes", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setlogcons", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setpriv", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setserial", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setsid", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/setuidgid", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sh", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sha1sum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sha256sum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sha3sum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sha512sum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/showkey", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/shred", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/shuf", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/slattach", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sleep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/smemcap", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/softlimit", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sort", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/split", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ssl_client", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/start-stop-daemon", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/stat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/strings", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/stty", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/su", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sulogin", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sum", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sv", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/svc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/svlogd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/svok", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/swapoff", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/swapon", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/switch_root", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sync", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/sysctl", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/syslogd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tac", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tail", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tar", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/taskset", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tcpsvd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tee", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/telnet", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/telnetd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/test", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tftp", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tftpd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/time", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/timeout", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/top", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/touch", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tr", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/traceroute", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/traceroute6", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/true", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/truncate", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tty", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ttysize", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/tunctl", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubiattach", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubidetach", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubimkvol", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubirename", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubirmvol", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubirsvol", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/ubiupdatevol", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/udhcpc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/udhcpd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/udpsvd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/uevent", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/umount", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/uname", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unexpand", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/uniq", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unix2dos", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unlink", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unlzma", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unshare", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unxz", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/unzip", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/uptime", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/users", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/usleep", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/uudecode", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/uuencode", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/vconfig", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/vi", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/vlock", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/volname", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/w", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/wall", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/watch", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/watchdog", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/wc", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/wget", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/which", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/who", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/whoami", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/whois", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/xargs", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/xxd", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/xz", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/xzcat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/yes", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/zcat", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin/zcip", - "typeFlag": 49, - "linkName": "bin/[", - "size": 0, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "bin", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "dev", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "etc/group", - "typeFlag": 48, - "linkName": "", - "size": 307, - "fileMode": 436, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "etc/localtime", - "typeFlag": 48, - "linkName": "", - "size": 127, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "etc/network/if-down.d", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "etc/network/if-post-down.d", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "etc/network/if-pre-up.d", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "etc/network/if-up.d", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "etc/network", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "etc/passwd", - "typeFlag": 48, - "linkName": "", - "size": 340, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "etc/shadow", - "typeFlag": 48, - "linkName": "", - "size": 243, - "fileMode": 384, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "etc", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "home", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 65534, - "gid": 65534, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "tmp", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2148532735, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "usr/sbin", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 1, - "gid": 1, - "isDir": true - }, - { - "path": "usr", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "var/spool/mail", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 8, - "gid": 8, - "isDir": true - }, - { - "path": "var/spool", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "var/www", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "var", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 1, - "id": "1871059774abe6914075e4a919b778fa1561f577d620ae52438a9635e6241936", - "digestId": "sha256:a65b7d7ac139a0e4337bc3c73ce511f937d6140ef61a0108f7d4b8aab8d67274", - "sizeBytes": 6405, - "command": "#(nop) ADD file:139c3708fb6261126453e34483abd8bf7b26ed16d952fd976994d68e72d93be2 in /somefile.txt ", - "fileList": [ - { - "path": "somefile.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 436, - "uid": 0, - "gid": 0, - "isDir": false - } - ] - }, - { - "index": 2, - "id": "49fe2a475548bfa4d493fc796fce41f30704e3d4cbff3e45dd3e06f463236d1d", - "digestId": "sha256:93e208d471756ffbac88cf9c25feb442007f221d3bd73231e27b747a0a68927c", - "sizeBytes": 0, - "command": "mkdir -p /root/example/really/nested", - "fileList": [ - { - "path": "root/example/really/nested", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root/example/really", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root/example", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 3, - "id": "80cd2ca1ffc89962b9349c80280c2bc551acbd11e09b16badb0669f8e2369020", - "digestId": "sha256:4abad3abe3cb99ad7a492a9d9f6b3d66287c1646843c74128bbbec4f7be5aa9e", - "sizeBytes": 6405, - "command": "cp /somefile.txt /root/example/somefile1.txt", - "fileList": [ - { - "path": "root/example/somefile1.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/example", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 4, - "id": "c99e2f8d3f6282668f0d30dc1db5e67a51d7a1dcd7ff6ddfa0f90760836778ec", - "digestId": "sha256:14c9a6ffcb6a0f32d1035f97373b19608e2d307961d8be156321c3f1c1504cbf", - "sizeBytes": 6405, - "command": "chmod 444 /root/example/somefile1.txt", - "fileList": [ - { - "path": "root/example/somefile1.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 292, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/example", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 5, - "id": "5eca617bdc3bc06134fe957a30da4c57adb7c340a6d749c8edc4c15861c928d7", - "digestId": "sha256:778fb5770ef466f314e79cc9dc418eba76bfc0a64491ce7b167b76aa52c736c4", - "sizeBytes": 6405, - "command": "cp /somefile.txt /root/example/somefile2.txt", - "fileList": [ - { - "path": "root/example/somefile2.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/example", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 6, - "id": "f07c3eb887572395408f8e11a07af945e4da5f02b3188bb06b93fad713ca0b99", - "digestId": "sha256:f275b8a31a71deb521cc048e6021e2ff6fa52bedb25c9b7bbe129a0195ddca5f", - "sizeBytes": 6405, - "command": "cp /somefile.txt /root/example/somefile3.txt", - "fileList": [ - { - "path": "root/example/somefile3.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/example", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 7, - "id": "461885fc22589158dee3c5b9f01cc41c87805439f58b4399d733b51aa305cbf9", - "digestId": "sha256:dd1effc5eb19894c3e9b57411c98dd1cf30fa1de4253c7fae53c9cea67267d83", - "sizeBytes": 6405, - "command": "mv /root/example/somefile3.txt /root/saved.txt", - "fileList": [ - { - "path": "root/example/.wh.somefile3.txt", - "typeFlag": 48, - "linkName": "", - "size": 0, - "fileMode": 0, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/example", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root/saved.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 8, - "id": "a10327f68ffed4afcba78919052809a8f774978a6b87fc117d39c53c4842f72c", - "digestId": "sha256:8d1869a0a066cdd12e48d648222866e77b5e2814f773bb3bd8774ab4052f0f1d", - "sizeBytes": 6405, - "command": "cp /root/saved.txt /root/.saved.txt", - "fileList": [ - { - "path": "root/.saved.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 9, - "id": "f2fc54e25cb7966dc9732ec671a77a1c5c104e732bd15ad44a2dc1ac42368f84", - "digestId": "sha256:bc2e36423fa31a97223fd421f22c35466220fa160769abf697b8eb58c896b468", - "sizeBytes": 0, - "command": "rm -rf /root/example/", - "fileList": [ - { - "path": "root/.wh.example", - "typeFlag": 48, - "linkName": "", - "size": 0, - "fileMode": 0, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 10, - "id": "aad36d0b05e71c7e6d4dfe0ca9ed6be89e2e0d8995dafe83438299a314e91071", - "digestId": "sha256:7f648d45ee7b6de2292162fba498b66cbaaf181da9004fcceef824c72dbae445", - "sizeBytes": 2187, - "command": "#(nop) ADD dir:7ec14b81316baa1a31c38c97686a8f030c98cba2035c968412749e33e0c4427e in /root/.data/ ", - "fileList": [ - { - "path": "root/.data/tag.sh", - "typeFlag": 48, - "linkName": "", - "size": 917, - "fileMode": 509, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/.data/test.sh", - "typeFlag": 48, - "linkName": "", - "size": 1270, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/.data", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 11, - "id": "3d4ad907517a021d86a4102d2764ad2161e4818bbd144e41d019bfc955434181", - "digestId": "sha256:a4b8f95f266d5c063c9a9473c45f2f85ddc183e37941b5e6b6b9d3c00e8e0457", - "sizeBytes": 6405, - "command": "cp /root/saved.txt /tmp/saved.again1.txt", - "fileList": [ - { - "path": "tmp/saved.again1.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "tmp", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2148532735, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 12, - "id": "81b1b002d4b4c1325a9cad9990b5277e7f29f79e0f24582344c0891178f95905", - "digestId": "sha256:22a44d45780a541e593a8862d80f3e14cb80b6bf76aa42ce68dc207a35bf3a4a", - "sizeBytes": 6405, - "command": "cp /root/saved.txt /root/.data/saved.again2.txt", - "fileList": [ - { - "path": "root/.data/saved.again2.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 420, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root/.data", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484141, - "uid": 0, - "gid": 0, - "isDir": true - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - }, - { - "index": 13, - "id": "cfb35bb5c127d848739be5ca726057e6e2c77b2849f588e7aebb642c0d3d4b7b", - "digestId": "sha256:ba689cac6a98c92d121fa5c9716a1bab526b8bb1fd6d43625c575b79e97300c5", - "sizeBytes": 6405, - "command": "chmod +x /root/saved.txt", - "fileList": [ - { - "path": "root/saved.txt", - "typeFlag": 48, - "linkName": "", - "size": 6405, - "fileMode": 493, - "uid": 0, - "gid": 0, - "isDir": false - }, - { - "path": "root", - "typeFlag": 53, - "linkName": "", - "size": 0, - "fileMode": 2147484096, - "uid": 0, - "gid": 0, - "isDir": true - } - ] - } - ], - "image": { - "sizeBytes": 1220598, - "inefficientBytes": 32025, - "efficiencyScore": 0.9844212134184309, - "fileReference": [ - { - "count": 2, - "sizeBytes": 12810, - "file": "/root/saved.txt" - }, - { - "count": 2, - "sizeBytes": 12810, - "file": "/root/example/somefile1.txt" - }, - { - "count": 2, - "sizeBytes": 6405, - "file": "/root/example/somefile3.txt" - } - ] - } -}` - - actualResult := string(payload) - if expectedResult != actualResult { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(expectedResult, actualResult, false) - - t.Errorf("Test_Export: unexpected export result:\n%v", dmp.DiffPrettyText(diffs)) - } -} diff --git a/runtime/export/file_reference.go b/runtime/export/file_reference.go deleted file mode 100644 index 14aac0e..0000000 --- a/runtime/export/file_reference.go +++ /dev/null @@ -1,7 +0,0 @@ -package export - -type fileReference struct { - References int `json:"count"` - SizeBytes uint64 `json:"sizeBytes"` - Path string `json:"file"` -} diff --git a/runtime/export/image.go b/runtime/export/image.go deleted file mode 100644 index 2052371..0000000 --- a/runtime/export/image.go +++ /dev/null @@ -1,8 +0,0 @@ -package export - -type image struct { - SizeBytes uint64 `json:"sizeBytes"` - InefficientBytes uint64 `json:"inefficientBytes"` - EfficiencyScore float64 `json:"efficiencyScore"` - InefficientFiles []fileReference `json:"fileReference"` -} diff --git a/runtime/export/layer.go b/runtime/export/layer.go deleted file mode 100644 index 85d5f57..0000000 --- a/runtime/export/layer.go +++ /dev/null @@ -1,14 +0,0 @@ -package export - -import ( - "github.com/wagoodman/dive/dive/filetree" -) - -type layer struct { - Index int `json:"index"` - ID string `json:"id"` - DigestID string `json:"digestId"` - SizeBytes uint64 `json:"sizeBytes"` - Command string `json:"command"` - FileList []filetree.FileInfo `json:"fileList"` -} diff --git a/runtime/options.go b/runtime/options.go deleted file mode 100644 index c9c5a1e..0000000 --- a/runtime/options.go +++ /dev/null @@ -1,17 +0,0 @@ -package runtime - -import ( - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive" -) - -type Options struct { - Ci bool - Image string - Source dive.ImageSource - IgnoreErrors bool - ExportFile string - CiConfig *viper.Viper - BuildArgs []string -} diff --git a/runtime/run.go b/runtime/run.go deleted file mode 100644 index 7e4814c..0000000 --- a/runtime/run.go +++ /dev/null @@ -1,160 +0,0 @@ -package runtime - -import ( - "fmt" - "os" - "time" - - "github.com/dustin/go-humanize" - "github.com/sirupsen/logrus" - "github.com/spf13/afero" - - "github.com/wagoodman/dive/dive" - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ci" - "github.com/wagoodman/dive/runtime/export" - "github.com/wagoodman/dive/runtime/ui" - "github.com/wagoodman/dive/utils" -) - -func run(enableUi bool, options Options, imageResolver image.Resolver, events eventChannel, filesystem afero.Fs) { - var img *image.Image - var err error - defer close(events) - - doExport := options.ExportFile != "" - doBuild := len(options.BuildArgs) > 0 - - if doBuild { - events.message(utils.TitleFormat("Building image...")) - img, err = imageResolver.Build(options.BuildArgs) - if err != nil { - events.exitWithErrorMessage("cannot build image", err) - return - } - } else { - events.message(utils.TitleFormat("Image Source: ") + options.Source.String() + "://" + options.Image) - events.message(utils.TitleFormat("Extracting image from "+imageResolver.Name()+"...") + " (this can take a while for large images)") - img, err = imageResolver.Fetch(options.Image) - if err != nil { - events.exitWithErrorMessage("cannot fetch image", err) - return - } - } - - events.message(utils.TitleFormat("Analyzing image...")) - analysis, err := img.Analyze() - if err != nil { - events.exitWithErrorMessage("cannot analyze image", err) - return - } - - if doExport { - events.message(utils.TitleFormat(fmt.Sprintf("Exporting image to '%s'...", options.ExportFile))) - bytes, err := export.NewExport(analysis).Marshal() - if err != nil { - events.exitWithErrorMessage("cannot marshal export payload", err) - return - } - - file, err := filesystem.OpenFile(options.ExportFile, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - events.exitWithErrorMessage("cannot open export file", err) - return - } - defer file.Close() - - _, err = file.Write(bytes) - if err != nil { - events.exitWithErrorMessage("cannot write to export file", err) - } - return - } - - if options.Ci { - events.message(fmt.Sprintf(" efficiency: %2.4f %%", analysis.Efficiency*100)) - events.message(fmt.Sprintf(" wastedBytes: %d bytes (%s)", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))) - events.message(fmt.Sprintf(" userWastedPercent: %2.4f %%", analysis.WastedUserPercent*100)) - - evaluator := ci.NewCiEvaluator(options.CiConfig) - pass := evaluator.Evaluate(analysis) - events.message(evaluator.Report()) - - if !pass { - events.exitWithError(nil) - } - - return - } else { - events.message(utils.TitleFormat("Building cache...")) - treeStack := filetree.NewComparer(analysis.RefTrees) - errors := treeStack.BuildCache() - if errors != nil { - for _, err := range errors { - events.message(" " + err.Error()) - } - if !options.IgnoreErrors { - events.exitWithError(fmt.Errorf("file tree has path errors (use '--ignore-errors' to attempt to continue)")) - return - } - } - - if enableUi { - // it appears there is a race condition where termbox.Init() will - // block nearly indefinitely when running as the first process in - // a Docker container when started within ~25ms of container startup. - // I can't seem to determine the exact root cause, however, a large - // enough sleep will prevent this behavior (todo: remove this hack) - time.Sleep(100 * time.Millisecond) - - err = ui.Run(options.Image, imageResolver, analysis, treeStack) - if err != nil { - events.exitWithError(err) - return - } - } - } -} - -func Run(options Options) { - var exitCode int - var events = make(eventChannel) - - imageResolver, err := dive.GetImageResolver(options.Source) - if err != nil { - message := "cannot determine image provider" - logrus.Error(message) - logrus.Error(err) - fmt.Fprintf(os.Stderr, "%s: %+v\n", message, err) - os.Exit(1) - } - - go run(true, options, imageResolver, events, afero.NewOsFs()) - - for event := range events { - if event.stdout != "" { - fmt.Println(event.stdout) - } - - if event.stderr != "" { - _, err := fmt.Fprintln(os.Stderr, event.stderr) - if err != nil { - fmt.Println("error: could not write to buffer:", err) - } - } - - if event.err != nil { - logrus.Error(event.err) - _, err := fmt.Fprintln(os.Stderr, event.err.Error()) - if err != nil { - fmt.Println("error: could not write to buffer:", err) - } - } - - if event.errorOnExit { - exitCode = 1 - } - } - os.Exit(exitCode) -} diff --git a/runtime/run_test.go b/runtime/run_test.go deleted file mode 100644 index 6362ea7..0000000 --- a/runtime/run_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package runtime - -import ( - "fmt" - "os" - "testing" - - "github.com/lunixbochs/vtclean" - "github.com/spf13/afero" - "github.com/spf13/viper" - - "github.com/wagoodman/dive/dive" - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/dive/image/docker" -) - -type defaultResolver struct{} - -func (r *defaultResolver) Name() string { - return "default-resolver" -} - -func (r *defaultResolver) Extract(id string, l string, p string) error { - return nil -} - -func (r *defaultResolver) Fetch(id string) (*image.Image, error) { - archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar") - if err != nil { - return nil, err - } - return archive.ToImage() -} - -func (r *defaultResolver) Build(args []string) (*image.Image, error) { - return r.Fetch("") -} - -type failedBuildResolver struct{} - -func (r *failedBuildResolver) Name() string { - return "failed-build-resolver" -} - -func (r *failedBuildResolver) Extract(id string, l string, p string) error { - return fmt.Errorf("some extract failure") -} - -func (r *failedBuildResolver) Fetch(id string) (*image.Image, error) { - archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar") - if err != nil { - return nil, err - } - return archive.ToImage() -} - -func (r *failedBuildResolver) Build(args []string) (*image.Image, error) { - return nil, fmt.Errorf("some build failure") -} - -type failedFetchResolver struct{} - -func (r *failedFetchResolver) Name() string { - return "failed-fetch-resolver" -} - -func (r *failedFetchResolver) Extract(id string, l string, p string) error { - return fmt.Errorf("some extract failure") -} - -func (r *failedFetchResolver) Fetch(id string) (*image.Image, error) { - return nil, fmt.Errorf("some fetch failure") -} - -func (r *failedFetchResolver) Build(args []string) (*image.Image, error) { - return nil, fmt.Errorf("some build failure") -} - -// func showEvents(events []testEvent) { -// for _, e := range events { -// fmt.Printf("{stdout:\"%s\", stderr:\"%s\", errorOnExit: %v, errMessage: \"%s\"},\n", -// strings.Replace(vtclean.Clean(e.stdout, false), "\n", "\\n", -1), -// strings.Replace(vtclean.Clean(e.stderr, false), "\n", "\\n", -1), -// e.errorOnExit, -// e.errMessage) -// } -// } - -type testEvent struct { - stdout string - stderr string - errMessage string - errorOnExit bool -} - -func newTestEvent(e event) testEvent { - var errMsg string - if e.err != nil { - errMsg = e.err.Error() - } - return testEvent{ - stdout: e.stdout, - stderr: e.stderr, - errMessage: errMsg, - errorOnExit: e.errorOnExit, - } -} - -func configureCi() *viper.Viper { - ciConfig := viper.New() - ciConfig.SetDefault("rules.lowestEfficiency", "0.9") - ciConfig.SetDefault("rules.highestWastedBytes", "1000") - ciConfig.SetDefault("rules.highestUserWastedPercent", "0.1") - return ciConfig -} - -func TestRun(t *testing.T) { - table := map[string]struct { - resolver image.Resolver - options Options - events []testEvent - }{ - "fetch-case": { - resolver: &defaultResolver{}, - options: Options{ - Ci: false, - Image: "dive-example", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: nil, - BuildArgs: nil, - }, - events: []testEvent{ - {stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Extracting image from default-resolver... (this can take a while for large images)", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""}, - }, - }, - "fetch-with-no-build-options-case": { - resolver: &defaultResolver{}, - options: Options{ - Ci: false, - Image: "dive-example", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: nil, - // note: empty slice is passed - BuildArgs: []string{}, - }, - events: []testEvent{ - {stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Extracting image from default-resolver... (this can take a while for large images)", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""}, - }, - }, - "build-case": { - resolver: &defaultResolver{}, - options: Options{ - Ci: false, - Image: "dive-example", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: nil, - BuildArgs: []string{"an-option"}, - }, - events: []testEvent{ - {stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""}, - }, - }, - "failed-fetch": { - resolver: &failedFetchResolver{}, - options: Options{ - Ci: false, - Image: "dive-example", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: nil, - BuildArgs: nil, - }, - events: []testEvent{ - {stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Extracting image from failed-fetch-resolver... (this can take a while for large images)", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "", stderr: "cannot fetch image", errorOnExit: true, errMessage: "some fetch failure"}, - }, - }, - "failed-build": { - resolver: &failedBuildResolver{}, - options: Options{ - Ci: false, - Image: "doesn't-matter", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: nil, - BuildArgs: []string{"an-option"}, - }, - events: []testEvent{ - {stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "", stderr: "cannot build image", errorOnExit: true, errMessage: "some build failure"}, - }, - }, - "ci-go-case": { - resolver: &defaultResolver{}, - options: Options{ - Ci: true, - Image: "doesn't-matter", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: configureCi(), - BuildArgs: []string{"an-option"}, - }, - events: []testEvent{ - {stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: " efficiency: 98.4421 %", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: " wastedBytes: 32025 bytes (32 kB)", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: " userWastedPercent: 48.3491 %", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Inefficient Files:\nCount Wasted Space File Path\n 2 13 kB /root/saved.txt\n 2 13 kB /root/example/somefile1.txt\n 2 6.4 kB /root/example/somefile3.txt\nResults:\n FAIL: highestUserWastedPercent: too many bytes wasted, relative to the user bytes added (%-user-wasted-bytes=0.4834911001404049 > threshold=0.1)\n FAIL: highestWastedBytes: too many bytes wasted (wasted-bytes=32025 > threshold=1000)\n PASS: lowestEfficiency\nResult:FAIL [Total:3] [Passed:1] [Failed:2] [Warn:0] [Skipped:0]\n", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "", stderr: "", errorOnExit: true, errMessage: ""}, - }, - }, - "empty-ci-config-case": { - resolver: &defaultResolver{}, - options: Options{ - Ci: true, - Image: "doesn't-matter", - Source: dive.SourceDockerEngine, - ExportFile: "", - CiConfig: viper.New(), - BuildArgs: []string{"an-option"}, - }, - events: []testEvent{ - {stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: " efficiency: 98.4421 %", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: " wastedBytes: 32025 bytes (32 kB)", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: " userWastedPercent: 48.3491 %", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Inefficient Files:\nCount Wasted Space File Path\nNone\nResults:\n MISCONFIGURED: highestUserWastedPercent: invalid config value (''): strconv.ParseFloat: parsing \"\": invalid syntax\n MISCONFIGURED: highestWastedBytes: invalid config value (''): strconv.ParseFloat: parsing \"\": invalid syntax\n MISCONFIGURED: lowestEfficiency: invalid config value (''): strconv.ParseFloat: parsing \"\": invalid syntax\nCI Misconfigured\n", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "", stderr: "", errorOnExit: true, errMessage: ""}, - }, - }, - "export-go-case": { - resolver: &defaultResolver{}, - options: Options{ - Ci: true, - Image: "doesn't-matter", - Source: dive.SourceDockerEngine, - ExportFile: "some-file.json", - CiConfig: configureCi(), - BuildArgs: []string{"an-option"}, - }, - events: []testEvent{ - {stdout: "Building image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""}, - {stdout: "Exporting image to 'some-file.json'...", stderr: "", errorOnExit: false, errMessage: ""}, - }, - }, - } - - for name, test := range table { - var ec = make(eventChannel) - var events = make([]testEvent, 0) - var filesystem = afero.NewMemMapFs() - - go run(false, test.options, test.resolver, ec, filesystem) - - for event := range ec { - events = append(events, newTestEvent(event)) - } - - // fmt.Println(name) - // showEvents(events) - // fmt.Println() - - if len(test.events) != len(events) { - t.Fatalf("%s.%s: expected # events='%v', got '%v'", t.Name(), name, len(test.events), len(events)) - } - - for idx, actualEvent := range events { - expectedEvent := test.events[idx] - - if expectedEvent.errorOnExit != actualEvent.errorOnExit { - t.Errorf("%s.%s: expected errorOnExit='%v', got '%v'", t.Name(), name, expectedEvent.errorOnExit, actualEvent.errorOnExit) - } - - actualEventStdoutClean := vtclean.Clean(actualEvent.stdout, false) - expectedEventStdoutClean := vtclean.Clean(expectedEvent.stdout, false) - - if expectedEventStdoutClean != actualEventStdoutClean { - t.Errorf("%s.%s: expected stdout='%v', got '%v'", t.Name(), name, expectedEventStdoutClean, actualEventStdoutClean) - } - - actualEventStderrClean := vtclean.Clean(actualEvent.stderr, false) - expectedEventStderrClean := vtclean.Clean(expectedEvent.stderr, false) - - if expectedEventStderrClean != actualEventStderrClean { - t.Errorf("%s.%s: expected stderr='%v', got '%v'", t.Name(), name, expectedEventStderrClean, actualEventStderrClean) - } - - if expectedEvent.errMessage != actualEvent.errMessage { - t.Errorf("%s.%s: expected error='%v', got '%v'", t.Name(), name, expectedEvent.errMessage, actualEvent.errMessage) - } - - if test.options.ExportFile != "" { - if _, err := filesystem.Stat(test.options.ExportFile); os.IsNotExist(err) { - t.Errorf("%s.%s: expected export file but did not find one", t.Name(), name) - } - } - } - } -} diff --git a/runtime/ui/app.go b/runtime/ui/app.go deleted file mode 100644 index ac92249..0000000 --- a/runtime/ui/app.go +++ /dev/null @@ -1,165 +0,0 @@ -package ui - -import ( - "sync" - - "github.com/awesome-gocui/gocui" - "github.com/sirupsen/logrus" - - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/key" - "github.com/wagoodman/dive/runtime/ui/layout" - "github.com/wagoodman/dive/runtime/ui/layout/compound" -) - -const debug = false - -// type global -type app struct { - gui *gocui.Gui - controllers *Controller - layout *layout.Manager -} - -var ( - once sync.Once - appSingleton *app -) - -func newApp(gui *gocui.Gui, imageName string, resolver image.Resolver, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) { - var err error - once.Do(func() { - var controller *Controller - var globalHelpKeys []*key.Binding - - controller, err = NewCollection(gui, imageName, resolver, analysis, cache) - if err != nil { - return - } - - // note: order matters when adding elements to the layout - lm := layout.NewManager() - lm.Add(controller.views.Status, layout.LocationFooter) - lm.Add(controller.views.Filter, layout.LocationFooter) - lm.Add(compound.NewLayerDetailsCompoundLayout(controller.views.Layer, controller.views.LayerDetails, controller.views.ImageDetails), layout.LocationColumn) - lm.Add(controller.views.Tree, layout.LocationColumn) - - // todo: access this more programmatically - if debug { - lm.Add(controller.views.Debug, layout.LocationColumn) - } - gui.Cursor = false - // g.Mouse = true - gui.SetManagerFunc(lm.Layout) - - // var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook) - // - // onExit = func() { - // profileObj.Stop() - // } - - appSingleton = &app{ - gui: gui, - controllers: controller, - layout: lm, - } - - var infos = []key.BindingInfo{ - { - ConfigKeys: []string{"keybinding.quit"}, - OnAction: appSingleton.quit, - Display: "Quit", - }, - { - ConfigKeys: []string{"keybinding.toggle-view"}, - OnAction: controller.ToggleView, - Display: "Switch view", - }, - { - ConfigKeys: []string{"keybinding.right"}, - OnAction: controller.NextPane, - }, - { - ConfigKeys: []string{"keybinding.left"}, - OnAction: controller.PrevPane, - }, - { - ConfigKeys: []string{"keybinding.filter-files"}, - OnAction: controller.ToggleFilterView, - IsSelected: controller.views.Filter.IsVisible, - Display: "Filter", - }, - { - ConfigKeys: []string{"keybinding.close-filter-files"}, - OnAction: controller.CloseFilterView, - }, - } - - globalHelpKeys, err = key.GenerateBindings(gui, "", infos) - if err != nil { - return - } - - controller.views.Status.AddHelpKeys(globalHelpKeys...) - - // perform the first update and render now that all resources have been loaded - err = controller.UpdateAndRender() - if err != nil { - return - } - }) - - return appSingleton, err -} - -// var profileObj = profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook) -// var onExit func() - -// debugPrint writes the given string to the debug pane (if the debug pane is enabled) -// func debugPrint(s string) { -// if controllers.Tree != nil && controllers.Tree.gui != nil { -// v, _ := controllers.Tree.gui.View("debug") -// if v != nil { -// if len(v.BufferLines()) > 20 { -// v.Clear() -// } -// _, _ = fmt.Fprintln(v, s) -// } -// } -// } - -// quit is the gocui callback invoked when the user hits Ctrl+C -func (a *app) quit() error { - // profileObj.Stop() - // onExit() - - return gocui.ErrQuit -} - -// Run is the UI entrypoint. -func Run(imageName string, resolver image.Resolver, analysis *image.AnalysisResult, treeStack filetree.Comparer) error { - var err error - - g, err := gocui.NewGui(gocui.OutputNormal, true) - if err != nil { - return err - } - defer g.Close() - - _, err = newApp(g, imageName, resolver, analysis, treeStack) - if err != nil { - return err - } - - key, mod := gocui.MustParse("Ctrl+Z") - if err := g.SetKeybinding("", key, mod, handle_ctrl_z); err != nil { - return err - } - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - logrus.Error("main loop error: ", err) - return err - } - return nil -} diff --git a/runtime/ui/view/layer_change_listener.go b/runtime/ui/view/layer_change_listener.go deleted file mode 100644 index 3a7096f..0000000 --- a/runtime/ui/view/layer_change_listener.go +++ /dev/null @@ -1,5 +0,0 @@ -package view - -import "github.com/wagoodman/dive/runtime/ui/viewmodel" - -type LayerChangeListener func(viewmodel.LayerSelection) error diff --git a/runtime/ui/view/views.go b/runtime/ui/view/views.go deleted file mode 100644 index 785f5d3..0000000 --- a/runtime/ui/view/views.go +++ /dev/null @@ -1,85 +0,0 @@ -package view - -import ( - "github.com/awesome-gocui/gocui" - - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/dive/image" -) - -type IView interface { - Setup(*gocui.View, *gocui.View) error - Name() string - IsVisible() bool -} - -type Views struct { - Tree *FileTree - Layer *Layer - Status *Status - Filter *Filter - LayerDetails *LayerDetails - ImageDetails *ImageDetails - Debug *Debug -} - -var _ []IView = []IView{ - &FileTree{}, - &Layer{}, - &Filter{}, - &LayerDetails{}, - &ImageDetails{}, - &Debug{}, -} - -func NewViews(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*Views, error) { - Layer, err := newLayerView(g, analysis.Layers) - if err != nil { - return nil, err - } - - treeStack := analysis.RefTrees[0] - Tree, err := newFileTreeView(g, treeStack, analysis.RefTrees, cache) - if err != nil { - return nil, err - } - - Status := newStatusView(g) - - // set the layer view as the first selected view - Status.SetCurrentView(Layer) - - Filter := newFilterView(g) - - LayerDetails := &LayerDetails{gui: g} - ImageDetails := &ImageDetails{ - gui: g, - imageName: imageName, - imageSize: analysis.SizeBytes, - efficiency: analysis.Efficiency, - inefficiencies: analysis.Inefficiencies, - } - - Debug := newDebugView(g) - - return &Views{ - Tree: Tree, - Layer: Layer, - Status: Status, - Filter: Filter, - ImageDetails: ImageDetails, - LayerDetails: LayerDetails, - Debug: Debug, - }, nil -} - -func (views *Views) All() []Renderer { - return []Renderer{ - views.Tree, - views.Layer, - views.Status, - views.Filter, - views.LayerDetails, - views.ImageDetails, - } -}