Skip to content

Commit

Permalink
with export and CI tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wagoodman committed Dec 28, 2018
1 parent 6e7bdf8 commit 1bf3be1
Show file tree
Hide file tree
Showing 22 changed files with 455 additions and 102 deletions.
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) build -t dive-example:latest -f .data/Dockerfile.example .

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
30 changes: 30 additions & 0 deletions 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 TUI):
```
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 Down
2 changes: 1 addition & 1 deletion filetree/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

const (
AttributeFormat = "%s%s %10s %10s "
AttributeFormat = "%s%s %11s %10s "
)

var diffTypeColor = map[DiffType]*color.Color{
Expand Down
2 changes: 1 addition & 1 deletion filetree/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func TestDirSize(t *testing.T) {
tree1.AddPath("/etc/nginx/public3/thing2", FileInfo{Size: 300})

node, _ := tree1.GetNode("/etc/nginx")
expected, actual := "---------- 0:0 600 B ", node.MetadataString()
expected, actual := "---------- 0:0 600 B ", node.MetadataString()
if expected != actual {
t.Errorf("Expected metadata '%s' got '%s'", expected, actual)
}
Expand Down
53 changes: 16 additions & 37 deletions image/docker_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ import (

var dockerVersion string

func newDockerImageAnalyzer() Analyzer {
return &dockerImageAnalyzer{}
func newDockerImageAnalyzer(imageId string) Analyzer {
return &dockerImageAnalyzer{
// store discovered json files in a map so we can read the image in one pass
jsonFiles: make(map[string][]byte),
layerMap: make(map[string]*filetree.FileTree),
id: imageId,
}
}

func newDockerImageManifest(manifestBytes []byte) dockerImageManifest {
Expand Down Expand Up @@ -49,40 +54,31 @@ func newDockerImageConfig(configBytes []byte) dockerImageConfig {
return imageConfig
}

func (image *dockerImageAnalyzer) Parse(imageID string) error {
func (image *dockerImageAnalyzer) Fetch() (io.ReadCloser, 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
return nil, err
}
_, _, err = image.client.ImageInspectWithRaw(ctx, imageID)
_, _, err = image.client.ImageInspectWithRaw(ctx, image.id)
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)
fmt.Println("Image not available locally. Trying to pull '" + image.id + "'...")
utils.RunDockerCmd("pull", image.id)
}

tarFile, _, err := image.getReader(imageID)
readCloser, err := image.client.ImageSave(ctx, []string{image.id})
if err != nil {
return err
return nil, err
}
defer tarFile.Close()

err = image.read(tarFile)
if err != nil {
return err
}
return nil
return readCloser, nil
}

func (image *dockerImageAnalyzer) read(tarFile io.ReadCloser) error {
func (image *dockerImageAnalyzer) Parse(tarFile io.ReadCloser) error {
tarReader := tar.NewReader(tarFile)

var currentLayer uint
Expand Down Expand Up @@ -195,23 +191,6 @@ func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
}, 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()
Expand Down
4 changes: 2 additions & 2 deletions image/root.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package image

type AnalyzerFactory func() Analyzer
type AnalyzerFactory func(string) 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()
return factory(imageID)
}
19 changes: 19 additions & 0 deletions image/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package image

import (
"os"
)

func TestLoadDockerImageTar(tarPath string) (*AnalysisResult, error) {
f, err := os.Open(tarPath)
if err != nil {
return nil, err
}
defer f.Close()
analyzer := newDockerImageAnalyzer("dive-test:latest")
err = analyzer.Parse(f)
if err != nil {
return nil, err
}
return analyzer.Analyze()
}
4 changes: 3 additions & 1 deletion image/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package image
import (
"github.com/docker/docker/client"
"github.com/wagoodman/dive/filetree"
"io"
)

type Parser interface {
}

type Analyzer interface {
Parse(id string) error
Fetch() (io.ReadCloser, error)
Parse(io.ReadCloser) error
Analyze() (*AnalysisResult, error)
}

Expand Down
Loading

0 comments on commit 1bf3be1

Please sign in to comment.