Compare commits

...

75 commits

Author SHA1 Message Date
Nate Jones
b87a4d5637 Adding release script 2022-10-01 18:29:48 -07:00
Nate Jones
3ebdb75ed3 Bump version to v0.6.4 2022-10-01 18:06:26 -07:00
Nate Jones
f925e91395 Switch to using go modules 2022-10-01 18:06:19 -07:00
Nate Jones
15f77275c4 version bump to v0.6.3 2018-04-19 21:13:18 -07:00
Nate Jones
28bcf8034b fix/add vendor json file 2018-04-19 21:12:27 -07:00
Nate Jones
4d5c52e735 version bump to v0.6.2 2018-04-19 20:57:54 -07:00
Nate Jones
2af445f27a vendor deps with govendor 2018-04-19 20:57:06 -07:00
Nate Jones
7b8aa57592 add canonical import path, for docker image building
with centurylink/golang-builder
2018-04-19 19:28:44 -07:00
Nate Jones
b4f269312f bump version to v0.6.1 2018-04-19 08:19:47 -07:00
Nate Jones
f765f7d6cd Merge branch 'master' into treemap 2018-04-16 17:23:49 -07:00
Nate Jones
f63f9dbe37 Merge branch 'fabianlee' 2018-04-16 17:17:27 -07:00
Nate Jones
d30fd054ad Merge branch 'master' into fabianlee 2018-04-16 17:17:13 -07:00
Nate Jones
b84946b83e add note about showing image createdby, small formatting 2018-04-16 17:15:31 -07:00
Nate Jones
85dd6df8b2 conditionalize createdby display, make display options consistent 2018-04-16 17:11:16 -07:00
Nate Jones
4c4b3c154a go fmt 2018-04-16 17:08:57 -07:00
Nate Jones
51c099c18c Merge branch 'master' into fabianlee 2018-04-16 16:33:29 -07:00
Nate Jones
c932e4c843 Merge pull request #36 from nsarafa/graphviz-install-readme-instructions
Graphviz apt-get, and brew installation instructions init
2017-10-12 20:31:52 -07:00
Anders F Björklund
f20218e43a Add area support for dot, for generating treemaps
There is another Graphviz command called "patchwork",
that can generate treemaps when given a digraph .dot.

The area is scaled to megabytes, which seems to work.
It shouldn't have any ill effects, for regular "dot".
2017-10-12 07:41:19 +02:00
fabianlee
40e1c4e1ce unnecessary comment about param being short d or full dot 2017-05-29 19:17:19 +00:00
nsarafa
651855127c Graphviz apt-get, and brew installation instructions init 2017-05-27 19:12:04 -04:00
fabianlee
edcada1c88 test 2017-05-25 10:52:40 +00:00
fabianlee
1ce624cd37 using names for merge 2017-05-25 10:51:50 +00:00
fabianlee
9e07d53e05 build and push instructions added 2017-05-23 23:52:57 +00:00
fabianlee
c75e40ffd8 more prominent example of using tcp docker client 2017-05-23 23:26:43 +00:00
fabianlee
76415ba88f docs 2017-05-23 23:20:42 +00:00
fabianlee
2df85b6786 document switch for only running containers and package name of graphviz on debian 2017-05-23 23:19:14 +00:00
fabianlee
1f8a6a6f0c updated documentation to force statically linked build, example for remote docker client sending DOCKER_HOST 2017-05-23 23:16:24 +00:00
fabianlee
25acff1b44 switch for only running containers 2017-05-22 16:14:10 +00:00
fabianlee
360c954944 comments 2017-05-22 15:48:18 +00:00
fabianlee
cf340cd457 containers: extra labels for name, removing links for multiply named containers 2017-05-22 15:44:33 +00:00
fabianlee
18ad950cd1 added truncated command and size to make up for idea that since Docker 1.10 content addressability there is usually <missing> id 2017-05-22 01:55:01 +00:00
fabianlee
ff917a13b1 removed canonical import comment which allows forking 2017-05-22 01:50:25 +00:00
Nate Jones
ee1ab834db version bump to v0.5.0 2016-12-08 12:25:17 -08:00
Nate Jones
94f2cc64c9 Merge branch 'stdin_to_flag' 2016-12-08 12:23:54 -08:00
Nate Jones
bdb64a088c don't try stdin mode unless a flag is passed 2016-12-08 12:18:12 -08:00
Nate Jones
9863e97953 version bump to v0.4.2 2016-06-14 12:21:53 -07:00
Nate Jones
16075245dd fix embedded docker run instructions, add selinux help
closes #17
2016-06-13 20:48:21 -07:00
Nate Jones
5a116b8dff clarify applicability of --no-trunc, closes #28 2016-06-13 20:31:16 -07:00
Nate Jones
2e9e1972e7 strip prefix, match docker's view 2016-06-13 20:28:13 -07:00
Nate Jones
412fb7db34 adding twitter user 2016-06-10 21:30:59 -07:00
Nate Jones
8a7f885238 version bump to v0.4.1 2016-06-10 21:16:40 -07:00
Nate Jones
e619ee311a fix piped image info missing image id 2016-06-10 21:14:36 -07:00
Nate Jones
ed784c7a95 added non-human numeric output, closes #25 2016-06-10 21:13:47 -07:00
Nate Jones
4bd432cec3 align image labels with tree view, closes #24 2016-06-10 20:42:45 -07:00
Nate Jones
f898de1f61 Merge remote-tracking branch 'remram44/readme' 2016-06-10 20:17:49 -07:00
Remi Rampin
cd77167841 Don't fail if server returns CRLF 2016-05-25 16:53:12 -04:00
Remi Rampin
ad9654ce57 Add example using Docker image without volume 2016-05-25 12:18:35 -04:00
Remi Rampin
861a7e02b6 Replace tail -n +5 with appropriate sed
`sed '1,/^$/d'` skips everything until the first empty line.
2016-05-25 12:14:01 -04:00
Nate Jones
551ef0434f version bump to v0.4 2016-05-05 11:10:30 -07:00
Nate Jones
1e678b70e3 change size label depending on size type 2016-05-05 11:10:25 -07:00
Nate Jones
7ea47fc9cf Merge branch 'support_new_image_model' 2016-05-05 11:10:07 -07:00
Nate Jones
14181721e5 Add support for new Docker image model
This uses image history to synthesize the missing images
2016-05-05 11:08:51 -07:00
Nate Jones
783915aa09 add '-it' to docker run command
* forces docker to properly make stdin work
* required for 1.11
2016-04-19 06:43:52 -07:00
Nate Jones
41f9b67538 Merge remote-tracking branch 'deizel/fix-only-labelled-flag' 2016-02-28 16:50:25 -08:00
Chris Burke
9bcca08f2b Fix --only-labelled flag in readme 2016-02-28 06:31:04 -06:00
Nate Jones
153b934646 Merge branch 'dock-r/improve/labelled' 2016-02-19 07:31:13 -08:00
Davide Fiorentino
6c094efa09 update labeled to labelled
for consinstency with code.
2015-12-18 16:49:14 +00:00
Nate Jones
ae855d6a5a bump version to v0.3 2015-11-08 11:53:15 -08:00
Nate Jones
e936558a78 option to show incremental size diff between images
closes #14
2015-11-08 11:33:00 -08:00
Nate Jones
80970dcee2 make calls consistent, move options to end 2015-11-08 11:01:08 -08:00
Nate Jones
20d1b3797a Merge branch 'only_labelled' 2015-11-07 20:02:44 -08:00
Nate Jones
f8eb6a8bc7 small refactoring, tests work again 2015-11-07 20:02:21 -08:00
Nate Jones
f8106ccec6 go fmt 2015-11-07 18:32:49 -08:00
Nate Jones
b86486fdbc Merge branch 'pr/13' into only_labelled 2015-11-07 18:31:37 -08:00
gkovacs81
beb1db17a3 Merge branch 'master' of https://github.com/gkovacs81/dockviz.git 2015-10-13 18:48:37 +02:00
gkovacs81
676c01bda2 Fix text 2015-10-13 18:46:25 +02:00
gkovacs81
dc21ad12a3 Fix example 2015-10-13 18:31:08 +02:00
gkovacs81
19368a42d8 Update documentation 2015-10-13 18:24:26 +02:00
gkovacs81
b95175dae8 Start image for --dot 2015-10-13 17:57:26 +02:00
gkovacs81
4da0559824 Remove parameter 2015-10-12 22:07:54 +02:00
gkovacs81
0dcdc66e63 Remove debug line 2015-10-12 22:02:29 +02:00
gkovacs81
5f3ba9a693 --only-labeled for dot 2015-10-12 21:58:21 +02:00
gkovacs81
6eeb877c5e Fix print tree by startimage
Print tree nodes if only-labeled
2015-10-11 18:29:48 +02:00
gkovacs81
c25c0eb801 Improve WalkTree 2015-10-11 12:52:26 +02:00
gkovacs81
071f77cbbc Print only labeled images with --only-labeled option 2015-10-09 22:31:17 +02:00
15 changed files with 1927 additions and 204 deletions

