Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CI integration #143

Merged
merged 4 commits into from
Dec 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions .data/Dockerfile → .data/Dockerfile.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:latest
FROM busybox:latest
ADD README.md /somefile.txt
RUN mkdir -p /root/example/really/nested
RUN cp /somefile.txt /root/example/somefile1.txt
Expand All @@ -8,7 +8,7 @@ RUN cp /somefile.txt /root/example/somefile3.txt
RUN mv /root/example/somefile3.txt /root/saved.txt
RUN cp /root/saved.txt /root/.saved.txt
RUN rm -rf /root/example/
ADD .data/ /root/.data/
ADD .scripts/ /root/.data/
RUN cp /root/saved.txt /tmp/saved.again1.txt
RUN cp /root/saved.txt /root/.data/saved.again2.txt
RUN chmod +x /root/saved.txt
14 changes: 14 additions & 0 deletions .data/Dockerfile.test-image
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM busybox:latest
ADD README.md /somefile.txt
RUN mkdir -p /root/example/really/nested
RUN cp /somefile.txt /root/example/somefile1.txt
RUN chmod 444 /root/example/somefile1.txt
RUN cp /somefile.txt /root/example/somefile2.txt
RUN cp /somefile.txt /root/example/somefile3.txt
RUN mv /root/example/somefile3.txt /root/saved.txt
RUN cp /root/saved.txt /root/.saved.txt
RUN rm -rf /root/example/
ADD .scripts/ /root/.data/
RUN cp /root/saved.txt /tmp/saved.again1.txt
RUN cp /root/saved.txt /root/.data/saved.again2.txt
RUN chmod +x /root/saved.txt
Binary file added .data/demo-ci.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .data/test-docker-image.tar
Binary file not shown.
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/.git
/.scripts
/.data
/dist
/ui
/utils
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
/vendor
/.image
*.log
/dist
/dist
.cover
55 changes: 55 additions & 0 deletions .scripts/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/sh
# Generate test coverage statistics for Go packages.
#
# Works around the fact that `go test -coverprofile` currently does not work
# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909
#
# Usage: script/coverage [--html|--coveralls]
#
# --html Additionally create HTML report and open it in browser
# --coveralls Push coverage statistics to coveralls.io
#
# Source: https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage

set -e

workdir=.cover
profile="$workdir/cover.out"
mode=count

generate_cover_data() {
rm -rf "$workdir"
mkdir "$workdir"

for pkg in "$@"; do
f="$workdir/$(echo $pkg | tr / -).cover"
go test -v -covermode="$mode" -coverprofile="$f" "$pkg"
done

echo "mode: $mode" >"$profile"
grep -h -v "^mode:" "$workdir"/*.cover >>"$profile"
}

show_cover_report() {
go tool cover -${1}="$profile"
}

push_to_coveralls() {
echo "Pushing coverage statistics to coveralls.io"
goveralls -coverprofile="$profile"
}

generate_cover_data $(go list ./...)
case "$1" in
"")
show_cover_report func
;;
--html)
show_cover_report html
;;
--coveralls)
push_to_coveralls
;;
*)
echo >&2 "error: invalid option: $1"; exit 1 ;;
esac
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ BIN = dive
all: clean build

run: build
./build/$(BIN) build -t dive-test:latest -f .data/Dockerfile .
./build/$(BIN) build -t dive-example:latest -f .data/Dockerfile.example .

run-ci: build
CI=true ./build/$(BIN) dive-example:latest --ci-config .data/.dive-ci

run-large: build
./build/$(BIN) amir20/clashleaders:latest
Expand All @@ -21,16 +24,22 @@ install:
test: build
go test -cover -v ./...

coverage: build
./.scripts/test.sh

validate:
@! gofmt -s -d -l . 2>&1 | grep -vE '^\.git/'
go vet ./...

lint: build
golint -set_exit_status $$(go list ./...)

generate-test-data:
docker build -t dive-test:latest -f .data/Dockerfile.test-image . && docker image save -o .data/test-docker-image.tar dive-test:latest && echo "Exported test data!"

clean:
rm -rf build
rm -rf vendor
go clean

.PHONY: build install test lint clean release validate
.PHONY: build install test lint clean release validate generate-test-data
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ or if you want to build your image then jump straight into analyzing it:
dive build -t <some-tag> .
```

Additionally you can run this in your CI pipeline to ensure you're keeping wasted space to a minimum (this skips the UI):
```
CI=true dive <your-image>
```

![Image](.data/demo-ci.png)

**This is beta quality!** *Feel free to submit an issue if you want a new feature or find a bug :)*

## Basic Features
Expand Down Expand Up @@ -47,6 +54,9 @@ You can build a Docker image and do an immediate analysis with one command:
You only need to replace your `docker build` command with the same `dive build`
command.

**CI Integration**
Analyze and image and get a pass/fail result based on the image efficiency and wasted space. Simply set `CI=true` in the environment when invoking any valid dive command.


## Installation

Expand Down Expand Up @@ -127,6 +137,26 @@ docker run --rm -it \
wagoodman/dive:latest <dive arguments...>
```

## CI Integration

When running dive with the environment variable `CI=true` then the dive UI will be bypassed and will instead analyze your docker image, giving it a pass/fail indication via return code. Currently there are three metrics supported via a `.dive-ci` file that you can put at the root of your repo:
```
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% or more of the image, mark as failed.
# Note: the base image layer is NOT included in the total image size.
# Expressed as a percentage between 0-1; fails if the threshold is met or crossed.
highestUserWastedPercent: 0.20
```
You can override the CI config path with the `--ci-config` option.

## KeyBindings

Key Binding | Description
Expand All @@ -144,7 +174,7 @@ Key Binding | Description
<kbd>PageUp</kbd> | Filetree view: scroll up a page
<kbd>PageDown</kbd> | Filetree view: scroll down a page

## Configuration
## UI Configuration

No configuration is necessary, however, you can create a config file and override values:
```yaml
Expand Down
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,
})
}
Loading