dive-zfs/image/image.go

314 lines
6.7 KiB
Go

package image
import (
"archive/tar"
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/client"
"github.com/wagoodman/dive/filetree"
"golang.org/x/net/context"
)
const (
LayerFormat = "%-25s %5s %7s %s"
)
func check(e error) {
if e != nil {
panic(e)
}
}
type ImageManifest struct {
ConfigPath string `json:"Config"`
RepoTags []string `json:"RepoTags"`
LayerTarPaths []string `json:"Layers"`
}
type ImageConfig struct {
History []ImageHistoryEntry `json:"history"`
RootFs RootFs `json:"rootfs"`
}
type RootFs struct {
Type string `json:"type"`
DiffIds []string `json:"diff_ids"`
}
type ImageHistoryEntry struct {
ID string
Size uint64
Created string `json:"created"`
Author string `json:"author"`
CreatedBy string `json:"created_by"`
EmptyLayer bool `json:"empty_layer"`
}
func NewImageManifest(reader *tar.Reader, header *tar.Header) ImageManifest {
size := header.Size
manifestBytes := make([]byte, size)
_, err := reader.Read(manifestBytes)
if err != nil && err != io.EOF {
panic(err)
}
var manifest []ImageManifest
err = json.Unmarshal(manifestBytes, &manifest)
if err != nil {
panic(err)
}
return manifest[0]
}
func NewImageConfig(reader *tar.Reader, header *tar.Header) ImageConfig {
size := header.Size
configBytes := make([]byte, size)
_, err := reader.Read(configBytes)
if err != nil && err != io.EOF {
panic(err)
}
var imageConfig ImageConfig
err = json.Unmarshal(configBytes, &imageConfig)
if err != nil {
panic(err)
}
layerIdx := 0
for idx := range imageConfig.History {
if imageConfig.History[idx].EmptyLayer {
imageConfig.History[idx].ID = "<missing>"
} else {
imageConfig.History[idx].ID = imageConfig.RootFs.DiffIds[layerIdx]
layerIdx++
}
}
return imageConfig
}
func GetImageConfig(imageTarPath string, manifest ImageManifest) ImageConfig{
var config ImageConfig
// read through the image contents and build a tree
fmt.Println("Fetching image config...")
tarFile, err := os.Open(imageTarPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer tarFile.Close()
tarReader := tar.NewReader(tarFile)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
if name == manifest.ConfigPath {
config = NewImageConfig(tarReader, header)
}
}
// obtain the image history
return config
}
func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
var manifest ImageManifest
var layerMap = make(map[string]*filetree.FileTree)
var trees []*filetree.FileTree = make([]*filetree.FileTree, 0)
// save this image to disk temporarily to get the content info
// fmt.Println("Fetching image...")
imageTarPath, tmpDir := saveImage(imageID)
// imageTarPath := "/tmp/dive932744808/image.tar"
// tmpDir := "/tmp/dive031537738"
// fmt.Println(tmpDir)
defer os.RemoveAll(tmpDir)
// read through the image contents and build a tree
fmt.Println("Reading image...")
tarFile, err := os.Open(imageTarPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer tarFile.Close()
tarReader := tar.NewReader(tarFile)
for {
header, err := tarReader.Next()
// log.Debug(header.Name)
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
if name == "manifest.json" {
manifest = NewImageManifest(tarReader, header)
}
switch header.Typeflag {
case tar.TypeDir:
continue
case tar.TypeReg:
// todo: process this loop in parallel, visualize with jotframe
if strings.HasSuffix(name, "layer.tar") {
tree := filetree.NewFileTree()
tree.Name = name
fileInfos := getFileList(tarReader, header)
for _, element := range fileInfos {
tree.FileSize += uint64(element.TarHeader.FileInfo().Size())
tree.AddPath(element.Path, element)
}
layerMap[tree.Name] = tree
}
default:
fmt.Printf("ERRG: unknown tar entry: %v: %s\n", header.Typeflag, name)
}
}
// obtain the image history
config := GetImageConfig(imageTarPath, manifest)
// build the content tree
fmt.Println("Building tree...")
for _, treeName := range manifest.LayerTarPaths {
trees = append(trees, layerMap[treeName])
}
// build the layers array
layers := make([]*Layer, len(trees))
// note that the image config stores images in reverse chronological order, so iterate backwards through layers
// as you iterate chronologically through history (ignoring history items that have no layer contents)
layerIdx := len(trees)-1
for idx := 0; idx < len(config.History); idx++ {
// ignore empty layers, we are only observing layers with content
if config.History[idx].EmptyLayer {
continue
}
config.History[idx].Size = uint64(trees[(len(trees)-1)-layerIdx].FileSize)
layers[layerIdx] = &Layer{
History: config.History[idx],
Index: layerIdx,
Tree: trees[layerIdx],
RefTrees: trees,
}
if len(manifest.LayerTarPaths) > idx {
layers[layerIdx].TarPath = manifest.LayerTarPaths[layerIdx]
}
layerIdx--
}
return layers, trees
}
func saveImage(imageID string) (string, string) {
ctx := context.Background()
dockerClient, err := client.NewClientWithOpts()
if err != nil {
panic(err)
}
readCloser, err := dockerClient.ImageSave(ctx, []string{imageID})
check(err)
defer readCloser.Close()
tmpDir, err := ioutil.TempDir("", "dive")
check(err)
imageTarPath := filepath.Join(tmpDir, "image.tar")
imageFile, err := os.Create(imageTarPath)
check(err)
defer func() {
if err := imageFile.Close(); err != nil {
panic(err)
}
}()
imageWriter := bufio.NewWriter(imageFile)
buf := make([]byte, 1024)
for {
n, err := readCloser.Read(buf)
if err != nil && err != io.EOF {
panic(err)
}
if n == 0 {
break
}
if _, err := imageWriter.Write(buf[:n]); err != nil {
panic(err)
}
}
if err = imageWriter.Flush(); err != nil {
panic(err)
}
return imageTarPath, tmpDir
}
func getFileList(parentReader *tar.Reader, header *tar.Header) []filetree.FileInfo {
var files []filetree.FileInfo
var tarredBytes = make([]byte, header.Size)
_, err := parentReader.Read(tarredBytes)
if err != nil && err != io.EOF {
panic(err)
}
reader := bytes.NewReader(tarredBytes)
tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
switch header.Typeflag {
case tar.TypeXGlobalHeader:
fmt.Printf("ERRG: XGlobalHeader: %v: %s\n", header.Typeflag, name)
case tar.TypeXHeader:
fmt.Printf("ERRG: XHeader: %v: %s\n", header.Typeflag, name)
default:
files = append(files, filetree.NewFileInfo(tarReader, header, name))
}
}
return files
}