Skip to content

Commit

Permalink
add ci integration (to stdout)
Browse files Browse the repository at this point in the history
  • Loading branch information
wagoodman committed Dec 23, 2018
1 parent a830f34 commit 1fb9b31
Show file tree
Hide file tree
Showing 15 changed files with 490 additions and 155 deletions.
15 changes: 15 additions & 0 deletions .data/.dive-ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
plugins:
- plugin1

rules:
# If the efficiency is measured below X%, mark as failed. (expressed as a percentage between 0-1)
lowestEfficiency: 0.95

# If the amount of wasted space is at least X or larger than X, mark as failed. (expressed in B, KB, MB, and GB)
highestWastedBytes: 20Mb

# If the amount of wasted space makes up for X% of the image, mark as failed. (fail if the threshold is met or crossed; expressed as a percentage between 0-1)
highestUserWastedPercent: 0.10

plugin1/rule1: error
125 changes: 6 additions & 119 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package cmd

import (
"encoding/json"
"fmt"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/wagoodman/dive/filetree"
"github.com/wagoodman/dive/image"
"github.com/wagoodman/dive/ui"
"github.com/wagoodman/dive/runtime"
"github.com/wagoodman/dive/utils"
"io/ioutil"
)

// doAnalyzeCmd takes a docker image tag, digest, or id and displays the
Expand All @@ -35,117 +30,9 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) {
utils.Exit(1)
}

run(userImage)
}

type export struct {
Layer []exportLayer `json:"layer"`
Image exportImage `json:"image"`
}

type exportLayer struct {
Index int `json:"index"`
DigestID string `json:"digestId"`
SizeBytes uint64 `json:"sizeBytes"`
Command string `json:"command"`
}
type exportImage struct {
SizeBytes uint64 `json:"sizeBytes"`
InefficientBytes uint64 `json:"inefficientBytes"`
EfficiencyScore float64 `json:"efficiencyScore"`
InefficientFiles []inefficientFiles `json:"inefficientFiles"`
}

type inefficientFiles struct {
Count int `json:"count"`
SizeBytes uint64 `json:"sizeBytes"`
File string `json:"file"`
}

func newExport(analysis *image.AnalysisResult) *export {
data := export{}
data.Layer = make([]exportLayer, len(analysis.Layers))
data.Image.InefficientFiles = make([]inefficientFiles, len(analysis.Inefficiencies))

// export layers in order
for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- {
layer := analysis.Layers[revIdx]
idx := (len(analysis.Layers) - 1) - revIdx

data.Layer[idx] = exportLayer{
Index: idx,
DigestID: layer.Id(),
SizeBytes: layer.Size(),
Command: layer.Command(),
}
}

// export image info
data.Image.SizeBytes = 0
for idx := 0; idx < len(analysis.Layers); idx++ {
data.Image.SizeBytes += analysis.Layers[idx].Size()
}

data.Image.EfficiencyScore = analysis.Efficiency

for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
data.Image.InefficientBytes += uint64(fileData.CumulativeSize)

data.Image.InefficientFiles[idx] = inefficientFiles{
Count: len(fileData.Nodes),
SizeBytes: uint64(fileData.CumulativeSize),
File: fileData.Path,
}
}

return &data
}

func exportStatistics(analysis *image.AnalysisResult) {
data := newExport(analysis)
payload, err := json.MarshalIndent(&data, "", " ")
if err != nil {
panic(err)
}
err = ioutil.WriteFile(exportFile, payload, 0644)
if err != nil {
panic(err)
}
}

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
}

func run(imageID string) {
color.New(color.Bold).Println("Analyzing Image")
result := fetchAndAnalyze(imageID)

if exportFile != "" {
exportStatistics(result)
color.New(color.Bold).Println(fmt.Sprintf("Exported to %s", exportFile))
utils.Exit(0)
}

fmt.Println(" Building cache...")
cache := filetree.NewFileTreeCache(result.RefTrees)
cache.Build()

ui.Run(result, cache)
runtime.Run(runtime.Options{
ImageId: userImage,
ExportFile: exportFile,
CiConfigFile: ciConfigFile,
})
}
28 changes: 5 additions & 23 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package cmd

