From e39e64619140ec72c7cc75d4e0fdb4655fc6fcf6 Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@gmail.com>
Date: Tue, 8 Oct 2019 18:55:03 -0400
Subject: [PATCH] adding docker-archive source option

---
 README.md                                     | 13 +++--
 cmd/analyze.go                                |  6 +--
 cmd/build.go                                  |  2 +-
 cmd/root.go                                   |  6 +--
 dive/get_image_handler.go                     | 44 ----------------
 dive/get_image_resolver.go                    | 51 +++++++++++++++++++
 dive/image/docker/archive_resolver.go         | 31 +++++++++++
 .../{resolver.go => engine_resolver.go}       | 14 ++---
 dive/image/docker/image_archive.go            |  2 +-
 dive/image/podman/resolver_linux.go           |  2 +-
 runtime/options.go                            |  4 +-
 runtime/run.go                                |  9 +++-
 12 files changed, 113 insertions(+), 71 deletions(-)
 delete mode 100644 dive/get_image_handler.go
 create mode 100644 dive/get_image_resolver.go
 create mode 100644 dive/image/docker/archive_resolver.go
 rename dive/image/docker/{resolver.go => engine_resolver.go} (84%)

diff --git a/README.md b/README.md
index 6520738..4e85f97 100644
--- a/README.md
+++ b/README.md
@@ -61,14 +61,17 @@ command.
 **CI Integration**
 Analyze and image and get a pass/fail result based on the image efficiency and wasted space. Simply set `CI=true` in the environment when invoking any valid dive command.
 
-**Supported Container Engines**
-- Docker (default)
-- Podman (linux only)
-
+**With Multiple Image Sources and Container Engines Supported**
+With the `--source` option, you can select where to fetch the container image from:
 ```bash
-dive <your-image-tag> --engine podman
+dive <your-image-tag> --source podman
 ```
 
+With valid `source` options as such:
+- `docker`: Docker engine (the default option)
+- `docker-archive`: A Docker Tar Archive from disk
+- `podman`: Podman engine (linux only)
+
 ## Installation
 
 **Ubuntu/Debian**
diff --git a/cmd/analyze.go b/cmd/analyze.go
index 4e6c27e..3c6f0ab 100644
--- a/cmd/analyze.go
+++ b/cmd/analyze.go
@@ -39,7 +39,7 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
 		os.Exit(1)
 	}
 
