Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fc27dcd

Browse files
authoredDec 30, 2018
Add CI integration (wagoodman#143)
1 parent ad32c0a commit fc27dcd

30 files changed

+892
-204
lines changed
 

‎.data/.dive-ci

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
plugins:
3+
- plugin1
4+
5+
rules:
6+
# If the efficiency is measured below X%, mark as failed. (expressed as a percentage between 0-1)
7+
lowestEfficiency: 0.95
8+
9+
# If the amount of wasted space is at least X or larger than X, mark as failed. (expressed in B, KB, MB, and GB)
10+
highestWastedBytes: 20Mb
11+
12+
# 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)
13+
highestUserWastedPercent: 0.10
14+
15+
plugin1/rule1: error
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM alpine:latest
1+
FROM busybox:latest
22
ADD README.md /somefile.txt
33
RUN mkdir -p /root/example/really/nested
44
RUN cp /somefile.txt /root/example/somefile1.txt
@@ -8,7 +8,7 @@ RUN cp /somefile.txt /root/example/somefile3.txt
88
RUN mv /root/example/somefile3.txt /root/saved.txt
99
RUN cp /root/saved.txt /root/.saved.txt
1010
RUN rm -rf /root/example/
11-
ADD .data/ /root/.data/
11+
ADD .scripts/ /root/.data/
1212
RUN cp /root/saved.txt /tmp/saved.again1.txt
1313
RUN cp /root/saved.txt /root/.data/saved.again2.txt
1414
RUN chmod +x /root/saved.txt

‎.data/Dockerfile.test-image

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM busybox:latest
2+
ADD README.md /somefile.txt
3+
RUN mkdir -p /root/example/really/nested
4+
RUN cp /somefile.txt /root/example/somefile1.txt
5+
RUN chmod 444 /root/example/somefile1.txt
6+
RUN cp /somefile.txt /root/example/somefile2.txt
7+
RUN cp /somefile.txt /root/example/somefile3.txt
8+
RUN mv /root/example/somefile3.txt /root/saved.txt
9+
RUN cp /root/saved.txt /root/.saved.txt
10+
RUN rm -rf /root/example/
11+
ADD .scripts/ /root/.data/
12+
RUN cp /root/saved.txt /tmp/saved.again1.txt
13+
RUN cp /root/saved.txt /root/.data/saved.again2.txt
14+
RUN chmod +x /root/saved.txt

‎.data/demo-ci.png

178 KB
Loading

‎.data/test-docker-image.tar

1.45 MB
Binary file not shown.

‎.dockerignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/.git
2-
/.scripts
2+
/.data
33
/dist
44
/ui
55
/utils

‎.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
/vendor
1919
/.image
2020
*.log
21-
/dist
21+
/dist
22+
.cover

‎.scripts/test.sh

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/sh
2+
# Generate test coverage statistics for Go packages.
3+
#
4+
# Works around the fact that `go test -coverprofile` currently does not work
5+
# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909
6+
#
7+
# Usage: script/coverage [--html|--coveralls]
8+
#
9+
# --html Additionally create HTML report and open it in browser
10+
# --coveralls Push coverage statistics to coveralls.io
11+
#
12+
# Source: https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage
13+
14+
set -e
15+
16+
workdir=.cover
17+
profile="$workdir/cover.out"
18+
mode=count
19+
20+
generate_cover_data() {
21+
rm -rf "$workdir"
22+
mkdir "$workdir"
23+
24+
for pkg in "$@"; do
25+
f="$workdir/$(echo $pkg | tr / -).cover"
26+
go test -v -covermode="$mode" -coverprofile="$f" "$pkg"
27+
done
28+
29+
echo "mode: $mode" >"$profile"
30+
grep -h -v "^mode:" "$workdir"/*.cover >>"$profile"
31+
}
32+
33+
show_cover_report() {
34+
go tool cover -${1}="$profile"
35+
}
36+
37+
push_to_coveralls() {
38+
echo "Pushing coverage statistics to coveralls.io"
39+
goveralls -coverprofile="$profile"
40+
}
41+
42+
generate_cover_data $(go list ./...)
43+
case "$1" in
44+
"")
45+
show_cover_report func
46+
;;
47+
--html)
48+
show_cover_report html
49+
;;
50+
--coveralls)
51+
push_to_coveralls
52+
;;
53+
*)
54+
echo >&2 "error: invalid option: $1"; exit 1 ;;
55+
esac

‎Makefile

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ BIN = dive
33
all: clean build
44

55
run: build
6-
./build/$(BIN) build -t dive-test:latest -f .data/Dockerfile .
6+
./build/$(BIN) build -t dive-example:latest -f .data/Dockerfile.example .
7+
8+
run-ci: build
9+
CI=true ./build/$(BIN) dive-example:latest --ci-config .data/.dive-ci
710

811
run-large: build
912
./build/$(BIN) amir20/clashleaders:latest
@@ -21,16 +24,22 @@ install:
2124
test: build
2225
go test -cover -v ./...
2326

27+
coverage: build
28+
./.scripts/test.sh
29+
2430
validate:
2531
@! gofmt -s -d -l . 2>&1 | grep -vE '^\.git/'
2632
go vet ./...
2733

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

37+
generate-test-data:
38+
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!"
39+
3140
clean:
3241
rm -rf build
3342
rm -rf vendor
3443
go clean
3544

36-
.PHONY: build install test lint clean release validate
45+
.PHONY: build install test lint clean release validate generate-test-data

‎README.md

+31-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ or if you want to build your image then jump straight into analyzing it:
1616
dive build -t <some-tag> .
1717
```
1818

19+
Additionally you can run this in your CI pipeline to ensure you're keeping wasted space to a minimum (this skips the UI):
20+
```
21+
CI=true dive <your-image>
22+
```
23+
24+
![Image](.data/demo-ci.png)
25+
1926
**This is beta quality!** *Feel free to submit an issue if you want a new feature or find a bug :)*
2027

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

57+
**CI Integration**
58+
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.
59+
5060

5161
## Installation
5262

@@ -127,6 +137,26 @@ docker run --rm -it \
127137
wagoodman/dive:latest <dive arguments...>
128138
```
129139

140+
## CI Integration
141+
142+
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:
143+
```
144+
rules:
145+
# If the efficiency is measured below X%, mark as failed.
146+
# Expressed as a percentage between 0-1.
147+
lowestEfficiency: 0.95
148+
149+
# If the amount of wasted space is at least X or larger than X, mark as failed.
150+
# Expressed in B, KB, MB, and GB.
151+
highestWastedBytes: 20MB
152+
153+
# If the amount of wasted space makes up for X% or more of the image, mark as failed.
154+
# Note: the base image layer is NOT included in the total image size.
155+
# Expressed as a percentage between 0-1; fails if the threshold is met or crossed.
156+
highestUserWastedPercent: 0.20
157+
```
158+
You can override the CI config path with the `--ci-config` option.
159+
130160
## KeyBindings
131161

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

147-
## Configuration
177+
## UI Configuration
148178

149179
No configuration is necessary, however, you can create a config file and override values:
150180
```yaml

‎cmd/analyze.go

+6-119
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
package cmd
22

33
import (
4-
"encoding/json"
54
"fmt"
6-
"github.com/fatih/color"
75
"github.com/spf13/cobra"
8-
"github.com/wagoodman/dive/filetree"
9-
"github.com/wagoodman/dive/image"
10-
"github.com/wagoodman/dive/ui"
6+
"github.com/wagoodman/dive/runtime"
117
"github.com/wagoodman/dive/utils"
12-
"io/ioutil"
138
)
149

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

38-
run(userImage)
39-
}
40-
41-
type export struct {
42-
Layer []exportLayer `json:"layer"`
43-
Image exportImage `json:"image"`
44-
}
45-
46-
type exportLayer struct {
47-
Index int `json:"index"`
48-
DigestID string `json:"digestId"`
49-
SizeBytes uint64 `json:"sizeBytes"`
50-
Command string `json:"command"`
51-
}
52-
type exportImage struct {
53-
SizeBytes uint64 `json:"sizeBytes"`
54-
InefficientBytes uint64 `json:"inefficientBytes"`
55-
EfficiencyScore float64 `json:"efficiencyScore"`
56-
InefficientFiles []inefficientFiles `json:"inefficientFiles"`
57-
}
58-
59-
type inefficientFiles struct {
60-
Count int `json:"count"`
61-
SizeBytes uint64 `json:"sizeBytes"`
62-
File string `json:"file"`
63-
}
64-
65-
func newExport(analysis *image.AnalysisResult) *export {
66-
data := export{}
67-
data.Layer = make([]exportLayer, len(analysis.Layers))
68-
data.Image.InefficientFiles = make([]inefficientFiles, len(analysis.Inefficiencies))
69-
70-
// export layers in order
71-
for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- {
72-
layer := analysis.Layers[revIdx]
73-
idx := (len(analysis.Layers) - 1) - revIdx
74-
75-
data.Layer[idx] = exportLayer{
76-
Index: idx,
77-
DigestID: layer.Id(),
78-
SizeBytes: layer.Size(),
79-
Command: layer.Command(),
80-
}
81-
}
82-
83-
// export image info
84-
data.Image.SizeBytes = 0
85-
for idx := 0; idx < len(analysis.Layers); idx++ {
86-
data.Image.SizeBytes += analysis.Layers[idx].Size()
87-
}
88-
89-
data.Image.EfficiencyScore = analysis.Efficiency
90-
91-
for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
92-
fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
93-
data.Image.InefficientBytes += uint64(fileData.CumulativeSize)
94-
95-
data.Image.InefficientFiles[idx] = inefficientFiles{
96-
Count: len(fileData.Nodes),
97-
SizeBytes: uint64(fileData.CumulativeSize),
98-
File: fileData.Path,
99-
}
100-
}
101-
102-
return &data
103-
}
104-
105-
func exportStatistics(analysis *image.AnalysisResult) {
106-
data := newExport(analysis)
107-
payload, err := json.MarshalIndent(&data, "", " ")
108-
if err != nil {
109-
panic(err)
110-
}
111-
err = ioutil.WriteFile(exportFile, payload, 0644)
112-
if err != nil {
113-
panic(err)
114-
}
115-
}
116-
117-
func fetchAndAnalyze(imageID string) *image.AnalysisResult {
118-
analyzer := image.GetAnalyzer(imageID)
119-
120-
fmt.Println(" Fetching image...")
121-
err := analyzer.Parse(imageID)
122-
if err != nil {
123-
fmt.Printf("cannot fetch image: %v\n", err)
124-
utils.Exit(1)
125-
}
126-
127-
fmt.Println(" Analyzing image...")
128-
result, err := analyzer.Analyze()
129-
if err != nil {
130-
fmt.Printf("cannot doAnalyzeCmd image: %v\n", err)
131-
utils.Exit(1)
132-
}
133-
return result
134-
}
135-
136-
func run(imageID string) {
137-
color.New(color.Bold).Println("Analyzing Image")
138-
result := fetchAndAnalyze(imageID)
139-
140-
if exportFile != "" {
141-
exportStatistics(result)
142-
color.New(color.Bold).Println(fmt.Sprintf("Exported to %s", exportFile))
143-
utils.Exit(0)
144-
}
145-
146-
fmt.Println(" Building cache...")
147-
cache := filetree.NewFileTreeCache(result.RefTrees)
148-
cache.Build()
149-
150-
ui.Run(result, cache)
33+
runtime.Run(runtime.Options{
34+
ImageId: userImage,
35+
ExportFile: exportFile,
36+
CiConfigFile: ciConfigFile,
37+
})
15138
}

‎cmd/build.go

+5-23
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package cmd
22

33
import (
4-
log "github.com/sirupsen/logrus"
54
"github.com/spf13/cobra"
5+
"github.com/wagoodman/dive/runtime"
66
"github.com/wagoodman/dive/utils"
7-
"io/ioutil"
8-
"os"
97
)
108

119
// buildCmd represents the build command
@@ -23,25 +21,9 @@ func init() {
2321
// doBuildCmd implements the steps taken for the build command
2422
func doBuildCmd(cmd *cobra.Command, args []string) {
2523
defer utils.Cleanup()
26-
iidfile, err := ioutil.TempFile("/tmp", "dive.*.iid")
27-
if err != nil {
28-
utils.Cleanup()
29-
log.Fatal(err)
30-
}
31-
defer os.Remove(iidfile.Name())
3224

33-
allArgs := append([]string{"--iidfile", iidfile.Name()}, args...)
34-
err = utils.RunDockerCmd("build", allArgs...)
35-
if err != nil {
36-
utils.Cleanup()
37-
log.Fatal(err)
38-
}
39-
40-
imageId, err := ioutil.ReadFile(iidfile.Name())
41-
if err != nil {
42-
utils.Cleanup()
43-
log.Fatal(err)
44-
}
45-
46-
run(string(imageId))
25+
runtime.Run(runtime.Options{
26+
BuildArgs: args,
27+
ExportFile: exportFile,
28+
})
4729
}
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Please sign in to comment.