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"
2017-05-22 03:55:01 +02:00
"github.com/dustin/go-humanize"
2015-03-08 23:28:01 +01:00
2014-04-25 06:09:44 +02:00
"bytes"
2016-05-01 22:53:06 +02:00
"crypto/sha256"
"encoding/hex"
2014-04-22 06:04:38 +02:00
"encoding/json"
"fmt"
"io/ioutil"
"os"
2016-05-01 22:53:06 +02:00
"strconv"
2014-04-22 06:04:38 +02:00
"strings"
)
type Image struct {
Id string
ParentId string ` json:",omitempty" `
RepoTags [ ] string ` json:",omitempty" `
VirtualSize int64
Size int64
Created int64
2016-05-01 22:53:06 +02:00
OrigId string
CreatedBy string
2014-04-22 06:04:38 +02:00
}
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)." `
2016-06-14 05:31:16 +02:00
NoTruncate bool ` short:"n" long:"no-trunc" description:"Don't truncate the image IDs (only works with tree mode)." `
2015-11-08 20:33:00 +01:00
Incremental bool ` short:"i" long:"incremental" description:"Display image size as incremental rather than cumulative." `
2015-10-13 18:46:25 +02:00
OnlyLabelled bool ` short:"l" long:"only-labelled" description:"Print only labelled images/containers." `
2016-06-11 06:13:42 +02:00
NoHuman bool ` short:"c" long:"no-human" description:"Don't humanize the sizes." `
}
type DisplayOpts struct {
NoTruncate bool
Incremental bool
NoHuman bool
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
}
2016-12-08 21:18:12 +01:00
if globalOptions . Stdin && ( stat . Mode ( ) & os . ModeCharDevice ) == 0 {
2015-03-08 23:28:01 +01:00
// 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
}
2016-06-11 06:14:36 +02:00
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
2015-03-08 23:28:01 +01:00
} 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
2016-05-01 22:53:06 +02:00
ver , err := getAPIVersion ( client )
2015-03-08 23:28:01 +01:00
if err != nil {
2015-08-18 15:33:22 +02:00
if in_docker := os . Getenv ( "IN_DOCKER" ) ; len ( in_docker ) > 0 {
2016-06-14 05:48:21 +02:00
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'" )
2015-08-18 15:33:22 +02:00
} else {
return fmt . Errorf ( "Unable to connect: %s\nFor help, run 'dockviz help'" , err )
}
2015-03-08 23:28:01 +01:00
}
2016-05-01 22:53:06 +02:00
if ver [ 0 ] == 1 && ver [ 1 ] <= 21 {
clientImages , err := client . ListImages ( docker . ListImagesOptions { All : true } )
if err != nil {
return err
}
2015-03-08 23:28:01 +01:00
2016-05-01 22:53:06 +02:00
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
}
}
2014-05-13 16:06:20 +02:00
}
2014-04-22 06:04:38 +02:00
2015-11-08 03:32:49 +01:00
if imagesCommand . Tree || imagesCommand . Dot {
2015-10-12 21:58:21 +02:00
var startImage * Image
2014-05-04 23:22:25 +02:00
if len ( args ) > 0 {
2015-11-08 05:02:21 +01:00
startImage , err = findStartImage ( args [ 0 ] , images )
2015-08-02 05:29:29 +02:00
2015-11-08 05:02:21 +01:00
if err != nil {
return err
2015-08-02 05:29:29 +02:00
}
2014-05-04 23:22:25 +02:00
}
2015-11-08 03:32:49 +01: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 }
}
2015-11-08 03:32:49 +01:00
2015-10-12 21:58:21 +02:00
// build helper map (image -> children)
2015-11-08 05:02:21 +01:00
imagesByParent := collectChildren ( images )
2015-11-08 03:32:49 +01:00
2015-10-12 21:58:21 +02:00
// filter images
2015-11-08 03:32:49 +01:00
if imagesCommand . OnlyLabelled {
2015-10-12 21:58:21 +02:00
* images , imagesByParent = filterImages ( images , & imagesByParent )
}
2015-11-08 03:32:49 +01:00
2015-10-12 21:58:21 +02:00
if imagesCommand . Tree {
2016-06-11 06:13:42 +02:00
dispOpts := DisplayOpts {
imagesCommand . NoTruncate ,
imagesCommand . Incremental ,
imagesCommand . NoHuman ,
}
fmt . Print ( jsonToTree ( roots , imagesByParent , dispOpts ) )
2015-10-12 21:58:21 +02:00
}
if imagesCommand . Dot {
2015-11-08 05:02:21 +01:00
fmt . Print ( jsonToDot ( roots , imagesByParent ) )
2015-10-12 21:58:21 +02:00
}
2015-11-08 03:32:49 +01:00
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
}
2016-05-01 22:53:06 +02:00
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 ) ) )
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
}
}
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
}
2015-11-08 05:02:21 +01:00
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
}
2016-06-11 06:13:42 +02:00
func jsonToTree ( images [ ] Image , byParent map [ string ] [ ] Image , dispOpts DisplayOpts ) string {
2015-11-08 05:02:21 +01:00
var buffer bytes . Buffer
2016-06-11 06:13:42 +02:00
jsonToText ( & buffer , images , byParent , dispOpts , "" )
2015-11-08 05:02:21 +01:00
return buffer . String ( )
}
func jsonToDot ( roots [ ] Image , byParent map [ string ] [ ] Image ) string {
var buffer bytes . Buffer
buffer . WriteString ( "digraph docker {\n" )
imagesToDot ( & buffer , roots , byParent )
buffer . WriteString ( " base [style=invisible]\n}\n" )
return buffer . String ( )
}
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 }
}
}
2015-11-08 03:32:49 +01:00
2015-10-12 21:58:21 +02:00
return imagesByParent
}
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
}
2015-11-08 03:32:49 +01:00
2015-10-12 21:58:21 +02:00
return roots
}
2014-05-04 23:22:25 +02:00
2015-11-08 03:32:49 +01: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
2015-10-12 21:58:21 +02:00
// 1. it has a label
// 2. it is root
2015-11-08 03:32:49 +01:00
// 3. it is a node
2015-10-12 21:58:21 +02:00
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
2015-11-08 03:32:49 +01:00
for j := 0 ; j < len ( filteredImages ) ; j ++ {
2015-10-12 21:58:21 +02:00
if filteredImages [ j ] . ParentId == ( * images ) [ i ] . Id {
filteredImages [ j ] . ParentId = ( * images ) [ i ] . ParentId
}
}
2015-11-08 03:32:49 +01:00
for j := 0 ; j < len ( * images ) ; j ++ {
2015-10-12 21:58:21 +02:00
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 )
2015-11-08 03:32:49 +01:00
2015-10-12 21:58:21 +02:00
return filteredImages , filteredChildren
2014-05-04 23:22:25 +02:00
}
2016-06-11 06:13:42 +02:00
func jsonToText ( buffer * bytes . Buffer , images [ ] Image , byParent map [ string ] [ ] Image , dispOpts DisplayOpts , 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 {
2016-06-11 06:13:42 +02:00
PrintTreeNode ( buffer , image , dispOpts , prefix + "└─" )
2015-10-12 21:58:21 +02:00
nextPrefix = " "
} else {
2016-06-11 06:13:42 +02:00
PrintTreeNode ( buffer , image , dispOpts , prefix + "├─" )
2015-10-12 21:58:21 +02:00
nextPrefix = "│ "
2015-10-11 12:52:26 +02:00
}
if subimages , exists := byParent [ image . Id ] ; exists {
2016-06-11 06:13:42 +02:00
jsonToText ( buffer , subimages , byParent , dispOpts , prefix + nextPrefix )
2014-05-04 23:22:25 +02:00
}
}
} else {
for _ , image := range images {
2016-06-11 06:13:42 +02:00
PrintTreeNode ( buffer , image , dispOpts , prefix + "└─" )
2014-05-04 23:22:25 +02:00
if subimages , exists := byParent [ image . Id ] ; exists {
2016-06-11 06:13:42 +02:00
jsonToText ( buffer , subimages , byParent , dispOpts , prefix + " " )
2014-05-04 23:22:25 +02:00
}
}
}
}
2016-06-11 06:13:42 +02:00
func PrintTreeNode ( buffer * bytes . Buffer , image Image , dispOpts DisplayOpts , prefix string ) {
2014-05-04 23:22:25 +02:00
var imageID string
2016-06-11 06:13:42 +02:00
if dispOpts . NoTruncate {
2016-05-01 22:53:06 +02:00
imageID = image . OrigId
2014-05-04 23:22:25 +02:00
} else {
2016-06-14 05:28:13 +02:00
imageID = truncate ( stripPrefix ( image . OrigId ) , 12 )
2014-05-04 23:22:25 +02:00
}
2015-11-08 20:33:00 +01:00
var size int64
2016-05-05 19:58:14 +02:00
var sizeLabel string
2016-06-11 06:13:42 +02:00
if dispOpts . Incremental {
2016-05-05 19:58:14 +02:00
sizeLabel = "Size"
2015-11-08 20:33:00 +01:00
size = image . Size
} else {
2016-05-05 19:58:14 +02:00
sizeLabel = "Virtual Size"
2015-11-08 20:33:00 +01:00
size = image . VirtualSize
}
2016-06-11 06:13:42 +02:00
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 ) )
2014-05-04 23:22:25 +02:00
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 ] )
}
2016-05-01 22:53:06 +02:00
func truncate ( id string , length int ) string {
if len ( id ) > length {
return id [ 0 : length ]
} else if len ( id ) > 0 {
return id
} else {
return ""
}
2014-04-22 06:04:38 +02:00
}
2016-06-14 05:28:13 +02:00
func stripPrefix ( id string ) string {
if strings . Contains ( id , ":" ) {
idParts := strings . Split ( id , ":" )
return idParts [ len ( idParts ) - 1 ]
}
return id
}
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 {
2017-05-22 03:55:01 +02:00
2014-04-25 06:09:44 +02:00
if image . ParentId == "" {
2016-05-01 22:53:06 +02:00
buffer . WriteString ( fmt . Sprintf ( " base -> \"%s\" [style=invis]\n" , truncate ( image . Id , 12 ) ) )
2014-04-25 06:09:44 +02:00
} else {
2016-05-01 22:53:06 +02:00
buffer . WriteString ( fmt . Sprintf ( " \"%s\" -> \"%s\"\n" , truncate ( image . ParentId , 12 ) , truncate ( image . Id , 12 ) ) )
2014-04-25 06:09:44 +02:00
}
if image . RepoTags [ 0 ] != "<none>:<none>" {
2016-06-14 05:28:13 +02:00
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" ) ) )
2016-06-11 05:42:45 +02:00
} else {
2017-05-22 03:55:01 +02:00
// show partial command and size to make up for
// the fact that since Docker 1.10 content addressing
// image ids are usually empty and report as <missing>
2017-05-22 17:44:33 +02:00
SanitizedCommand := SanitizeCommand ( image . CreatedBy , 30 )
2017-05-22 03:55:01 +02:00
buffer . WriteString ( fmt . Sprintf ( " \"%s\" [label=\"%s\"]\n" , truncate ( image . Id , 12 ) , truncate ( stripPrefix ( image . OrigId ) , 12 ) + "\n" + SanitizedCommand + "\n" + humanize . Bytes ( uint64 ( image . Size ) ) ) )
2014-04-25 06:09:44 +02:00
}
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-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 ( )
}
2017-05-22 03:55:01 +02:00
func SanitizeCommand ( CommandStr string , MaxLength int ) string {
temp := CommandStr
2017-05-22 17:44:33 +02:00
// remove prefixes that don't add meaning
2017-05-22 03:55:01 +02:00
if ( strings . HasPrefix ( temp , "/bin/sh -c" ) ) {
temp = strings . TrimSpace ( temp [ 10 : ] )
}
if ( strings . HasPrefix ( temp , "#(nop)" ) ) {
temp = strings . TrimSpace ( temp [ 6 : ] )
}
2017-05-22 17:44:33 +02:00
// remove double and single quotes which make dot format invalid
2017-05-22 03:55:01 +02:00
temp = strings . Replace ( temp , "\"" , " " , - 1 )
temp = strings . Replace ( temp , "'" , " " , - 1 )
2017-05-22 17:44:33 +02:00
// remove double spaces inside
temp = strings . Join ( strings . Fields ( temp ) , " " )
2017-05-22 03:55:01 +02:00
return truncate ( temp , MaxLength )
}
2017-05-22 17:44:33 +02:00
2014-04-21 18:30:55 +02:00
func init ( ) {
parser . AddCommand ( "images" ,
"Visualize docker images." ,
"" ,
& imagesCommand )
}