Merge branch 'fabianlee'

This commit is contained in:
Nate Jones 2018-04-16 17:17:27 -07:00
commit f63f9dbe37
4 changed files with 153 additions and 33 deletions

View file

@ -12,8 +12,14 @@ different ways, to help you understand what's going on inside the system.
* 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" 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"
``` ```
2. Visualize images by running `dockviz images -t`, which has similar output to `docker images -t`. 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. * 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. * If you would like to visualize outside the container you will have to install [Graphviz](http://www.graphviz.org) first.
@ -35,7 +41,11 @@ brew update && brew install graphviz
Currently, containers are visualized with labelled 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 $ 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") ![](sample/containers.png "Container")
@ -46,8 +56,6 @@ Image info is visualized with lines indicating parent images:
``` ```
$ dockviz images -d | dot -Tpng -o images.png $ dockviz images -d | dot -Tpng -o images.png
OR
$ dockviz images --dot | dot -Tpng -o images.png
``` ```
![](sample/images.png "Image") ![](sample/images.png "Image")
@ -197,6 +205,7 @@ image layers when they show up with "<missing>" image Ids.
Dockviz supports connecting to the Docker daemon directly. It defaults to `unix:///var/run/docker.sock`, but respects the following as well: 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/). * 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. * 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. Dockviz also supports receiving Docker image or container json data on standard input.
@ -219,6 +228,30 @@ See the [releases](https://github.com/justone/dockviz/releases) area for binarie
# Build # Build
```bash ```bash
go get ./... # install graphviz in host environment for rendering (Debian example)
go build sudo apt-get install git graphviz -y
# pull latest code
cd $GOPATH
git clone https://github.com/nate/dockviz.git
# force static compilation for go language
export CGO_ENABLED=0
# build go program
cd dockviz
go get
go build -a
# 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
``` ```

2
cli.go
View file

@ -1,4 +1,4 @@
package main // import "github.com/justone/dockviz" package main
import ( import (
"fmt" "fmt"

View file

@ -24,6 +24,7 @@ type Container struct {
type ContainersCommand struct { type ContainersCommand struct {
Dot bool `short:"d" long:"dot" description:"Show container information as Graphviz dot."` 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."` 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 var containersCommand ContainersCommand
@ -81,7 +82,7 @@ func (x *ContainersCommand) Execute(args []string) error {
} }
if containersCommand.Dot { if containersCommand.Dot {
fmt.Printf(jsonContainersToDot(containers)) fmt.Printf(jsonContainersToDot(containers, containersCommand.OnlyRunning))
} else { } else {
return fmt.Errorf("Please specify --dot") return fmt.Errorf("Please specify --dot")
} }
@ -115,24 +116,53 @@ func parseContainersJSON(rawJSON []byte) (*[]Container, error) {
return &containers, nil return &containers, nil
} }
func jsonContainersToDot(containers *[]Container) string { func jsonContainersToDot(containers *[]Container,OnlyRunning bool) string {
var buffer bytes.Buffer var buffer bytes.Buffer
buffer.WriteString("digraph docker {\n") 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 _, 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 var containerName string
//fmt.Printf("container status/Names %s/%s\n",container.Status,container.Names)
for _, name := range container.Names { for _, name := range container.Names {
if strings.Count(name, "/") == 1 { if strings.Count(name, "/") == 1 {
containerName = name[1:] containerName = name[1:]
} }
} }
for _, name := range container.Names { for _, name := range container.Names {
nameParts := strings.Split(name, "/") nameParts := strings.Split(name, "/")
if len(nameParts) > 2 { 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" containerBackground = "paleturquoise"
} }
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"%s\",style=\"filled,rounded\"];\n", containerName, containerName, truncate(container.Id, 12), 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") buffer.WriteString("}\n")
@ -152,6 +182,11 @@ func jsonContainersToDot(containers *[]Container) string {
return buffer.String() return buffer.String()
} }
func IsPrimaryContainerName(Name string,PrimaryContainerNames map[string]string) bool {
_,ok := PrimaryContainerNames[Name]
return ok
}
func init() { func init() {
parser.AddCommand("containers", parser.AddCommand("containers",
"Visualize docker containers.", "Visualize docker containers.",

100
images.go
View file

@ -26,19 +26,21 @@ type Image struct {
} }
type ImagesCommand struct { type ImagesCommand struct {
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]"` 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]"` 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)."` 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)."` 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."` 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."` OnlyLabelled bool `short:"l" long:"only-labelled" description:"Print only labelled images/containers."`
NoHuman bool `short:"c" long:"no-human" description:"Don't humanize the sizes."` 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 { type DisplayOpts struct {
NoTruncate bool NoTruncate bool
Incremental bool Incremental bool
NoHuman bool NoHuman bool
ShowCreatedBy bool
} }
var imagesCommand ImagesCommand var imagesCommand ImagesCommand
@ -156,16 +158,17 @@ func (x *ImagesCommand) Execute(args []string) error {
*images, imagesByParent = filterImages(images, &imagesByParent) *images, imagesByParent = filterImages(images, &imagesByParent)
} }
dispOpts := DisplayOpts{
imagesCommand.NoTruncate,
imagesCommand.Incremental,
imagesCommand.NoHuman,
imagesCommand.ShowCreatedBy,
}
if imagesCommand.Tree { if imagesCommand.Tree {
dispOpts := DisplayOpts{
imagesCommand.NoTruncate,
imagesCommand.Incremental,
imagesCommand.NoHuman,
}
fmt.Print(jsonToTree(roots, imagesByParent, dispOpts)) fmt.Print(jsonToTree(roots, imagesByParent, dispOpts))
} }
if imagesCommand.Dot { if imagesCommand.Dot {
fmt.Print(jsonToDot(roots, imagesByParent)) fmt.Print(jsonToDot(roots, imagesByParent, dispOpts))
} }
} else if imagesCommand.Short { } else if imagesCommand.Short {
@ -282,11 +285,11 @@ func jsonToTree(images []Image, byParent map[string][]Image, dispOpts DisplayOpt
return buffer.String() return buffer.String()
} }
func jsonToDot(roots []Image, byParent map[string][]Image) string { func jsonToDot(roots []Image, byParent map[string][]Image, dispOpts DisplayOpts) string {
var buffer bytes.Buffer var buffer bytes.Buffer
buffer.WriteString("digraph docker {\n") buffer.WriteString("digraph docker {\n")
imagesToDot(&buffer, roots, byParent) imagesToDot(&buffer, roots, byParent, dispOpts)
buffer.WriteString(" base [style=invisible]\n}\n") buffer.WriteString(" base [style=invisible]\n}\n")
return buffer.String() return buffer.String()
@ -399,10 +402,12 @@ func PrintTreeNode(buffer *bytes.Buffer, image Image, dispOpts DisplayOpts, pref
buffer.WriteString(fmt.Sprintf("%s%s %s: %s", prefix, imageID, sizeLabel, sizeStr)) buffer.WriteString(fmt.Sprintf("%s%s %s: %s", prefix, imageID, sizeLabel, sizeStr))
if image.RepoTags[0] != "<none>:<none>" { if image.RepoTags[0] != "<none>:<none>" {
buffer.WriteString(fmt.Sprintf(" Tags: %s\n", strings.Join(image.RepoTags, ", "))) buffer.WriteString(fmt.Sprintf(" Tags: %s", strings.Join(image.RepoTags, ", ")))
} else {
buffer.WriteString(fmt.Sprintf("\n"))
} }
if dispOpts.ShowCreatedBy {
buffer.WriteString(fmt.Sprintf(" (%s)", SanitizeCommand(image.CreatedBy, 100)))
}
buffer.WriteString(fmt.Sprintf("\n"))
} }
func humanSize(raw int64) string { func humanSize(raw int64) string {
@ -453,20 +458,45 @@ func parseImagesJSON(rawJSON []byte) (*[]Image, error) {
return &images, nil return &images, nil
} }
func imagesToDot(buffer *bytes.Buffer, images []Image, byParent map[string][]Image) { func imagesToDot(buffer *bytes.Buffer, images []Image, byParent map[string][]Image, dispOpts DisplayOpts) {
for _, image := range images { for _, image := range images {
if image.ParentId == "" { if image.ParentId == "" {
buffer.WriteString(fmt.Sprintf(" base -> \"%s\" [style=invis]\n", truncate(image.Id, 12))) buffer.WriteString(fmt.Sprintf(" base -> \"%s\" [style=invis]\n", truncate(image.Id, 12)))
} else { } else {
buffer.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\"\n", truncate(image.ParentId, 12), truncate(image.Id, 12))) buffer.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\"\n", truncate(image.ParentId, 12), truncate(image.Id, 12)))
} }
if image.RepoTags[0] != "<none>:<none>" { 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, 12), truncate(stripPrefix(image.OrigId), 12), strings.Join(image.RepoTags, "\\n"))) buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", truncate(image.Id, 12), truncate(stripPrefix(image.OrigId), 12), strings.Join(image.RepoTags, "\\n")))
} else { } else {
buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\"]\n", truncate(image.Id, 12), truncate(stripPrefix(image.OrigId), 12))) 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\"]\n", truncate(image.Id, 12), strings.Join(labelParts, "\n")))
} }
if subimages, exists := byParent[image.Id]; exists { if subimages, exists := byParent[image.Id]; exists {
imagesToDot(buffer, subimages, byParent) imagesToDot(buffer, subimages, byParent, dispOpts)
} }
} }
} }
@ -502,6 +532,28 @@ func jsonToShort(images *[]Image) string {
return buffer.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() { func init() {
parser.AddCommand("images", parser.AddCommand("images",
"Visualize docker images.", "Visualize docker images.",