Refactor image preprocessing (#121)
This commit is contained in:
parent
910c33fdf0
commit
9f9a8f2c05
@ -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
|
||||
}
|
||||
|
11
cmd/build.go
11
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)))
|
||||
}
|
||||
|
@ -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
263
image/docker_image.go
Normal 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
74
image/docker_layer.go
Normal 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())
|
||||
}
|
310
image/image.go
310
image/image.go
@ -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
|
||||
}
|
@ -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
10
image/root.go
Normal 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
73
image/types.go
Normal 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
|
||||
}
|
@ -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)))
|
||||
|
||||
|
@ -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 {
|
||||
|
8
ui/ui.go
8
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)
|
||||
|
||||
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
49
utils/progress.go
Normal 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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user