adding docker-archive source option

This commit is contained in:
Alex Goodman 2019-10-08 18:55:03 -04:00
parent 0e49dd0fec
commit e39e646191
No known key found for this signature in database
GPG Key ID: 98AF011C5C78EB7E
12 changed files with 113 additions and 71 deletions

View File

@ -61,14 +61,17 @@ command.
**CI Integration** **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. 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** **With Multiple Image Sources and Container Engines Supported**
- Docker (default) With the `--source` option, you can select where to fetch the container image from:
- Podman (linux only)
```bash ```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 ## Installation
**Ubuntu/Debian** **Ubuntu/Debian**

View File

@ -39,7 +39,7 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
engine, err := cmd.PersistentFlags().GetString("engine") engine, err := cmd.PersistentFlags().GetString("source")
if err != nil { if err != nil {
fmt.Printf("unable to determine engine: %v\n", err) fmt.Printf("unable to determine engine: %v\n", err)
os.Exit(1) os.Exit(1)
@ -47,8 +47,8 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
runtime.Run(runtime.Options{ runtime.Run(runtime.Options{
Ci: isCi, Ci: isCi,
Engine: dive.GetEngine(engine), Source: dive.ParseImageSource(engine),
ImageId: userImage, Image: userImage,
ExportFile: exportFile, ExportFile: exportFile,
CiConfig: ciConfig, CiConfig: ciConfig,
}) })

View File

@ -29,7 +29,7 @@ func doBuildCmd(cmd *cobra.Command, args []string) {
runtime.Run(runtime.Options{ runtime.Run(runtime.Options{
Ci: isCi, Ci: isCi,
Engine: dive.GetEngine(engine), Source: dive.ParseImageSource(engine),
BuildArgs: args, BuildArgs: args,
ExportFile: exportFile, ExportFile: exportFile,
CiConfig: ciConfig, CiConfig: ciConfig,

View File

@ -46,6 +46,7 @@ func init() {
func initCli() { 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().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive/*.yaml, or $XDG_CONFIG_HOME/dive.yaml)")
rootCmd.PersistentFlags().String("source", "docker", "The container engine to fetch the image from. Allowed values: "+strings.Join(dive.ImageSources, ", "))
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number") rootCmd.PersistentFlags().BoolP("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().BoolVar(&isCi, "ci", false, "Skip the interactive TUI and validate against CI rules (same as env var CI=true)")
rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.") rootCmd.Flags().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. // initConfig reads in config file and ENV variables if set.

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -13,13 +13,13 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
type resolver struct{} type engineResolver struct{}
func NewResolver() *resolver { func NewResolverFromEngine() *engineResolver {
return &resolver{} 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) reader, err := r.fetchArchive(id)
if err != nil { if err != nil {
@ -34,7 +34,7 @@ func (r *resolver) Fetch(id string) (*image.Image, error) {
return img.ToImage() 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) id, err := buildImageFromCli(args)
if err != nil { if err != nil {
return nil, err return nil, err
@ -42,11 +42,11 @@ func (r *resolver) Build(args []string) (*image.Image, error) {
return r.Fetch(id) 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 err error
var dockerClient *client.Client var dockerClient *client.Client
// pull the resolver 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 := os.Getenv("DOCKER_HOST")

View File

@ -149,7 +149,7 @@ func (img *ImageArchive) ToImage() (*image.Image, error) {
// build the layers array // build the layers array
layers := make([]*image.Layer, 0) 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) // as you iterate chronologically through history (ignoring history items that have no layer contents)
// Note: history is not required metadata in a docker image! // Note: history is not required metadata in a docker image!
histIdx := 0 histIdx := 0

View File

@ -13,7 +13,7 @@ import (
type resolver struct{} type resolver struct{}
func NewResolver() *resolver { func NewResolverFromEngine() *resolver {
return &resolver{} return &resolver{}
} }

View File

@ -7,8 +7,8 @@ import (
type Options struct { type Options struct {
Ci bool Ci bool
ImageId string Image string
Engine dive.Engine Source dive.ImageSource
ExportFile string ExportFile string
CiConfig *viper.Viper CiConfig *viper.Viper
BuildArgs []string BuildArgs []string

View File

@ -25,6 +25,8 @@ func runCi(analysis *image.AnalysisResult, options Options) {
evaluator := ci.NewCiEvaluator(options.CiConfig) evaluator := ci.NewCiEvaluator(options.CiConfig)
pass := evaluator.Evaluate(analysis) pass := evaluator.Evaluate(analysis)
// todo: report should return a string?
evaluator.Report() evaluator.Report()
if pass { if pass {
@ -33,6 +35,9 @@ func runCi(analysis *image.AnalysisResult, options Options) {
os.Exit(1) 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) { func Run(options Options) {
var err error var err error
doExport := options.ExportFile != "" 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 // 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 { if err != nil {
fmt.Printf("cannot determine image provider: %v\n", err) fmt.Printf("cannot determine image provider: %v\n", err)
os.Exit(1) os.Exit(1)
@ -60,7 +65,7 @@ func Run(options Options) {
} }
} else { } else {
fmt.Println(utils.TitleFormat("Fetching image...") + " (this can take a while for large images)") 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 { if err != nil {
fmt.Printf("cannot fetch image: %v\n", err) fmt.Printf("cannot fetch image: %v\n", err)
os.Exit(1) os.Exit(1)