Reintegrate fork joschi/dive (#570)

* chore: configure Renovate (#1)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update github artifact actions to v4 (#20)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update docker/login-action action to v3 (#19)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/setup-go action to v5 (#18)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/checkout action to v4 (#17)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/cache action to v4 (#16)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* build: use Go 1.23.x to build project

* chore(deps): update build tools

- golangci-lint v1.16.0: https://github.com/golangci/golangci-lint/releases/tag/v1.61.0
- GoReleaser v1.61.0: https://github.com/goreleaser/goreleaser/releases/tag/v1.26.2
- Chronicle v0.8.0: https://github.com/anchore/chronicle/releases/tag/v0.8.0
- Glow v1.5.1: https://github.com/charmbracelet/glow/releases/tag/v1.5.1

* chore: temporarily lower coverage threshold to 30%

Old coverage:
```
go test -race -coverprofile ./.tmp/unit-coverage-details.txt ./...
?   	github.com/wagoodman/dive	[no test files]
?   	github.com/wagoodman/dive/cmd	[no test files]
?   	github.com/wagoodman/dive/dive	[no test files]
?   	github.com/wagoodman/dive/dive/image	[no test files]
ok  	github.com/wagoodman/dive/dive/filetree	0.032s	coverage: 58.0% of statements
?   	github.com/wagoodman/dive/dive/image/podman	[no test files]
ok  	github.com/wagoodman/dive/dive/image/docker	0.076s	coverage: 43.6% of statements
?   	github.com/wagoodman/dive/runtime/ui	[no test files]
?   	github.com/wagoodman/dive/runtime/ui/format	[no test files]
?   	github.com/wagoodman/dive/runtime/ui/key	[no test files]
ok  	github.com/wagoodman/dive/runtime	0.531s	coverage: 53.3% of statements
ok  	github.com/wagoodman/dive/runtime/ci	0.087s	coverage: 62.1% of statements
ok  	github.com/wagoodman/dive/runtime/export	0.096s	coverage: 100.0% of statements
?   	github.com/wagoodman/dive/runtime/ui/layout/compound	[no test files]
?   	github.com/wagoodman/dive/runtime/ui/view	[no test files]
ok  	github.com/wagoodman/dive/runtime/ui/layout	0.021s	coverage: 82.6% of statements
?   	github.com/wagoodman/dive/utils	[no test files]
ok  	github.com/wagoodman/dive/runtime/ui/viewmodel	1.202s	coverage: 55.3% of statements
Coverage: 57.5%
```

New coverage:
```
go test -race -coverprofile ./.tmp/unit-coverage-details.txt ./...
	github.com/wagoodman/dive/dive		coverage: 0.0% of statements
	github.com/wagoodman/dive/cmd		coverage: 0.0% of statements
	github.com/wagoodman/dive		coverage: 0.0% of statements
	github.com/wagoodman/dive/dive/image		coverage: 0.0% of statements
	github.com/wagoodman/dive/dive/image/podman		coverage: 0.0% of statements
ok  	github.com/wagoodman/dive/dive/filetree	1.027s	coverage: 58.0% of statements
ok  	github.com/wagoodman/dive/dive/image/docker	1.064s	coverage: 43.6% of statements
	github.com/wagoodman/dive/runtime/ui		coverage: 0.0% of statements
	github.com/wagoodman/dive/runtime/ui/format		coverage: 0.0% of statements
	github.com/wagoodman/dive/runtime/ui/key		coverage: 0.0% of statements
	github.com/wagoodman/dive/runtime/ui/layout/compound		coverage: 0.0% of statements
	github.com/wagoodman/dive/runtime/ui/view		coverage: 0.0% of statements
ok  	github.com/wagoodman/dive/runtime	1.382s	coverage: 53.3% of statements
ok  	github.com/wagoodman/dive/runtime/ci	1.055s	coverage: 62.1% of statements
	github.com/wagoodman/dive/utils		coverage: 0.0% of statements
ok  	github.com/wagoodman/dive/runtime/export	1.048s	coverage: 100.0% of statements
ok  	github.com/wagoodman/dive/runtime/ui/layout	1.012s	coverage: 82.6% of statements
ok  	github.com/wagoodman/dive/runtime/ui/viewmodel	2.202s	coverage: 55.3% of statements
Coverage: 31.8%
Coverage below threshold of 55.0%
```

* chore(deps): update fountainhead/action-wait-for-check action to v1.2.0 (#25)

https://github.com/fountainhead/action-wait-for-check/releases/tag/v1.2.0

* ci: validate Renovate configuration file on build

* ci: add gomodTidy and gomodUpdateImportPaths post update options to Renovate

* fix(deps): update module github.com/dustin/go-humanize to v1.0.1 (#5)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update alpine docker tag to v3.20 (#7)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update github.com/awesome-gocui/keybinding digest to 8602903 (#2)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/cobra to v0.0.7 (#6)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/fatih/color to v1.18.0 (#9)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/google/uuid to v1.6.0 (#10)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/sergi/go-diff to v1.3.1 (#11)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/cespare/xxhash to v2 (#21)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/docker to v24.0.9+incompatible (#3)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/viper to v1.19.0 (#14)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/sirupsen/logrus to v1.9.3 (#12)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.30.0 (#15)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/cobra to v1 (#23)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/docker to v27 (#22)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/cli to v27.3.1+incompatible (#27)

* ci: run Linux acceptance tests in parallel (#28)

* chore(deps): update module logrusorgru/aurora to v4 (major) (#29)

github.com/logrusorgru/aurora changed the license from the WTFPL to the Unlicense due to pkg.go.dev restrictions.

Refs 304bc2c7ed

* build: squash Docker image layers (#30)

Refs wagoodman/dive#535

* chore(deps): update Docker CLI version to 27.3.1 (#31)

* ci: update GoReleaser configuration (#33)

* chore(deps): update GoReleaser to v2.4.4 (major) (#32)

* revert: run Linux acceptance tests in parallel

This reverts commit 19714728c9.

* ci: fix image name, actions/checkout@v4, docker/login-action name

* fix: create `~/.docker` directory in Docker images (#34)

Error:
```
❯ docker run -ti --rm  -v /var/run/docker.sock:/var/run/docker.sock joschi/dive:0.13.0-alpha.1 busybox:latest
Image Source: docker://busybox:latest
Extracting image from docker-engine... (this can take a while for large images)
> could not determine docker host: stat /root/.docker: no such file or directory
cannot fetch image
unable to parse docker host ``
```

* build: added ppc64le support (wagoodman/dive#551)

Co-authored-by: Pooja Shah <53046887+pooja0805@users.noreply.github.com>

* fix: fix OCI format, GZIP file can be <1024 bytes (wagoodman/dive#511)

Fixes wagoodman/dive#507
Fixes wagoodman/dive#510
Fixes wagoodman/dive#526
Fixes wagoodman/dive#534

Co-authored-by: Maddog2050 <17902029+Maddog2050@users.noreply.github.com>

* docs: fix typos (wagoodman/dive#531)

Found via `codespell -L ot,te` and `typos --hidden --format brief`

Closes wagoodman/dive#464

Co-authored-by: Kian-Meng Ang <kianmeng@cpan.org>

* docs: add more Windows installation options to the README (wagoodman/dive#470)

Fixes wagoodman/dive#346

Co-authored-by: Nikolas Grottendieck <git@nikolasgrottendieck.com>

* docs: Update README.md (wagoodman/dive#506)

Made copy and paste easy for zsh users
- auto escape character issue

Co-authored-by: YóUnǎi <c0d3r.nodiru.gaji@gmail.com>

* docs: update the install command to the latest Go version (wagoodman/dive#509)

`go get` no longer works with recent versions of `go`.

Co-authored-by: Trevor Gross <tmgross@umich.edu>

* chore: remove repetitive words (wagoodman/dive#515)

Co-authored-by: thirdkeyword <fliterdashen@gmail.com>
Signed-off-by: thirdkeyword <fliterdashen@gmail.com>

* fix: close tmp files (wagoodman/dive#517)

Co-authored-by: guoguangwu <guoguangwug@gmail.com>
Signed-off-by: guoguangwu <guoguangwug@gmail.com>

* chore: fix phony ci-release target (wagoodman/dive#530)

Co-authored-by: Richard Steinmetz <richard@steinmetz.cloud>

* docs: warning message for Snap approach on Ubuntu/Debian (wagoodman/dive#552)

https://github.com/wagoodman/dive/issues/546 demonstrates the trouble it may cause.

Co-authored-by: Zhang Yuanfeng <71358306+YuanfengZhang@users.noreply.github.com>

* docs: update curl commands in README (wagoodman/dive#533)

Co-authored-by: Ali Afsharzadeh <afsharzadeh8@gmail.com>

* feat: improve "Fetching" message (wagoodman/dive#482)

The Fetching... message was confusing.

This replaces it with a clearer messages to avoid confusion.

Additional fix: show original error unless image is not found

Only try doing a pull if the image isn't found. Everything else should
just generate the error so the user can fix it.

Fixes wagoodman/dive#360

Co-authored-by: Christian Höltje <docwhat@gerf.org>

* fix: line wrap toggle now updates status bar indicator (wagoodman/dive#497)

Fixes wagoodman/dive#496

Co-authored-by: Scott Moore <scott.moore@viavisolutions.com>

* feat: show setuid, setgid and sticky attributes (wagoodman/dive#524)

See https://en.wikipedia.org/wiki/File-system_permissions#Notation_of_traditional_Unix_permissions

Co-authored-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>

* feat(docker): Honor the host specified in current docker context (wagoodman/dive#490)

This patch adds support for detecting the "docker host" to connect to which is set in the current docker context.
One can have multiple such contexts and one of them can be activated via `docker context use <context-name>`.

Fixes wagoodman/dive#397
Fixes wagoodman/dive#408
Fixes wagoodman/dive#412
Fixes wagoodman/dive#463
Fixes wagoodman/dive#495

Co-authored-by: Rajiv Kushwaha <raj25by10@gmail.com>

* fix: can't inspect ubuntu:24.04 with Podman (wagoodman/dive#476)

The problem was caused by `net/url.Parse()`:
```
panic: parse "podman://ubuntu:24.04": invalid port ":24.04" after host
```

Failure:
```
$ ./dive podman://ubuntu:24.04
Image Source: docker://podman://ubuntu:24.04
Fetching image... (this can take a while for large images)
Handler not available locally. Trying to pull 'podman://ubuntu:24.04'...
cannot fetch image
cannot find docker client executable
```

Success:
```
$ ./dive podman://ubuntu:24.04
Image Source: podman://ubuntu:24.04
...

$ ./dive ubuntu:24.04 --source podman
Image Source: podman://ubuntu:24.04
...

$ ./dive podman://ubuntu:24.04 --source docker
Image Source: podman://ubuntu:24.04
...
```

Fixes wagoodman/dive#475

Co-authored-by: Anatoli Babenia <anatoli@rainforce.org>

* fix: enable layer scrolling (wagoodman/dive#521)

Fixes wagoodman/dive#469
Fixes wagoodman/dive#494
Fixes wagoodman/dive#540
Refs wagoodman/dive#473
Refs wagoodman/dive#478
Refs wagoodman/dive#520

Co-authored-by: st-gr <38470677+st-gr@users.noreply.github.com>

* feat: add layer-wise filesystem information to the analysis json file (wagoodman/dive#458)

Add layer-wise filesystem information to the analysis which is written to a JSON file
when running dive with `-j` or `--json` flag.

Co-authored-by: Akash Nayak <akash19nayak@gmail.com>

* feat: add size to Layer Details view (wagoodman/dive#522)

Refs https://github.com/wagoodman/dive/issues/469#issuecomment-1685322270

Co-authored-by: st-gr <38470677+st-gr@users.noreply.github.com>

* feat: add CTRL+e for extracting current focused file

Refs wagoodman/dive#224

Co-authored-by: kaedwen <kaedwen@heinrich.blue>

* feat: vim-like arrow, scroll, and close filtering motions (wagoodman/dive#501)

* Adding configurable keybindings for up/down arrows (`k` and `j` vim motions can be used as alternative to up/down arrows).
Thanks to @gwendolyngoetz for implementing this feature [Adding configurable keybindings for up/down arrows #499](https://github.com/wagoodman/dive/pull/499)
* Add configurable keybindings for left/right arrows (`h` and `l` vim motions can be used as alternative to left/right arrows)
* Add `u` and `d` keys for page up/down alternatives (I didn't want to replace default `ctrl+u` toggle-unmodified-files keybinding so I used`u` and `d` like `Vimium` extension )
* Add `esc` key to close filtering (Implemented a new method by utilizing the existing toggle filter method, without touching its current behavior)

Refs wagoodman/dive#129
Refs wagoodman/dive#415
Refs wagoodman/dive#499

Co-authored-by: Gwendolyn Goetz <gwendolyngoetz@users.noreply.github.com>
Co-authored-by: Mehmet Ümit Özden <ozdenmehmetumit@gmail.com>

* fix: gracefully check for Docker configuration (#37)

Refs https://github.com/jesseduffield/lazydocker/pull/489

* refactor!: migrate Go module from wagoodman/dive to joschi/dive (#36)

* refactor!: migrate Go module from wagoodman/dive to joschi/dive
* fix: bring back :latest Docker image

* feat: create multi-arch container images with AMD64 and ARM64 (#38)

* feat: create multi-arch container images with AMD64 and ARM64
* fix: use joschi/dive:latest-amd64 in CI

* ci: use correct container registry in `ci-test-docker-image` (#39)

* docs: fix Homebrew instructions in README

* fix(deps): update module golang.org/x/net to v0.31.0 (#40)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update suzuki-shunsuke/github-action-renovate-config-validator action to v1.1.1 (#43)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.32.0 (#44)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update alpine docker tag to v3.21 (#45)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/cli to v27.4.1+incompatible (#46)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.33.0 (#48)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/docker to v27.4.1+incompatible (#47)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.34.0 (#49)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/afero to v1.12.0 (#50)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/cli to v27.5.0+incompatible (#51)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/docker to v27.5.0+incompatible (#52)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/cli to v27.5.1+incompatible (#54)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/docker to v27.5.1+incompatible (#55)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: support oci zstd compression (#53)

Co-authored-by: Jochen Schalanda <jochen@schalanda.name>

* fix(deps): update module github.com/klauspost/compress to v1.17.11 (#57)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.35.0 (#59)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/cobra to v1.9.0 (#60)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/cobra to v1.9.1 (#62)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/klauspost/compress to v1.18.0 (#63)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/cli to v28 (#64)

* fix(deps): update module github.com/docker/cli to v28

* Replace deprecated function in engineResolver.fetchArchive()

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jochen Schalanda <jochen@schalanda.name>

* build: bump Go toolchain from 1.23.x to 1.24.x (#61)

* build: bump Go toolchain from 1.23.x to 1.24.x

* chore(deps): bump golangci-lint from v1.61.0 to v1.64.5

* fix: non-constant format string in call to (*testing.common).Errorf

* test: add layer_set_state_test

* fix(deps): update module github.com/docker/cli to v28.0.1+incompatible (#66)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/docker/docker to v28.0.1+incompatible (#65)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.36.0 (#73)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module golang.org/x/net to v0.37.0 (#74)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/viper to v1.20.0 (#76)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/spf13/afero to v1.14.0 (#75)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor!: revert module name back to github.com/wagoodman/dive

Partially reverts e46f931a8a

* chore: remove Renovate configuration

Reverts aa75fbf36f

* Revert "ci: validate Renovate configuration file on build"

This reverts commit 8d938774e2.

---------

Signed-off-by: thirdkeyword <fliterdashen@gmail.com>
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Pooja Shah <53046887+pooja0805@users.noreply.github.com>
Co-authored-by: Maddog2050 <17902029+Maddog2050@users.noreply.github.com>
Co-authored-by: Kian-Meng Ang <kianmeng@cpan.org>
Co-authored-by: Nikolas Grottendieck <git@nikolasgrottendieck.com>
Co-authored-by: YóUnǎi <c0d3r.nodiru.gaji@gmail.com>
Co-authored-by: Trevor Gross <tmgross@umich.edu>
Co-authored-by: thirdkeyword <fliterdashen@gmail.com>
Co-authored-by: guoguangwu <guoguangwug@gmail.com>
Co-authored-by: Richard Steinmetz <richard@steinmetz.cloud>
Co-authored-by: Zhang Yuanfeng <71358306+YuanfengZhang@users.noreply.github.com>
Co-authored-by: Ali Afsharzadeh <afsharzadeh8@gmail.com>
Co-authored-by: Christian Höltje <docwhat@gerf.org>
Co-authored-by: Scott Moore <scott.moore@viavisolutions.com>
Co-authored-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
Co-authored-by: Rajiv Kushwaha <raj25by10@gmail.com>
Co-authored-by: Anatoli Babenia <anatoli@rainforce.org>
Co-authored-by: st-gr <38470677+st-gr@users.noreply.github.com>
Co-authored-by: Akash Nayak <akash19nayak@gmail.com>
Co-authored-by: kaedwen <kaedwen@heinrich.blue>
Co-authored-by: Gwendolyn Goetz <gwendolyngoetz@users.noreply.github.com>
Co-authored-by: Mehmet Ümit Özden <ozdenmehmetumit@gmail.com>
Co-authored-by: steven-halaka <steven.halaka@twosixtech.com>
This commit is contained in:
Jochen Schalanda 2025-03-18 05:15:09 +01:00 committed by GitHub
commit e65b32c4d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 5545 additions and 460 deletions

View file

@ -5,6 +5,7 @@ permit:
- MPL.* - MPL.*
- ISC - ISC
- WTFPL - WTFPL
- Unlicense
ignore-packages: ignore-packages:
# crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Library # crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Library

2
.data/Dockerfile.minimal Normal file
View file

@ -0,0 +1,2 @@
FROM scratch
COPY README.md /README.md

Binary file not shown.

BIN
.data/test-gzip-image.tar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.data/test-zstd-image.tar Normal file

Binary file not shown.

View file

@ -4,7 +4,7 @@ inputs:
go-version: go-version:
description: "Go version to install" description: "Go version to install"
required: true required: true
default: "1.20.x" default: "1.24.x"
use-go-cache: use-go-cache:
description: "Restore go cache" description: "Restore go cache"
required: true required: true
@ -24,13 +24,13 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: ${{ inputs.go-version }} go-version: ${{ inputs.go-version }}
- name: Restore tool cache - name: Restore tool cache
id: tool-cache id: tool-cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ${{ github.workspace }}/.tmp path: ${{ github.workspace }}/.tmp
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }} key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
@ -40,7 +40,7 @@ runs:
- name: Restore go module cache - name: Restore go module cache
id: go-mod-cache id: go-mod-cache
if: inputs.use-go-cache == 'true' if: inputs.use-go-cache == 'true'
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/go/pkg/mod ~/go/pkg/mod
@ -56,7 +56,7 @@ runs:
- name: Restore go build cache - name: Restore go build cache
id: go-cache id: go-cache
if: inputs.use-go-cache == 'true' if: inputs.use-go-cache == 'true'
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.cache/go-build ~/.cache/go-build

View file

@ -11,7 +11,7 @@ jobs:
environment: release environment: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Check if tag already exists - name: Check if tag already exists
# note: this will fail if the tag already exists # note: this will fail if the tag already exists
@ -20,7 +20,7 @@ jobs:
git tag ${{ github.event.inputs.version }} git tag ${{ github.event.inputs.version }}
- name: Check static analysis results - name: Check static analysis results
uses: fountainhead/action-wait-for-check@v1.1.0 uses: fountainhead/action-wait-for-check@v1.2.0
id: static-analysis id: static-analysis
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -29,7 +29,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit test results - name: Check unit test results
uses: fountainhead/action-wait-for-check@v1.1.0 uses: fountainhead/action-wait-for-check@v1.2.0
id: unit id: unit
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux) - name: Check acceptance test results (linux)
uses: fountainhead/action-wait-for-check@v1.1.0 uses: fountainhead/action-wait-for-check@v1.2.0
id: acceptance-linux id: acceptance-linux
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -47,7 +47,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac) - name: Check acceptance test results (mac)
uses: fountainhead/action-wait-for-check@v1.1.0 uses: fountainhead/action-wait-for-check@v1.2.0
id: acceptance-mac id: acceptance-mac
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -56,7 +56,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (windows) - name: Check acceptance test results (windows)
uses: fountainhead/action-wait-for-check@v1.1.0 uses: fountainhead/action-wait-for-check@v1.2.0
id: acceptance-windows id: acceptance-windows
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -64,7 +64,6 @@ jobs:
checkName: "Acceptance tests (Windows)" checkName: "Acceptance tests (Windows)"
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Quality gate - name: Quality gate
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' || steps.acceptance-windows.outputs.conclusion != 'success' if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' || steps.acceptance-windows.outputs.conclusion != 'success'
run: | run: |
@ -82,15 +81,25 @@ jobs:
permissions: permissions:
# for tagging # for tagging
contents: write contents: write
# for pushing container images
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps: steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Bootstrap environment - name: Bootstrap environment
uses: ./.github/actions/bootstrap uses: ./.github/actions/bootstrap
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Tag release - name: Tag release
run: | run: |
git tag ${{ github.event.inputs.version }} git tag ${{ github.event.inputs.version }}
@ -98,11 +107,12 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to container registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKER_PASSWORD }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & publish release artifacts - name: Build & publish release artifacts
run: make ci-release run: make ci-release

View file

@ -7,13 +7,12 @@ on:
pull_request: pull_request:
jobs: jobs:
Static-Analysis: Static-Analysis:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Static analysis" name: "Static analysis"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Bootstrap environment - name: Bootstrap environment
uses: ./.github/actions/bootstrap uses: ./.github/actions/bootstrap
@ -21,21 +20,19 @@ jobs:
- name: Run static analysis - name: Run static analysis
run: make static-analysis run: make static-analysis
Unit-Test: Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests" name: "Unit tests"
strategy: strategy:
matrix: matrix:
platform: platform:
- ubuntu-latest - ubuntu-latest
# - macos-latest # todo: mac runners are expensive minute-wise # - macos-latest # todo: mac runners are expensive minute-wise
# - windows-latest # todo: support windows # - windows-latest # todo: support windows
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Bootstrap environment - name: Bootstrap environment
uses: ./.github/actions/bootstrap uses: ./.github/actions/bootstrap
@ -43,16 +40,21 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: make unit run: make unit
Build-Snapshot-Artifacts: Build-Snapshot-Artifacts:
name: "Build snapshot artifacts" name: "Build snapshot artifacts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Bootstrap environment - name: Bootstrap environment
uses: ./.github/actions/bootstrap uses: ./.github/actions/bootstrap
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build snapshot artifacts - name: Build snapshot artifacts
run: make snapshot run: make snapshot
@ -65,28 +67,26 @@ jobs:
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach). # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
# see https://github.com/actions/upload-artifact/issues/199 for more info # see https://github.com/actions/upload-artifact/issues/199 for more info
- name: Upload snapshot artifacts - name: Upload snapshot artifacts
uses: actions/cache/save@v3 uses: actions/cache/save@v4
with: with:
path: snapshot path: snapshot
key: snapshot-build-${{ github.run_id }} key: snapshot-build-${{ github.run_id }}
# ... however the cache trick doesn't work on windows :( # ... however the cache trick doesn't work on windows :(
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
with: with:
name: windows-artifacts name: windows-artifacts
path: snapshot/dive_windows_amd64_v1/dive.exe path: snapshot/dive_windows_amd64_v1/dive.exe
Acceptance-Linux: Acceptance-Linux:
name: "Acceptance tests (Linux)" name: "Acceptance tests (Linux)"
needs: [ Build-Snapshot-Artifacts ] needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Download snapshot build - name: Download snapshot build
uses: actions/cache/restore@v3 uses: actions/cache/restore@v4
with: with:
path: snapshot path: snapshot
key: snapshot-build-${{ github.run_id }} key: snapshot-build-${{ github.run_id }}
@ -100,17 +100,15 @@ jobs:
- name: Test RPM package installation - name: Test RPM package installation
run: make ci-test-rpm-package-install run: make ci-test-rpm-package-install
Acceptance-Mac: Acceptance-Mac:
name: "Acceptance tests (Mac)" name: "Acceptance tests (Mac)"
needs: [ Build-Snapshot-Artifacts ] needs: [Build-Snapshot-Artifacts]
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Download snapshot build - name: Download snapshot build
uses: actions/cache/restore@v3 uses: actions/cache/restore@v4
with: with:
path: snapshot path: snapshot
key: snapshot-build-${{ github.run_id }} key: snapshot-build-${{ github.run_id }}
@ -118,16 +116,14 @@ jobs:
- name: Test darwin run - name: Test darwin run
run: make ci-test-mac-run run: make ci-test-mac-run
Acceptance-Windows: Acceptance-Windows:
name: "Acceptance tests (Windows)" name: "Acceptance tests (Windows)"
needs: [ Build-Snapshot-Artifacts ] needs: [Build-Snapshot-Artifacts]
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: windows-artifacts name: windows-artifacts

View file

@ -67,8 +67,8 @@ linters-settings:
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code # - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
# - scopelint # deprecated # - scopelint # deprecated
# - testpackage # - testpackage
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) # - wsl # this doesn't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
# - varcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. # - varcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - deadcode # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. # - deadcode # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - structcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. # - structcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - rowserrcheck # we're not using sql.Rows at all in the codebase # - rowserrcheck # we're not using sql.Rows at all in the codebase

View file

@ -1,3 +1,5 @@
version: 2
release: release:
# If set to auto, will mark the release as not ready for production in case there is an indicator for this in the # If set to auto, will mark the release as not ready for production in case there is an indicator for this in the
# tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production. # tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production.
@ -17,6 +19,7 @@ builds:
goarch: goarch:
- amd64 - amd64
- arm64 - arm64
- ppc64le
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.buildTime={{.Date}}`. ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.buildTime={{.Date}}`.
brews: brews:
@ -43,15 +46,44 @@ nfpms:
- deb - deb
dockers: dockers:
- - id: docker-amd64
ids: ids:
- dive - dive
dockerfile: Dockerfile use: buildx
# todo: on 1.0 remove 'v' prefix goarch: amd64
image_templates: image_templates:
- "wagoodman/dive:latest" - '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:latest-amd64'
- "wagoodman/dive:{{ .Tag }}" - '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:{{ .Version }}-amd64'
- "wagoodman/dive:v{{ .Major }}"
- "wagoodman/dive:v{{ .Major }}.{{ .Minor }}"
build_flag_templates: build_flag_templates:
- "--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}" - "--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
- id: docker-arm64
ids:
- dive
use: buildx
goarch: arm64
image_templates:
- '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:latest-arm64'
- '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:{{ .Version }}-arm64'
build_flag_templates:
- "--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm64/v8"
docker_manifests:
- name_template: '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:latest'
image_templates:
- '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:latest-amd64'
- '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:latest-arm64'
- name_template: '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:{{ .Version }}'
image_templates:
- '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:{{ .Version }}-amd64'
- '{{ envOrDefault "REGISTRY" "docker.io" }}/wagoodman/dive:{{ .Version }}-arm64'

View file

@ -1,10 +1,12 @@
FROM alpine:3.18 FROM alpine:3.21 AS base
ARG DOCKER_CLI_VERSION=${DOCKER_CLI_VERSION} ARG DOCKER_CLI_VERSION=${DOCKER_CLI_VERSION}
RUN wget -O- https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz | \ RUN wget -O- https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz | \
tar -xzf - docker/docker --strip-component=1 && \ tar -xzf - docker/docker --strip-component=1 -C /usr/local/bin
mv docker /usr/local/bin
COPY dive /usr/local/bin/ COPY dive /usr/local/bin/
FROM scratch
COPY --from=base /usr/local/bin /usr/local/bin
ENTRYPOINT ["/usr/local/bin/dive"] ENTRYPOINT ["/usr/local/bin/dive"]

View file

@ -1,24 +1,24 @@
BIN = dive BIN = dive
TEMP_DIR = ./.tmp TEMP_DIR = ./.tmp
PWD := ${CURDIR} PWD := ${CURDIR}
PRODUCTION_REGISTRY = docker.io REGISTRY ?= docker.io
SHELL = /bin/bash -o pipefail SHELL = /bin/bash -o pipefail
TEST_IMAGE = busybox:latest TEST_IMAGE = busybox:latest
# Tool versions ################################# # Tool versions #################################
GOLANG_CI_VERSION = v1.52.2 GOLANG_CI_VERSION = v1.64.5
GOBOUNCER_VERSION = v0.4.0 GOBOUNCER_VERSION = v0.4.0
GORELEASER_VERSION = v1.19.1 GORELEASER_VERSION = v2.4.4
GOSIMPORTS_VERSION = v0.3.8 GOSIMPORTS_VERSION = v0.3.8
CHRONICLE_VERSION = v0.6.0 CHRONICLE_VERSION = v0.8.0
GLOW_VERSION = v1.5.0 GLOW_VERSION = v1.5.1
DOCKER_CLI_VERSION = 23.0.6 DOCKER_CLI_VERSION = 28.0.0
# Command templates ################################# # Command templates #################################
LINT_CMD = $(TEMP_DIR)/golangci-lint run --tests=false --timeout=2m --config .golangci.yaml LINT_CMD = $(TEMP_DIR)/golangci-lint run --tests=false --timeout=2m --config .golangci.yaml
GOIMPORTS_CMD = $(TEMP_DIR)/gosimports -local github.com/wagoodman GOIMPORTS_CMD = $(TEMP_DIR)/gosimports -local github.com/wagoodman
RELEASE_CMD = DOCKER_CLI_VERSION=$(DOCKER_CLI_VERSION) $(TEMP_DIR)/goreleaser release --clean RELEASE_CMD = DOCKER_CLI_VERSION=$(DOCKER_CLI_VERSION) $(TEMP_DIR)/goreleaser release --clean
SNAPSHOT_CMD = $(RELEASE_CMD) --skip-publish --snapshot --skip-sign SNAPSHOT_CMD = $(RELEASE_CMD) --skip=publish --skip=sign --snapshot
CHRONICLE_CMD = $(TEMP_DIR)/chronicle CHRONICLE_CMD = $(TEMP_DIR)/chronicle
GLOW_CMD = $(TEMP_DIR)/glow GLOW_CMD = $(TEMP_DIR)/glow
@ -34,7 +34,7 @@ SUCCESS := $(BOLD)$(GREEN)
# Test variables ################################# # Test variables #################################
# the quality gate lower threshold for unit test total % coverage (by function statements) # the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 55 COVERAGE_THRESHOLD := 30
## Build variables ################################# ## Build variables #################################
DIST_DIR = dist DIST_DIR = dist
@ -86,7 +86,7 @@ bootstrap-tools: $(TEMP_DIR)
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION) curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANG_CI_VERSION) curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANG_CI_VERSION)
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(GOBOUNCER_VERSION) curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(GOBOUNCER_VERSION)
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION) GOBIN="$(realpath $(TEMP_DIR))" go install github.com/goreleaser/goreleaser/v2@$(GORELEASER_VERSION)
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION) GOBIN="$(realpath $(TEMP_DIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION) GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION)
@ -125,7 +125,34 @@ bootstrap: bootstrap-go bootstrap-tools ## Download and install all go dependenc
.PHONY: generate-test-data .PHONY: generate-test-data
generate-test-data: generate-test-data:
docker build -t dive-test:latest -f .data/Dockerfile.test-image . && docker image save -o .data/test-docker-image.tar dive-test:latest && echo 'Exported test data!' docker build -t dive-test:latest -f .data/Dockerfile.test-image . && \
docker image save -o .data/test-docker-image.tar dive-test:latest && \
echo 'Exported test data!'
.PHONY: generate-compressed-test-images
generate-compressed-test-images:
for alg in uncompressed gzip estargz zstd; do \
for exporter in docker image; do \
docker buildx build \
-f .data/Dockerfile.minimal \
--tag test-dive-$${exporter}:$${alg} \
--output type=$${exporter},force-compression=true,compression=$${alg} . ; \
done ; \
done && \
echo 'Exported test data!'
.PHONY: generate-compressed-test-data
generate-compressed-test-data:
for alg in uncompressed gzip estargz zstd; \
do \
docker buildx build \
-f .data/Dockerfile.minimal \
--output type=tar,dest=.data/test-$${alg}-image.tar,force-compression=true,compression=$${alg} . ; \
docker buildx build \
-f .data/Dockerfile.minimal \
--output type=oci,dest=.data/test-oci-$${alg}-image.tar,force-compression=true,compression=$${alg} . ; \
done && \
echo 'Exported test data!'
## Static analysis targets ################################# ## Static analysis targets #################################
@ -186,7 +213,7 @@ ci-test-docker-image:
--rm \ --rm \
-t \ -t \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
'${PRODUCTION_REGISTRY}/wagoodman/dive:latest' \ '${REGISTRY}/wagoodman/dive:latest-amd64' \
'${TEST_IMAGE}' \ '${TEST_IMAGE}' \
--ci --ci
@ -227,12 +254,18 @@ ci-test-rpm-package-install:
" "
.PHONY: ci-test-linux-run .PHONY: ci-test-linux-run
ci-test-linux-run: ci-test-linux-run: generate-compressed-test-images
ls -la $(SNAPSHOT_DIR) ls -la $(SNAPSHOT_DIR)
ls -la $(SNAPSHOT_DIR)/dive_linux_amd64_v1 ls -la $(SNAPSHOT_DIR)/dive_linux_amd64_v1
chmod 755 $(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive && \ chmod 755 $(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive && \
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive '${TEST_IMAGE}' --ci && \ $(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive '${TEST_IMAGE}' --ci && \
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci $(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
for alg in uncompressed gzip estargz zstd; do \
for exporter in docker image; do \
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive "test-dive-$${exporter}:$${alg}" --ci ; \
done && \
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive --source docker-archive .data/test-oci-$${alg}-image.tar --ci --ci-config .data/.dive-ci; \
done
# we're not attempting to test docker, just our ability to run on these systems. This avoids setting up docker in CI. # we're not attempting to test docker, just our ability to run on these systems. This avoids setting up docker in CI.
.PHONY: ci-test-mac-run .PHONY: ci-test-mac-run
@ -282,7 +315,7 @@ $(CHANGELOG):
release: ## Cut a new release release: ## Cut a new release
@.github/scripts/trigger-release.sh @.github/scripts/trigger-release.sh
.PHONY: release .PHONY: ci-release
ci-release: ci-check clean-dist $(CHANGELOG) ci-release: ci-check clean-dist $(CHANGELOG)
$(call title,Publishing release artifacts) $(call title,Publishing release artifacts)
@ -315,9 +348,8 @@ clean-changelog:
rm -f $(CHANGELOG) VERSION rm -f $(CHANGELOG) VERSION
## Halp! ################################# ## Help! #################################
.PHONY: help .PHONY: help
help: help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'

View file

@ -5,7 +5,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT%202.0-blue.svg)](https://github.com/wagoodman/dive/blob/main/LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT%202.0-blue.svg)](https://github.com/wagoodman/dive/blob/main/LICENSE)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat)](https://www.paypal.me/wagoodman) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat)](https://www.paypal.me/wagoodman)
**A tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.** **A tool for exploring a Docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.**
![Image](.data/demo.gif) ![Image](.data/demo.gif)
@ -15,9 +15,9 @@ To analyze a Docker image simply run dive with an image tag/id/digest:
dive <your-image-tag> dive <your-image-tag>
``` ```
or you can dive with docker command directly or you can dive with Docker directly:
``` ```
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive" alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/wagoodman/dive"
dive <your-image-tag> dive <your-image-tag>
# for example # for example
@ -29,7 +29,7 @@ or if you want to build your image then jump straight into analyzing it:
dive build -t <some-tag> . dive build -t <some-tag> .
``` ```
Building on Macbook (supporting only the Docker container engine) Building on macOS (supporting only the Docker container engine):
```bash ```bash
docker run --rm -it \ docker run --rm -it \
@ -37,7 +37,7 @@ docker run --rm -it \
-v "$(pwd)":"$(pwd)" \ -v "$(pwd)":"$(pwd)" \
-w "$(pwd)" \ -w "$(pwd)" \
-v "$HOME/.dive.yaml":"$HOME/.dive.yaml" \ -v "$HOME/.dive.yaml":"$HOME/.dive.yaml" \
wagoodman/dive:latest build -t <some-tag> . ghcr.io/wagoodman/dive:latest build -t <some-tag> .
``` ```
Additionally you can run this in your CI pipeline to ensure you're keeping wasted space to a minimum (this skips the UI): Additionally you can run this in your CI pipeline to ensure you're keeping wasted space to a minimum (this skips the UI):
@ -98,7 +98,7 @@ With valid `source` options as such:
Using debs: Using debs:
```bash ```bash
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb curl -fOL "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb"
sudo apt install ./dive_${DIVE_VERSION}_linux_amd64.deb sudo apt install ./dive_${DIVE_VERSION}_linux_amd64.deb
``` ```
@ -110,10 +110,16 @@ sudo snap connect dive:docker-executables docker:docker-executables
sudo snap connect dive:docker-daemon docker:docker-daemon sudo snap connect dive:docker-daemon docker:docker-daemon
``` ```
> [!CAUTION]
> The Snap method is not recommended if you installed Docker via `apt-get`, since it might break your existing Docker daemon.
>
> See also: https://github.com/wagoodman/dive/issues/546
**RHEL/Centos** **RHEL/Centos**
```bash ```bash
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.rpm curl -fOL "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.rpm"
rpm -i dive_${DIVE_VERSION}_linux_amd64.rpm rpm -i dive_${DIVE_VERSION}_linux_amd64.rpm
``` ```
@ -130,7 +136,8 @@ pacman -S dive
If you use [Homebrew](https://brew.sh): If you use [Homebrew](https://brew.sh):
```bash ```bash
brew install dive brew tap wagoodman/dive
brew install wagoodman/dive/dive
``` ```
If you use [MacPorts](https://www.macports.org): If you use [MacPorts](https://www.macports.org):
@ -143,13 +150,31 @@ Or download the latest Darwin build from the [releases page](https://github.com/
**Windows** **Windows**
Download the [latest release](https://github.com/wagoodman/dive/releases/latest). If you use [Chocolatey](https://chocolatey.org)
```powershell
choco install dive
```
If you use [scoop](https://scoop.sh/)
```powershell
scoop install main/dive
```
If you use [winget](https://learn.microsoft.com/en-gb/windows/package-manager/):
```powershell
winget install --id wagoodman.dive
```
Or download the latest Windows build from the [releases page](https://github.com/wagoodman/dive/releases/latest).
**Go tools** **Go tools**
Requires Go version 1.10 or higher. Requires Go version 1.10 or higher.
```bash ```bash
go get github.com/wagoodman/dive go install github.com/wagoodman/dive@latest
``` ```
*Note*: installing in this way you will not see a proper version when running `dive -v`. *Note*: installing in this way you will not see a proper version when running `dive -v`.
@ -166,27 +191,21 @@ nix-env -iA nixpkgs.dive
**Docker** **Docker**
```bash ```bash
docker pull wagoodman/dive docker pull ghcr.io/wagoodman/dive
``` ```
or When running you'll need to include the Docker socket file:
```bash
docker pull quay.io/wagoodman/dive
```
When running you'll need to include the docker socket file:
```bash ```bash
docker run --rm -it \ docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest <dive arguments...> ghcr.io/wagoodman/dive:latest <dive arguments...>
``` ```
Docker for Windows (showing PowerShell compatible line breaks; collapse to a single line for Command Prompt compatibility) Docker for Windows (showing PowerShell compatible line breaks; collapse to a single line for Command Prompt compatibility)
```bash ```bash
docker run --rm -it ` docker run --rm -it `
-v /var/run/docker.sock:/var/run/docker.sock ` -v /var/run/docker.sock:/var/run/docker.sock `
wagoodman/dive:latest <dive arguments...> ghcr.io/wagoodman/dive:latest <dive arguments...>
``` ```
**Note:** depending on the version of docker you are running locally you may need to specify the docker API version as an environment variable: **Note:** depending on the version of docker you are running locally you may need to specify the docker API version as an environment variable:
@ -198,7 +217,7 @@ or if you are running with a docker image:
docker run --rm -it \ docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-e DOCKER_API_VERSION=1.37 \ -e DOCKER_API_VERSION=1.37 \
wagoodman/dive:latest <dive arguments...> ghcr.io/wagoodman/dive:latest <dive arguments...>
``` ```
## CI Integration ## CI Integration
@ -228,8 +247,11 @@ Key Binding | Description
<kbd>Ctrl + C</kbd> or <kbd>Q</kbd> | Exit <kbd>Ctrl + C</kbd> or <kbd>Q</kbd> | Exit
<kbd>Tab</kbd> | Switch between the layer and filetree views <kbd>Tab</kbd> | Switch between the layer and filetree views
<kbd>Ctrl + F</kbd> | Filter files <kbd>Ctrl + F</kbd> | Filter files
<kbd>PageUp</kbd> | Scroll up a page <kbd>ESC</kbd> | Close filter files
<kbd>PageDown</kbd> | Scroll down a page <kbd>PageUp</kbd> or <kbd>U</kbd> | Scroll up a page
<kbd>PageDown</kbd> or <kbd>D</kbd> | Scroll down a page
<kbd>Up</kbd> or <kbd>K</kbd> | Move up one line within a page
<kbd>Down</kbd> or <kbd>J</kbd> | Move down one line within a page
<kbd>Ctrl + A</kbd> | Layer view: see aggregated image modifications <kbd>Ctrl + A</kbd> | Layer view: see aggregated image modifications
<kbd>Ctrl + L</kbd> | Layer view: see current layer modifications <kbd>Ctrl + L</kbd> | Layer view: see current layer modifications
<kbd>Space</kbd> | Filetree view: collapse/uncollapse a directory <kbd>Space</kbd> | Filetree view: collapse/uncollapse a directory
@ -239,8 +261,8 @@ Key Binding | Description
<kbd>Ctrl + M</kbd> | Filetree view: show/hide modified files <kbd>Ctrl + M</kbd> | Filetree view: show/hide modified files
<kbd>Ctrl + U</kbd> | Filetree view: show/hide unmodified files <kbd>Ctrl + U</kbd> | Filetree view: show/hide unmodified files
<kbd>Ctrl + B</kbd> | Filetree view: show/hide file attributes <kbd>Ctrl + B</kbd> | Filetree view: show/hide file attributes
<kbd>PageUp</kbd> | Filetree view: scroll up a page <kbd>PageUp</kbd> or <kbd>U</kbd> | Filetree view: scroll up a page
<kbd>PageDown</kbd> | Filetree view: scroll down a page <kbd>PageDown</kbd> or <kbd>D</kbd> | Filetree view: scroll down a page
## UI Configuration ## UI Configuration
@ -262,6 +284,11 @@ keybinding:
quit: ctrl+c quit: ctrl+c
toggle-view: tab toggle-view: tab
filter-files: ctrl+f, ctrl+slash filter-files: ctrl+f, ctrl+slash
close-filter-files: esc
up: up,k
down: down,j
left: left,h
right: right,l
# Layer view specific bindings # Layer view specific bindings
compare-all: ctrl+a compare-all: ctrl+a
@ -275,8 +302,8 @@ keybinding:
toggle-modified-files: ctrl+m toggle-modified-files: ctrl+m
toggle-unmodified-files: ctrl+u toggle-unmodified-files: ctrl+u
toggle-filetree-attributes: ctrl+b toggle-filetree-attributes: ctrl+b
page-up: pgup page-up: pgup,u
page-down: pgdn page-down: pgdn,d
diff: diff:
# You can change the default files shown in the filetree (right pane). All diff types are shown by default. # You can change the default files shown in the filetree (right pane). All diff types are shown by default.

View file

@ -80,6 +80,7 @@ func initConfig() {
viper.SetDefault("keybinding.quit", "ctrl+c,q") viper.SetDefault("keybinding.quit", "ctrl+c,q")
viper.SetDefault("keybinding.toggle-view", "tab") viper.SetDefault("keybinding.toggle-view", "tab")
viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash") viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash")
viper.SetDefault("keybinding.close-filter-files", "esc")
// keybindings: layer view // keybindings: layer view
viper.SetDefault("keybinding.compare-all", "ctrl+a") viper.SetDefault("keybinding.compare-all", "ctrl+a")
viper.SetDefault("keybinding.compare-layer", "ctrl+l") viper.SetDefault("keybinding.compare-layer", "ctrl+l")
@ -93,8 +94,13 @@ func initConfig() {
viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m") viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m")
viper.SetDefault("keybinding.toggle-unmodified-files", "ctrl+u") viper.SetDefault("keybinding.toggle-unmodified-files", "ctrl+u")
viper.SetDefault("keybinding.toggle-wrap-tree", "ctrl+p") viper.SetDefault("keybinding.toggle-wrap-tree", "ctrl+p")
viper.SetDefault("keybinding.page-up", "pgup") viper.SetDefault("keybinding.extract-file", "ctrl+e")
viper.SetDefault("keybinding.page-down", "pgdn") 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("diff.hide", "")

View file

@ -65,7 +65,7 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
stackedTree, failedPaths, err := StackTreeRange(trees, 0, currentTree-1) stackedTree, failedPaths, err := StackTreeRange(trees, 0, currentTree-1)
if len(failedPaths) > 0 { if len(failedPaths) > 0 {
for _, path := range failedPaths { for _, path := range failedPaths {
logrus.Errorf(path.String()) logrus.Errorf("%s", path.String())
} }
} }
if err != nil { if err != nil {

View file

@ -10,7 +10,7 @@ func checkError(t *testing.T, err error, message string) {
} }
} }
func TestEfficency(t *testing.T) { func TestEfficiency(t *testing.T) {
trees := make([]*FileTree, 3) trees := make([]*FileTree, 3)
for idx := range trees { for idx := range trees {
trees[idx] = NewFileTree() trees[idx] = NewFileTree()
@ -56,7 +56,7 @@ func TestEfficency(t *testing.T) {
} }
} }
func TestEfficency_ScratchImage(t *testing.T) { func TestEfficiency_ScratchImage(t *testing.T) {
trees := make([]*FileTree, 3) trees := make([]*FileTree, 3)
for idx := range trees { for idx := range trees {
trees[idx] = NewFileTree() trees[idx] = NewFileTree()

View file

@ -5,21 +5,21 @@ import (
"io" "io"
"os" "os"
"github.com/cespare/xxhash" "github.com/cespare/xxhash/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// FileInfo contains tar metadata for a specific FileNode // FileInfo contains tar metadata for a specific FileNode
type FileInfo struct { type FileInfo struct {
Path string Path string `json:"path"`
TypeFlag byte TypeFlag byte `json:"typeFlag"`
Linkname string Linkname string `json:"linkName"`
hash uint64 hash uint64 //`json:"hash"`
Size int64 Size int64 `json:"size"`
Mode os.FileMode Mode os.FileMode `json:"fileMode"`
Uid int Uid int `json:"uid"`
Gid int Gid int `json:"gid"`
IsDir bool IsDir bool `json:"isDir"`
} }
// NewFileInfoFromTarHeader extracts the metadata from a tar header and file contents and generates a new FileInfo object. // NewFileInfoFromTarHeader extracts the metadata from a tar header and file contents and generates a new FileInfo object.

View file

@ -141,11 +141,33 @@ func (node *FileNode) MetadataString() string {
return "" return ""
} }
fileMode := permbits.FileMode(node.Data.FileInfo.Mode).String()
dir := "-" dir := "-"
if node.Data.FileInfo.IsDir { if node.Data.FileInfo.IsDir {
dir = "d" dir = "d"
} }
fm := permbits.FileMode(node.Data.FileInfo.Mode)
var fileMode strings.Builder
fileMode.Grow(9)
cond := func(c bool, x, y byte) byte {
if c {
return x
} else {
return y
}
}
fileMode.WriteByte(cond(fm.UserRead(), 'r', '-'))
fileMode.WriteByte(cond(fm.UserWrite(), 'w', '-'))
fileMode.WriteByte(cond(fm.UserExecute(), cond(fm.Setuid(), 's', 'x'), cond(fm.Setuid(), 'S', '-')))
fileMode.WriteByte(cond(fm.GroupRead(), 'r', '-'))
fileMode.WriteByte(cond(fm.GroupWrite(), 'w', '-'))
fileMode.WriteByte(cond(fm.GroupExecute(), cond(fm.Setgid(), 's', 'x'), cond(fm.Setgid(), 'S', '-')))
fileMode.WriteByte(cond(fm.OtherRead(), 'r', '-'))
fileMode.WriteByte(cond(fm.OtherWrite(), 'w', '-'))
fileMode.WriteByte(cond(fm.OtherExecute(), cond(fm.Sticky(), 't', 'x'), cond(fm.Sticky(), 'T', '-')))
user := node.Data.FileInfo.Uid user := node.Data.FileInfo.Uid
group := node.Data.FileInfo.Gid group := node.Data.FileInfo.Gid
userGroup := fmt.Sprintf("%d:%d", user, group) userGroup := fmt.Sprintf("%d:%d", user, group)
@ -156,7 +178,7 @@ func (node *FileNode) MetadataString() string {
size := humanize.Bytes(uint64(sizeBytes)) size := humanize.Bytes(uint64(sizeBytes))
return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size)) return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode.String(), userGroup, size))
} }
func (node *FileNode) GetSize() int64 { func (node *FileNode) GetSize() int64 {

View file

@ -269,7 +269,7 @@ func (tree *FileTree) AddPath(filepath string, data FileInfo) (*FileNode, []*Fil
if node == nil { if node == nil {
// the child could not be added // the child could not be added
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node: '%s' (path:'%s')", name, filepath)) return node, addedNodes, fmt.Errorf("could not add child node: '%s' (path:'%s')", name, filepath)
} }
} }

View file

@ -5,7 +5,7 @@ var GlobalFileTreeCollapse bool
// NodeData is the payload for a FileNode // NodeData is the payload for a FileNode
type NodeData struct { type NodeData struct {
ViewInfo ViewInfo ViewInfo ViewInfo
FileInfo FileInfo FileInfo FileInfo `json:"fileInfo"`
DiffType DiffType DiffType DiffType
} }

View file

@ -21,13 +21,13 @@ func TestMergeDiffTypes(t *testing.T) {
b := Unmodified b := Unmodified
merged := a.merge(b) merged := a.merge(b)
if merged != Unmodified { if merged != Unmodified {
t.Errorf("Expected Unchaged (0) but got %v", merged) t.Errorf("Expected Unchanged (0) but got %v", merged)
} }
a = Modified a = Modified
b = Unmodified b = Unmodified
merged = a.merge(b) merged = a.merge(b)
if merged != Modified { if merged != Modified {
t.Errorf("Expected Unchaged (0) but got %v", merged) t.Errorf("Expected Unchanged (0) but got %v", merged)
} }
} }

View file

@ -2,7 +2,6 @@ package dive
import ( import (
"fmt" "fmt"
"net/url"
"strings" "strings"
"github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/dive/image"
@ -41,14 +40,13 @@ func ParseImageSource(r string) ImageSource {
} }
func DeriveImageSource(image string) (ImageSource, string) { func DeriveImageSource(image string) (ImageSource, string) {
u, err := url.Parse(image) s := strings.SplitN(image, "://", 2)
if err != nil { if len(s) < 2 {
return SourceUnknown, "" return SourceUnknown, ""
} }
scheme, imageSource := s[0], s[1]
imageSource := strings.TrimPrefix(image, u.Scheme+"://") switch scheme {
switch u.Scheme {
case SourceDockerEngine.String(): case SourceDockerEngine.String():
return SourceDockerEngine, imageSource return SourceDockerEngine, imageSource
case SourcePodmanEngine.String(): case SourcePodmanEngine.String():

View file

@ -13,6 +13,11 @@ func NewResolverFromArchive() *archiveResolver {
return &archiveResolver{} return &archiveResolver{}
} }
// Name returns the name of the resolver to display to the user.
func (r *archiveResolver) Name() string {
return "docker-archive"
}
func (r *archiveResolver) Fetch(path string) (*image.Image, error) { func (r *archiveResolver) Fetch(path string) (*image.Image, error) {
reader, err := os.Open(path) reader, err := os.Open(path)
if err != nil { if err != nil {
@ -30,3 +35,7 @@ func (r *archiveResolver) Fetch(path string) (*image.Image, error) {
func (r *archiveResolver) Build(args []string) (*image.Image, error) { func (r *archiveResolver) Build(args []string) (*image.Image, error) {
return nil, fmt.Errorf("build option not supported for docker archive resolver") return nil, fmt.Errorf("build option not supported for docker archive resolver")
} }
func (r *archiveResolver) Extract(id string, l string, p string) error {
return fmt.Errorf("not implemented")
}

View file

@ -10,6 +10,7 @@ func buildImageFromCli(buildArgs []string) (string, error) {
return "", err return "", err
} }
defer os.Remove(iidfile.Name()) defer os.Remove(iidfile.Name())
defer iidfile.Close()
allArgs := append([]string{"--iidfile", iidfile.Name()}, buildArgs...) allArgs := append([]string{"--iidfile", iidfile.Name()}, buildArgs...)
err = runDockerCmd("build", allArgs...) err = runDockerCmd("build", allArgs...)

View file

@ -44,3 +44,12 @@ func newConfig(configBytes []byte) config {
return imageConfig return imageConfig
} }
func isConfig(configBytes []byte) bool {
var imageConfig config
err := json.Unmarshal(configBytes, &imageConfig)
if err != nil {
return false
}
return imageConfig.RootFs.Type == "layers"
}

View file

@ -0,0 +1,7 @@
//go:build !windows
package docker
const (
defaultDockerHost = "unix:///var/run/docker.sock"
)

View file

@ -0,0 +1,5 @@
package docker
const (
defaultDockerHost = "npipe:////.pipe/docker_engine"
)

View file

@ -7,7 +7,10 @@ import (
"os" "os"
"strings" "strings"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
ddocker "github.com/docker/cli/cli/context/docker"
ctxstore "github.com/docker/cli/cli/context/store"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -20,6 +23,11 @@ func NewResolverFromEngine() *engineResolver {
return &engineResolver{} return &engineResolver{}
} }
// Name returns the name of the resolver to display to the user.
func (r *engineResolver) Name() string {
return "docker-engine"
}
func (r *engineResolver) Fetch(id string) (*image.Image, error) { func (r *engineResolver) Fetch(id string) (*image.Image, error) {
reader, err := r.fetchArchive(id) reader, err := r.fetchArchive(id)
if err != nil { if err != nil {
@ -42,6 +50,19 @@ func (r *engineResolver) Build(args []string) (*image.Image, error) {
return r.Fetch(id) return r.Fetch(id)
} }
func (r *engineResolver) Extract(id string, l string, p string) error {
reader, err := r.fetchArchive(id)
if err != nil {
return err
}
if err := ExtractFromImage(io.NopCloser(reader), l, p); err == nil {
return nil
}
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(id string) (io.ReadCloser, error) {
var err error var err error
var dockerClient *client.Client var dockerClient *client.Client
@ -49,8 +70,12 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
// pull the engineResolver if it does not exist // pull the engineResolver if it does not exist
ctx := context.Background() ctx := context.Background()
host := os.Getenv("DOCKER_HOST") host, err := determineDockerHost()
var clientOpts []client.Opt if err != nil {
fmt.Printf("> could not determine docker host: %v\n", err)
}
clientOpts := []client.Opt{client.FromEnv}
clientOpts = append(clientOpts, client.WithHost(host))
switch strings.Split(host, ":")[0] { switch strings.Split(host, ":")[0] {
case "ssh": case "ssh":
@ -66,7 +91,8 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
} }
return client.WithHTTPClient(httpClient)(c) return client.WithHTTPClient(httpClient)(c)
}) })
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithHost(host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer)) clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
default: default:
@ -74,8 +100,6 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
if os.Getenv("DOCKER_TLS_VERIFY") != "" && os.Getenv("DOCKER_CERT_PATH") == "" { if os.Getenv("DOCKER_TLS_VERIFY") != "" && os.Getenv("DOCKER_CERT_PATH") == "" {
os.Setenv("DOCKER_CERT_PATH", "~/.docker") os.Setenv("DOCKER_CERT_PATH", "~/.docker")
} }
clientOpts = append(clientOpts, client.FromEnv)
} }
clientOpts = append(clientOpts, client.WithAPIVersionNegotiation()) clientOpts = append(clientOpts, client.WithAPIVersionNegotiation())
@ -83,12 +107,17 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, _, err = dockerClient.ImageInspectWithRaw(ctx, id) _, err = dockerClient.ImageInspect(ctx, id)
if err != nil { if err != nil {
// don't use the API, the CLI has more informative output // check if the error is due to the image not existing locally
fmt.Println("Handler not available locally. Trying to pull '" + id + "'...") if client.IsErrNotFound(err) {
err = runDockerCmd("pull", id) fmt.Println("The image is not available locally. Trying to pull '" + id + "'...")
if err != nil { err = runDockerCmd("pull", id)
if err != nil {
return nil, err
}
} else {
// Some other error occurred, return it
return nil, err return nil, err
} }
} }
@ -100,3 +129,63 @@ func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
return readCloser, nil return readCloser, nil
} }
// determineDockerHost tries to the determine the docker host that we should connect to
// in the following order of decreasing precedence:
// - value of "DOCKER_HOST" environment variable
// - host retrieved from the current context (specified via DOCKER_CONTEXT)
// - "default docker host" for the host operating system, otherwise
func determineDockerHost() (string, error) {
// If the docker host is explicitly set via the "DOCKER_HOST" environment variable,
// then its a no-brainer :shrug:
if os.Getenv("DOCKER_HOST") != "" {
return os.Getenv("DOCKER_HOST"), nil
}
currentContext := os.Getenv("DOCKER_CONTEXT")
if currentContext == "" {
cf, err := cliconfig.Load(cliconfig.Dir())
if err != nil {
return "", err
}
currentContext = cf.CurrentContext
}
if currentContext == "" {
// If a docker context is neither specified via the "DOCKER_CONTEXT" environment variable nor via the
// $HOME/.docker/config file, then we fall back to connecting to the "default docker host" meant for
// the host operating system.
return defaultDockerHost, nil
}
storeConfig := ctxstore.NewConfig(
func() interface{} { return &ddocker.EndpointMeta{} },
ctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),
)
st := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)
md, err := st.GetMetadata(currentContext)
if err != nil {
return "", err
}
dockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]
if !ok {
return "", err
}
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
if !ok {
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
}
if dockerEPMeta.Host != "" {
return dockerEPMeta.Host, nil
}
// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').
// For example:
// ```sh
// docker context create foo --docker "host="
// ```
// In such scenario, we mimic the `docker` cli and try to connect to the "default docker host".
return defaultDockerHost, nil
}

View file

@ -9,8 +9,11 @@ import (
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"strings" "strings"
"github.com/klauspost/compress/zstd"
"github.com/wagoodman/dive/dive/filetree" "github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/dive/image"
) )
@ -93,25 +96,21 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
// never consume more bytes than this buffer contains so we can start again. // never consume more bytes than this buffer contains so we can start again.
// 512 bytes ought to be enough (as that's the size of a TAR entry header), // 512 bytes ought to be enough (as that's the size of a TAR entry header),
// but play it safe with 1024 bytes. This should also include very small layers // but play it safe with 1024 bytes. This should also include very small layers.
// (unless they've also been gzipped, but Docker does not appear to do it)
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
n, err := io.ReadFull(tarReader, buffer) n, err := io.ReadFull(tarReader, buffer)
if err != nil && err != io.ErrUnexpectedEOF { if err != nil && err != io.ErrUnexpectedEOF {
return img, err return img, err
} }
// Only try reading a TAR if file is "big enough" originalReader := func() io.Reader {
if n == cap(buffer) { return io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)
var unwrappedReader io.Reader }
unwrappedReader, err = gzip.NewReader(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
if err != nil {
// Not a gzipped entry
unwrappedReader = io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)
}
// Try reading a TAR // Try reading a gzip/estargz compressed layer
layerReader := tar.NewReader(unwrappedReader) gzipReader, err := gzip.NewReader(originalReader())
if err == nil {
layerReader := tar.NewReader(gzipReader)
tree, err := processLayerTar(name, layerReader) tree, err := processLayerTar(name, layerReader)
if err == nil { if err == nil {
currentLayer++ currentLayer++
@ -121,13 +120,36 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
} }
} }
// Not a TAR (or smaller than our buffer), might be a JSON file // Try reading a zstd compressed layer
zstdReader, err := zstd.NewReader(originalReader())
if err == nil {
layerReader := tar.NewReader(zstdReader)
tree, err := processLayerTar(name, layerReader)
if err == nil {
currentLayer++
// add the layer to the image
img.layerMap[tree.Name] = tree
continue
}
}
// Try reading a plain tar layer
layerReader := tar.NewReader(originalReader())
tree, err := processLayerTar(name, layerReader)
if err == nil {
currentLayer++
// add the layer to the image
img.layerMap[tree.Name] = tree
continue
}
// Not a TAR/GZIP/ZSTD, might be a JSON file
decoder := json.NewDecoder(bytes.NewReader(buffer[:n])) decoder := json.NewDecoder(bytes.NewReader(buffer[:n]))
token, err := decoder.Token() token, err := decoder.Token()
if _, ok := token.(json.Delim); err == nil && ok { if _, ok := token.(json.Delim); err == nil && ok {
// Looks like a JSON object (or array) // Looks like a JSON object (or array)
// XXX: should we add a header.Size check too? // XXX: should we add a header.Size check too?
fileBuffer, err := io.ReadAll(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)) fileBuffer, err := io.ReadAll(originalReader())
if err != nil { if err != nil {
return img, err return img, err
} }
@ -139,11 +161,31 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
} }
manifestContent, exists := jsonFiles["manifest.json"] manifestContent, exists := jsonFiles["manifest.json"]
if !exists { if exists {
return img, fmt.Errorf("could not find image manifest") img.manifest = newManifest(manifestContent)
} } else {
// manifest.json is not part of the OCI spec, docker includes it for compatibility
// Provide compatibility by finding the config and using our layerMap
var configPath string
for path, content := range jsonFiles {
if isConfig(content) {
configPath = path
break
}
}
if len(configPath) == 0 {
return img, fmt.Errorf("could not find image manifest")
}
img.manifest = newManifest(manifestContent) var layerPaths []string
for k := range img.layerMap {
layerPaths = append(layerPaths, k)
}
img.manifest = manifest{
ConfigPath: configPath,
LayerTarPaths: layerPaths,
}
}
configContent, exists := jsonFiles[img.manifest.ConfigPath] configContent, exists := jsonFiles[img.manifest.ConfigPath]
if !exists { if !exists {
@ -256,3 +298,80 @@ func (img *ImageArchive) ToImage() (*image.Image, error) {
Layers: layers, Layers: layers,
}, nil }, nil
} }
func ExtractFromImage(tarFile io.ReadCloser, l string, p string) error {
tarReader := tar.NewReader(tarFile)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
switch header.Typeflag {
case tar.TypeReg:
if name == l {
err = extractInner(tar.NewReader(tarReader), p)
if err != nil {
return err
}
return nil
}
default:
continue
}
}
return nil
}
func extractInner(reader *tar.Reader, p string) error {
target := strings.TrimPrefix(p, "/")
for {
header, err := reader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
switch header.Typeflag {
case tar.TypeReg:
if strings.HasPrefix(name, target) {
err := os.MkdirAll(filepath.Dir(name), 0755)
if err != nil {
return err
}
out, err := os.Create(name)
if err != nil {
return err
}
_, err = io.Copy(out, reader)
if err != nil {
return err
}
}
default:
continue
}
}
return nil
}

View file

@ -13,6 +13,7 @@ func buildImageFromCli(buildArgs []string) (string, error) {
return "", err return "", err
} }
defer os.Remove(iidfile.Name()) defer os.Remove(iidfile.Name())
defer iidfile.Close()
allArgs := append([]string{"--iidfile", iidfile.Name()}, buildArgs...) allArgs := append([]string{"--iidfile", iidfile.Name()}, buildArgs...)
err = runPodmanCmd("build", allArgs...) err = runPodmanCmd("build", allArgs...)

View file

@ -17,6 +17,11 @@ func NewResolverFromEngine() *resolver {
return &resolver{} return &resolver{}
} }
// Name returns the name of the resolver to display to the user.
func (r *resolver) Name() string {
return "podman"
}
func (r *resolver) Build(args []string) (*image.Image, error) { func (r *resolver) Build(args []string) (*image.Image, error) {
id, err := buildImageFromCli(args) id, err := buildImageFromCli(args)
if err != nil { if err != nil {
@ -36,6 +41,21 @@ func (r *resolver) Fetch(id string) (*image.Image, error) {
return nil, fmt.Errorf("unable to resolve image '%s': %+v", id, err) return nil, fmt.Errorf("unable to resolve image '%s': %+v", id, err)
} }
func (r *resolver) Extract(id string, l string, p string) error {
// todo: add podman fetch attempt via varlink first...
err, reader := streamPodmanCmd("image", "save", id)
if err != nil {
return err
}
if err := docker.ExtractFromImage(io.NopCloser(reader), l, p); err == nil {
return nil
}
return fmt.Errorf("unable to extract from image '%s': %+v", id, err)
}
func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) { func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) {
err, reader := streamPodmanCmd("image", "save", id) err, reader := streamPodmanCmd("image", "save", id)
if err != nil { if err != nil {

View file

@ -15,6 +15,10 @@ func NewResolverFromEngine() *resolver {
return &resolver{} return &resolver{}
} }
// Name returns the name of the resolver to display to the user.
func (r *resolver) Name() string {
return "podman"
}
func (r *resolver) Build(args []string) (*image.Image, error) { func (r *resolver) Build(args []string) (*image.Image, error) {
return nil, fmt.Errorf("unsupported platform") return nil, fmt.Errorf("unsupported platform")
} }
@ -22,3 +26,7 @@ func (r *resolver) Build(args []string) (*image.Image, error) {
func (r *resolver) Fetch(id string) (*image.Image, error) { func (r *resolver) Fetch(id string) (*image.Image, error) {
return nil, fmt.Errorf("unsupported platform") return nil, fmt.Errorf("unsupported platform")
} }
func (r *resolver) Extract(id string, l string, p string) error {
return fmt.Errorf("unsupported platform")
}

View file

@ -1,6 +1,8 @@
package image package image
type Resolver interface { type Resolver interface {
Name() string
Fetch(id string) (*Image, error) Fetch(id string) (*Image, error)
Build(options []string) (*Image, error) Build(options []string) (*Image, error)
Extract(id string, layer string, path string) error
} }

84
go.mod
View file

@ -1,61 +1,75 @@
module github.com/wagoodman/dive module github.com/wagoodman/dive
go 1.19 go 1.24
require ( require (
github.com/awesome-gocui/gocui v1.1.0 github.com/awesome-gocui/gocui v1.1.0
github.com/awesome-gocui/keybinding v1.0.1-0.20190805183143-864552bd36b7 github.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f
github.com/cespare/xxhash v1.1.0 github.com/cespare/xxhash/v2 v2.3.0
github.com/docker/cli v0.0.0-20190906153656-016a3232168d github.com/docker/cli v28.0.1+incompatible
github.com/docker/docker v24.0.7+incompatible github.com/docker/docker v28.0.1+incompatible
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.7.0 github.com/fatih/color v1.18.0
github.com/google/uuid v1.1.1 github.com/google/uuid v1.6.0
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b github.com/klauspost/compress v1.18.0
github.com/logrusorgru/aurora/v4 v4.0.0
github.com/lunixbochs/vtclean v1.0.0 github.com/lunixbochs/vtclean v1.0.0
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee
github.com/sergi/go-diff v1.0.0 github.com/sergi/go-diff v1.3.1
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.2.2 github.com/spf13/afero v1.14.0
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.4.0 github.com/spf13/viper v1.20.0
golang.org/x/net v0.17.0 golang.org/x/net v0.37.0
) )
require ( require (
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect github.com/containerd/log v0.1.0 // 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-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.4.7 // 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/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.0 // indirect github.com/gdamore/tcell/v2 v2.4.0 // 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/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/magiconair/properties v1.8.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.10 // indirect github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml v1.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.1.0 // indirect github.com/rivo/uniseg v0.1.0 // indirect
github.com/spf13/cast v1.3.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/cast v1.7.1 // indirect
github.com/stretchr/testify v1.4.0 // indirect github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/sys v0.13.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
golang.org/x/term v0.13.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
golang.org/x/text v0.13.0 // indirect go.opentelemetry.io/otel v1.31.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
gotest.tools v2.2.0+incompatible // 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 gotest.tools/v3 v3.5.0 // indirect
) )

321
go.sum
View file

@ -1,251 +1,207 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= 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/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/awesome-gocui/gocui v0.5.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= 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 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=
github.com/awesome-gocui/keybinding v1.0.1-0.20190805183143-864552bd36b7 h1:DDdWoFOtXWySkgCiGGn80TM/E2FS2T1qJBJJxup9+Vo= github.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f h1:u5xQfLwWC98BFToYDifqEcgK2ht2FFlbvRlzRnMb0cQ=
github.com/awesome-gocui/keybinding v1.0.1-0.20190805183143-864552bd36b7/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag= 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/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v0.0.0-20190906153656-016a3232168d h1:gwX/88xJZfxZV1yjhhuQpWTmEgJis7/XGCVu3iDIZYU= github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
github.com/docker/cli v0.0.0-20190906153656-016a3232168d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 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 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b h1:PMbSa9CgaiQR9NLlUTwKi+7aeLl3GG5JX5ERJxfQ3IE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 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 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.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 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/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs= 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/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
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= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/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=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@ -253,22 +209,21 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -7,7 +7,7 @@ import (
"strings" "strings"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora" "github.com/logrusorgru/aurora/v4"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/dive/image"

View file

@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora" "github.com/logrusorgru/aurora/v4"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/dive/image"

View file

@ -3,6 +3,9 @@ package export
import ( import (
"encoding/json" "encoding/json"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
diveImage "github.com/wagoodman/dive/dive/image" diveImage "github.com/wagoodman/dive/dive/image"
) )
@ -11,6 +14,7 @@ type export struct {
Image image `json:"image"` Image image `json:"image"`
} }
// NewExport exports the analysis to a JSON
func NewExport(analysis *diveImage.AnalysisResult) *export { func NewExport(analysis *diveImage.AnalysisResult) *export {
data := export{ data := export{
Layer: make([]layer, len(analysis.Layers)), Layer: make([]layer, len(analysis.Layers)),
@ -24,12 +28,22 @@ func NewExport(analysis *diveImage.AnalysisResult) *export {
// export layers in order // export layers in order
for idx, curLayer := range analysis.Layers { 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{ data.Layer[idx] = layer{
Index: curLayer.Index, Index: curLayer.Index,
ID: curLayer.Id, ID: curLayer.Id,
DigestID: curLayer.Digest, DigestID: curLayer.Digest,
SizeBytes: curLayer.Size, SizeBytes: curLayer.Size,
Command: curLayer.Command, Command: curLayer.Command,
FileList: layerFileList,
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,14 @@
package export package export
import (
"github.com/wagoodman/dive/dive/filetree"
)
type layer struct { type layer struct {
Index int `json:"index"` Index int `json:"index"`
ID string `json:"id"` ID string `json:"id"`
DigestID string `json:"digestId"` DigestID string `json:"digestId"`
SizeBytes uint64 `json:"sizeBytes"` SizeBytes uint64 `json:"sizeBytes"`
Command string `json:"command"` Command string `json:"command"`
FileList []filetree.FileInfo `json:"fileList"`
} }

View file

@ -35,7 +35,7 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev
} }
} else { } else {
events.message(utils.TitleFormat("Image Source: ") + options.Source.String() + "://" + options.Image) events.message(utils.TitleFormat("Image Source: ") + options.Source.String() + "://" + options.Image)
events.message(utils.TitleFormat("Fetching image...") + " (this can take a while for large images)") events.message(utils.TitleFormat("Extracting image from "+imageResolver.Name()+"...") + " (this can take a while for large images)")
img, err = imageResolver.Fetch(options.Image) img, err = imageResolver.Fetch(options.Image)
if err != nil { if err != nil {
events.exitWithErrorMessage("cannot fetch image", err) events.exitWithErrorMessage("cannot fetch image", err)
@ -108,7 +108,7 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev
// enough sleep will prevent this behavior (todo: remove this hack) // enough sleep will prevent this behavior (todo: remove this hack)
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
err = ui.Run(options.Image, analysis, treeStack) err = ui.Run(options.Image, imageResolver, analysis, treeStack)
if err != nil { if err != nil {
events.exitWithError(err) events.exitWithError(err)
return return

View file

@ -16,6 +16,14 @@ import (
type defaultResolver struct{} 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) { func (r *defaultResolver) Fetch(id string) (*image.Image, error) {
archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar") archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar")
if err != nil { if err != nil {
@ -30,6 +38,14 @@ func (r *defaultResolver) Build(args []string) (*image.Image, error) {
type failedBuildResolver struct{} 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) { func (r *failedBuildResolver) Fetch(id string) (*image.Image, error) {
archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar") archive, err := docker.TestLoadArchive("../.data/test-docker-image.tar")
if err != nil { if err != nil {
@ -44,6 +60,14 @@ func (r *failedBuildResolver) Build(args []string) (*image.Image, error) {
type failedFetchResolver struct{} 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) { func (r *failedFetchResolver) Fetch(id string) (*image.Image, error) {
return nil, fmt.Errorf("some fetch failure") return nil, fmt.Errorf("some fetch failure")
} }
@ -108,7 +132,7 @@ func TestRun(t *testing.T) {
}, },
events: []testEvent{ events: []testEvent{
{stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""}, {stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""},
{stdout: "Fetching image... (this can take a while for large images)", 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: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
{stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""}, {stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""},
}, },
@ -126,7 +150,7 @@ func TestRun(t *testing.T) {
}, },
events: []testEvent{ events: []testEvent{
{stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""}, {stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""},
{stdout: "Fetching image... (this can take a while for large images)", 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: "Analyzing image...", stderr: "", errorOnExit: false, errMessage: ""},
{stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""}, {stdout: "Building cache...", stderr: "", errorOnExit: false, errMessage: ""},
}, },
@ -159,7 +183,7 @@ func TestRun(t *testing.T) {
}, },
events: []testEvent{ events: []testEvent{
{stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""}, {stdout: "Image Source: docker://dive-example", stderr: "", errorOnExit: false, errMessage: ""},
{stdout: "Fetching image... (this can take a while for large images)", 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"}, {stdout: "", stderr: "cannot fetch image", errorOnExit: true, errMessage: "some fetch failure"},
}, },
}, },

View file

@ -27,13 +27,13 @@ var (
appSingleton *app appSingleton *app
) )
func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) { func newApp(gui *gocui.Gui, imageName string, resolver image.Resolver, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) {
var err error var err error
once.Do(func() { once.Do(func() {
var controller *Controller var controller *Controller
var globalHelpKeys []*key.Binding var globalHelpKeys []*key.Binding
controller, err = NewCollection(gui, imageName, analysis, cache) controller, err = NewCollection(gui, imageName, resolver, analysis, cache)
if err != nil { if err != nil {
return return
} }
@ -77,12 +77,12 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
Display: "Switch view", Display: "Switch view",
}, },
{ {
Key: gocui.KeyArrowRight, ConfigKeys: []string{"keybinding.right"},
OnAction: controller.NextPane, OnAction: controller.NextPane,
}, },
{ {
Key: gocui.KeyArrowLeft, ConfigKeys: []string{"keybinding.left"},
OnAction: controller.PrevPane, OnAction: controller.PrevPane,
}, },
{ {
ConfigKeys: []string{"keybinding.filter-files"}, ConfigKeys: []string{"keybinding.filter-files"},
@ -90,6 +90,10 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
IsSelected: controller.views.Filter.IsVisible, IsSelected: controller.views.Filter.IsVisible,
Display: "Filter", Display: "Filter",
}, },
{
ConfigKeys: []string{"keybinding.close-filter-files"},
OnAction: controller.CloseFilterView,
},
} }
globalHelpKeys, err = key.GenerateBindings(gui, "", infos) globalHelpKeys, err = key.GenerateBindings(gui, "", infos)
@ -134,7 +138,7 @@ func (a *app) quit() error {
} }
// Run is the UI entrypoint. // Run is the UI entrypoint.
func Run(imageName string, analysis *image.AnalysisResult, treeStack filetree.Comparer) error { func Run(imageName string, resolver image.Resolver, analysis *image.AnalysisResult, treeStack filetree.Comparer) error {
var err error var err error
g, err := gocui.NewGui(gocui.OutputNormal, true) g, err := gocui.NewGui(gocui.OutputNormal, true)
@ -143,7 +147,7 @@ func Run(imageName string, analysis *image.AnalysisResult, treeStack filetree.Co
} }
defer g.Close() defer g.Close()
_, err = newApp(g, imageName, analysis, treeStack) _, err = newApp(g, imageName, resolver, analysis, treeStack)
if err != nil { if err != nil {
return err return err
} }

View file

@ -13,19 +13,23 @@ import (
) )
type Controller struct { type Controller struct {
gui *gocui.Gui gui *gocui.Gui
views *view.Views views *view.Views
resolver image.Resolver
imageName string
} }
func NewCollection(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) { 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) views, err := view.NewViews(g, imageName, analysis, cache)
if err != nil { if err != nil {
return nil, err return nil, err
} }
controller := &Controller{ controller := &Controller{
gui: g, gui: g,
views: views, views: views,
resolver: resolver,
imageName: imageName,
} }
// layer view cursor down event should trigger an update in the file tree // layer view cursor down event should trigger an update in the file tree
@ -34,6 +38,9 @@ func NewCollection(g *gocui.Gui, imageName string, analysis *image.AnalysisResul
// update the status pane when a filetree option is changed by the user // update the status pane when a filetree option is changed by the user
controller.views.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange) controller.views.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange)
// update the status pane when a filetree option is changed by the user
controller.views.Tree.AddViewExtractListener(controller.onFileTreeViewExtract)
// update the tree view while the user types into the filter view // update the tree view while the user types into the filter view
controller.views.Filter.AddFilterEditListener(controller.onFilterEdit) controller.views.Filter.AddFilterEditListener(controller.onFilterEdit)
@ -53,6 +60,10 @@ func NewCollection(g *gocui.Gui, imageName string, analysis *image.AnalysisResul
return controller, nil return controller, nil
} }
func (c *Controller) onFileTreeViewExtract(p string) error {
return c.resolver.Extract(c.imageName, c.views.LayerDetails.CurrentLayer.Id, p)
}
func (c *Controller) onFileTreeViewOptionChange() error { func (c *Controller) onFileTreeViewOptionChange() error {
err := c.views.Status.Update() err := c.views.Status.Update()
if err != nil { if err != nil {
@ -212,6 +223,15 @@ func (c *Controller) ToggleView() (err error) {
return c.UpdateAndRender() return c.UpdateAndRender()
} }
func (c *Controller) CloseFilterView() error {
// filter view needs to be visible
if c.views.Filter.IsVisible() {
// toggle filter view
return c.ToggleFilterView()
}
return nil
}
func (c *Controller) ToggleFilterView() error { func (c *Controller) ToggleFilterView() error {
// delete all user input from the tree view // delete all user input from the tree view
err := c.views.Filter.ToggleVisible() err := c.views.Filter.ToggleVisible()

View file

@ -33,7 +33,7 @@ func GenerateBindings(gui *gocui.Gui, influence string, infos []BindingInfo) ([]
var err error var err error
var binding *Binding var binding *Binding
if info.ConfigKeys != nil && len(info.ConfigKeys) > 0 { if len(info.ConfigKeys) > 0 {
binding, err = NewBindingFromConfig(gui, influence, info.ConfigKeys, info.Display, info.OnAction) binding, err = NewBindingFromConfig(gui, influence, info.ConfigKeys, info.Display, info.OnAction)
} else { } else {
binding, err = NewBinding(gui, influence, info.Key, info.Modifier, info.Display, info.OnAction) binding, err = NewBinding(gui, influence, info.Key, info.Modifier, info.Display, info.OnAction)

View file

@ -72,7 +72,7 @@ func (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX,
} }
func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
logrus.Tracef("LayerDetailsCompountLayout.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name()) logrus.Tracef("LayerDetailsCompoundLayout.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name())
layouts := []view.IView{ layouts := []view.IView{
cl.layer, cl.layer,

View file

@ -20,7 +20,7 @@ type Debug struct {
selectedView Helper selectedView Helper
} }
// newDebugView creates a new view object attached the the global [gocui] screen object. // newDebugView creates a new view object attached the global [gocui] screen object.
func newDebugView(gui *gocui.Gui) (controller *Debug) { func newDebugView(gui *gocui.Gui) (controller *Debug) {
controller = new(Debug) controller = new(Debug)

View file

@ -17,6 +17,8 @@ import (
type ViewOptionChangeListener func() error 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. // shows selected layer or aggregate file ASCII tree.
type FileTree struct { type FileTree struct {
@ -29,11 +31,12 @@ type FileTree struct {
filterRegex *regexp.Regexp filterRegex *regexp.Regexp
listeners []ViewOptionChangeListener listeners []ViewOptionChangeListener
extractListeners []ViewExtractListener
helpKeys []*key.Binding helpKeys []*key.Binding
requestedWidthRatio float64 requestedWidthRatio float64
} }
// newFileTreeView creates a new view object attached the the global [gocui] screen object. // 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) { func newFileTreeView(gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (controller *FileTree, err error) {
controller = new(FileTree) controller = new(FileTree)
controller.listeners = make([]ViewOptionChangeListener, 0) controller.listeners = make([]ViewOptionChangeListener, 0)
@ -60,6 +63,10 @@ func (v *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListe
v.listeners = append(v.listeners, listener...) v.listeners = append(v.listeners, listener...)
} }
func (v *FileTree) AddViewExtractListener(listener ...ViewExtractListener) {
v.extractListeners = append(v.extractListeners, listener...)
}
func (v *FileTree) SetTitle(title string) { func (v *FileTree) SetTitle(title string) {
v.title = title v.title = title
} }
@ -103,6 +110,11 @@ func (v *FileTree) Setup(view, header *gocui.View) error {
OnAction: v.toggleSortOrder, OnAction: v.toggleSortOrder,
Display: "Toggle sort order", Display: "Toggle sort order",
}, },
{
ConfigKeys: []string{"keybinding.extract-file"},
OnAction: v.extractFile,
Display: "Extract File",
},
{ {
ConfigKeys: []string{"keybinding.toggle-added-files"}, ConfigKeys: []string{"keybinding.toggle-added-files"},
OnAction: func() error { return v.toggleShowDiffType(filetree.Added) }, OnAction: func() error { return v.toggleShowDiffType(filetree.Added) },
@ -148,24 +160,24 @@ func (v *FileTree) Setup(view, header *gocui.View) error {
OnAction: v.PageDown, OnAction: v.PageDown,
}, },
{ {
Key: gocui.KeyArrowDown, ConfigKeys: []string{"keybinding.down"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorDown, OnAction: v.CursorDown,
}, },
{ {
Key: gocui.KeyArrowUp, ConfigKeys: []string{"keybinding.up"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorUp, OnAction: v.CursorUp,
}, },
{ {
Key: gocui.KeyArrowLeft, ConfigKeys: []string{"keybinding.left"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorLeft, OnAction: v.CursorLeft,
}, },
{ {
Key: gocui.KeyArrowRight, ConfigKeys: []string{"keybinding.right"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorRight, OnAction: v.CursorRight,
}, },
} }
@ -303,9 +315,32 @@ func (v *FileTree) toggleSortOrder() error {
return v.Render() return v.Render()
} }
func (v *FileTree) extractFile() error {
node := v.vm.CurrentNode(v.filterRegex)
for _, listener := range v.extractListeners {
err := listener(node.Path())
if err != nil {
return err
}
}
return nil
}
func (v *FileTree) toggleWrapTree() error { func (v *FileTree) toggleWrapTree() error {
v.view.Wrap = !v.view.Wrap v.view.Wrap = !v.view.Wrap
return nil
err := v.Update()
if err != nil {
return err
}
err = v.Render()
if err != nil {
return err
}
// we need to render the changes to the status pane as well (not just this contoller/view)
return v.notifyOnViewOptionChangeListeners()
} }
func (v *FileTree) notifyOnViewOptionChangeListeners() error { func (v *FileTree) notifyOnViewOptionChangeListeners() error {
@ -335,7 +370,7 @@ func (v *FileTree) toggleAttributes() error {
return err return err
} }
// we need to render the changes to the status pane as well (not just this contoller/view) // we need to render the changes to the status pane as well (not just this controller/view)
return v.notifyOnViewOptionChangeListeners() return v.notifyOnViewOptionChangeListeners()
} }
@ -352,7 +387,7 @@ func (v *FileTree) toggleShowDiffType(diffType filetree.DiffType) error {
return err return err
} }
// we need to render the changes to the status pane as well (not just this contoller/view) // we need to render the changes to the status pane as well (not just this controller/view)
return v.notifyOnViewOptionChangeListeners() return v.notifyOnViewOptionChangeListeners()
} }

View file

@ -27,7 +27,7 @@ type Filter struct {
filterEditListeners []FilterEditListener filterEditListeners []FilterEditListener
} }
// newFilterView creates a new view object attached the the global [gocui] screen object. // newFilterView creates a new view object attached the global [gocui] screen object.
func newFilterView(gui *gocui.Gui) (controller *Filter) { func newFilterView(gui *gocui.Gui) (controller *Filter) {
controller = new(Filter) controller = new(Filter)

View file

@ -44,14 +44,14 @@ func (v *ImageDetails) Setup(body, header *gocui.View) error {
var infos = []key.BindingInfo{ var infos = []key.BindingInfo{
{ {
Key: gocui.KeyArrowDown, ConfigKeys: []string{"keybinding.down"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorDown, OnAction: v.CursorDown,
}, },
{ {
Key: gocui.KeyArrowUp, ConfigKeys: []string{"keybinding.up"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorUp, OnAction: v.CursorUp,
}, },
{ {
ConfigKeys: []string{"keybinding.page-up"}, ConfigKeys: []string{"keybinding.page-up"},

View file

@ -28,7 +28,7 @@ type Layer struct {
helpKeys []*key.Binding helpKeys []*key.Binding
} }
// newLayerView creates a new view object attached the the global [gocui] screen object. // newLayerView creates a new view object attached the global [gocui] screen object.
func newLayerView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { func newLayerView(gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) {
controller = new(Layer) controller = new(Layer)
@ -116,14 +116,14 @@ func (v *Layer) Setup(body *gocui.View, header *gocui.View) error {
Display: "Show aggregated changes", Display: "Show aggregated changes",
}, },
{ {
Key: gocui.KeyArrowDown, ConfigKeys: []string{"keybinding.down"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorDown, OnAction: v.CursorDown,
}, },
{ {
Key: gocui.KeyArrowUp, ConfigKeys: []string{"keybinding.up"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorUp, OnAction: v.CursorUp,
}, },
{ {
ConfigKeys: []string{"keybinding.page-up"}, ConfigKeys: []string{"keybinding.page-up"},
@ -221,6 +221,14 @@ func (v *Layer) CursorUp() error {
return nil return nil
} }
// SetOrigin updates the origin of the layer view pane.
func (v *Layer) SetOrigin(x, y int) error {
if err := v.body.SetOrigin(x, y); err != nil {
return err
}
return nil
}
// SetCursor resets the cursor and orients the file tree view based on the given layer index. // SetCursor resets the cursor and orients the file tree view based on the given layer index.
func (v *Layer) SetCursor(layer int) error { func (v *Layer) SetCursor(layer int) error {
v.vm.LayerIndex = layer v.vm.LayerIndex = layer
@ -340,6 +348,15 @@ func (v *Layer) Render() error {
return err return err
} }
} }
// Adjust origin, if necessary
maxBodyDisplayHeight := int(v.height())
if v.vm.LayerIndex > maxBodyDisplayHeight {
if err := v.SetOrigin(0, v.vm.LayerIndex-maxBodyDisplayHeight); err != nil {
return err
}
}
return nil return nil
}) })
return nil return nil

View file

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/awesome-gocui/gocui" "github.com/awesome-gocui/gocui"
"github.com/dustin/go-humanize"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/dive/image"
@ -39,14 +40,14 @@ func (v *LayerDetails) Setup(body, header *gocui.View) error {
var infos = []key.BindingInfo{ var infos = []key.BindingInfo{
{ {
Key: gocui.KeyArrowDown, ConfigKeys: []string{"keybinding.down"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorDown, OnAction: v.CursorDown,
}, },
{ {
Key: gocui.KeyArrowUp, ConfigKeys: []string{"keybinding.up"},
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
OnAction: v.CursorUp, OnAction: v.CursorUp,
}, },
} }
@ -79,12 +80,14 @@ func (v *LayerDetails) Render() error {
var lines = make([]string, 0) var lines = make([]string, 0)
tags := "(none)" tags := "(none)"
if v.CurrentLayer.Names != nil && len(v.CurrentLayer.Names) > 0 { if len(v.CurrentLayer.Names) > 0 {
tags = strings.Join(v.CurrentLayer.Names, ", ") tags = strings.Join(v.CurrentLayer.Names, ", ")
} }
lines = append(lines, []string{ lines = append(lines, []string{
format.Header("Tags: ") + tags, format.Header("Tags: ") + tags,
format.Header("Id: ") + v.CurrentLayer.Id, format.Header("Id: ") + v.CurrentLayer.Id,
format.Header("Size: ") + humanize.Bytes(v.CurrentLayer.Size),
format.Header("Digest: ") + v.CurrentLayer.Digest, format.Header("Digest: ") + v.CurrentLayer.Digest,
format.Header("Command:"), format.Header("Command:"),
v.CurrentLayer.Command, v.CurrentLayer.Command,

View file

@ -25,7 +25,7 @@ type Status struct {
helpKeys []*key.Binding helpKeys []*key.Binding
} }
// newStatusView creates a new view object attached the the global [gocui] screen object. // newStatusView creates a new view object attached the global [gocui] screen object.
func newStatusView(gui *gocui.Gui) (controller *Status) { func newStatusView(gui *gocui.Gui) (controller *Status) {
controller = new(Status) controller = new(Status)

View file

@ -38,7 +38,7 @@ type FileTreeViewModel struct {
Buffer bytes.Buffer Buffer bytes.Buffer
} }
// NewFileTreeViewModel creates a new view object attached the the global [gocui] screen object. // 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(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (treeViewModel *FileTreeViewModel, err error) {
treeViewModel = new(FileTreeViewModel) treeViewModel = new(FileTreeViewModel)
@ -161,6 +161,11 @@ func (vm *FileTreeViewModel) CursorDown() bool {
return true 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)
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
var visitor func(*filetree.FileNode) error var visitor func(*filetree.FileNode) error

View file

@ -55,7 +55,7 @@ func helperCheckDiff(t *testing.T, expected, actual []byte) {
if !bytes.Equal(expected, actual) { if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New() dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true) diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf(dmp.DiffPrettyText(diffs)) t.Errorf("%s", dmp.DiffPrettyText(diffs))
t.Errorf("%s: bytes mismatch", t.Name()) t.Errorf("%s: bytes mismatch", t.Name())
} }
} }

View file

@ -0,0 +1,52 @@
package viewmodel
import (
"testing"
)
func TestGetCompareIndexes(t *testing.T) {
tests := []struct {
name string
layerIndex int
compareMode LayerCompareMode
compareStartIndex int
expected [4]int
}{
{
name: "LayerIndex equals CompareStartIndex",
layerIndex: 2,
compareMode: CompareSingleLayer,
compareStartIndex: 2,
expected: [4]int{2, 2, 2, 2},
},
{
name: "CompareMode is CompareSingleLayer",
layerIndex: 3,
compareMode: CompareSingleLayer,
compareStartIndex: 1,
expected: [4]int{1, 2, 3, 3},
},
{
name: "Default CompareMode",
layerIndex: 4,
compareMode: CompareAllLayers,
compareStartIndex: 1,
expected: [4]int{1, 1, 2, 4},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &LayerSetState{
LayerIndex: tt.layerIndex,
CompareMode: tt.compareMode,
CompareStartIndex: tt.compareStartIndex,
}
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := state.GetCompareIndexes()
actual := [4]int{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
if actual != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, actual)
}
})
}
}

View file

@ -25,7 +25,7 @@ drwxr-xr-x 0:0 0 B │ │ │ └── nested
-rw-r--r-- 0:0 6.4 kB │ │ └── somefile3.txt -rw-r--r-- 0:0 6.4 kB │ │ └── somefile3.txt
-rwxr-xr-x 0:0 6.4 kB │ └── saved.txt -rwxr-xr-x 0:0 6.4 kB │ └── saved.txt
-rw-rw-r-- 0:0 6.4 kB ├── somefile.txt -rw-rw-r-- 0:0 6.4 kB ├── somefile.txt
drwxrwxrwx 0:0 6.4 kB ├── tmp drwxrwxrwt 0:0 6.4 kB ├── tmp
-rw-r--r-- 0:0 6.4 kB │ └── saved.again1.txt -rw-r--r-- 0:0 6.4 kB │ └── saved.again1.txt
drwxr-xr-x 0:0 0 B ├── usr drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin drwxr-xr-x 1:1 0 B │ └── sbin

View file

@ -3,7 +3,7 @@ drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├─⊕ etc drwxr-xr-x 0:0 1.0 kB ├─⊕ etc
drwxr-xr-x 65534:65534 0 B ├── home drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp drwxrwxrwt 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var drwxr-xr-x 0:0 0 B └── var

View file

@ -3,7 +3,7 @@ drwxr-xr-x 0:0 0 B ├── dev
drwxr-xr-x 0:0 1.0 kB ├─⊕ etc drwxr-xr-x 0:0 1.0 kB ├─⊕ etc
drwxr-xr-x 65534:65534 0 B ├── home drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp drwxrwxrwt 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├─⊕ usr drwxr-xr-x 0:0 0 B ├─⊕ usr
drwxr-xr-x 0:0 0 B └─⊕ var drwxr-xr-x 0:0 0 B └─⊕ var

View file

@ -12,7 +12,7 @@ drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw------- 0:0 243 B │ └── shadow -rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp drwxrwxrwt 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var drwxr-xr-x 0:0 0 B └── var

View file

@ -406,7 +406,7 @@ drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw------- 0:0 243 B │ └── shadow -rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root drwx------ 0:0 0 B ├── root
drwxrwxrwx 0:0 0 B ├── tmp drwxrwxrwt 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var drwxr-xr-x 0:0 0 B └── var

View file

@ -11,7 +11,7 @@ drwxr-xr-x 0:0 0 B │ │ └── if-up.d
-rw-r--r-- 0:0 340 B │ ├── passwd -rw-r--r-- 0:0 340 B │ ├── passwd
-rw------- 0:0 243 B │ └── shadow -rw------- 0:0 243 B │ └── shadow
drwxr-xr-x 65534:65534 0 B ├── home drwxr-xr-x 65534:65534 0 B ├── home
drwxrwxrwx 0:0 0 B ├── tmp drwxrwxrwt 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var drwxr-xr-x 0:0 0 B └── var

View file

@ -13,7 +13,7 @@ drwxr-xr-x 0:0 0 B │ │ └── if-up.d
drwxr-xr-x 65534:65534 0 B ├── home drwxr-xr-x 65534:65534 0 B ├── home
drwx------ 0:0 0 B ├── root drwx------ 0:0 0 B ├── root
-rw-rw-r-- 0:0 6.4 kB ├── somefile.txt -rw-rw-r-- 0:0 6.4 kB ├── somefile.txt
drwxrwxrwx 0:0 0 B ├── tmp drwxrwxrwt 0:0 0 B ├── tmp
drwxr-xr-x 0:0 0 B ├── usr drwxr-xr-x 0:0 0 B ├── usr
drwxr-xr-x 1:1 0 B │ └── sbin drwxr-xr-x 1:1 0 B │ └── sbin
drwxr-xr-x 0:0 0 B └── var drwxr-xr-x 0:0 0 B └── var

View file

@ -3,7 +3,7 @@ package utils
import ( import (
"strings" "strings"
"github.com/logrusorgru/aurora" "github.com/logrusorgru/aurora/v4"
) )
func TitleFormat(s string) string { func TitleFormat(s string) string {