222
README.md
View file

@ -1,3 +1,5 @@
Follow [@dockviz](https://twitter.com/dockviz) for updates.
# dockviz: Visualizing Docker Data
This command takes Docker image and container information and presents in
@ -7,23 +9,43 @@ different ways, to help you understand what's going on inside the system.
1. Install dockviz. Either:
* Download the [latest release](https://github.com/justone/dockviz/releases).
* Set up an alias to run it from the (5.8 MB) docker image:
* Set up an alias to run it from the (5.8 MB) docker image:
```
# if docker client using local unix socket
alias dockviz="docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz"
# if docker client using tcp
alias dockviz="docker run -it --rm -e DOCKER_HOST='tcp://127.0.0.1:2375' nate/dockviz"
```
```
alias dockviz="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz"
```
2. Visualize images by running `dockviz images -t`, which has similar output to `docker images -t`.
* Image can be visualized as [Graphviz](http://www.graphviz.org), or as a tree or short summary in the terminal. Only Graphviz output has been implemented for containers.
* If you would like to visualize outside the container you will have to install [Graphviz](http://www.graphviz.org) first.
Image can be visualized as [Graphviz](http://www.graphviz.org), or as a tree or short summary in the terminal. Only Graphviz output has been implemented for containers.
```
apt-get update && apt-get install graphviz
```
or
```
brew update && brew install graphviz
```
# Output Examples
## Containers
Currently, containers are visualized with labeled lines for links. Containers that aren't running are greyed out.
Currently, containers are visualized with labelled lines for links. Containers that aren't running are greyed out.
```
# show all containers
$ dockviz containers -d | dot -Tpng -o containers.png
# only show running containers
$ dockviz containers -d -r | dot -Tpng -o containers.png
```
![](sample/containers.png "Container")
@ -38,6 +60,24 @@ $ dockviz images -d | dot -Tpng -o images.png
![](sample/images.png "Image")
```
$ dockviz images -d -l | dot -Tpng -o images.png
OR
$ dockviz images --dot --only-labelled | dot -Tpng -o images.png
```
![](sample/images_only_labelled.png "Image")
Or as a treemap:
```
$ dockviz images -d | patchwork -Tpng -o treemap.png
OR
$ dockviz images --dot | patchwork -Tpng -o treemap.png
```
![](sample/treemap.png "Image")
Or in short form:
```
@ -52,50 +92,141 @@ Or as a tree in the terminal:
```
$ dockviz images -t
└─511136ea3c5a Virtual Size: 0.0 B
|─f10ebce2c0e1 Virtual Size: 103.7 MB
| └─82cdea7ab5b5 Virtual Size: 103.9 MB
| └─5dbd9cb5a02f Virtual Size: 103.9 MB
| └─74fe38d11401 Virtual Size: 209.6 MB Tags: ubuntu:12.04, ubuntu:precise
|─ef519c9ee91a Virtual Size: 100.9 MB
| └─07302703becc Virtual Size: 101.2 MB
| └─cf8dc907452c Virtual Size: 101.2 MB
| └─a7cf8ae4e998 Virtual Size: 171.3 MB Tags: ubuntu:12.10, ubuntu:quantal
| |─e18d8001204e Virtual Size: 171.3 MB
| | └─d0525208a46c Virtual Size: 171.3 MB
| | └─59dac4bae93b Virtual Size: 242.5 MB
| | └─89541b3b35f2 Virtual Size: 511.8 MB
| | └─7dac4e98548e Virtual Size: 511.8 MB
| | └─341d0cc3fac8 Virtual Size: 511.8 MB
| | └─2f96171d2098 Virtual Size: 511.8 MB
| | └─67b8b7262a67 Virtual Size: 513.7 MB
| | └─0fe9a2bc50fe Virtual Size: 513.7 MB
| | └─8c32832f07ba Virtual Size: 513.7 MB
| | └─cc4e1358bc80 Virtual Size: 513.7 MB
| | └─5c0d04fba9df Virtual Size: 513.7 MB Tags: nate/mongodb:latest
| └─398d592f2009 Virtual Size: 242.2 MB
| └─0cd8e7f50270 Virtual Size: 243.6 MB
| └─594b6f8e6f92 Virtual Size: 243.6 MB
| └─f832a63e87a4 Virtual Size: 243.6 MB Tags: redis:latest
─f10ebce2c0e1 Virtual Size: 103.7 MB
└─82cdea7ab5b5 Virtual Size: 103.9 MB
└─5dbd9cb5a02f Virtual Size: 103.9 MB
└─74fe38d11401 Virtual Size: 209.6 MB Tags: ubuntu:12.04, ubuntu:precise
─ef519c9ee91a Virtual Size: 100.9 MB
└─07302703becc Virtual Size: 101.2 MB
└─cf8dc907452c Virtual Size: 101.2 MB
└─a7cf8ae4e998 Virtual Size: 171.3 MB Tags: ubuntu:12.10, ubuntu:quantal
│ │─e18d8001204e Virtual Size: 171.3 MB
│ │ └─d0525208a46c Virtual Size: 171.3 MB
│ │ └─59dac4bae93b Virtual Size: 242.5 MB
│ │ └─89541b3b35f2 Virtual Size: 511.8 MB
│ │ └─7dac4e98548e Virtual Size: 511.8 MB
│ │ └─341d0cc3fac8 Virtual Size: 511.8 MB
│ │ └─2f96171d2098 Virtual Size: 511.8 MB
│ │ └─67b8b7262a67 Virtual Size: 513.7 MB
│ │ └─0fe9a2bc50fe Virtual Size: 513.7 MB
│ │ └─8c32832f07ba Virtual Size: 513.7 MB
│ │ └─cc4e1358bc80 Virtual Size: 513.7 MB
│ │ └─5c0d04fba9df Virtual Size: 513.7 MB Tags: nate/mongodb:latest
└─398d592f2009 Virtual Size: 242.2 MB
└─0cd8e7f50270 Virtual Size: 243.6 MB
└─594b6f8e6f92 Virtual Size: 243.6 MB
└─f832a63e87a4 Virtual Size: 243.6 MB Tags: redis:latest
└─02dae1c13f51 Virtual Size: 98.3 MB
└─e7206bfc66aa Virtual Size: 98.5 MB
└─cb12405ee8fa Virtual Size: 98.5 MB
└─316b678ddf48 Virtual Size: 169.4 MB Tags: ubuntu:13.04, ubuntu:raring
```
Only showing labelled images:
```
$ dockviz images -t -l
└─511136ea3c5a Virtual Size: 0.0 B
├─f10ebce2c0e1 Virtual Size: 103.7 MB
│ └─74fe38d11401 Virtual Size: 209.6 MB Tags: ubuntu:12.04, ubuntu:precise
├─ef519c9ee91a Virtual Size: 100.9 MB
│ └─a7cf8ae4e998 Virtual Size: 171.3 MB Tags: ubuntu:12.10, ubuntu:quantal
│ ├─5c0d04fba9df Virtual Size: 513.7 MB Tags: nate/mongodb:latest
│ └─f832a63e87a4 Virtual Size: 243.6 MB Tags: redis:latest
└─02dae1c13f51 Virtual Size: 98.3 MB
└─316b678ddf48 Virtual Size: 169.4 MB Tags: ubuntu:13.04, ubuntu:raring
```
Showing incremental size rather than cumulative:
```
$ dockviz images -t -i
└─511136ea3c5a Virtual Size: 0.0 B
├─f10ebce2c0e1 Virtual Size: 103.7 MB
│ └─82cdea7ab5b5 Virtual Size: 255.5 KB
│ └─5dbd9cb5a02f Virtual Size: 1.9 KB
│ └─74fe38d11401 Virtual Size: 105.7 MB Tags: ubuntu:12.04, ubuntu:precise
├─ef519c9ee91a Virtual Size: 100.9 MB
│ └─07302703becc Virtual Size: 251.0 KB
│ └─cf8dc907452c Virtual Size: 1.9 KB
│ └─a7cf8ae4e998 Virtual Size: 70.1 MB Tags: ubuntu:12.10, ubuntu:quantal
│ ├─e18d8001204e Virtual Size: 29.4 KB
│ │ └─d0525208a46c Virtual Size: 71.0 B
│ │ └─59dac4bae93b Virtual Size: 71.2 MB
│ │ └─89541b3b35f2 Virtual Size: 269.3 MB
│ │ └─7dac4e98548e Virtual Size: 0.0 B
│ │ └─341d0cc3fac8 Virtual Size: 0.0 B
│ │ └─2f96171d2098 Virtual Size: 0.0 B
│ │ └─67b8b7262a67 Virtual Size: 1.9 MB
│ │ └─0fe9a2bc50fe Virtual Size: 656.0 B
│ │ └─8c32832f07ba Virtual Size: 383.0 B
│ │ └─cc4e1358bc80 Virtual Size: 0.0 B
│ │ └─5c0d04fba9df Virtual Size: 0.0 B Tags: nate/mongodb:latest
│ └─398d592f2009 Virtual Size: 70.9 MB
│ └─0cd8e7f50270 Virtual Size: 1.4 MB
│ └─594b6f8e6f92 Virtual Size: 0.0 B
│ └─f832a63e87a4 Virtual Size: 0.0 B Tags: redis:latest
└─02dae1c13f51 Virtual Size: 98.3 MB
└─e7206bfc66aa Virtual Size: 190.0 KB
└─cb12405ee8fa Virtual Size: 1.9 KB
└─316b678ddf48 Virtual Size: 70.8 MB Tags: ubuntu:13.04, ubuntu:raring
```
Showing non-human numbers, for precision:
```
$ dockviz images -t -i -c
└─511136ea3c5a Size: 0
├─f10ebce2c0e1 Size: 103675325
│ └─82cdea7ab5b5 Size: 255455
│ └─5dbd9cb5a02f Size: 1919
│ └─74fe38d11401 Size: 105667193 Tags: ubuntu:12.04, ubuntu:precise
├─ef519c9ee91a Size: 100930679
│ └─07302703becc Size: 250998
│ └─cf8dc907452c Size: 1919
│ └─a7cf8ae4e998 Size: 70094270 Tags: ubuntu:12.10, ubuntu:quantal
│ ├─e18d8001204e Size: 29352
│ │ └─d0525208a46c Size: 71
│ │ └─59dac4bae93b Size: 71150930
│ │ └─89541b3b35f2 Size: 269335966
│ │ └─7dac4e98548e Size: 0
│ │ └─341d0cc3fac8 Size: 0
│ │ └─2f96171d2098 Size: 0
│ │ └─67b8b7262a67 Size: 1866379
│ │ └─0fe9a2bc50fe Size: 656
│ │ └─8c32832f07ba Size: 383
│ │ └─cc4e1358bc80 Size: 0
│ │ └─5c0d04fba9df Size: 0 Tags: nate/mongodb:latest
│ └─398d592f2009 Size: 70917743
│ └─0cd8e7f50270 Size: 1418392
│ └─594b6f8e6f92 Size: 0
│ └─f832a63e87a4 Size: 0 Tags: redis:latest
└─02dae1c13f51 Size: 98348330
└─e7206bfc66aa Size: 190007
└─cb12405ee8fa Size: 1903
└─316b678ddf48 Size: 70822908 Tags: ubuntu:13.04, ubuntu:raring
```
It is also possible to show the image's CreatedBy field, for help identifying
image layers when they show up with "<missing>" image Ids.
# Running
Dockviz supports connecting to the Docker daemon directly. It defaults to `unix:///var/run/docker.sock`, but respects the following as well:
* The `DOCKER_HOST`, `DOCKER_CERT_PATH`, and `DOCKER_TLS_VERIFY` environment variables, as set up by [boot2docker](http://boot2docker.io/) or [docker-machine](https://docs.docker.com/machine/).
* Command line arguments (e.g. `--tlscacert`), like those that Docker itself supports.
Dockviz also supports receiving Docker image or container json data on standard input.
```
$ curl -s http://localhost:4243/images/json?all=1 | dockviz images --tree
$ curl -s http://localhost:4243/containers/json?all=1 | dockviz containers --dot | dot -Tpng -o containers.png
$ echo -e "GET /images/json?all=1 HTTP/1.0\r\n" | nc -U /var/run/docker.sock | tail -n +5 | dockviz images --tree
$ echo -e "GET /containers/json?all=1 HTTP/1.0\r\n" | nc -U /var/run/docker.sock | tail -n +5 | dockviz containers --dot | dot -Tpng -o containers.png
$ curl -s http://localhost:4243/images/json?all=1 | dockviz --stdin images --tree
$ curl -s http://localhost:4243/containers/json?all=1 | dockviz --stdin containers --dot | dot -Tpng -o containers.png
$ echo -e "GET /images/json?all=1 HTTP/1.0\r\n" | nc -U /var/run/docker.sock | sed '1,/^[[:space:]]*$/d' | dockviz --stdin images --tree
$ echo -e "GET /containers/json?all=1 HTTP/1.0\r\n" | nc -U /var/run/docker.sock | sed '1,/^[[:space:]]*$/d' | dockviz --stdin containers --dot | dot -Tpng -o containers.png
$ echo -e "GET /images/json?all=1 HTTP/1.0\r\n" | nc -U /var/run/docker.sock | sed '1,/^[[:space:]]*$/d' | docker run -i nate/dockviz --stdin images --tree
$ echo -e "GET /containers/json?all=1 HTTP/1.0\r\n" | nc -U /var/run/docker.sock | sed '1,/^[[:space:]]*$/d' | docker run -i nate/dockviz --stdin containers --dot | dot -Tpng -o containers.png
```
Note: GNU netcat doesn't support `-U` (UNIX socket) flag, so OpenBSD variant can be used.
@ -107,6 +238,27 @@ See the [releases](https://github.com/justone/dockviz/releases) area for binarie
# Build
```bash
go get ./...
# install graphviz in host environment for rendering (Debian example)
sudo apt-get install git graphviz -y
# pull latest code
mkdir -p $GOPATH/src/github.com/nate/ && cd $GOPATH/src/github.com/nate/
git clone https://github.com/nate/dockviz.git && cd dockviz
# force static compilation for go language
export CGO_ENABLED=0
# fetch vendored dependencies
govendor sync
# build
go build
# build docker image
docker build --no-cache=true -t "nate/dockviz:1.0" -t "nate/dockviz:latest" .
# push to docker hub
docker login --username=mygituser --password=xxxxxxx
docker push nate/dockviz
```

3
cli.go
View file

@ -14,12 +14,13 @@ type GlobalOptions struct {
TLSVerify bool `long:"tlsverify" description:"Use TLS and verify the remote"`
Host string `long:"host" short:"H" value-name:"unix:///var/run/docker.sock" description:"Docker host to connect to"`
Version func() `long:"version" short:"v" description:"Display version information."`
Stdin bool `long:"stdin" description:"Enable reading image information from stdin (pre-Docker-1.11 only)"`
}
var globalOptions GlobalOptions
var parser = flags.NewParser(&globalOptions, flags.Default)
var version = "v0.2.2"
var version = "v0.6.4"
func main() {
globalOptions.Version = func() {

View file

@ -24,6 +24,7 @@ type Container struct {
type ContainersCommand struct {
Dot bool `short:"d" long:"dot" description:"Show container information as Graphviz dot."`
NoTruncate bool `short:"n" long:"no-trunc" description:"Don't truncate the container IDs."`
OnlyRunning bool `short:"r" long:"running" description:"Only show running containers, not Exited"`
}
var containersCommand ContainersCommand
@ -37,7 +38,7 @@ func (x *ContainersCommand) Execute(args []string) error {
return fmt.Errorf("error reading stdin stat", err)
}
if (stat.Mode() & os.ModeCharDevice) == 0 {
if globalOptions.Stdin && (stat.Mode()&os.ModeCharDevice) == 0 {
// read in stdin
stdin, err := ioutil.ReadAll(os.Stdin)
if err != nil {
@ -58,7 +59,7 @@ func (x *ContainersCommand) Execute(args []string) error {
clientContainers, err := client.ListContainers(docker.ListContainersOptions{All: true})
if err != nil {
if in_docker := os.Getenv("IN_DOCKER"); len(in_docker) > 0 {
return fmt.Errorf("Unable to access Docker socket, please run like this:\n docker run --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz containers <args>\nFor more help, run 'dockviz help'")
return fmt.Errorf("Unable to access Docker socket, please run like this:\n docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz containers <args>\nFor more help, run 'dockviz help'")
} else {
return fmt.Errorf("Unable to connect: %s\nFor help, run 'dockviz help'", err)
}
@ -81,7 +82,7 @@ func (x *ContainersCommand) Execute(args []string) error {
}
if containersCommand.Dot {
fmt.Printf(jsonContainersToDot(containers))
fmt.Printf(jsonContainersToDot(containers, containersCommand.OnlyRunning))
} else {
return fmt.Errorf("Please specify --dot")
}
@ -115,24 +116,53 @@ func parseContainersJSON(rawJSON []byte) (*[]Container, error) {
return &containers, nil
}
func jsonContainersToDot(containers *[]Container) string {
func jsonContainersToDot(containers *[]Container,OnlyRunning bool) string {
var buffer bytes.Buffer
buffer.WriteString("digraph docker {\n")
// build list of all primary container names
// this is so we can throw away links to
// non-primary container name
var PrimaryContainerNames map[string]string
PrimaryContainerNames = make(map[string]string)
for _, container := range *containers {
for _, name := range container.Names {
if strings.Count(name, "/") == 1 {
PrimaryContainerNames[name[1:]] = name[1:]
}
}
}
// stores ony first value of link to avoid duplicates
var LinkMap map[string]string
LinkMap = make(map[string]string)
for _, container := range *containers {
if OnlyRunning && strings.HasPrefix(container.Status,"Exit") { continue }
var containerName string
//fmt.Printf("container status/Names %s/%s\n",container.Status,container.Names)
for _, name := range container.Names {
if strings.Count(name, "/") == 1 {
containerName = name[1:]
}
}
for _, name := range container.Names {
nameParts := strings.Split(name, "/")
if len(nameParts) > 2 {
buffer.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\" [label = \" %s\" ]\n", containerName, nameParts[1], nameParts[len(nameParts)-1]))
//fmt.Printf("\t%s to %s\n",containerName,nameParts[1])
// source and dest should be primary container names
if IsPrimaryContainerName(containerName,PrimaryContainerNames) && IsPrimaryContainerName(nameParts[1],PrimaryContainerNames) {
// only create link if none exists already
if _,ok := LinkMap[containerName + "-" + nameParts[1]]; !ok {
LinkMap[containerName + "-" + nameParts[1]] = "exists"
buffer.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\" [label = \" %s\" ]\n", containerName, nameParts[1], nameParts[len(nameParts)-1] ))
}
}
}
}
@ -144,7 +174,7 @@ func jsonContainersToDot(containers *[]Container) string {
containerBackground = "paleturquoise"
}
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"%s\",style=\"filled,rounded\"];\n", containerName, containerName, truncate(container.Id), containerBackground))
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\\n%s\",shape=box,fillcolor=\"%s\",style=\"filled,rounded\"];\n", containerName, container.Image, containerName, truncate(container.Id, 12), containerBackground))
}
buffer.WriteString("}\n")
@ -152,6 +182,11 @@ func jsonContainersToDot(containers *[]Container) string {
return buffer.String()
}
func IsPrimaryContainerName(Name string,PrimaryContainerNames map[string]string) bool {
_,ok := PrimaryContainerNames[Name]
return ok
}
func init() {
parser.AddCommand("containers",
"Visualize docker containers.",

41
go.mod Normal file
View file

@ -0,0 +1,41 @@
module github.com/justone/dockviz
go 1.19
require (
github.com/fsouza/go-dockerclient v1.8.3
github.com/jessevdk/go-flags v1.5.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Microsoft/hcsshim v0.9.4 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/containerd v1.6.8 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/docker/docker v20.10.18+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
github.com/moby/sys/mount v0.3.3 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc1 // indirect
github.com/opencontainers/runc v1.1.4 // indirect
github.com/opencontainers/selinux v1.10.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/tools v0.1.12 // indirect
gotest.tools v2.2.0+incompatible // indirect
)

1097
go.sum Normal file

File diff suppressed because it is too large Load diff

12
help.go
View file

@ -24,6 +24,18 @@ Dockviz supports connecting to the Docker daemon directly. It defaults to
Dockviz also supports receiving Docker image or container json data on standard
input: curl -s http://localhost:4243/images/json?all=1 | dockviz images --tree
Running inside a Docker image:
Dockviz can be run as a Docker image, like this:
$ docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz
If you get an error that Dockviz is unable to access the Docker socket, even
when specified, SELinux may be getting in the way. See this issue comment for
more information:
https://github.com/justone/dockviz/issues/12#issuecomment-186682489
Visualizing:
Dockviz can visualize both images and containers. For more information on the

501
images.go
View file

@ -4,10 +4,13 @@ import (
"github.com/fsouza/go-dockerclient"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
@ -18,19 +21,31 @@ type Image struct {
VirtualSize int64
Size int64
Created int64
OrigId string
CreatedBy string
}
type ImagesCommand struct {
Dot bool `short:"d" long:"dot" description:"Show image information as Graphviz dot."`
Tree bool `short:"t" long:"tree" description:"Show image information as tree."`
Short bool `short:"s" long:"short" description:"Show short summary of images (repo name and list of tags)."`
NoTruncate bool `short:"n" long:"no-trunc" description:"Don't truncate the image IDs."`
Dot bool `short:"d" long:"dot" description:"Show image information as Graphviz dot. You can add a start image id or name -d/--dot [id/name]"`
Tree bool `short:"t" long:"tree" description:"Show image information as tree. You can add a start image id or name -t/--tree [id/name]"`
Short bool `short:"s" long:"short" description:"Show short summary of images (repo name and list of tags)."`
NoTruncate bool `short:"n" long:"no-trunc" description:"Don't truncate the image IDs (only works with tree mode)."`
Incremental bool `short:"i" long:"incremental" description:"Display image size as incremental rather than cumulative."`
OnlyLabelled bool `short:"l" long:"only-labelled" description:"Print only labelled images/containers."`
ShowCreatedBy bool `long:"show-created-by" description:"Show the image 'CreatedBy' to help identify layers."`
NoHuman bool `short:"c" long:"no-human" description:"Don't humanize the sizes."`
}
type DisplayOpts struct {
NoTruncate bool
Incremental bool
NoHuman bool
ShowCreatedBy bool
}
var imagesCommand ImagesCommand
func (x *ImagesCommand) Execute(args []string) error {
var images *[]Image
stat, err := os.Stdin.Stat()
@ -38,7 +53,7 @@ func (x *ImagesCommand) Execute(args []string) error {
return fmt.Errorf("error reading stdin stat", err)
}
if (stat.Mode() & os.ModeCharDevice) == 0 {
if globalOptions.Stdin && (stat.Mode()&os.ModeCharDevice) == 0 {
// read in stdin
stdin, err := ioutil.ReadAll(os.Stdin)
if err != nil {
@ -50,6 +65,22 @@ func (x *ImagesCommand) Execute(args []string) error {
return err
}
var ims []Image
for _, image := range *images {
ims = append(ims, Image{
image.Id,
image.ParentId,
image.RepoTags,
image.VirtualSize,
image.Size,
image.Created,
image.Id,
"",
})
}
images = &ims
} else {
client, err := connect()
@ -57,75 +88,89 @@ func (x *ImagesCommand) Execute(args []string) error {
return err
}
clientImages, err := client.ListImages(docker.ListImagesOptions{All: true})
ver, err := getAPIVersion(client)
if err != nil {
if in_docker := os.Getenv("IN_DOCKER"); len(in_docker) > 0 {
return fmt.Errorf("Unable to access Docker socket, please run like this:\n docker run --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz images <args>\nFor more help, run 'dockviz help'")
return fmt.Errorf("Unable to access Docker socket, please run like this:\n docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz images <args>\nFor more help, run 'dockviz help'")
} else {
return fmt.Errorf("Unable to connect: %s\nFor help, run 'dockviz help'", err)
}
}
var ims []Image
for _, image := range clientImages {
// fmt.Println(image)
ims = append(ims, Image{
image.ID,
image.ParentID,
image.RepoTags,
image.VirtualSize,
image.Size,
image.Created,
})
}
if ver[0] == 1 && ver[1] <= 21 {
clientImages, err := client.ListImages(docker.ListImagesOptions{All: true})
if err != nil {
return err
}
images = &ims
var ims []Image
for _, image := range clientImages {
ims = append(ims, Image{
image.ID,
image.ParentID,
image.RepoTags,
image.VirtualSize,
image.Size,
image.Created,
image.ID,
"",
})
}
images = &ims
} else {
clientImages, err := client.ListImages(docker.ListImagesOptions{})
if err != nil {
return err
}
images, err = synthesizeImagesFromHistory(client, clientImages)
if err != nil {
return err
}
}
}
if imagesCommand.Dot {
fmt.Printf(jsonToDot(images))
} else if imagesCommand.Tree {
var startImage = ""
if imagesCommand.Tree || imagesCommand.Dot {
var startImage *Image
if len(args) > 0 {
startImage, err = findStartImage(args[0], images)
// attempt to find the start image, which can be specified as an
// image ID or a repository name
startImageArg := args[0]
startImageRepo := args[0]
// in case a repo name was specified, append ":latest" if it isn't
// already there
if !strings.HasSuffix(startImageRepo, ":latest") {
startImageRepo = fmt.Sprintf("%s:latest", startImageRepo)
}
IMAGES:
for _, image := range *images {
// check if the start image arg matches an image id
if strings.Index(image.Id, startImageArg) == 0 {
startImage = image.Id
break IMAGES
}
// check if the start image arg matches an repository name
if image.RepoTags[0] != "<none>:<none>" {
for _, repotag := range image.RepoTags {
if repotag == startImageRepo {
startImage = image.Id
break IMAGES
}
}
}
}
if startImage == "" {
return fmt.Errorf("Unable to find image %s.", startImageArg)
if err != nil {
return err
}
}
fmt.Printf(jsonToTree(images, startImage, imagesCommand.NoTruncate))
// select the start image of the tree
var roots []Image
if startImage == nil {
roots = collectRoots(images)
} else {
startImage.ParentId = ""
roots = []Image{*startImage}
}
// build helper map (image -> children)
imagesByParent := collectChildren(images)
// filter images
if imagesCommand.OnlyLabelled {
*images, imagesByParent = filterImages(images, &imagesByParent)
}
dispOpts := DisplayOpts{
imagesCommand.NoTruncate,
imagesCommand.Incremental,
imagesCommand.NoHuman,
imagesCommand.ShowCreatedBy,
}
if imagesCommand.Tree {
fmt.Print(jsonToTree(roots, imagesByParent, dispOpts))
}
if imagesCommand.Dot {
fmt.Print(jsonToDot(roots, imagesByParent, dispOpts))
}
} else if imagesCommand.Short {
fmt.Printf(jsonToShort(images))
} else {
@ -135,86 +180,238 @@ func (x *ImagesCommand) Execute(args []string) error {
return nil
}
func jsonToTree(images *[]Image, startImageArg string, noTrunc bool) string {
var buffer bytes.Buffer
var startImage Image
var roots []Image
var byParent = make(map[string][]Image)
for _, image := range *images {
if image.ParentId == "" {
roots = append(roots, image)
} else {
if children, exists := byParent[image.ParentId]; exists {
byParent[image.ParentId] = append(children, image)
} else {
byParent[image.ParentId] = []Image{image}
}
func synthesizeImagesFromHistory(client *docker.Client, images []docker.APIImages) (*[]Image, error) {
var newImages []Image
newImageRoster := make(map[string]*Image)
for _, image := range images {
var previous string
var vSize int64
history, err := client.ImageHistory(image.ID)
if err != nil {
return &newImages, err
}
for i := len(history) - 1; i >= 0; i-- {
var newID string
h := sha256.New()
h.Write([]byte(previous))
h.Write([]byte(history[i].CreatedBy))
h.Write([]byte(strconv.FormatInt(history[i].Created, 10)))
h.Write([]byte(strconv.FormatInt(history[i].Size, 10)))
newID = fmt.Sprintf("synth:%s", hex.EncodeToString(h.Sum(nil)))
if startImageArg != "" {
if startImageArg == image.Id || startImageArg == truncate(image.Id) {
startImage = image
}
for _, repotag := range image.RepoTags {
if repotag == startImageArg {
startImage = image
vSize = vSize + history[i].Size
existingImage, ok := newImageRoster[newID]
if !ok {
newImageRoster[newID] = &Image{
newID,
previous,
history[i].Tags,
vSize,
history[i].Size,
history[i].Created,
history[i].ID,
history[i].CreatedBy,
}
} else {
if len(history[i].Tags) > 0 {
existingImage.RepoTags = append(existingImage.RepoTags, history[i].Tags...)
}
}
previous = newID
}
}
if startImageArg != "" {
WalkTree(&buffer, noTrunc, []Image{startImage}, byParent, "")
} else {
WalkTree(&buffer, noTrunc, roots, byParent, "")
for _, image := range newImageRoster {
if len(image.RepoTags) == 0 {
image.RepoTags = []string{"<none>:<none>"}
} else {
visited := make(map[string]bool)
for _, tag := range image.RepoTags {
visited[tag] = true
}
image.RepoTags = []string{}
for tag, _ := range visited {
image.RepoTags = append(image.RepoTags, tag)
}
}
newImages = append(newImages, *image)
}
return &newImages, nil
}
func findStartImage(name string, images *[]Image) (*Image, error) {
var startImage *Image
// attempt to find the start image, which can be specified as an
// image ID or a repository name
startImageArg := name
startImageRepo := name
// if tag is not defined, find by :latest tag
if strings.Index(startImageRepo, ":") == -1 {
startImageRepo = fmt.Sprintf("%s:latest", startImageRepo)
}
IMAGES:
for _, image := range *images {
// find by image id
if strings.Index(image.Id, startImageArg) == 0 {
startImage = &image
break IMAGES
}
// find by image name (name and tag)
for _, repotag := range image.RepoTags {
if repotag == startImageRepo {
startImage = &image
break IMAGES
}
}
}
if startImage == nil {
return nil, fmt.Errorf("Unable to find image %s = %s.", startImageArg, startImageRepo)
}
return startImage, nil
}
func jsonToTree(images []Image, byParent map[string][]Image, dispOpts DisplayOpts) string {
var buffer bytes.Buffer
jsonToText(&buffer, images, byParent, dispOpts, "")
return buffer.String()
}
func WalkTree(buffer *bytes.Buffer, noTrunc bool, images []Image, byParent map[string][]Image, prefix string) {
if len(images) > 1 {
length := len(images)
func jsonToDot(roots []Image, byParent map[string][]Image, dispOpts DisplayOpts) string {
var buffer bytes.Buffer
buffer.WriteString("digraph docker {\n")
imagesToDot(&buffer, roots, byParent, dispOpts)
buffer.WriteString(" base [style=invisible]\n}\n")
return buffer.String()
}
func collectChildren(images *[]Image) map[string][]Image {
var imagesByParent = make(map[string][]Image)
for _, image := range *images {
if children, exists := imagesByParent[image.ParentId]; exists {
imagesByParent[image.ParentId] = append(children, image)
} else {
imagesByParent[image.ParentId] = []Image{image}
}
}
return imagesByParent
}
func collectRoots(images *[]Image) []Image {
var roots []Image
for _, image := range *images {
if image.ParentId == "" {
roots = append(roots, image)
}
}
return roots
}
func filterImages(images *[]Image, byParent *map[string][]Image) (filteredImages []Image, filteredChildren map[string][]Image) {
for i := 0; i < len(*images); i++ {
// image is visible
// 1. it has a label
// 2. it is root
// 3. it is a node
var visible bool = (*images)[i].RepoTags[0] != "<none>:<none>" || (*images)[i].ParentId == "" || len((*byParent)[(*images)[i].Id]) > 1
if visible {
filteredImages = append(filteredImages, (*images)[i])
} else {
// change childs parent id
// if items are filtered with only one child
for j := 0; j < len(filteredImages); j++ {
if filteredImages[j].ParentId == (*images)[i].Id {
filteredImages[j].ParentId = (*images)[i].ParentId
}
}
for j := 0; j < len(*images); j++ {
if (*images)[j].ParentId == (*images)[i].Id {
(*images)[j].ParentId = (*images)[i].ParentId
}
}
}
}
filteredChildren = collectChildren(&filteredImages)
return filteredImages, filteredChildren
}
func jsonToText(buffer *bytes.Buffer, images []Image, byParent map[string][]Image, dispOpts DisplayOpts, prefix string) {
var length = len(images)
if length > 1 {
for index, image := range images {
var nextPrefix string = ""
if index+1 == length {
PrintTreeNode(buffer, noTrunc, image, prefix+"└─")
if subimages, exists := byParent[image.Id]; exists {
WalkTree(buffer, noTrunc, subimages, byParent, prefix+" ")
}
PrintTreeNode(buffer, image, dispOpts, prefix+"└─")
nextPrefix = " "
} else {
PrintTreeNode(buffer, noTrunc, image, prefix+"├─")
if subimages, exists := byParent[image.Id]; exists {
WalkTree(buffer, noTrunc, subimages, byParent, prefix+"│ ")
}
PrintTreeNode(buffer, image, dispOpts, prefix+"├─")
nextPrefix = "│ "
}
if subimages, exists := byParent[image.Id]; exists {
jsonToText(buffer, subimages, byParent, dispOpts, prefix+nextPrefix)
}
}
} else {
for _, image := range images {
PrintTreeNode(buffer, noTrunc, image, prefix+"└─")
PrintTreeNode(buffer, image, dispOpts, prefix+"└─")
if subimages, exists := byParent[image.Id]; exists {
WalkTree(buffer, noTrunc, subimages, byParent, prefix+" ")
jsonToText(buffer, subimages, byParent, dispOpts, prefix+" ")
}
}
}
}
func PrintTreeNode(buffer *bytes.Buffer, noTrunc bool, image Image, prefix string) {
func PrintTreeNode(buffer *bytes.Buffer, image Image, dispOpts DisplayOpts, prefix string) {
var imageID string
if noTrunc {
imageID = image.Id
if dispOpts.NoTruncate {
imageID = image.OrigId
} else {
imageID = truncate(image.Id)
imageID = truncate(stripPrefix(image.OrigId), 12)
}
buffer.WriteString(fmt.Sprintf("%s%s Virtual Size: %s", prefix, imageID, humanSize(image.VirtualSize)))
if image.RepoTags[0] != "<none>:<none>" {
buffer.WriteString(fmt.Sprintf(" Tags: %s\n", strings.Join(image.RepoTags, ", ")))
var size int64
var sizeLabel string
if dispOpts.Incremental {
sizeLabel = "Size"
size = image.Size
} else {
buffer.WriteString(fmt.Sprintf("\n"))
sizeLabel = "Virtual Size"
size = image.VirtualSize
}
var sizeStr string
if dispOpts.NoHuman {
sizeStr = strconv.FormatInt(size, 10)
} else {
sizeStr = humanSize(size)
}
buffer.WriteString(fmt.Sprintf("%s%s %s: %s", prefix, imageID, sizeLabel, sizeStr))
if image.RepoTags[0] != "<none>:<none>" {
buffer.WriteString(fmt.Sprintf(" Tags: %s", strings.Join(image.RepoTags, ", ")))
}
if dispOpts.ShowCreatedBy {
buffer.WriteString(fmt.Sprintf(" (%s)", SanitizeCommand(image.CreatedBy, 100)))
}
buffer.WriteString(fmt.Sprintf("\n"))
}
func megabytes(bytes int64) float64 {
return float64(bytes) / (1024 * 1024)
}
func humanSize(raw int64) string {
@ -235,8 +432,22 @@ func humanSize(raw int64) string {
return fmt.Sprintf("%.01f %s", rawFloat, sizes[ind])
}
func truncate(id string) string {
return id[0:12]
func truncate(id string, length int) string {
if len(id) > length {
return id[0:length]
} else if len(id) > 0 {
return id
} else {
return ""
}
}
func stripPrefix(id string) string {
if strings.Contains(id, ":") {
idParts := strings.Split(id, ":")
return idParts[len(idParts)-1]
}
return id
}
func parseImagesJSON(rawJSON []byte) (*[]Image, error) {
@ -251,25 +462,47 @@ func parseImagesJSON(rawJSON []byte) (*[]Image, error) {
return &images, nil
}
func jsonToDot(images *[]Image) string {
func imagesToDot(buffer *bytes.Buffer, images []Image, byParent map[string][]Image, dispOpts DisplayOpts) {
for _, image := range images {
var buffer bytes.Buffer
buffer.WriteString("digraph docker {\n")
for _, image := range *images {
if image.ParentId == "" {
buffer.WriteString(fmt.Sprintf(" base -> \"%s\" [style=invis]\n", truncate(image.Id)))
buffer.WriteString(fmt.Sprintf(" base -> \"%s\" [style=invis]\n", truncate(image.Id, 12)))
} else {
buffer.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\"\n", truncate(image.ParentId), truncate(image.Id)))
buffer.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\"\n", truncate(image.ParentId, 12), truncate(image.Id, 12)))
}
if image.RepoTags[0] != "<none>:<none>" {
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", truncate(image.Id), truncate(image.Id), strings.Join(image.RepoTags, "\\n")))
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\",area=%f,shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", truncate(image.Id, 12), truncate(stripPrefix(image.OrigId), 12), strings.Join(image.RepoTags, "\\n"), megabytes(image.Size)))
} else {
labelParts := []string{truncate(stripPrefix(image.OrigId), 12)}
if dispOpts.ShowCreatedBy {
labelParts = append(labelParts, SanitizeCommand(image.CreatedBy, 30))
}
var size int64
var sizeLabel string
if dispOpts.Incremental {
sizeLabel = "Size"
size = image.Size
} else {
sizeLabel = "Virtual Size"
size = image.VirtualSize
}
var sizeStr string
if dispOpts.NoHuman {
sizeStr = strconv.FormatInt(size, 10)
} else {
sizeStr = humanSize(size)
}
labelParts = append(labelParts, fmt.Sprintf("%s: %s", sizeLabel, sizeStr))
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\",area=%f]\n", truncate(image.Id, 12), strings.Join(labelParts, "\n"), megabytes(image.Size)))
}
if subimages, exists := byParent[image.Id]; exists {
imagesToDot(buffer, subimages, byParent, dispOpts)
}
}
buffer.WriteString(" base [style=invisible]\n}\n")
return buffer.String()
}
func jsonToShort(images *[]Image) string {
@ -303,6 +536,28 @@ func jsonToShort(images *[]Image) string {
return buffer.String()
}
func SanitizeCommand(CommandStr string, MaxLength int) string {
temp := CommandStr
// remove prefixes that don't add meaning
if strings.HasPrefix(temp, "/bin/sh -c") {
temp = strings.TrimSpace(temp[10:])
}
if strings.HasPrefix(temp, "#(nop)") {
temp = strings.TrimSpace(temp[6:])
}
// remove double and single quotes which make dot format invalid
temp = strings.Replace(temp, "\"", " ", -1)
temp = strings.Replace(temp, "'", " ", -1)
// remove double spaces inside
temp = strings.Join(strings.Fields(temp), " ")
return truncate(temp, MaxLength)
}
func init() {
parser.AddCommand("images",
"Visualize docker images.",

BIN
images_only_labeled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -19,6 +19,7 @@ type TreeTest struct {
json string
startImage string
noTrunc bool
incr bool
regexps []string
}
@ -56,7 +57,12 @@ func Test_Dot(t *testing.T) {
for _, dotTest := range dotTests {
im, _ := parseImagesJSON([]byte(dotTest.json))
result := jsonToDot(im)
byParent := collectChildren(im)
roots := collectRoots(im)
// TODO: test start image limiting
result := jsonToDot(roots, byParent)
for _, regexp := range allRegex {
if !regexp.MatchString(result) {
@ -73,23 +79,36 @@ func Test_Dot(t *testing.T) {
}
func Test_Tree(t *testing.T) {
treeJSON := `[ { "VirtualSize": 662553464, "Size": 0, "RepoTags": [ "foo:latest" ], "ParentId": "735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Id": "c87be8e5e697c735f5db5626147582d2ae3f2088574c5faaf8d4d1bccab99470", "Created": 1386142123 }, { "VirtualSize": 682553464, "Size": 0, "RepoTags": [ "<none>:<none>" ], "ParentId": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Id": "626147582d2ae3735f5db5f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Created": 1386142123 }, { "VirtualSize": 712553464, "Size": 0, "RepoTags": [ "base:latest" ], "ParentId": "626147582d2ae3735f5db5f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Id": "574c5faaf8d4d1bccab994626147582d2ae3735f5db5f2c87be8e5e697c08870", "Created": 1386142123 }, { "VirtualSize": 752553464, "Size": 0, "RepoTags": [ "<none>:<none>" ], "ParentId": "574c5faaf8d4d1bccab994626147582d2ae3735f5db5f2c87be8e5e697c08870", "Id": "aaf8d4d1bccab994574c5f626147582d2ae3735f5db5f2c87be8e5e697c08870", "Created": 1386142123 }, { "VirtualSize": 662553464, "Size": 0, "RepoTags": [ "<none>:<none>" ], "ParentId": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Id": "735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Created": 1386142123 }, { "VirtualSize": 662553464, "Size": 662553464, "RepoTags": [ "<none>:<none>" ], "ParentId": "", "Id": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Created": 1386114144 } ]`
treeJSON := `[{"VirtualSize":674553464,"Size":2000000,"RepoTags":["foo:latest"],"ParentId":"735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470","Id":"c87be8e5e697c735f5db5626147582d2ae3f2088574c5faaf8d4d1bccab99470","Created":1386142123},{"VirtualSize":682553464,"Size":20000000,"RepoTags":["<none>:<none>"],"ParentId":"4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358","Id":"626147582d2ae3735f5db5f2c87be8e5e697c088574c5faaf8d4d1bccab99470","Created":1386142123},{"VirtualSize":712553464,"Size":30000000,"RepoTags":["base:latest"],"ParentId":"626147582d2ae3735f5db5f2c87be8e5e697c088574c5faaf8d4d1bccab99470","Id":"574c5faaf8d4d1bccab994626147582d2ae3735f5db5f2c87be8e5e697c08870","Created":1386142123},{"VirtualSize":752553464,"Size":40000000,"RepoTags":["<none>:<none>"],"ParentId":"574c5faaf8d4d1bccab994626147582d2ae3735f5db5f2c87be8e5e697c08870","Id":"aaf8d4d1bccab994574c5f626147582d2ae3735f5db5f2c87be8e5e697c08870","Created":1386142123},{"VirtualSize":672553464,"Size":10000000,"RepoTags":["<none>:<none>"],"ParentId":"4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358","Id":"735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470","Created":1386142123},{"VirtualSize":662553464,"Size":662553464,"RepoTags":["<none>:<none>"],"ParentId":"","Id":"4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358","Created":1386114144}]`
treeTests := []TreeTest{
TreeTest{
json: treeJSON,
startImage: "",
noTrunc: false,
incr: false,
regexps: []string{
`(?m)└─4c1208b690c6`,
`(?m) └─735f5db56261`,
`(?m) └─c87be8e5e697`,
`(?m)└─4c1208b690c6 Virtual Size: 662.6 MB`,
`(?m) └─735f5db56261 Virtual Size: 672.6 MB`,
`(?m) └─c87be8e5e697 Virtual Size: 674.6 MB Tags: foo:latest`,
},
},
TreeTest{
json: treeJSON,
startImage: "",
noTrunc: false,
incr: true,
regexps: []string{
`(?m)└─4c1208b690c6 Virtual Size: 662.6 MB`,
`(?m) └─735f5db56261 Virtual Size: 10.0 MB`,
`(?m) └─c87be8e5e697 Virtual Size: 2.0 MB Tags: foo:latest`,
},
},
TreeTest{
json: treeJSON,
startImage: "626147582d2a",
noTrunc: false,
incr: false,
regexps: []string{
`(?m)└─626147582d2a`,
`(?m) └─574c5faaf8d4`,
@ -100,6 +119,7 @@ func Test_Tree(t *testing.T) {
json: treeJSON,
startImage: "base:latest",
noTrunc: true,
incr: false,
regexps: []string{
`(?m)└─574c5faaf8d4d1bccab994626147582d2ae3735f5db5f2c87be8e5e697c08870`,
`(?m) └─aaf8d4d1bccab994574c5f626147582d2ae3735f5db5f2c87be8e5e697c08870`,
@ -109,7 +129,16 @@ func Test_Tree(t *testing.T) {
for _, treeTest := range treeTests {
im, _ := parseImagesJSON([]byte(treeTest.json))
result := jsonToTree(im, treeTest.startImage, treeTest.noTrunc)
byParent := collectChildren(im)
var roots []Image
if len(treeTest.startImage) > 0 {
startImage, _ := findStartImage(treeTest.startImage, im)
startImage.ParentId = ""
roots = []Image{*startImage}
} else {
roots = collectRoots(im)
}
result := jsonToTree(roots, byParent, treeTest.noTrunc, treeTest.incr)
for _, regexp := range compileRegexps(t, treeTest.regexps) {
if !regexp.MatchString(result) {

63
release.sh Executable file
View file

@ -0,0 +1,63 @@
#!/bin/bash
ORG=justone
NAME=dockviz
ARCHS="darwin/amd64 linux/amd64 windows/amd64"
set -ex
if [[ ! $(type -P gox) ]]; then
echo "Error: gox not found."
echo "To fix: run 'go get github.com/mitchellh/gox', and/or add \$GOPATH/bin to \$PATH"
exit 1
fi
if [[ -z $GITHUB_TOKEN ]]; then
echo "Error: GITHUB_TOKEN not set."
exit 1
fi
if [[ ! $(type -P github-release) ]]; then
echo "Error: github-release not found."
exit 1
fi
VER=$1
if [[ -z $VER ]]; then
echo "Need to specify version."
exit 1
fi
PRE_ARG=
if [[ $VER =~ pre ]]; then
PRE_ARG="--pre-release"
fi
# git tag $VER
echo "Building $VER"
echo
rm -v ${NAME}* || true
gox -ldflags "-X main.version=$VER" -osarch="$ARCHS"
echo "* " > desc
echo "" >> desc
echo "\`\`\`" >> desc
echo "$ sha1sum ${NAME}_*" >> desc
sha1sum ${NAME}_* >> desc
echo "$ sha256sum ${NAME}_*" >> desc
sha256sum ${NAME}_* >> desc
echo "\`\`\`" >> desc
vi desc
git push --tags
sleep 2
cat desc | github-release release $PRE_ARG --user ${ORG} --repo ${NAME} --tag $VER --name $VER --description -
for file in ${NAME}_*; do
github-release upload --user ${ORG} --repo ${NAME} --tag $VER --name $file --file $file
done

View file

@ -1,37 +1,61 @@
digraph docker {
"0fe9a2bc50fe" -> "8c32832f07ba"
"cc4e1358bc80" -> "5c0d04fba9df"
"5c0d04fba9df" [label="5c0d04fba9df\nnate/mongodb:latest",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"67b8b7262a67" -> "0fe9a2bc50fe"
"2f96171d2098" -> "67b8b7262a67"
"8c32832f07ba" -> "cc4e1358bc80"
"89541b3b35f2" -> "7dac4e98548e"
"341d0cc3fac8" -> "2f96171d2098"
"7dac4e98548e" -> "341d0cc3fac8"
"59dac4bae93b" -> "89541b3b35f2"
"d0525208a46c" -> "59dac4bae93b"
"e18d8001204e" -> "d0525208a46c"
"a7cf8ae4e998" -> "e18d8001204e"
"0cd8e7f50270" -> "594b6f8e6f92"
"594b6f8e6f92" -> "f832a63e87a4"
"f832a63e87a4" [label="f832a63e87a4\nredis:latest",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"398d592f2009" -> "0cd8e7f50270"
"a7cf8ae4e998" -> "398d592f2009"
"5dbd9cb5a02f" -> "74fe38d11401"
"74fe38d11401" [label="74fe38d11401\nubuntu:12.04\nubuntu:precise",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"82cdea7ab5b5" -> "5dbd9cb5a02f"
"f10ebce2c0e1" -> "82cdea7ab5b5"
"511136ea3c5a" -> "f10ebce2c0e1"
"cf8dc907452c" -> "a7cf8ae4e998"
"a7cf8ae4e998" [label="a7cf8ae4e998\nubuntu:12.10\nubuntu:quantal",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"ef519c9ee91a" -> "07302703becc"
"07302703becc" -> "cf8dc907452c"
"511136ea3c5a" -> "ef519c9ee91a"
"cb12405ee8fa" -> "316b678ddf48"
"316b678ddf48" [label="316b678ddf48\nubuntu:13.04\nubuntu:raring",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"e7206bfc66aa" -> "cb12405ee8fa"
"02dae1c13f51" -> "e7206bfc66aa"
"511136ea3c5a" -> "02dae1c13f51"
base -> "511136ea3c5a" [style=invis]
"511136ea3c5a" [label="511136ea3c5a",area=0.000000]
"511136ea3c5a" -> "f10ebce2c0e1"
"f10ebce2c0e1" [label="f10ebce2c0e1",area=98.872495]
"f10ebce2c0e1" -> "82cdea7ab5b5"
"82cdea7ab5b5" [label="82cdea7ab5b5",area=0.243621]
"82cdea7ab5b5" -> "5dbd9cb5a02f"
"5dbd9cb5a02f" [label="5dbd9cb5a02f",area=0.001830]
"5dbd9cb5a02f" -> "74fe38d11401"
"74fe38d11401" [label="74fe38d11401\nubuntu:12.04\nubuntu:precise",area=100.772088,shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"511136ea3c5a" -> "ef519c9ee91a"
"ef519c9ee91a" [label="ef519c9ee91a",area=96.254996]
"ef519c9ee91a" -> "07302703becc"
"07302703becc" [label="07302703becc",area=0.239370]
"07302703becc" -> "cf8dc907452c"
"cf8dc907452c" [label="cf8dc907452c",area=0.001830]
"cf8dc907452c" -> "a7cf8ae4e998"
"a7cf8ae4e998" [label="a7cf8ae4e998\nubuntu:12.10\nubuntu:quantal",area=66.847105,shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"a7cf8ae4e998" -> "e18d8001204e"
"e18d8001204e" [label="e18d8001204e",area=0.027992]
"e18d8001204e" -> "d0525208a46c"
"d0525208a46c" [label="d0525208a46c",area=0.000068]
"d0525208a46c" -> "59dac4bae93b"
"59dac4bae93b" [label="59dac4bae93b",area=67.854815]
"59dac4bae93b" -> "89541b3b35f2"
"89541b3b35f2" [label="89541b3b35f2",area=256.858793]
"89541b3b35f2" -> "7dac4e98548e"
"7dac4e98548e" [label="7dac4e98548e",area=0.000000]
"7dac4e98548e" -> "341d0cc3fac8"
"341d0cc3fac8" [label="341d0cc3fac8",area=0.000000]
"341d0cc3fac8" -> "2f96171d2098"
"2f96171d2098" [label="2f96171d2098",area=0.000000]
"2f96171d2098" -> "67b8b7262a67"
"67b8b7262a67" [label="67b8b7262a67",area=1.779918]
"67b8b7262a67" -> "0fe9a2bc50fe"
"0fe9a2bc50fe" [label="0fe9a2bc50fe",area=0.000626]
"0fe9a2bc50fe" -> "8c32832f07ba"
"8c32832f07ba" [label="8c32832f07ba",area=0.000365]
"8c32832f07ba" -> "cc4e1358bc80"
"cc4e1358bc80" [label="cc4e1358bc80",area=0.000000]
"cc4e1358bc80" -> "5c0d04fba9df"
"5c0d04fba9df" [label="5c0d04fba9df\nnate/mongodb:latest",area=0.000000,shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"a7cf8ae4e998" -> "398d592f2009"
"398d592f2009" [label="398d592f2009",area=67.632430]
"398d592f2009" -> "0cd8e7f50270"
"0cd8e7f50270" [label="0cd8e7f50270",area=1.352684]
"0cd8e7f50270" -> "594b6f8e6f92"
"594b6f8e6f92" [label="594b6f8e6f92",area=0.000000]
"594b6f8e6f92" -> "f832a63e87a4"
"f832a63e87a4" [label="f832a63e87a4\nredis:latest",area=0.000000,shape=box,fillcolor="paleturquoise",style="filled,rounded"];
"511136ea3c5a" -> "02dae1c13f51"
"02dae1c13f51" [label="02dae1c13f51",area=93.792276]
"02dae1c13f51" -> "e7206bfc66aa"
"e7206bfc66aa" [label="e7206bfc66aa",area=0.181205]
"e7206bfc66aa" -> "cb12405ee8fa"
"cb12405ee8fa" [label="cb12405ee8fa",area=0.001815]
"cb12405ee8fa" -> "316b678ddf48"
"316b678ddf48" [label="316b678ddf48\nubuntu:13.04\nubuntu:raring",area=67.541988,shape=box,fillcolor="paleturquoise",style="filled,rounded"];
base [style=invisible]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
sample/treemap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

14
util.go
View file

@ -49,3 +49,17 @@ func connect() (*docker.Client, error) {
}
return client, nil
}
func getAPIVersion(client *docker.Client) ([]int, error) {
env, err := client.Version()
if err != nil {
return []int{}, err
}
ver, err := docker.NewAPIVersion(env.Get("ApiVersion"))
if err != nil {
return []int{}, err
}
return ver, nil
}