summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Goodman <wagoodman@users.noreply.github.com>2018-12-08 11:46:09 -0500
committerGitHub <noreply@github.com>2018-12-08 11:46:09 -0500
commit9f9a8f2c05ddf4d997fa55fb43946f81fa02237b (patch)
tree9ba3b684a014d09d561667e9153920889544d814
parent910c33fdf0889e925e8b423a6f94ddccd9df5be7 (diff)
Refactor image preprocessing (#121)
-rw-r--r--cmd/analyze.go27
-rw-r--r--cmd/build.go11
-rw-r--r--cmd/root.go2
-rw-r--r--image/docker_image.go263
-rw-r--r--image/docker_layer.go74
-rw-r--r--image/image.go310
-rw-r--r--image/layer.go57
-rw-r--r--image/root.go10
-rw-r--r--image/types.go73
-rw-r--r--ui/detailsview.go5
-rw-r--r--ui/layerview.go20
-rw-r--r--ui/ui.go8
-rw-r--r--utils/progress.go49
13 files changed, 509 insertions, 400 deletions
diff --git a/cmd/analyze.go b/cmd/analyze.go
index 0e45139..ab28979 100644
--- a/cmd/analyze.go
+++ b/cmd/analyze.go
@@ -10,9 +10,9 @@ import (
"github.com/wagoodman/dive/utils"
)
-// analyze takes a docker image tag, digest, or id and displays the
+// doAnalyzeCmd takes a docker image tag, digest, or id and displays the
// image analysis to the screen
-func analyze(cmd *cobra.Command, args []string) {
+func doAnalyzeCmd(cmd *cobra.Command, args []string) {
defer utils.Cleanup()
if len(args) == 0 {
printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
@@ -33,6 +33,25 @@ func analyze(cmd *cobra.Command, args []string) {
utils.Exit(1)
}
color.New(color.Bold).Println("Analyzing Image")
- manifest, refTrees, efficiency, inefficiencies := image.InitializeData(userImage)
- ui.Run(manifest, refTrees, efficiency, inefficiencies)
+
+ ui.Run(fetchAndAnalyze(userImage))
+}
+
+func fetchAndAnalyze(imageID string) *image.AnalysisResult {
+ analyzer := image.GetAnalyzer(imageID)
+
+ fmt.Println(" Fetching image...")
+ err := analyzer.Parse(imageID)
+ if err != nil {
+ fmt.Printf("cannot fetch image: %v\n", err)
+ utils.Exit(1)
+ }
+
+ fmt.Println(" Analyzing image...")
+ result, err := analyzer.Analyze()
+ if err != nil {
+ fmt.Printf("cannot doAnalyzeCmd image: %v\n", err)
+ utils.Exit(1)
+ }
+ return result
}
diff --git a/cmd/build.go b/cmd/build.go
index c982df8..680fc5f 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -4,7 +4,6 @@ import (
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "github.com/wagoodman/dive/image"
"github.com/wagoodman/dive/ui"
"github.com/wagoodman/dive/utils"
"io/ioutil"
@@ -16,15 +15,15 @@ var buildCmd = &cobra.Command{
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,
+ Run: doBuildCmd,
}
func init() {
rootCmd.AddCommand(buildCmd)
}
-// doBuild implements the steps taken for the build command
-func doBuild(cmd *cobra.Command, args []string) {
+// doBuildCmd implements the steps taken for the build command
+func doBuildCmd(cmd *cobra.Command, args []string) {
defer utils.Cleanup()
iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid")
if err != nil {
@@ -47,6 +46,6 @@ func doBuild(cmd *cobra.Command, args []string) {
}
color.New(color.Bold).Println("Analyzing Image")
- manifest, refTrees, efficiency, inefficiencies := image.InitializeData(string(imageId))
- ui.Run(manifest, refTrees, efficiency, inefficiencies)
+
+ ui.Run(fetchAndAnalyze(string(imageId)))
}
diff --git a/cmd/root.go b/cmd/root.go
index 5e27ac6..1ec4d3f 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -22,7 +22,7 @@ var rootCmd = &cobra.Command{
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.MaximumNArgs(1),
- Run: analyze,
+ Run: doAnalyzeCmd,
}
// Execute adds all child commands to the root command and sets flags appropriately.
diff --git a/image/docker_image.go b/image/docker_image.go
new file mode 100644
index 0000000..0e3c309
--- /dev/null
+++ b/image/docker_image.go
@@ -0,0 +1,263 @@
+package image
+
+import (
+ "archive/tar"
+ "encoding/json"
+ "fmt"
+ "github.com/docker/docker/client"
+ "github.com/sirupsen/logrus"
+ "github.com/wagoodman/dive/filetree"
+ "github.com/wagoodman/dive/utils"
+ "golang.org/x/net/context"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+var dockerVersion string
+
+func newDockerImageAnalyzer() Analyzer {
+ return &dockerImageAnalyzer{}
+}
+
+func newDockerImageManifest(manifestBytes []byte) dockerImageManifest {
+ var manifest []dockerImageManifest
+ err := json.Unmarshal(manifestBytes, &manifest)
+ if err != nil {
+ logrus.Panic(err)
+ }
+ return manifest[0]
+}
+
+func newDockerImageConfig(configBytes []byte) dockerImageConfig {
+ var imageConfig dockerImageConfig
+ err := json.Unmarshal(configBytes, &imageConfig)
+ if err != nil {
+ logrus.Panic(err)
+ }
+
+ layerIdx := 0
+ for idx := range imageConfig.History {
+ if imageConfig.History[idx].EmptyLayer {
+ imageConfig.History[idx].ID = "<missing>"
+ } else {
+ imageConfig.History[idx].ID = imageConfig.RootFs.DiffIds[layerIdx]
+ layerIdx++
+ }
+ }
+
+ return imageConfig
+}
+
+func (image *dockerImageAnalyzer) Parse(imageID string) error {
+ var err error
+ image.id = imageID
+ // store discovered json files in a map so we can read the image in one pass
+ image.jsonFiles = make(map[string][]byte)
+ image.layerMap = make(map[string]*filetree.FileTree)
+
+ // pull the image if it does not exist
+ ctx := context.Background()
+ image.client, err = client.NewClientWithOpts(client.WithVersion(dockerVersion), client.FromEnv)
+ if err != nil {
+ return err
+ }
+ _, _, err = image.client.ImageInspectWithRaw(ctx, imageID)
+ if err != nil {
+ // don't use the API, the CLI has more informative output
+ fmt.Println("Image not available locally. Trying to pull '" + imageID + "'...")
+ utils.RunDockerCmd("pull", imageID)
+ }
+
+ tarFile, _, err := image.getReader(imageID)
+ if err != nil {
+ return err
+ }
+ defer tarFile.Close()
+
+ err = image.read(tarFile)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (image *dockerImageAnalyzer) read(tarFile io.ReadCloser) error {
+ tarReader := tar.NewReader(tarFile)
+
+ var currentLayer uint
+ for {
+ header, err := tarReader.Next()
+
+ if err == io.EOF {
+ fmt.Println(" ╧")
+ break
+ }
+
+ if err != nil {
+ fmt.Println(err)
+ utils.Exit(1)
+ }
+
+ name := header.Name
+
+ // some layer tars can be relative layer symlinks to other layer tars
+ if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {
+
+ if strings.HasSuffix(name, "layer.tar") {
+ currentLayer++
+ if err != nil {
+ return err
+ }
+ layerReader := tar.NewReader(tarReader)
+ err := image.processLayerTar(name, currentLayer, layerReader)
+ if err != nil {
+ return err
+ }
+ } else if strings.HasSuffix(name, ".json") {
+ fileBuffer, err := ioutil.ReadAll(tarReader)
+ if err != nil {
+ return err
+ }
+ image.jsonFiles[name] = fileBuffer
+ }
+ }
+ }
+
+ return nil
+}
+
+func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
+ image.trees = make([]*filetree.FileTree, 0)
+
+ manifest := newDockerImageManifest(image.jsonFiles["manifest.json"])
+ config := newDockerImageConfig(image.jsonFiles[manifest.ConfigPath])
+
+ // build the content tree
+ for _, treeName := range manifest.LayerTarPaths {
+ image.trees = append(image.trees, image.layerMap[treeName])
+ }
+
+ // build the layers array
+ image.layers = make([]*dockerLayer, len(image.trees))
+
+ // note that the image 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)
+ layerIdx := len(image.trees) - 1
+ tarPathIdx := 0
+ for idx := 0; idx < len(config.History); idx++ {
+ // ignore empty layers, we are only observing layers with content
+ if config.History[idx].EmptyLayer {
+ continue
+ }
+
+ tree := image.trees[(len(image.trees)-1)-layerIdx]
+ config.History[idx].Size = uint64(tree.FileSize)
+
+ image.layers[layerIdx] = &dockerLayer{
+ history: config.History[idx],
+ index: layerIdx,
+ tree: image.trees[layerIdx],
+ tarPath: manifest.LayerTarPaths[tarPathIdx],
+ }
+
+ layerIdx--
+ tarPathIdx++
+ }
+
+ efficiency, inefficiencies := filetree.Efficiency(image.trees)
+
+ layers := make([]Layer, len(image.layers))
+ for i, v := range image.layers {
+ layers[i] = v
+ }
+
+ return &AnalysisResult{
+ Layers: layers,
+ RefTrees: image.trees,
+ Efficiency: efficiency,
+ Inefficiencies: inefficiencies,
+ }, nil
+}
+
+func (image *dockerImageAnalyzer) getReader(imageID string) (io.ReadCloser, int64, error) {
+
+ ctx := context.Background()
+ result, _, err := image.client.ImageInspectWithRaw(ctx, imageID)
+ if err != nil {
+ return nil, -1, err
+ }
+ totalSize := result.Size
+
+ readCloser, err := image.client.ImageSave(ctx, []string{imageID})
+ if err != nil {
+ return nil, -1, err
+ }
+
+ return readCloser, totalSize, nil
+}
+
+// todo: it is bad that this is printing out to the screen. As the interface gets more flushed out, an event update mechanism should be built in (so the caller can format and print updates)
+func (image *dockerImageAnalyzer) processLayerTar(name string, layerIdx uint, reader *tar.Reader) error {
+ tree := filetree.NewFileTree()
+ tree.Name = name
+
+ title := fmt.Sprintf("[layer: %2d]", layerIdx)
+ message := fmt.Sprintf(" ├─ %s %s ", title, "working...")
+ fmt.Printf("\r%s", message)
+
+ fileInfos, err := image.getFileList(reader)
+ if err != nil {
+ return err
+ }
+
+ shortName := name[:15]
+ pb := utils.NewProgressBar(int64(len(fileInfos)), 30)
+ for idx, element := range fileInfos {
+ tree.FileSize += uint64(element.TarHeader.FileInfo().Size())
+ _, err := tree.AddPath(element.Path, element)
+ if err != nil {
+ return err
+ }
+
+ if pb.Update(int64(idx)) {
+ message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
+ fmt.Printf("\r%s", message)
+ }
+ }
+ pb.Done()
+ message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
+ fmt.Printf("\r%s\n", message)
+
+ image.layerMap[tree.Name] = tree
+ return nil
+}
+
+func (image *dockerImageAnalyzer) getFileList(tarReader *tar.Reader) ([]filetree.FileInfo, error) {
+ var files []filetree.FileInfo
+
+ for {
+ header, err := tarReader.Next()
+
+ if err == io.EOF {
+ break
+ }
+
+ if err != nil {
+ fmt.Println(err)
+ utils.Exit(1)
+ }
+
+ name := header.Name
+
+ switch header.Typeflag {
+ case tar.TypeXGlobalHeader:
+ return nil, fmt.Errorf("unexptected tar file: (XGlobalHeader): type=%v name=%s", header.Typeflag, name)
+ case tar.TypeXHeader:
+ return nil, fmt.Errorf("unexptected tar file (XHeader): type=%v name=%s", header.Typeflag, name)
+ default:
+ files = append(files, filetree.NewFileInfo(tarReader, header, name))
+ }
+ }
+ return files, nil
+}
diff --git a/image/docker_layer.go b/image/docker_layer.go
new file mode 100644
index 0000000..b5146b8
--- /dev/null
+++ b/image/docker_layer.go
@@ -0,0 +1,74 @@
+package image
+
+import (
+ "fmt"
+ "github.com/dustin/go-humanize"
+ "github.com/wagoodman/dive/filetree"
+ "strings"
+)
+
+const (
+ LayerFormat = "%-25s %7s %s"
+)
+
+// ShortId returns the truncated id of the current layer.
+func (layer *dockerLayer) TarId() string {
+ return strings.TrimSuffix(layer.tarPath, "/layer.tar")
+}
+
+// ShortId returns the truncated id of the current layer.
+func (layer *dockerLayer) Id() string {
+ return layer.history.ID
+}
+
+// index returns the relative position of the layer within the image.
+func (layer *dockerLayer) Index() int {
+ return layer.index
+}
+
+// Size returns the number of bytes that this image is.
+func (layer *dockerLayer) Size() uint64 {
+ return layer.history.Size
+}
+
+// Tree returns the file tree representing the current layer.
+func (layer *dockerLayer) Tree() *filetree.FileTree {
+ return layer.tree
+}
+
+// ShortId returns the truncated id of the current layer.
+func (layer *dockerLayer) Command() string {
+ return strings.TrimPrefix(layer.history.CreatedBy, "/bin/sh -c ")
+}
+
+// ShortId returns the truncated id of the current layer.
+func (layer *dockerLayer) ShortId() string {
+ rangeBound := 25
+ id := layer.Id()
+ if length := len(id); length < 25 {
+ rangeBound = length
+ }
+ id = id[0:rangeBound]
+
+ // show the tagged image as the last layer
+ // if len(layer.History.Tags) > 0 {
+ // id = "[" + strings.Join(layer.History.Tags, ",") + "]"
+ // }
+
+ return id
+}
+
+// String represents a layer in a columnar format.
+func (layer *dockerLayer) String() string {
+
+ if layer.index == 0 {
+ return fmt.Sprintf(LayerFormat,
+ layer.ShortId(),
+ humanize.Bytes(layer.Size()),
+ "FROM "+layer.ShortId())
+ }
+ return fmt.Sprintf(LayerFormat,
+ layer.ShortId(),
+ humanize.Bytes(layer.Size()),
+ layer.Command())
+}
diff --git a/image/image.go b/image/image.go
deleted file mode 100644
index cd80ffb..0000000
--- a/image/image.go
+++ /dev/null
@@ -1,310 +0,0 @@
-package image
-
-import (
- "archive/tar"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "strings"
-
- "github.com/sirupsen/logrus"
-
- "github.com/docker/docker/client"
- "github.com/wagoodman/dive/filetree"
- "github.com/wagoodman/dive/utils"
- "golang.org/x/net/context"
-)
-
-// TODO: this file should be rethought... but since it's only for preprocessing it'll be tech debt for now.
-var dockerVersion string
-
-func check(e error) {
- if e != nil {
- panic(e)
- }
-}
-
-type ProgressBar struct {
- percent int
- rawTotal int64
- rawCurrent int64
-}
-
-func NewProgressBar(total int64) *ProgressBar {
- return &ProgressBar{
- rawTotal: total,
- }
-}
-
-func (pb *ProgressBar) Done() {
- pb.rawCurrent = pb.rawTotal
- pb.percent = 100
-}
-
-func (pb *ProgressBar) Update(currentValue int64) (hasChanged bool) {
- pb.rawCurrent = currentValue
- percent := int(100.0 * (float64(pb.rawCurrent) / float64(pb.rawTotal)))
- if percent != pb.percent {
- hasChanged = true
- }
- pb.percent = percent
- return hasChanged
-}
-
-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
-
- return "[" + strings.Repeat("=", done) + strings.Repeat(">", head) + strings.Repeat(" ", todo) + "]" + fmt.Sprintf(" %d %% (%d/%d)", pb.percent, pb.rawCurrent, pb.rawTotal)
-}
-
-type ImageManifest struct {
- ConfigPath string `json:"Config"`
- RepoTags []string `json:"RepoTags"`
- LayerTarPaths []string `json:"Layers"`
-}
-
-type ImageConfig struct {
- History []ImageHistoryEntry `json:"history"`
- RootFs RootFs `json:"rootfs"`
-}
-
-type RootFs struct {
- Type string `json:"type"`
- DiffIds []string `json:"diff_ids"`
-}
-
-type ImageHistoryEntry struct {
- ID string
- Size uint64
- Created string `json:"created"`
- Author string `json:"author"`
- CreatedBy string `json:"created_by"`
- EmptyLayer bool `json:"empty_layer"`
-}
-
-func NewImageManifest(manifestBytes []byte) ImageManifest {
- var manifest []ImageManifest
- err := json.Unmarshal(manifestBytes, &manifest)
- if err != nil {
- logrus.Panic(err)
- }
- return manifest[0]
-}
-
-func NewImageConfig(configBytes []byte) ImageConfig {
- var imageConfig ImageConfig
- err := json.Unmarshal(configBytes, &imageConfig)
- if err != nil {
- logrus.Panic(err)
- }
-
- layerIdx := 0
- for idx := range imageConfig.History {
- if imageConfig.History[idx].EmptyLayer {
- imageConfig.History[idx].ID = "<missing>"
- } else {
- imageConfig.History[idx].ID = imageConfig.RootFs.DiffIds[layerIdx]
- layerIdx++
- }
- }
-
- return imageConfig
-}
-
-func processLayerTar(layerMap map[string]*filetree.FileTree, name string, reader *tar.Reader, layerProgress string) {
- tree := filetree.NewFileTree()
- tree.Name = name
-
- fileInfos := getFileList(reader)
-
- shortName := name[:15]
- pb := NewProgressBar(int64(len(fileInfos)))
- for idx, element := range fileInfos {
- tree.FileSize += uint64(element.TarHeader.FileInfo().Size())
- tree.AddPath(element.Path, element)
-
- if pb.Update(int64(idx)) {
- message := fmt.Sprintf(" ├─ %s %s : %s", layerProgress, shortName, pb.String())
- fmt.Printf("\r%s", message)
- }
- }
- pb.Done()
- message := fmt.Sprintf(" ├─ %s %s : %s", layerProgress, shortName, pb.String())
- fmt.Printf("\r%s\n", message)
-
- layerMap[tree.Name] = tree
-}
-
-func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree, float64, filetree.EfficiencySlice) {
- var layerMap = make(map[string]*filetree.FileTree)
- var trees = make([]*filetree.FileTree, 0)
-
- // pull the image if it does not exist
- ctx := context.Background()
- dockerClient, err := client.NewClientWithOpts(client.WithVersion(dockerVersion), client.FromEnv)
- if err != nil {
- fmt.Println("Could not connect to the Docker daemon:" + err.Error())
- utils.Exit(1)
- }
- _, _, err = dockerClient.ImageInspectWithRaw(ctx, imageID)
- if err != nil {
- // don't use the API, the CLI has more informative output
- fmt.Println("Image not available locally. Trying to pull '" + imageID + "'...")
- utils.RunDockerCmd("pull", imageID)
- }
-
- tarFile, _ := getImageReader(imageID)
- defer tarFile.Close()
-
- var currentLayer uint
-
- tarReader := tar.NewReader(tarFile)
-
- // json files are small. Let's store the in a map so we can read the image in one pass
- jsonFiles := make(map[string][]byte)
-
- for {
- header, err := tarReader.Next()
-
- if err == io.EOF {
- fmt.Println(" ╧")
- break
- }
-
- if err != nil {
- fmt.Println(err)
- utils.Exit(1)
- }
-
- layerProgress := fmt.Sprintf("[layer: %2d]", currentLayer)
-
- name := header.Name
- // some layer tars can be relative layer symlinks to other layer tars
- if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {
-
- if strings.HasSuffix(name, "layer.tar") {
- currentLayer++
- if err != nil {
- logrus.Panic(err)
- }
- message := fmt.Sprintf(" ├─ %s %s ", layerProgress, "working...")
- fmt.Printf("\r%s", message)
-
- layerReader := tar.NewReader(tarReader)
- processLayerTar(layerMap, name, layerReader, layerProgress)
- } else if strings.HasSuffix(name, ".json") {
- fileBuffer, err := ioutil.ReadAll(tarReader)
- if err != nil {
- logrus.Panic(err)
- }
- jsonFiles[name] = fileBuffer
- }
- }
- }
-
- manifest := NewImageManifest(jsonFiles["manifest.json"])
- config := NewImageConfig(jsonFiles[manifest.ConfigPath])
-
- // build the content tree
- fmt.Println(" Building tree...")
- for _, treeName := range manifest.LayerTarPaths {
- trees = append(trees, layerMap[treeName])
- }
-
- // build the layers array
- layers := make([]*Layer, len(trees))
-
- // note that the image 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)
- layerIdx := len(trees) - 1
- tarPathIdx := 0
- for idx := 0; idx < len(config.History); idx++ {
- // ignore empty layers, we are only observing layers with content
- if config.History[idx].EmptyLayer {
- continue
- }
-
- tree := trees[(len(trees)-1)-layerIdx]
- config.History[idx].Size = uint64(tree.FileSize)
-
- layers[layerIdx] = &Layer{
- History: config.History[idx],
- Index: layerIdx,
- Tree: trees[layerIdx],
- RefTrees: trees,
- TarPath: manifest.LayerTarPaths[tarPathIdx],
- }
-
- layerIdx--
- tarPathIdx++
- }
-
- fmt.Println(" Analyzing layers...")
- efficiency, inefficiencies := filetree.Efficiency(trees)
-
- return layers, trees, efficiency, inefficiencies
-}
-
-func getImageReader(imageID string) (io.ReadCloser, int64) {
- ctx := context.Background()
- dockerClient, err := client.NewClientWithOpts(client.WithVersion(dockerVersion), client.FromEnv)
- if err != nil {
- fmt.Println("Could not connect to the Docker daemon:" + err.Error())
- utils.Exit(1)
- }
-
- fmt.Println(" Fetching metadata...")
-
- result, _, err := dockerClient.ImageInspectWithRaw(ctx, imageID)
- if err != nil {
- fmt.Println(err.Error())
- utils.Exit(1)
- }
- totalSize := result.Size
-
- fmt.Println(" Fetching image...")
-
- readCloser, err := dockerClient.ImageSave(ctx, []string{imageID})
- check(err)
-
- return readCloser, totalSize
-}
-
-func getFileList(tarReader *tar.Reader) []filetree.FileInfo {
- var files []filetree.FileInfo
-
- for {
- header, err := tarReader.Next()
-
- if err == io.EOF {
- break
- }
-
- if err != nil {
- fmt.Println(err)
- utils.Exit(1)
- }
-
- name := header.Name
-
- switch header.Typeflag {
- case tar.TypeXGlobalHeader:
- fmt.Printf("ERRG: XGlobalHeader: %v: %s\n", header.Typeflag, name)
- case tar.TypeXHeader:
- fmt.Printf("ERRG: XHeader: %v: %s\n", header.Typeflag, name)
- default:
- files = append(files, filetree.NewFileInfo(tarReader, header, name))
- }
- }
- return files
-}
diff --git a/image/layer.go b/image/layer.go
deleted file mode 100644
index 2e8a4d1..0000000
--- a/image/layer.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package image
-
-import (
- "fmt"
- "github.com/dustin/go-humanize"
- "github.com/wagoodman/dive/filetree"
- "strings"
-)
-
-const (
- LayerFormat = "%-25s %7s %s"
-)
-
-// Layer represents a Docker image layer and metadata
-type Layer struct {
- TarPath string
- History ImageHistoryEntry
- Index int
- Tree *filetree.FileTree
- RefTrees []*filetree.FileTree
-}
-
-// ShortId returns the truncated id of the current layer.
-func (layer *Layer) TarId() string {
- return strings.TrimSuffix(layer.TarPath, "/layer.tar")
-}
-
-// ShortId returns the truncated id of the current layer.
-func (layer *Layer) Id() string {
- return layer.History.ID
-}
-
-// ShortId returns the truncated id of the current layer.
-func (layer *Layer) ShortId() string {
- rangeBound := 25
- id := layer.Id()
- if length := len(id); length < 25 {
- rangeBound = length
- }
- id = id[0:rangeBound]
-
- // show the tagged image as the last layer
- // if len(layer.History.Tags) > 0 {
- // id = "[" + strings.Join(layer.History.Tags, ",") + "]"
- // }
-
- return id
-}
-
-// String represents a layer in a columnar format.
-func (layer *Layer) String() string {
-
- return fmt.Sprintf(LayerFormat,
- layer.ShortId(),
- humanize.Bytes(uint64(layer.History.Size)),
- strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
-}
diff --git a/image/root.go b/image/root.go
new file mode 100644
index 0000000..8d64192
--- /dev/null
+++ b/image/root.go
@@ -0,0 +1,10 @@
+package image
+
+type AnalyzerFactory func() Analyzer
+
+func GetAnalyzer(imageID string) Analyzer {
+ // todo: add ability to have multiple image formats... for the meantime only use docker
+ var factory AnalyzerFactory = newDockerImageAnalyzer
+
+ return factory()
+}
diff --git a/image/types.go b/image/types.go
new file mode 100644
index 0000000..1996e1e
--- /dev/null
+++ b/image/types.go
@@ -0,0 +1,73 @@
+package image
+
+import (
+ "github.com/docker/docker/client"
+ "github.com/wagoodman/dive/filetree"
+)
+
+type Parser interface {
+}
+
+type Analyzer interface {
+ Parse(id string) error
+ Analyze() (*AnalysisResult, error)
+}
+
+type Layer interface {
+ Id() string
+ ShortId() string
+ Index() int
+ Command() string
+ Size() uint64
+ Tree() *filetree.FileTree
+ String() string
+}
+
+type AnalysisResult struct {
+ Layers []Layer
+ RefTrees []*filetree.FileTree
+ Efficiency float64
+ Inefficiencies filetree.EfficiencySlice
+}
+
+type dockerImageAnalyzer struct {
+ id string
+ client *client.Client
+ jsonFiles map[string][]byte
+ trees []*filetree.FileTree
+ layerMap map[string]*filetree.FileTree
+ layers []*dockerLayer
+}
+
+type dockerImageHistoryEntry struct {
+ ID string
+ Size uint64
+ Created string `json:"created"`
+ Author string `json:"author"`
+ CreatedBy string `json:"created_by"`
+ EmptyLayer bool `json:"empty_layer"`
+}
+
+type dockerImageManifest struct {
+ ConfigPath string `json:"Config"`
+ RepoTags []string `json:"RepoTags"`
+ LayerTarPaths []string `json:"Layers"`
+}
+
+type dockerImageConfig struct {
+ History []dockerImageHistoryEntry `json:"history"`
+ RootFs dockerRootFs `json:"rootfs"`
+}
+
+type dockerRootFs struct {
+ Type string `json:"type"`
+ DiffIds []string `json:"diff_ids"`
+}
+
+// Layer represents a Docker image layer and metadata
+type dockerLayer struct {
+ tarPath string
+ history dockerImageHistoryEntry
+ index int
+ tree *filetree.FileTree
+}
diff --git a/ui/detailsview.go b/ui/detailsview.go
index 2eaa802..f387703 100644
--- a/ui/detailsview.go
+++ b/ui/detailsview.go
@@ -128,9 +128,10 @@ func (view *DetailsView) Render() error {
// update contents
view.view.Clear()
fmt.Fprintln(view.view, Formatting.Header("Digest: ")+currentLayer.Id())
- fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
+ // TODO: add back in with view model
+ // fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
fmt.Fprintln(view.view, Formatting.Header("Command:"))
- fmt.Fprintln(view.view, currentLayer.History.CreatedBy)
+ fmt.Fprintln(view.view, currentLayer.Command())
fmt.Fprintln(view.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
diff --git a/ui/layerview.go b/ui/layerview.go
index a77adfd..b29b874 100644
--- a/ui/layerview.go
+++ b/ui/layerview.go
@@ -5,7 +5,6 @@ import (
"github.com/spf13/viper"
"github.com/wagoodman/dive/utils"
- "github.com/dustin/go-humanize"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/image"
@@ -20,7 +19,7 @@ type LayerView struct {
view *gocui.View
header *gocui.View
LayerIndex int
- Layers []*image.Layer
+ Layers []image.Layer
CompareMode CompareType
CompareStartIndex int
ImageSize uint64
@@ -30,7 +29,7 @@ type LayerView struct {
}
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
-func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (layerView *LayerView) {
+func NewLayerView(name string, gui *gocui.Gui, layers []image.Layer) (layerView *LayerView) {
layerView = new(LayerView)
// populate main fields
@@ -131,7 +130,7 @@ func (view *LayerView) SetCursor(layer int) error {
}
// currentLayer returns the Layer object currently selected.
-func (view *LayerView) currentLayer() *image.Layer {
+func (view *LayerView) currentLayer() image.Layer {
return view.Layers[(len(view.Layers)-1)-view.LayerIndex]
}
@@ -181,7 +180,7 @@ func (view *LayerView) renderCompareBar(layerIdx int) string {
func (view *LayerView) Update() error {
view.ImageSize = 0
for idx := 0; idx < len(view.Layers); idx++ {
- view.ImageSize += view.Layers[idx].History.Size
+ view.ImageSize += view.Layers[idx].Size()
}
return nil
}
@@ -212,17 +211,6 @@ func (view *LayerView) Render() error {
idx := (len(view.Layers) - 1) - revIdx
layerStr := layer.String()
- if idx == 0 {
- var layerId string
- if len(layer.History.ID) >= 25 {
- layerId = layer.History.ID[0:25]
- } else {
- layerId = fmt.Sprintf("%-25s", layer.History.ID)
- }
-
- layerStr = fmt.Sprintf(image.LayerFormat, layerId, humanize.Bytes(uint64(layer.History.Size)), "FROM "+layer.ShortId())
- }
-
compareBar := view.renderCompareBar(idx)
if idx == view.LayerIndex {
diff --git a/ui/ui.go b/ui/ui.go
index 8b3e815..6ad30e8 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -301,7 +301,7 @@ func renderStatusOption(control, title string, selected bool) string {
}
// Run is the UI entrypoint.
-func Run(layers []*image.Layer, refTrees []*filetree.FileTree, efficiency float64, inefficiencies filetree.EfficiencySlice) {
+func Run(analysis *image.AnalysisResult) {
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
Formatting.Header = color.New(color.Bold).SprintFunc()
@@ -325,10 +325,10 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree, efficiency float6
Views.lookup = make(map[string]View)