Refactor image preprocessing (#121)

This commit is contained in:
Alex Goodman 2018-12-08 11:46:09 -05:00 committed by GitHub
parent 910c33fdf0
commit 9f9a8f2c05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 509 additions and 400 deletions

View file

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

View file

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

View file

@ -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.

263
image/docker_image.go Normal file
View file

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

74
image/docker_layer.go Normal file
View file

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

View file

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

View file

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

10
image/root.go Normal file
View file

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

73
image/types.go Normal file
View file

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

View file

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

View file

@ -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 {

View file

@ -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)
Views.Layer = NewLayerView("side", g, layers)
Views.Layer = NewLayerView("side", g, analysis.Layers)
Views.lookup[Views.Layer.Name] = Views.Layer
Views.Tree = NewFileTreeView("main", g, filetree.StackRange(refTrees, 0, 0), refTrees)
Views.Tree = NewFileTreeView("main", g, filetree.StackRange(analysis.RefTrees, 0, 0), analysis.RefTrees)
Views.lookup[Views.Tree.Name] = Views.Tree
Views.Status = NewStatusView("status", g)
@ -337,7 +337,7 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree, efficiency float6
Views.Filter = NewFilterView("command", g)
Views.lookup[Views.Filter.Name] = Views.Filter
Views.Details = NewDetailsView("details", g, efficiency, inefficiencies)
Views.Details = NewDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies)
Views.lookup[Views.Details.Name] = Views.Details
g.Cursor = false

49
utils/progress.go Normal file
View file

@ -0,0 +1,49 @@
package utils
import (
"fmt"
"strings"
)
type progressBar struct {
width int
percent int
rawTotal int64
rawCurrent int64
}
func NewProgressBar(total int64, width int) *progressBar {
return &progressBar{
rawTotal: total,
width: width,
}
}
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 {
done := int((pb.percent * pb.width) / 100.0)
if done > pb.width {
done = pb.width
}
todo := pb.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)
}