From e39e64619140ec72c7cc75d4e0fdb4655fc6fcf6 Mon Sep 17 00:00:00 2001 From: Alex Goodman 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 --engine podman +dive --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)