import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/wagoodman/dive/runtime"
"github.com/wagoodman/dive/utils"
"io/ioutil"
"os"
)

// buildCmd represents the build command
Expand All @@ -23,25 +21,9 @@ func init() {
// 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 {
utils.Cleanup()
log.Fatal(err)
}
defer os.Remove(iidfile.Name())

allArgs := append([]string{"--iidfile", iidfile.Name()}, args...)
err = utils.RunDockerCmd("build", allArgs...)
if err != nil {
utils.Cleanup()
log.Fatal(err)
}

imageId, err := ioutil.ReadFile(iidfile.Name())
if err != nil {
utils.Cleanup()
log.Fatal(err)
}

run(string(imageId))
runtime.Run(runtime.Options{
BuildArgs: args,
ExportFile: exportFile,
})
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

var cfgFile string
var exportFile string
var ciConfigFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Expand Down Expand Up @@ -46,6 +47,7 @@ func init() {
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")

rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.")
rootCmd.Flags().StringVar(&ciConfigFile, "ci-config", ".dive-ci", "If CI=true in the environment, use the given yaml to drive validation rules.")
}

// initConfig reads in config file and ENV variables if set.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jroimartin/gocui v0.4.0
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcM
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
Expand Down
31 changes: 23 additions & 8 deletions image/docker_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (image *dockerImageAnalyzer) read(tarFile io.ReadCloser) error {
header, err := tarReader.Next()

if err == io.EOF {
fmt.Println(" ╧")
fmt.Println(" ╧")
break
}

Expand Down Expand Up @@ -167,16 +167,31 @@ func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {

efficiency, inefficiencies := filetree.Efficiency(image.trees)

var sizeBytes, userSizeBytes uint64
layers := make([]Layer, len(image.layers))
for i, v := range image.layers {
layers[i] = v
sizeBytes += v.Size()
if i != 0 {
userSizeBytes += v.Size()
}
}

var wastedBytes uint64
for idx := 0; idx < len(inefficiencies); idx++ {
fileData := inefficiencies[len(inefficiencies)-1-idx]
wastedBytes += uint64(fileData.CumulativeSize)
}

return &AnalysisResult{
Layers: layers,
RefTrees: image.trees,
Efficiency: efficiency,
Inefficiencies: inefficiencies,
Layers: layers,
RefTrees: image.trees,
Efficiency: efficiency,
UserSizeByes: userSizeBytes,
SizeBytes: sizeBytes,
WastedBytes: wastedBytes,
WastedUserPercent: float64(float64(wastedBytes) / float64(userSizeBytes)),
Inefficiencies: inefficiencies,
}, nil
}

Expand All @@ -203,7 +218,7 @@ func (image *dockerImageAnalyzer) processLayerTar(name string, layerIdx uint, re
tree.Name = name

title := fmt.Sprintf("[layer: %2d]", layerIdx)
message := fmt.Sprintf(" ├─ %s %s ", title, "working...")
message := fmt.Sprintf(" ├─ %s %s ", title, "working...")
fmt.Printf("\r%s", message)

fileInfos, err := image.getFileList(reader)
Expand All @@ -220,12 +235,12 @@ func (image *dockerImageAnalyzer) processLayerTar(name string, layerIdx uint, re
tree.AddPath(element.Path, element)

if pb.Update(int64(idx)) {
message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
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())
message = fmt.Sprintf(" ├─ %s %s : %s", title, shortName, pb.String())
fmt.Printf("\r%s\n", message)

image.layerMap[tree.Name] = tree
Expand Down
12 changes: 8 additions & 4 deletions image/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ type Layer interface {
}

type AnalysisResult struct {
Layers []Layer
RefTrees []*filetree.FileTree
Efficiency float64
Inefficiencies filetree.EfficiencySlice
Layers []Layer
RefTrees []*filetree.FileTree
Efficiency float64
SizeBytes uint64
UserSizeByes uint64 // this is all bytes except for the base image
WastedUserPercent float64 // = wasted-bytes/user-size-bytes
WastedBytes uint64
Inefficiencies filetree.EfficiencySlice
}

type dockerImageAnalyzer struct {
Expand Down
Loading

0 comments on commit 1fb9b31

Please sign in to comment.