-	engine, err := cmd.PersistentFlags().GetString("engine")
+	engine, err := cmd.PersistentFlags().GetString("source")
 	if err != nil {
 		fmt.Printf("unable to determine engine: %v\n", err)
 		os.Exit(1)
@@ -47,8 +47,8 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
 
 	runtime.Run(runtime.Options{
 		Ci:         isCi,
-		Engine:     dive.GetEngine(engine),
-		ImageId:    userImage,
+		Source:     dive.ParseImageSource(engine),
+		Image:      userImage,
 		ExportFile: exportFile,
 		CiConfig:   ciConfig,
 	})
diff --git a/cmd/build.go b/cmd/build.go
index 0cff973..9c65fab 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -29,7 +29,7 @@ func doBuildCmd(cmd *cobra.Command, args []string) {
 
 	runtime.Run(runtime.Options{
 		Ci:         isCi,
-		Engine:     dive.GetEngine(engine),
+		Source:     dive.ParseImageSource(engine),
 		BuildArgs:  args,
 		ExportFile: exportFile,
 		CiConfig:   ciConfig,
diff --git a/cmd/root.go b/cmd/root.go
index 877bca7..c58e46e 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -46,6 +46,7 @@ func init() {
 
 func initCli() {
 	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
+	rootCmd.PersistentFlags().String("source", "docker", "The container engine to fetch the image from. Allowed values: "+strings.Join(dive.ImageSources, ", "))
 	rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
 	rootCmd.Flags().BoolVar(&isCi, "ci", false, "Skip the interactive TUI and validate against CI rules (same as env var CI=true)")
 	rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.")
@@ -61,11 +62,6 @@ func initCli() {
 		}
 	}
 
-	rootCmd.PersistentFlags().String("engine", "docker", "The container engine to fetch the image from. Allowed values: "+strings.Join(dive.AllowedEngines, ", "))
-
-	if err := viper.BindPFlag("container-engine", rootCmd.PersistentFlags().Lookup("engine")); err != nil {
-		log.Fatal("Unable to bind 'engine' flag:", err)
-	}
 }
 
 // initConfig reads in config file and ENV variables if set.
diff --git a/dive/get_image_handler.go b/dive/get_image_handler.go
deleted file mode 100644
index d8e5cec..0000000
--- a/dive/get_image_handler.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package dive
-
-import (
-	"fmt"
-	"github.com/wagoodman/dive/dive/image"
-	"github.com/wagoodman/dive/dive/image/docker"
-	"github.com/wagoodman/dive/dive/image/podman"
-)
-
-type Engine int
-
-const (
-	EngineUnknown Engine = iota
-	EngineDocker
-	EnginePodman
-)
-
-func (engine Engine) String() string {
-	return [...]string{"unknown", "docker", "podman"}[engine]
-}
-
-var AllowedEngines = []string{EngineDocker.String(), EnginePodman.String()}
-
-func GetEngine(engine string) Engine {
-	switch engine {
-	case "docker":
-		return EngineDocker
-	case "podman":
-		return EnginePodman
-	default:
-		return EngineUnknown
-	}
-}
-
-func GetImageHandler(engine Engine) (image.Resolver, error) {
-	switch engine {
-	case EngineDocker:
-		return docker.NewResolver(), nil
-	case EnginePodman:
-		return podman.NewResolver(), nil
-	}
-
-	return nil, fmt.Errorf("unable to determine image provider")
-}
diff --git a/dive/get_image_resolver.go b/dive/get_image_resolver.go
new file mode 100644
index 0000000..3e9f9c6
--- /dev/null
+++ b/dive/get_image_resolver.go
@@ -0,0 +1,51 @@
+package dive
+
+import (
+	"fmt"
+	"github.com/wagoodman/dive/dive/image"
+	"github.com/wagoodman/dive/dive/image/docker"
+	"github.com/wagoodman/dive/dive/image/podman"
+)
+
+const (
+	SourceUnknown ImageSource = iota
+	SourceDockerEngine
+	SourcePodmanEngine
+	SourceDockerArchive
+)
+
+type ImageSource int
+
+var ImageSources = []string{SourceDockerEngine.String(), SourcePodmanEngine.String(), SourceDockerArchive.String()}
+
+func (r ImageSource) String() string {
+	return [...]string{"unknown", "docker", "podman", "docker-archive"}[r]
+}
+
+func ParseImageSource(r string) ImageSource {
+	switch r {
+	case "docker":
+		return SourceDockerEngine
+	case "podman":
+		return SourcePodmanEngine
+	case "docker-archive":
+		return SourceDockerArchive
+	case "docker-tar":
+		return SourceDockerArchive
+	default:
+		return SourceUnknown
+	}
+}
+
+func GetImageResolver(r ImageSource) (image.Resolver, error) {
+	switch r {
+	case SourceDockerEngine:
+		return docker.NewResolverFromEngine(), nil
+	case SourcePodmanEngine:
+		return podman.NewResolverFromEngine(), nil
+	case SourceDockerArchive:
+		return docker.NewResolverFromArchive(), nil
+	}
+
+	return nil, fmt.Errorf("unable to determine image resolver")
+}
diff --git a/dive/image/docker/archive_resolver.go b/dive/image/docker/archive_resolver.go
new file mode 100644
index 0000000..5cdc923
--- /dev/null
+++ b/dive/image/docker/archive_resolver.go
@@ -0,0 +1,31 @@
+package docker
+
+import (
+	"fmt"
+	"github.com/wagoodman/dive/dive/image"
+	"os"
+)
+
+type archiveResolver struct{}
+
+func NewResolverFromArchive() *archiveResolver {
+	return &archiveResolver{}
+}
+
+func (r *archiveResolver) Fetch(path string) (*image.Image, error) {
+	reader, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer reader.Close()
+
+	img, err := NewImageArchive(reader)
+	if err != nil {
+		return nil, err
+	}
+	return img.ToImage()
+}
+
+func (r *archiveResolver) Build(args []string) (*image.Image, error) {
+	return nil, fmt.Errorf("build option not supported for docker archive resolver")
+}
diff --git a/dive/image/docker/resolver.go b/dive/image/docker/engine_resolver.go
similarity index 84%
rename from dive/image/docker/resolver.go
rename to dive/image/docker/engine_resolver.go
index 461bbf9..3f91fe0 100644
--- a/dive/image/docker/resolver.go
+++ b/dive/image/docker/engine_resolver.go
@@ -13,13 +13,13 @@ import (
 	"golang.org/x/net/context"
 )
 
-type resolver struct{}
+type engineResolver struct{}
 
-func NewResolver() *resolver {
-	return &resolver{}
+func NewResolverFromEngine() *engineResolver {
+	return &engineResolver{}
 }
 
-func (r *resolver) Fetch(id string) (*image.Image, error) {
+func (r *engineResolver) Fetch(id string) (*image.Image, error) {
 
 	reader, err := r.fetchArchive(id)
 	if err != nil {
@@ -34,7 +34,7 @@ func (r *resolver) Fetch(id string) (*image.Image, error) {
 	return img.ToImage()
 }
 
-func (r *resolver) Build(args []string) (*image.Image, error) {
+func (r *engineResolver) Build(args []string) (*image.Image, error) {
 	id, err := buildImageFromCli(args)
 	if err != nil {
 		return nil, err
@@ -42,11 +42,11 @@ func (r *resolver) Build(args []string) (*image.Image, error) {
 	return r.Fetch(id)
 }
 
-func (r *resolver) fetchArchive(id string) (io.ReadCloser, error) {
+func (r *engineResolver) fetchArchive(id string) (io.ReadCloser, error) {
 	var err error
 	var dockerClient *client.Client
 
-	// pull the resolver if it does not exist
+	// pull the engineResolver if it does not exist
 	ctx := context.Background()
 
 	host := os.Getenv("DOCKER_HOST")
diff --git a/dive/image/docker/image_archive.go b/dive/image/docker/image_archive.go
index dd4b38d..7b2f806 100644
--- a/dive/image/docker/image_archive.go
+++ b/dive/image/docker/image_archive.go
@@ -149,7 +149,7 @@ func (img *ImageArchive) ToImage() (*image.Image, error) {
 	// build the layers array
 	layers := make([]*image.Layer, 0)
 
-	// note that the resolver config stores images in reverse chronological order, so iterate backwards through layers
+	// note that the engineResolver config stores images in reverse chronological order, so iterate backwards through layers
 	// as you iterate chronologically through history (ignoring history items that have no layer contents)
 	// Note: history is not required metadata in a docker image!
 	histIdx := 0
diff --git a/dive/image/podman/resolver_linux.go b/dive/image/podman/resolver_linux.go
index 4dbda15..928bd35 100644
--- a/dive/image/podman/resolver_linux.go
+++ b/dive/image/podman/resolver_linux.go
@@ -13,7 +13,7 @@ import (
 
 type resolver struct{}
 
-func NewResolver() *resolver {
+func NewResolverFromEngine() *resolver {
 	return &resolver{}
 }
 
diff --git a/runtime/options.go b/runtime/options.go
index a2b124e..5554a22 100644
--- a/runtime/options.go
+++ b/runtime/options.go
@@ -7,8 +7,8 @@ import (
 
 type Options struct {
 	Ci         bool
-	ImageId    string
-	Engine     dive.Engine
+	Image      string
+	Source     dive.ImageSource
 	ExportFile string
 	CiConfig   *viper.Viper
 	BuildArgs  []string
diff --git a/runtime/run.go b/runtime/run.go
index 024b4ca..a55f583 100644
--- a/runtime/run.go
+++ b/runtime/run.go
@@ -25,6 +25,8 @@ func runCi(analysis *image.AnalysisResult, options Options) {
 	evaluator := ci.NewCiEvaluator(options.CiConfig)
 
 	pass := evaluator.Evaluate(analysis)
+
+	// todo: report should return a string?
 	evaluator.Report()
 
 	if pass {
@@ -33,6 +35,9 @@ func runCi(analysis *image.AnalysisResult, options Options) {
 	os.Exit(1)
 }
 
+// todo: give channel of strings which the caller uses for fmt.print? or a more complex type?
+// todo: return err? or treat like a go routine?
+// todo: should there be a run() so that Run() can do the above and run() be the go routine? Then we test the behavior of run() (not Run())
 func Run(options Options) {
 	var err error
 	doExport := options.ExportFile != ""
@@ -43,7 +48,7 @@ func Run(options Options) {
 
 	// if build is given, get the handler based off of either the explicit runtime
 
-	imageResolver, err := dive.GetImageHandler(options.Engine)
+	imageResolver, err := dive.GetImageResolver(options.Source)
 	if err != nil {
 		fmt.Printf("cannot determine image provider: %v\n", err)
 		os.Exit(1)
@@ -60,7 +65,7 @@ func Run(options Options) {
 		}
 	} else {
 		fmt.Println(utils.TitleFormat("Fetching image...") + " (this can take a while for large images)")
-		img, err = imageResolver.Fetch(options.ImageId)
+		img, err = imageResolver.Fetch(options.Image)
 		if err != nil {
 			fmt.Printf("cannot fetch image: %v\n", err)
 			os.Exit(1)