Compare commits
81 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b87a4d5637 | ||
|
3ebdb75ed3 | ||
|
f925e91395 | ||
|
15f77275c4 | ||
|
28bcf8034b | ||
|
4d5c52e735 | ||
|
2af445f27a | ||
|
7b8aa57592 | ||
|
b4f269312f | ||
|
f765f7d6cd | ||
|
f63f9dbe37 | ||
|
d30fd054ad | ||
|
b84946b83e | ||
|
85dd6df8b2 | ||
|
4c4b3c154a | ||
|
51c099c18c | ||
|
c932e4c843 | ||
|
f20218e43a | ||
|
40e1c4e1ce | ||
|
651855127c | ||
|
edcada1c88 | ||
|
1ce624cd37 | ||
|
9e07d53e05 | ||
|
c75e40ffd8 | ||
|
76415ba88f | ||
|
2df85b6786 | ||
|
1f8a6a6f0c | ||
|
25acff1b44 | ||
|
360c954944 | ||
|
cf340cd457 | ||
|
18ad950cd1 | ||
|
ff917a13b1 | ||
|
ee1ab834db | ||
|
94f2cc64c9 | ||
|
bdb64a088c | ||
|
9863e97953 | ||
|
16075245dd | ||
|
5a116b8dff | ||
|
2e9e1972e7 | ||
|
412fb7db34 | ||
|
8a7f885238 | ||
|
e619ee311a | ||
|
ed784c7a95 | ||
|
4bd432cec3 | ||
|
f898de1f61 | ||
|
cd77167841 | ||
|
ad9654ce57 | ||
|
861a7e02b6 | ||
|
551ef0434f | ||
|
1e678b70e3 | ||
|
7ea47fc9cf | ||
|
14181721e5 | ||
|
783915aa09 | ||
|
41f9b67538 | ||
|
9bcca08f2b | ||
|
153b934646 | ||
|
6c094efa09 | ||
|
ae855d6a5a | ||
|
e936558a78 | ||
|
80970dcee2 | ||
|
20d1b3797a | ||
|
f8eb6a8bc7 | ||
|
f8106ccec6 | ||
|
b86486fdbc | ||
|
beb1db17a3 | ||
|
676c01bda2 | ||
|
dc21ad12a3 | ||
|
19368a42d8 | ||
|
b95175dae8 | ||
|
4da0559824 | ||
|
0dcdc66e63 | ||
|
5f3ba9a693 | ||
|
6eeb877c5e | ||
|
c25c0eb801 | ||
|
071f77cbbc | ||
|
097bd3285b | ||
|
04ee20c4ba | ||
|
b911e69180 | ||
|
7b58366a10 | ||
|
1efd686bd3 | ||
|
9628e24320 |
16 changed files with 1978 additions and 204 deletions
|
@ -1,3 +1,4 @@
|
|||
FROM scratch
|
||||
COPY dockviz /
|
||||
ENV IN_DOCKER true
|
||||
ENTRYPOINT ["/dockviz"]
|
||||
|
|
222
README.md
222
README.md
|
@ -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
|
||||
```
|
||||
|
||||

|
||||
|
@ -38,6 +60,24 @@ $ dockviz images -d | dot -Tpng -o images.png
|
|||
|
||||

|
||||
|
||||
```
|
||||
$ dockviz images -d -l | dot -Tpng -o images.png
|
||||
OR
|
||||
$ dockviz images --dot --only-labelled | dot -Tpng -o images.png
|
||||
```
|
||||
|
||||

|
||||
|
||||
Or as a treemap:
|
||||
|
||||
```
|
||||
$ dockviz images -d | patchwork -Tpng -o treemap.png
|
||||
OR
|
||||
$ dockviz images --dot | patchwork -Tpng -o treemap.png
|
||||
```
|
||||
|
||||

|
||||
|
||||
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
3
cli.go
|
@ -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.1"
|
||||
var version = "v0.6.4"
|
||||
|
||||
func main() {
|
||||
globalOptions.Version = func() {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -57,7 +58,11 @@ func (x *ContainersCommand) Execute(args []string) error {
|
|||
|
||||
clientContainers, err := client.ListContainers(docker.ListContainersOptions{All: true})
|
||||
if err != nil {
|
||||
return err
|
||||
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 -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)
|
||||
}
|
||||
}
|
||||
|
||||
var conts []Container
|
||||
|
@ -77,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")
|
||||
}
|
||||
|
@ -111,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] ))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -140,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")
|
||||
|
@ -148,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
41
go.mod
Normal 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
|
||||
)
|
54
help.go
Normal file
54
help.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type HelpCommand struct {
|
||||
// nothing yet
|
||||
}
|
||||
|
||||
var helpCommand HelpCommand
|
||||
|
||||
func (x *HelpCommand) Execute(args []string) error {
|
||||
fmt.Println(`Dockviz: Visualizing Docker Data
|
||||
|
||||
Connecting to Docker:
|
||||
|
||||
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 or docker-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
|
||||
|
||||
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
|
||||
options each subcommand supports, run them with the '--help' flag (e.g.
|
||||
'dockviz images --help').
|
||||
`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("help",
|
||||
"Help for dockviz.",
|
||||
"",
|
||||
&helpCommand)
|
||||
}
|
505
images.go
505
images.go
|
@ -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,71 +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 {
|
||||
return err
|
||||
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 -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 = startImageArg
|
||||
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 {
|
||||
|
@ -131,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 {
|
||||
|
@ -231,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) {
|
||||
|
@ -247,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 {
|
||||
|
@ -299,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
BIN
images_only_labeled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -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
63
release.sh
Executable 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
|
|
@ -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 |
BIN
sample/images_only_labelled.png
Normal file
BIN
sample/images_only_labelled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
sample/treemap.png
Normal file
BIN
sample/treemap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
14
util.go
14
util.go
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue