Skip to content

Commit 08d8251

Browse files
authored
Merge pull request #30 from thespad/containerd-patch
Patch in support for containerd image store
2 parents 84e5b43 + cb902b5 commit 08d8251

File tree

2 files changed

+260
-1
lines changed

2 files changed

+260
-1
lines changed

Dockerfile

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ RUN \
4646
"https://github.com/wagoodman/dive/archive/${APP_VERSION}.tar.gz" && \
4747
tar xf \
4848
/tmp/dive.tar.gz -C \
49-
/tmp/dive/ --strip-components=1 && \
49+
/tmp/dive/ --strip-components=1
50+
51+
COPY patch/image_archive.go /tmp/dive/dive/image/docker/image_archive.go
52+
53+
RUN \
5054
cd /tmp/dive && \
5155
go build -o /usr/local/bin/dive && \
5256
echo "**** installed dive version ${APP_VERSION} ****" && \

patch/image_archive.go

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package docker
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"compress/gzip"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"os"
11+
"path"
12+
"strings"
13+
14+
"github.com/wagoodman/dive/dive/filetree"
15+
"github.com/wagoodman/dive/dive/image"
16+
)
17+
18+
type ImageArchive struct {
19+
manifest manifest
20+
config config
21+
layerMap map[string]*filetree.FileTree
22+
}
23+
24+
func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
25+
img := &ImageArchive{
26+
layerMap: make(map[string]*filetree.FileTree),
27+
}
28+
29+
tarReader := tar.NewReader(tarFile)
30+
31+
// store discovered json files in a map so we can read the image in one pass
32+
jsonFiles := make(map[string][]byte)
33+
34+
var currentLayer uint
35+
for {
36+
header, err := tarReader.Next()
37+
38+
if err == io.EOF {
39+
break
40+
}
41+
42+
if err != nil {
43+
fmt.Println(err)
44+
os.Exit(1)
45+
}
46+
47+
name := header.Name
48+
49+
// some layer tars can be relative layer symlinks to other layer tars
50+
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {
51+
// For the Docker image format, use file name conventions
52+
if strings.HasSuffix(name, ".tar") {
53+
currentLayer++
54+
layerReader := tar.NewReader(tarReader)
55+
tree, err := processLayerTar(name, layerReader)
56+
if err != nil {
57+
return img, err
58+
}
59+
60+
// add the layer to the image
61+
img.layerMap[tree.Name] = tree
62+
} else if strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, "tgz") {
63+
currentLayer++
64+
65+
// Add gzip reader
66+
gz, err := gzip.NewReader(tarReader)
67+
if err != nil {
68+
return img, err
69+
}
70+
71+
// Add tar reader
72+
layerReader := tar.NewReader(gz)
73+
74+
// Process layer
75+
tree, err := processLayerTar(name, layerReader)
76+
if err != nil {
77+
return img, err
78+
}
79+
80+
// add the layer to the image
81+
img.layerMap[tree.Name] = tree
82+
} else if strings.HasSuffix(name, ".json") || strings.HasPrefix(name, "sha256:") {
83+
fileBuffer, err := io.ReadAll(tarReader)
84+
if err != nil {
85+
return img, err
86+
}
87+
jsonFiles[name] = fileBuffer
88+
} else if strings.HasPrefix(name, "blobs/") {
89+
// For the OCI-compatible image format (used since Docker 25), use mime sniffing
90+
// but limit this to only the blobs/ (containing the config, and the layers)
91+
92+
// The idea here is that we try various formats in turn, and those tries should
93+
// never consume more bytes than this buffer contains so we can start again.
94+
95+
// 512 bytes ought to be enough (as that's the size of a TAR entry header),
96+
// but play it safe with 1024 bytes. This should also include very small layers
97+
// (unless they've also been gzipped, but Docker does not appear to do it)
98+
buffer := make([]byte, 1024)
99+
n, err := io.ReadFull(tarReader, buffer)
100+
if err != nil && err != io.ErrUnexpectedEOF {
101+
return img, err
102+
}
103+
104+
var unwrappedReader io.Reader
105+
unwrappedReader, err = gzip.NewReader(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
106+
if err != nil {
107+
// Not a gzipped entry
108+
unwrappedReader = io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)
109+
}
110+
111+
// Try reading a TAR
112+
layerReader := tar.NewReader(unwrappedReader)
113+
tree, err := processLayerTar(name, layerReader)
114+
if err == nil {
115+
currentLayer++
116+
// add the layer to the image
117+
img.layerMap[tree.Name] = tree
118+
continue
119+
}
120+
121+
// Not a TAR (or smaller than our buffer), might be a JSON file
122+
decoder := json.NewDecoder(bytes.NewReader(buffer[:n]))
123+
token, err := decoder.Token()
124+
if _, ok := token.(json.Delim); err == nil && ok {
125+
// Looks like a JSON object (or array)
126+
// XXX: should we add a header.Size check too?
127+
fileBuffer, err := io.ReadAll(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
128+
if err != nil {
129+
return img, err
130+
}
131+
jsonFiles[name] = fileBuffer
132+
}
133+
// Ignore every other unknown file type
134+
}
135+
}
136+
}
137+
138+
manifestContent, exists := jsonFiles["manifest.json"]
139+
if !exists {
140+
return img, fmt.Errorf("could not find image manifest")
141+
}
142+
143+
img.manifest = newManifest(manifestContent)
144+
145+
configContent, exists := jsonFiles[img.manifest.ConfigPath]
146+
if !exists {
147+
return img, fmt.Errorf("could not find image config")
148+
}
149+
150+
img.config = newConfig(configContent)
151+
152+
return img, nil
153+
}
154+
155+
func processLayerTar(name string, reader *tar.Reader) (*filetree.FileTree, error) {
156+
tree := filetree.NewFileTree()
157+
tree.Name = name
158+
159+
fileInfos, err := getFileList(reader)
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
for _, element := range fileInfos {
165+
tree.FileSize += uint64(element.Size)
166+
167+
_, _, err := tree.AddPath(element.Path, element)
168+
if err != nil {
169+
return nil, err
170+
}
171+
}
172+
173+
return tree, nil
174+
}
175+
176+
func getFileList(tarReader *tar.Reader) ([]filetree.FileInfo, error) {
177+
var files []filetree.FileInfo
178+
179+
for {
180+
header, err := tarReader.Next()
181+
if err == io.EOF {
182+
break
183+
} else if err != nil {
184+
return nil, err
185+
}
186+
187+
// always ensure relative path notations are not parsed as part of the filename
188+
name := path.Clean(header.Name)
189+
if name == "." {
190+
continue
191+
}
192+
193+
switch header.Typeflag {
194+
case tar.TypeXGlobalHeader:
195+
return nil, fmt.Errorf("unexptected tar file: (XGlobalHeader): type=%v name=%s", header.Typeflag, name)
196+
case tar.TypeXHeader:
197+
return nil, fmt.Errorf("unexptected tar file (XHeader): type=%v name=%s", header.Typeflag, name)
198+
default:
199+
files = append(files, filetree.NewFileInfoFromTarHeader(tarReader, header, name))
200+
}
201+
}
202+
return files, nil
203+
}
204+
205+
func (img *ImageArchive) ToImage() (*image.Image, error) {
206+
trees := make([]*filetree.FileTree, 0)
207+
208+
// build the content tree
209+
for _, treeName := range img.manifest.LayerTarPaths {
210+
tr, exists := img.layerMap[treeName]
211+
if exists {
212+
trees = append(trees, tr)
213+
continue
214+
}
215+
return nil, fmt.Errorf("could not find '%s' in parsed layers", treeName)
216+
}
217+
218+
// build the layers array
219+
layers := make([]*image.Layer, 0)
220+
221+
// note that the engineResolver config stores images in reverse chronological order, so iterate backwards through layers
222+
// as you iterate chronologically through history (ignoring history items that have no layer contents)
223+
// Note: history is not required metadata in a docker image!
224+
histIdx := 0
225+
for idx, tree := range trees {
226+
// ignore empty layers, we are only observing layers with content
227+
historyObj := historyEntry{
228+
CreatedBy: "(missing)",
229+
}
230+
for nextHistIdx := histIdx; nextHistIdx < len(img.config.History); nextHistIdx++ {
231+
if !img.config.History[nextHistIdx].EmptyLayer {
232+
histIdx = nextHistIdx
233+
break
234+
}
235+
}
236+
if histIdx < len(img.config.History) && !img.config.History[histIdx].EmptyLayer {
237+
historyObj = img.config.History[histIdx]
238+
histIdx++
239+
}
240+
241+
historyObj.Size = tree.FileSize
242+
243+
dockerLayer := layer{
244+
history: historyObj,
245+
index: idx,
246+
tree: tree,
247+
}
248+
layers = append(layers, dockerLayer.ToLayer())
249+
}
250+
251+
return &image.Image{
252+
Trees: trees,
253+
Layers: layers,
254+
}, nil
255+
}

0 commit comments

Comments
 (0)