diff --git a/Makefile b/Makefile index a142a10..7d39415 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ BIN = dive all: clean build run: build - docker image ls | grep "dive-test" >/dev/null || docker build -t dive-test:latest . + docker image ls | grep "dive-test" >/dev/null || docker build -t dive-test:latest -f data/Dockerfile . ./build/$(BIN) dive-test build: diff --git a/README.md b/README.md index 0e8baff..aff38a2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # dive [](https://goreportcard.com/report/github.com/wagoodman/dive) -A tool for interrogating docker images. - +**A tool for interrogating docker images.** To analyze a Docker image simply run dive with an image tag/id/digest: ```bash diff --git a/cmd/build.go b/cmd/build.go index 485534c..d6fe2fd 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -7,15 +7,13 @@ import ( "github.com/wagoodman/dive/ui" "io/ioutil" "os" - "os/exec" - "strings" + "github.com/wagoodman/dive/utils" ) // buildCmd represents the build command var buildCmd = &cobra.Command{ - Use: "build", - Short: "Build and analyze a docker image", - Long: `Build and analyze a docker image`, + Use: "build [any valid `docker build` arguments]", + Short: "Builds and analyzes a docker image from a Dockerfile (this is a thin wrapper for the `docker build` command).", DisableFlagParsing: true, Run: doBuild, } @@ -33,7 +31,7 @@ func doBuild(cmd *cobra.Command, args []string) { defer os.Remove(iidfile.Name()) allArgs := append([]string{"--iidfile", iidfile.Name()}, args...) - err = runDockerCmd("build", allArgs...) + err = utils.RunDockerCmd("build", allArgs...) if err != nil { log.Fatal(err) } @@ -46,28 +44,3 @@ func doBuild(cmd *cobra.Command, args []string) { manifest, refTrees := image.InitializeData(string(imageId)) ui.Run(manifest, refTrees) } - -// runDockerCmd runs a given Docker command in the current tty -func runDockerCmd(cmdStr string, args ...string) error { - - allArgs := cleanArgs(append([]string{cmdStr}, args...)) - - cmd := exec.Command("docker", allArgs...) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} - -// cleanArgs trims the whitespace from the given set of strings. -func cleanArgs(s []string) []string { - var r []string - for _, str := range s { - if str != "" { - r = append(r, strings.Trim(str, " ")) - } - } - return r -} diff --git a/cmd/root.go b/cmd/root.go index 1e85094..c78421f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,15 +7,18 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "os" + "github.com/tebeka/atexit" + "github.com/k0kubun/go-ansi" ) var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "dive", + Use: "dive [IMAGE]", Short: "Docker Image Visualizer & Explorer", - Long: `Docker Image Visualizer & Explorer`, + Long: `This tool provides a way to discover and explore the contents of a docker image. Additionally the tool estimates +the amount of wasted space and identifies the offending files from the image.`, Args: cobra.ExactArgs(1), Run: analyze, } @@ -28,18 +31,19 @@ func Execute() { } } +func exitHandler() { + ansi.CursorShow() +} + func init() { + ansi.CursorHide() + atexit.Register(exitHandler) + cobra.OnInitialize(initConfig) cobra.OnInitialize(initLogging) - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml)") - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // TODO: add config options + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml)") } // initConfig reads in config file and ENV variables if set. diff --git a/Dockerfile b/data/Dockerfile similarity index 89% rename from Dockerfile rename to data/Dockerfile index 358f252..c3b6e8a 100644 --- a/Dockerfile +++ b/data/Dockerfile @@ -1,5 +1,5 @@ FROM alpine:latest -ADD README.md /somefile.txt +ADD ../README.md /somefile.txt RUN mkdir /root/example RUN cp /somefile.txt /root/example/somefile1.txt RUN cp /somefile.txt /root/example/somefile2.txt diff --git a/image/image.go b/image/image.go index ccdec04..a33fa06 100644 --- a/image/image.go +++ b/image/image.go @@ -13,10 +13,10 @@ import ( "strings" "github.com/docker/docker/client" - "github.com/k0kubun/go-ansi" "github.com/wagoodman/dive/filetree" "github.com/wagoodman/jotframe" "golang.org/x/net/context" + "github.com/wagoodman/dive/utils" ) // TODO: this file should be rethought... but since it's only for preprocessing it'll be tech debt for now. @@ -57,11 +57,14 @@ func (pb *ProgressBar) Update(currentValue int64) (hasChanged bool) { func (pb *ProgressBar) String() string { width := 40 done := int((pb.percent * width) / 100.0) + if done > width { + done = width + } todo := width - done + if todo < 0 { + todo = 0 + } head := 1 - // if pb.percent >= 100 { - // head = 0 - // } return "[" + strings.Repeat("=", done) + strings.Repeat(">", head) + strings.Repeat(" ", todo) + "]" + fmt.Sprintf(" %d %% (%d/%d)", pb.percent, pb.rawCurrent, pb.rawTotal) } @@ -194,13 +197,21 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { var layerMap = make(map[string]*filetree.FileTree) var trees = make([]*filetree.FileTree, 0) - ansi.CursorHide() + // pull the image if it does not exist + ctx := context.Background() + dockerClient, err := client.NewClientWithOpts() + if err != nil { + fmt.Println("Could not connect to the Docker daemon:"+err.Error()) + os.Exit(1) + } + _, _, err = dockerClient.ImageInspectWithRaw(ctx, imageID) + if err != nil { + // don't use the API, the CLI has more informative output + utils.RunDockerCmd("pull", imageID) + } // save this image to disk temporarily to get the content info imageTarPath, tmpDir := saveImage(imageID) - // imageTarPath := "/tmp/dive516670682/image.tar" - // tmpDir := "/tmp/dive516670682" - // fmt.Println(tmpDir) defer os.RemoveAll(tmpDir) // read through the image contents and build a tree @@ -313,8 +324,6 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { layerIdx-- } - ansi.CursorShow() - return layers, trees } @@ -322,7 +331,8 @@ func saveImage(imageID string) (string, string) { ctx := context.Background() dockerClient, err := client.NewClientWithOpts() if err != nil { - panic(err) + fmt.Println("Could not connect to the Docker daemon:"+err.Error()) + os.Exit(1) } frame := jotframe.NewFixedFrame(0, false, false, true) @@ -331,7 +341,6 @@ func saveImage(imageID string) (string, string) { io.WriteString(line, " Fetching metadata...") result, _, err := dockerClient.ImageInspectWithRaw(ctx, imageID) - check(err) totalSize := result.Size frame.Remove(line) diff --git a/utils/docker.go b/utils/docker.go new file mode 100644 index 0000000..339b0a9 --- /dev/null +++ b/utils/docker.go @@ -0,0 +1,33 @@ +package utils + +import ( + "os/exec" + "os" + "strings" +) + +// RunDockerCmd runs a given Docker command in the current tty +func RunDockerCmd(cmdStr string, args ...string) error { + + allArgs := cleanArgs(append([]string{cmdStr}, args...)) + + cmd := exec.Command("docker", allArgs...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} + +// cleanArgs trims the whitespace from the given set of strings. +func cleanArgs(s []string) []string { + var r []string + for _, str := range s { + if str != "" { + r = append(r, strings.Trim(str, " ")) + } + } + return r +} +