2014-04-21 18:30:55 +02:00
package main
2014-04-22 06:04:38 +02:00
import (
2015-03-08 23:28:01 +01:00
"github.com/fsouza/go-dockerclient"
2014-04-25 06:09:44 +02:00
"bytes"
2014-04-22 06:04:38 +02:00
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
)
type Image struct {
Id string
ParentId string ` json:",omitempty" `
RepoTags [ ] string ` json:",omitempty" `
VirtualSize int64
Size int64
Created int64
}
2014-04-21 18:30:55 +02:00
type ImagesCommand struct {
2015-10-13 18:46:25 +02:00
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." `
OnlyLabelled bool ` short:"l" long:"only-labelled" description:"Print only labelled images/containers." `
2014-04-21 18:30:55 +02:00
}
var imagesCommand ImagesCommand
2015-10-12 21:58:21 +02:00
func ( x * ImagesCommand ) Execute ( args [ ] string ) error {
2015-03-08 23:28:01 +01:00
var images * [ ] Image
stat , err := os . Stdin . Stat ( )
2014-04-22 06:04:38 +02:00
if err != nil {
2015-03-08 23:28:01 +01:00
return fmt . Errorf ( "error reading stdin stat" , err )
2014-04-22 06:04:38 +02:00
}
2015-03-08 23:28:01 +01:00
if ( stat . Mode ( ) & os . ModeCharDevice ) == 0 {
// read in stdin
stdin , err := ioutil . ReadAll ( os . Stdin )
if err != nil {
return fmt . Errorf ( "error reading all input" , err )
}
images , err = parseImagesJSON ( stdin )
if err != nil {
return err
}
} else {
2015-05-21 21:48:06 +02:00
client , err := connect ( )
2015-05-21 23:33:45 +02:00
if err != nil {
return err
}
2015-03-08 23:28:01 +01:00
clientImages , err := client . ListImages ( docker . ListImagesOptions { All : true } )
if err != nil {
2015-08-18 15:33:22 +02:00
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'" )
} else {
return fmt . Errorf ( "Unable to connect: %s\nFor help, run 'dockviz help'" , err )
}
2015-03-08 23:28:01 +01:00
}
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 ,
} )
}
images = & ims
2014-05-13 16:06:20 +02:00
}
2014-04-22 06:04:38 +02:00
2015-10-12 21:58:21 +02:00
if imagesCommand . Tree || imagesCommand . Dot {
var startImage * Image
2014-05-04 23:22:25 +02:00
if len ( args ) > 0 {
2015-08-02 05:29:29 +02:00
// attempt to find the start image, which can be specified as an
// image ID or a repository name
startImageArg := args [ 0 ]
startImageRepo := args [ 0 ]
2015-10-11 18:29:48 +02:00
// if tag is not defined, find by :latest tag
if strings . Index ( startImageRepo , ":" ) == - 1 {
2015-08-02 05:29:29 +02:00
startImageRepo = fmt . Sprintf ( "%s:latest" , startImageRepo )
}
IMAGES :
for _ , image := range * images {
2015-10-11 18:29:48 +02:00
// find by image id
2015-08-02 05:29:29 +02:00
if strings . Index ( image . Id , startImageArg ) == 0 {
2015-10-12 21:58:21 +02:00
startImage = & image
2015-08-02 05:29:29 +02:00
break IMAGES
}
2015-10-11 18:29:48 +02:00
// find by image name (name and tag)
for _ , repotag := range image . RepoTags {
if repotag == startImageRepo {
2015-10-12 21:58:21 +02:00
startImage = & image
2015-10-11 18:29:48 +02:00
break IMAGES
2015-08-02 05:29:29 +02:00
}
}
}
2015-10-12 21:58:21 +02:00
if startImage == nil {
2015-10-11 18:29:48 +02:00
return fmt . Errorf ( "Unable to find image %s = %s." , startImageArg , startImageRepo )
2015-08-02 05:29:29 +02:00
}
2014-05-04 23:22:25 +02:00
}
2015-10-12 21:58:21 +02:00
// select the start image of the tree
var roots [ ] Image
if startImage == nil {
roots = collectRoots ( images )
} else {
2015-10-13 17:57:26 +02:00
startImage . ParentId = ""
2015-10-12 21:58:21 +02:00
roots = [ ] Image { * startImage }
}
// build helper map (image -> children)
var imagesByParent = make ( map [ string ] [ ] Image )
imagesByParent = collectChildren ( images ) ;
// image ids truncate
// initialize image informations
// filter images
2015-10-13 18:46:25 +02:00
if imagesCommand . OnlyLabelled {
2015-10-12 21:58:21 +02:00
* images , imagesByParent = filterImages ( images , & imagesByParent )
}
var buffer bytes . Buffer
if imagesCommand . Tree {
2015-10-12 22:07:54 +02:00
jsonToText ( & buffer , imagesCommand . NoTruncate , roots , imagesByParent , "" )
2015-10-12 21:58:21 +02:00
}
if imagesCommand . Dot {
2015-10-13 17:57:26 +02:00
buffer . WriteString ( "digraph docker {\n" )
imagesToDot ( & buffer , roots , imagesByParent )
buffer . WriteString ( " base [style=invisible]\n}\n" )
buffer . String ( )
2015-10-12 21:58:21 +02:00
}
fmt . Print ( buffer . String ( ) )
2015-02-27 06:06:24 +01:00
} else if imagesCommand . Short {
fmt . Printf ( jsonToShort ( images ) )
2014-05-13 05:30:39 +02:00
} else {
2015-02-27 06:06:24 +01:00
return fmt . Errorf ( "Please specify either --dot, --tree, or --short" )
2014-04-21 18:30:55 +02:00
}
return nil
}
2014-05-04 23:22:25 +02:00
2015-10-12 21:58:21 +02:00
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
}
2014-05-04 23:22:25 +02:00
2015-10-12 21:58:21 +02:00
func collectRoots ( images * [ ] Image ) [ ] Image {
2014-05-04 23:22:25 +02:00
var roots [ ] Image
for _ , image := range * images {
if image . ParentId == "" {
roots = append ( roots , image )
}
2015-10-12 21:58:21 +02:00
}
return roots
}
2014-05-04 23:22:25 +02:00
2015-10-12 21:58:21 +02:00
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
2014-05-04 23:22:25 +02:00
}
}
}
}
2015-10-12 21:58:21 +02:00
filteredChildren = collectChildren ( & filteredImages )
return filteredImages , filteredChildren
2014-05-04 23:22:25 +02:00
}
2015-10-12 22:07:54 +02:00
func jsonToText ( buffer * bytes . Buffer , noTrunc bool , images [ ] Image , byParent map [ string ] [ ] Image , prefix string ) {
2015-10-11 12:52:26 +02:00
var length = len ( images )
if length > 1 {
2014-05-04 23:22:25 +02:00
for index , image := range images {
2015-10-11 12:52:26 +02:00
var nextPrefix string = ""
2015-10-12 21:58:21 +02:00
if index + 1 == length {
PrintTreeNode ( buffer , noTrunc , image , prefix + "└─" )
nextPrefix = " "
} else {
PrintTreeNode ( buffer , noTrunc , image , prefix + "├─" )
nextPrefix = "│ "
2015-10-11 12:52:26 +02:00
}
if subimages , exists := byParent [ image . Id ] ; exists {
2015-10-12 22:07:54 +02:00
jsonToText ( buffer , noTrunc , subimages , byParent , prefix + nextPrefix )
2014-05-04 23:22:25 +02:00
}
}
} else {
for _ , image := range images {
2015-10-12 21:58:21 +02:00
PrintTreeNode ( buffer , noTrunc , image , prefix + "└─" )
2014-05-04 23:22:25 +02:00
if subimages , exists := byParent [ image . Id ] ; exists {
2015-10-12 22:07:54 +02:00
jsonToText ( buffer , noTrunc , subimages , byParent , prefix + " " )
2014-05-04 23:22:25 +02:00
}
}
}
}
2015-10-12 21:58:21 +02:00
2014-05-04 23:22:25 +02:00
func PrintTreeNode ( buffer * bytes . Buffer , noTrunc bool , image Image , prefix string ) {
var imageID string
if noTrunc {
imageID = image . Id
} else {
imageID = truncate ( image . Id )
}
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 , ", " ) ) )
} else {
buffer . WriteString ( fmt . Sprintf ( "\n" ) )
}
}
func humanSize ( raw int64 ) string {
sizes := [ ] string { "B" , "KB" , "MB" , "GB" , "TB" }
rawFloat := float64 ( raw )
ind := 0
for {
if rawFloat < 1000 {
break
} else {
rawFloat = rawFloat / 1000
ind = ind + 1
}
}
return fmt . Sprintf ( "%.01f %s" , rawFloat , sizes [ ind ] )
}
2015-10-12 21:58:21 +02:00
2014-04-22 06:04:38 +02:00
func truncate ( id string ) string {
return id [ 0 : 12 ]
}
2015-10-12 21:58:21 +02:00
2014-05-13 16:06:20 +02:00
func parseImagesJSON ( rawJSON [ ] byte ) ( * [ ] Image , error ) {
2014-04-25 06:09:44 +02:00
var images [ ] Image
err := json . Unmarshal ( rawJSON , & images )
if err != nil {
return nil , fmt . Errorf ( "Error reading JSON: " , err )
}
return & images , nil
}
2015-10-13 17:57:26 +02:00
func imagesToDot ( buffer * bytes . Buffer , images [ ] Image , byParent map [ string ] [ ] Image ) {
for _ , image := range images {
2014-04-25 06:09:44 +02:00
if image . ParentId == "" {
buffer . WriteString ( fmt . Sprintf ( " base -> \"%s\" [style=invis]\n" , truncate ( image . Id ) ) )
} else {
buffer . WriteString ( fmt . Sprintf ( " \"%s\" -> \"%s\"\n" , truncate ( image . ParentId ) , truncate ( image . Id ) ) )
}
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" ) ) )
}
2015-10-13 17:57:26 +02:00
if subimages , exists := byParent [ image . Id ] ; exists {
imagesToDot ( buffer , subimages , byParent )
}
2014-04-25 06:09:44 +02:00
}
}
2015-10-12 21:58:21 +02:00
2015-02-27 06:06:24 +01:00
func jsonToShort ( images * [ ] Image ) string {
var buffer bytes . Buffer
var byRepo = make ( map [ string ] [ ] string )
for _ , image := range * images {
for _ , repotag := range image . RepoTags {
if repotag != "<none>:<none>" {
// parse the repo name and tag name out
// tag is after the last colon
lastColonIndex := strings . LastIndex ( repotag , ":" )
tagname := repotag [ lastColonIndex + 1 : ]
reponame := repotag [ 0 : lastColonIndex ]
if tags , exists := byRepo [ reponame ] ; exists {
byRepo [ reponame ] = append ( tags , tagname )
} else {
byRepo [ reponame ] = [ ] string { tagname }
}
}
}
}
for repo , tags := range byRepo {
buffer . WriteString ( fmt . Sprintf ( "%s: %s\n" , repo , strings . Join ( tags , ", " ) ) )
}
return buffer . String ( )
}
2015-10-12 21:58:21 +02:00
2014-04-21 18:30:55 +02:00
func init ( ) {
parser . AddCommand ( "images" ,
"Visualize docker images." ,
"" ,
& imagesCommand )
}