Skip to content

Commit 01ba0a9

Browse files
tonistiigiaaronlehmann
authored andcommittedNov 24, 2015
Add image store
The image store abstracts image handling. It keeps track of the available images, and makes it possible to delete existing images or register new ones. The image store holds references to the underlying layers for each image. The image/v1 package provides compatibility functions for interoperating with older (non-content-addressable) image structures. Signed-off-by: Tonis Tiigi <[email protected]>
1 parent 7de380c commit 01ba0a9

23 files changed

+2025
-140
lines changed
 

‎image/fixtures/post1.9/expected_computed_id

-1
This file was deleted.

‎image/fixtures/post1.9/expected_config

-1
This file was deleted.

‎image/fixtures/post1.9/layer_id

-1
This file was deleted.

‎image/fixtures/post1.9/parent_id

-1
This file was deleted.

‎image/fixtures/post1.9/v1compatibility

-1
This file was deleted.

‎image/fixtures/pre1.9/expected_computed_id

-1
This file was deleted.

‎image/fixtures/pre1.9/expected_config

-2
This file was deleted.

‎image/fixtures/pre1.9/layer_id

-1
This file was deleted.

‎image/fixtures/pre1.9/parent_id

-1
This file was deleted.

‎image/fixtures/pre1.9/v1compatibility

-1
This file was deleted.

‎image/fs.go

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package image
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"path/filepath"
8+
"sync"
9+
10+
"github.com/Sirupsen/logrus"
11+
"github.com/docker/distribution/digest"
12+
)
13+
14+
// IDWalkFunc is function called by StoreBackend.Walk
15+
type IDWalkFunc func(id ID) error
16+
17+
// StoreBackend provides interface for image.Store persistence
18+
type StoreBackend interface {
19+
Walk(f IDWalkFunc) error
20+
Get(id ID) ([]byte, error)
21+
Set(data []byte) (ID, error)
22+
Delete(id ID) error
23+
SetMetadata(id ID, key string, data []byte) error
24+
GetMetadata(id ID, key string) ([]byte, error)
25+
DeleteMetadata(id ID, key string) error
26+
}
27+
28+
// fs implements StoreBackend using the filesystem.
29+
type fs struct {
30+
sync.RWMutex
31+
root string
32+
}
33+
34+
const (
35+
contentDirName = "content"
36+
metadataDirName = "metadata"
37+
)
38+
39+
// NewFSStoreBackend returns new filesystem based backend for image.Store
40+
func NewFSStoreBackend(root string) (StoreBackend, error) {
41+
return newFSStore(root)
42+
}
43+
44+
func newFSStore(root string) (*fs, error) {
45+
s := &fs{
46+
root: root,
47+
}
48+
if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0700); err != nil {
49+
return nil, err
50+
}
51+
if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0700); err != nil {
52+
return nil, err
53+
}
54+
return s, nil
55+
}
56+
57+
func (s *fs) contentFile(id ID) string {
58+
dgst := digest.Digest(id)
59+
return filepath.Join(s.root, contentDirName, string(dgst.Algorithm()), dgst.Hex())
60+
}
61+
62+
func (s *fs) metadataDir(id ID) string {
63+
dgst := digest.Digest(id)
64+
return filepath.Join(s.root, metadataDirName, string(dgst.Algorithm()), dgst.Hex())
65+
}
66+
67+
// Walk calls the supplied callback for each image ID in the storage backend.
68+
func (s *fs) Walk(f IDWalkFunc) error {
69+
// Only Canonical digest (sha256) is currently supported
70+
s.RLock()
71+
dir, err := ioutil.ReadDir(filepath.Join(s.root, contentDirName, string(digest.Canonical)))
72+
s.RUnlock()
73+
if err != nil {
74+
return err
75+
}
76+
for _, v := range dir {
77+
dgst := digest.NewDigestFromHex(string(digest.Canonical), v.Name())
78+
if err := dgst.Validate(); err != nil {
79+
logrus.Debugf("Skipping invalid digest %s: %s", dgst, err)
80+
continue
81+
}
82+
if err := f(ID(dgst)); err != nil {
83+
return err
84+
}
85+
}
86+
return nil
87+
}
88+
89+
// Get returns the content stored under a given ID.
90+
func (s *fs) Get(id ID) ([]byte, error) {
91+
s.RLock()
92+
defer s.RUnlock()
93+
94+
return s.get(id)
95+
}
96+
97+
func (s *fs) get(id ID) ([]byte, error) {
98+
content, err := ioutil.ReadFile(s.contentFile(id))
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
// todo: maybe optional
104+
validated, err := digest.FromBytes(content)
105+
if err != nil {
106+
return nil, err
107+
}
108+
if ID(validated) != id {
109+
return nil, fmt.Errorf("failed to verify image: %v", id)
110+
}
111+
112+
return content, nil
113+
}
114+
115+
// Set stores content under a given ID.
116+
func (s *fs) Set(data []byte) (ID, error) {
117+
s.Lock()
118+
defer s.Unlock()
119+
120+
if len(data) == 0 {
121+
return "", fmt.Errorf("Invalid empty data")
122+
}
123+
124+
dgst, err := digest.FromBytes(data)
125+
if err != nil {
126+
return "", err
127+
}
128+
id := ID(dgst)
129+
filePath := s.contentFile(id)
130+
tempFilePath := s.contentFile(id) + ".tmp"
131+
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
132+
return "", err
133+
}
134+
if err := os.Rename(tempFilePath, filePath); err != nil {
135+
return "", err
136+
}
137+
138+
return id, nil
139+
}
140+
141+
// Delete removes content and metadata files associated with the ID.
142+
func (s *fs) Delete(id ID) error {
143+
s.Lock()
144+
defer s.Unlock()
145+
146+
if err := os.RemoveAll(s.metadataDir(id)); err != nil {
147+
return err
148+
}
149+
if err := os.Remove(s.contentFile(id)); err != nil {
150+
return err
151+
}
152+
return nil
153+
}
154+
155+
// SetMetadata sets metadata for a given ID. It fails if there's no base file.
156+
func (s *fs) SetMetadata(id ID, key string, data []byte) error {
157+
s.Lock()
158+
defer s.Unlock()
159+
if _, err := s.get(id); err != nil {
160+
return err
161+
}
162+
163+
baseDir := filepath.Join(s.metadataDir(id))
164+
if err := os.MkdirAll(baseDir, 0700); err != nil {
165+
return err
166+
}
167+
filePath := filepath.Join(s.metadataDir(id), key)
168+
tempFilePath := filePath + ".tmp"
169+
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
170+
return err
171+
}
172+
return os.Rename(tempFilePath, filePath)
173+
}
174+
175+
// GetMetadata returns metadata for a given ID.
176+
func (s *fs) GetMetadata(id ID, key string) ([]byte, error) {
177+
s.RLock()
178+
defer s.RUnlock()
179+
180+
if _, err := s.get(id); err != nil {
181+
return nil, err
182+
}
183+
return ioutil.ReadFile(filepath.Join(s.metadataDir(id), key))
184+
}
185+
186+
// DeleteMetadata removes the metadata associated with an ID.
187+
func (s *fs) DeleteMetadata(id ID, key string) error {
188+
s.Lock()
189+
defer s.Unlock()
190+
191+
return os.RemoveAll(filepath.Join(s.metadataDir(id), key))
192+
}

‎image/fs_test.go

+391
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
package image
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"crypto/sha256"
7+
"encoding/hex"
8+
"errors"
9+
"io/ioutil"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/docker/distribution/digest"
15+
)
16+
17+
func TestFSGetSet(t *testing.T) {
18+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
19+
if err != nil {
20+
t.Fatal(err)
21+
}
22+
defer os.RemoveAll(tmpdir)
23+
fs, err := NewFSStoreBackend(tmpdir)
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
28+
testGetSet(t, fs)
29+
}
30+
31+
func TestFSGetInvalidData(t *testing.T) {
32+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
defer os.RemoveAll(tmpdir)
37+
fs, err := NewFSStoreBackend(tmpdir)
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
42+
id, err := fs.Set([]byte("foobar"))
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
47+
dgst := digest.Digest(id)
48+
49+
if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, string(dgst.Algorithm()), dgst.Hex()), []byte("foobar2"), 0600); err != nil {
50+
t.Fatal(err)
51+
}
52+
53+
_, err = fs.Get(id)
54+
if err == nil {
55+
t.Fatal("Expected get to fail after data modification.")
56+
}
57+
}
58+
59+
func TestFSInvalidSet(t *testing.T) {
60+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
defer os.RemoveAll(tmpdir)
65+
fs, err := NewFSStoreBackend(tmpdir)
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
70+
id, err := digest.FromBytes([]byte("foobar"))
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
err = os.Mkdir(filepath.Join(tmpdir, contentDirName, string(id.Algorithm()), id.Hex()), 0700)
75+
if err != nil {
76+
t.Fatal(err)
77+
}
78+
79+
_, err = fs.Set([]byte("foobar"))
80+
if err == nil {
81+
t.Fatal("Expecting error from invalid filesystem data.")
82+
}
83+
}
84+
85+
func TestFSInvalidRoot(t *testing.T) {
86+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
defer os.RemoveAll(tmpdir)
91+
92+
tcases := []struct {
93+
root, invalidFile string
94+
}{
95+
{"root", "root"},
96+
{"root", "root/content"},
97+
{"root", "root/metadata"},
98+
}
99+
100+
for _, tc := range tcases {
101+
root := filepath.Join(tmpdir, tc.root)
102+
filePath := filepath.Join(tmpdir, tc.invalidFile)
103+
err := os.MkdirAll(filepath.Dir(filePath), 0700)
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
f, err := os.Create(filePath)
108+
if err != nil {
109+
t.Fatal(err)
110+
}
111+
f.Close()
112+
113+
_, err = NewFSStoreBackend(root)
114+
if err == nil {
115+
t.Fatalf("Expected error from root %q and invlid file %q", tc.root, tc.invalidFile)
116+
}
117+
118+
os.RemoveAll(root)
119+
}
120+
121+
}
122+
123+
func testMetadataGetSet(t *testing.T, store StoreBackend) {
124+
id, err := store.Set([]byte("foo"))
125+
if err != nil {
126+
t.Fatal(err)
127+
}
128+
id2, err := store.Set([]byte("bar"))
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
133+
tcases := []struct {
134+
id ID
135+
key string
136+
value []byte
137+
}{
138+
{id, "tkey", []byte("tval1")},
139+
{id, "tkey2", []byte("tval2")},
140+
{id2, "tkey", []byte("tval3")},
141+
}
142+
143+
for _, tc := range tcases {
144+
err = store.SetMetadata(tc.id, tc.key, tc.value)
145+
if err != nil {
146+
t.Fatal(err)
147+
}
148+
149+
actual, err := store.GetMetadata(tc.id, tc.key)
150+
if err != nil {
151+
t.Fatal(err)
152+
}
153+
if bytes.Compare(actual, tc.value) != 0 {
154+
t.Fatalf("Metadata expected %q, got %q", tc.value, actual)
155+
}
156+
}
157+
158+
_, err = store.GetMetadata(id2, "tkey2")
159+
if err == nil {
160+
t.Fatal("Expected error for getting metadata for unknown key")
161+
}
162+
163+
id3, err := digest.FromBytes([]byte("baz"))
164+
if err != nil {
165+
t.Fatal(err)
166+
}
167+
168+
err = store.SetMetadata(ID(id3), "tkey", []byte("tval"))
169+
if err == nil {
170+
t.Fatal("Expected error for setting metadata for unknown ID.")
171+
}
172+
173+
_, err = store.GetMetadata(ID(id3), "tkey")
174+
if err == nil {
175+
t.Fatal("Expected error for getting metadata for unknown ID.")
176+
}
177+
}
178+
179+
func TestFSMetadataGetSet(t *testing.T) {
180+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
181+
if err != nil {
182+
t.Fatal(err)
183+
}
184+
defer os.RemoveAll(tmpdir)
185+
fs, err := NewFSStoreBackend(tmpdir)
186+
if err != nil {
187+
t.Fatal(err)
188+
}
189+
190+
testMetadataGetSet(t, fs)
191+
}
192+
193+
func TestFSDelete(t *testing.T) {
194+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
195+
if err != nil {
196+
t.Fatal(err)
197+
}
198+
defer os.RemoveAll(tmpdir)
199+
fs, err := NewFSStoreBackend(tmpdir)
200+
if err != nil {
201+
t.Fatal(err)
202+
}
203+
204+
testDelete(t, fs)
205+
}
206+
207+
func TestFSWalker(t *testing.T) {
208+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
defer os.RemoveAll(tmpdir)
213+
fs, err := NewFSStoreBackend(tmpdir)
214+
if err != nil {
215+
t.Fatal(err)
216+
}
217+
218+
testWalker(t, fs)
219+
}
220+
221+
func TestFSInvalidWalker(t *testing.T) {
222+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
223+
if err != nil {
224+
t.Fatal(err)
225+
}
226+
defer os.RemoveAll(tmpdir)
227+
fs, err := NewFSStoreBackend(tmpdir)
228+
if err != nil {
229+
t.Fatal(err)
230+
}
231+
232+
fooID, err := fs.Set([]byte("foo"))
233+
if err != nil {
234+
t.Fatal(err)
235+
}
236+
237+
if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, "sha256/foobar"), []byte("foobar"), 0600); err != nil {
238+
t.Fatal(err)
239+
}
240+
241+
n := 0
242+
err = fs.Walk(func(id ID) error {
243+
if id != fooID {
244+
t.Fatalf("Invalid walker ID %q, expected %q", id, fooID)
245+
}
246+
n++
247+
return nil
248+
})
249+
if err != nil {
250+
t.Fatalf("Invalid data should not have caused walker error, got %v", err)
251+
}
252+
if n != 1 {
253+
t.Fatalf("Expected 1 walk initialization, got %d", n)
254+
}
255+
}
256+
257+
func testGetSet(t *testing.T, store StoreBackend) {
258+
type tcase struct {
259+
input []byte
260+
expected ID
261+
}
262+
tcases := []tcase{
263+
{[]byte("foobar"), ID("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")},
264+
}
265+
266+
randomInput := make([]byte, 8*1024)
267+
_, err := rand.Read(randomInput)
268+
if err != nil {
269+
t.Fatal(err)
270+
}
271+
// skipping use of digest pkg because its used by the imlementation
272+
h := sha256.New()
273+
_, err = h.Write(randomInput)
274+
if err != nil {
275+
t.Fatal(err)
276+
}
277+
tcases = append(tcases, tcase{
278+
input: randomInput,
279+
expected: ID("sha256:" + hex.EncodeToString(h.Sum(nil))),
280+
})
281+
282+
for _, tc := range tcases {
283+
id, err := store.Set([]byte(tc.input))
284+
if err != nil {
285+
t.Fatal(err)
286+
}
287+
if id != tc.expected {
288+
t.Fatalf("Expected ID %q, got %q", tc.expected, id)
289+
}
290+
}
291+
292+
for _, emptyData := range [][]byte{nil, {}} {
293+
_, err := store.Set(emptyData)
294+
if err == nil {
295+
t.Fatal("Expected error for nil input.")
296+
}
297+
}
298+
299+
for _, tc := range tcases {
300+
data, err := store.Get(tc.expected)
301+
if err != nil {
302+
t.Fatal(err)
303+
}
304+
if bytes.Compare(data, tc.input) != 0 {
305+
t.Fatalf("Expected data %q, got %q", tc.input, data)
306+
}
307+
}
308+
309+
for _, key := range []ID{"foobar:abc", "sha256:abc", "sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2a"} {
310+
_, err := store.Get(key)
311+
if err == nil {
312+
t.Fatalf("Expected error for ID %q.", key)
313+
}
314+
}
315+
316+
}
317+
318+
func testDelete(t *testing.T, store StoreBackend) {
319+
id, err := store.Set([]byte("foo"))
320+
if err != nil {
321+
t.Fatal(err)
322+
}
323+
id2, err := store.Set([]byte("bar"))
324+
if err != nil {
325+
t.Fatal(err)
326+
}
327+
328+
err = store.Delete(id)
329+
if err != nil {
330+
t.Fatal(err)
331+
}
332+
333+
_, err = store.Get(id)
334+
if err == nil {
335+
t.Fatalf("Expected getting deleted item %q to fail", id)
336+
}
337+
_, err = store.Get(id2)
338+
if err != nil {
339+
t.Fatal(err)
340+
}
341+
342+
err = store.Delete(id2)
343+
if err != nil {
344+
t.Fatal(err)
345+
}
346+
_, err = store.Get(id2)
347+
if err == nil {
348+
t.Fatalf("Expected getting deleted item %q to fail", id2)
349+
}
350+
}
351+
352+
func testWalker(t *testing.T, store StoreBackend) {
353+
id, err := store.Set([]byte("foo"))
354+
if err != nil {
355+
t.Fatal(err)
356+
}
357+
id2, err := store.Set([]byte("bar"))
358+
if err != nil {
359+
t.Fatal(err)
360+
}
361+
362+
tcases := make(map[ID]struct{})
363+
tcases[id] = struct{}{}
364+
tcases[id2] = struct{}{}
365+
n := 0
366+
err = store.Walk(func(id ID) error {
367+
delete(tcases, id)
368+
n++
369+
return nil
370+
})
371+
if err != nil {
372+
t.Fatal(err)
373+
}
374+
375+
if n != 2 {
376+
t.Fatalf("Expected 2 walk initializations, got %d", n)
377+
}
378+
if len(tcases) != 0 {
379+
t.Fatalf("Expected empty unwalked set, got %+v", tcases)
380+
}
381+
382+
// stop on error
383+
tcases = make(map[ID]struct{})
384+
tcases[id] = struct{}{}
385+
err = store.Walk(func(id ID) error {
386+
return errors.New("")
387+
})
388+
if err == nil {
389+
t.Fatalf("Exected error from walker.")
390+
}
391+
}

‎image/image.go

+70-91
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,23 @@ package image
22

33
import (
44
"encoding/json"
5-
"fmt"
6-
"regexp"
5+
"errors"
6+
"io"
77
"time"
88

9-
"github.com/Sirupsen/logrus"
109
"github.com/docker/distribution/digest"
11-
derr "github.com/docker/docker/errors"
12-
"github.com/docker/docker/pkg/version"
1310
"github.com/docker/docker/runconfig"
1411
)
1512

16-
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
13+
// ID is the content-addressable ID of an image.
14+
type ID digest.Digest
1715

18-
// noFallbackMinVersion is the minimum version for which v1compatibility
19-
// information will not be marshaled through the Image struct to remove
20-
// blank fields.
21-
var noFallbackMinVersion = version.Version("1.8.3")
22-
23-
// Descriptor provides the information necessary to register an image in
24-
// the graph.
25-
type Descriptor interface {
26-
ID() string
27-
Parent() string
28-
MarshalConfig() ([]byte, error)
16+
func (id ID) String() string {
17+
return digest.Digest(id).String()
2918
}
3019

31-
// Image stores the image configuration.
32-
// All fields in this struct must be marked `omitempty` to keep getting
33-
// predictable hashes from the old `v1Compatibility` configuration.
34-
type Image struct {
20+
// V1Image stores the V1 image configuration.
21+
type V1Image struct {
3522
// ID a unique 64 character identifier of the image
3623
ID string `json:"id,omitempty"`
3724
// Parent id of the image
@@ -55,95 +42,87 @@ type Image struct {
5542
// OS is the operating system used to build and run the image
5643
OS string `json:"os,omitempty"`
5744
// Size is the total size of the image including all layers it is composed of
58-
Size int64 `json:",omitempty"` // capitalized for backwards compatibility
59-
// ParentID specifies the strong, content address of the parent configuration.
60-
ParentID digest.Digest `json:"parent_id,omitempty"`
61-
// LayerID provides the content address of the associated layer.
62-
LayerID digest.Digest `json:"layer_id,omitempty"`
45+
Size int64 `json:",omitempty"`
6346
}
6447

65-
// NewImgJSON creates an Image configuration from json.
66-
func NewImgJSON(src []byte) (*Image, error) {
67-
ret := &Image{}
48+
// Image stores the image configuration
49+
type Image struct {
50+
V1Image
51+
Parent ID `json:"parent,omitempty"`
52+
RootFS *RootFS `json:"rootfs,omitempty"`
53+
History []History `json:"history,omitempty"`
6854

69-
// FIXME: Is there a cleaner way to "purify" the input json?
70-
if err := json.Unmarshal(src, ret); err != nil {
71-
return nil, err
72-
}
73-
return ret, nil
55+
// rawJSON caches the immutable JSON associated with this image.
56+
rawJSON []byte
57+
58+
// computedID is the ID computed from the hash of the image config.
59+
// Not to be confused with the legacy V1 ID in V1Image.
60+
computedID ID
7461
}
7562

76-
// ValidateID checks whether an ID string is a valid image ID.
77-
func ValidateID(id string) error {
78-
if ok := validHex.MatchString(id); !ok {
79-
return derr.ErrorCodeInvalidImageID.WithArgs(id)
80-
}
81-
return nil
63+
// RawJSON returns the immutable JSON associated with the image.
64+
func (img *Image) RawJSON() []byte {
65+
return img.rawJSON
66+
}
67+
68+
// ID returns the image's content-addressable ID.
69+
func (img *Image) ID() ID {
70+
return img.computedID
8271
}
8372

84-
// MakeImageConfig returns immutable configuration JSON for image based on the
85-
// v1Compatibility object, layer digest and parent StrongID. SHA256() of this
86-
// config is the new image ID (strongID).
87-
func MakeImageConfig(v1Compatibility []byte, layerID, parentID digest.Digest) ([]byte, error) {
73+
// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
74+
// that JSON that's been manipulated by a push/pull cycle with a legacy
75+
// registry won't end up with a different key order.
76+
func (img *Image) MarshalJSON() ([]byte, error) {
77+
type MarshalImage Image
8878

89-
// Detect images created after 1.8.3
90-
img, err := NewImgJSON(v1Compatibility)
79+
pass1, err := json.Marshal(MarshalImage(*img))
9180
if err != nil {
9281
return nil, err
9382
}
94-
useFallback := version.Version(img.DockerVersion).LessThan(noFallbackMinVersion)
95-
96-
if useFallback {
97-
// Fallback for pre-1.8.3. Calculate base config based on Image struct
98-
// so that fields with default values added by Docker will use same ID
99-
logrus.Debugf("Using fallback hash for %v", layerID)
100-
101-
v1Compatibility, err = json.Marshal(img)
102-
if err != nil {
103-
return nil, err
104-
}
105-
}
10683

10784
var c map[string]*json.RawMessage
108-
if err := json.Unmarshal(v1Compatibility, &c); err != nil {
85+
if err := json.Unmarshal(pass1, &c); err != nil {
10986
return nil, err
11087
}
111-
112-
if err := layerID.Validate(); err != nil {
113-
return nil, fmt.Errorf("invalid layerID: %v", err)
114-
}
115-
116-
c["layer_id"] = rawJSON(layerID)
117-
118-
if parentID != "" {
119-
if err := parentID.Validate(); err != nil {
120-
return nil, fmt.Errorf("invalid parentID %v", err)
121-
}
122-
c["parent_id"] = rawJSON(parentID)
123-
}
124-
125-
delete(c, "id")
126-
delete(c, "parent")
127-
delete(c, "Size") // Size is calculated from data on disk and is inconsitent
128-
12988
return json.Marshal(c)
13089
}
13190

132-
// StrongID returns image ID for the config JSON.
133-
func StrongID(configJSON []byte) (digest.Digest, error) {
134-
digester := digest.Canonical.New()
135-
if _, err := digester.Hash().Write(configJSON); err != nil {
136-
return "", err
137-
}
138-
dgst := digester.Digest()
139-
logrus.Debugf("H(%v) = %v", string(configJSON), dgst)
140-
return dgst, nil
91+
// History stores build commands that were used to create an image
92+
type History struct {
93+
// Created timestamp for build point
94+
Created time.Time `json:"created"`
95+
// Author of the build point
96+
Author string `json:"author,omitempty"`
97+
// CreatedBy keeps the Dockerfile command used while building image.
98+
CreatedBy string `json:"created_by,omitempty"`
99+
// Comment is custom mesage set by the user when creating the image.
100+
Comment string `json:"comment,omitempty"`
101+
// EmptyLayer is set to true if this history item did not generate a
102+
// layer. Otherwise, the history item is associated with the next
103+
// layer in the RootFS section.
104+
EmptyLayer bool `json:"empty_layer,omitempty"`
141105
}
142106

143-
func rawJSON(value interface{}) *json.RawMessage {
144-
jsonval, err := json.Marshal(value)
145-
if err != nil {
146-
return nil
107+
// Exporter provides interface for exporting and importing images
108+
type Exporter interface {
109+
Load(io.ReadCloser, io.Writer) error
110+
// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
111+
Save([]string, io.Writer) error
112+
}
113+
114+
// NewFromJSON creates an Image configuration from json.
115+
func NewFromJSON(src []byte) (*Image, error) {
116+
img := &Image{}
117+
118+
if err := json.Unmarshal(src, img); err != nil {
119+
return nil, err
147120
}
148-
return (*json.RawMessage)(&jsonval)
121+
if img.RootFS == nil {
122+
return nil, errors.New("Invalid image JSON, no RootFS key.")
123+
}
124+
125+
img.rawJSON = src
126+
127+
return img, nil
149128
}

‎image/image_test.go

+42-38
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,59 @@
11
package image
22

33
import (
4-
"bytes"
5-
"io/ioutil"
4+
"encoding/json"
5+
"sort"
6+
"strings"
67
"testing"
7-
8-
"github.com/docker/distribution/digest"
98
)
109

11-
var fixtures = []string{
12-
"fixtures/pre1.9",
13-
"fixtures/post1.9",
14-
}
10+
const sampleImageJSON = `{
11+
"architecture": "amd64",
12+
"os": "linux",
13+
"config": {},
14+
"rootfs": {
15+
"type": "layers",
16+
"diff_ids": []
17+
}
18+
}`
1519

16-
func loadFixtureFile(t *testing.T, path string) []byte {
17-
fileData, err := ioutil.ReadFile(path)
20+
func TestJSON(t *testing.T) {
21+
img, err := NewFromJSON([]byte(sampleImageJSON))
1822
if err != nil {
19-
t.Fatalf("error opening %s: %v", path, err)
23+
t.Fatal(err)
24+
}
25+
rawJSON := img.RawJSON()
26+
if string(rawJSON) != sampleImageJSON {
27+
t.Fatalf("Raw JSON of config didn't match: expected %+v, got %v", sampleImageJSON, rawJSON)
2028
}
21-
22-
return bytes.TrimSpace(fileData)
2329
}
2430

25-
// TestMakeImageConfig makes sure that MakeImageConfig returns the expected
26-
// canonical JSON for a reference Image.
27-
func TestMakeImageConfig(t *testing.T) {
28-
for _, fixture := range fixtures {
29-
v1Compatibility := loadFixtureFile(t, fixture+"/v1compatibility")
30-
expectedConfig := loadFixtureFile(t, fixture+"/expected_config")
31-
layerID := digest.Digest(loadFixtureFile(t, fixture+"/layer_id"))
32-
parentID := digest.Digest(loadFixtureFile(t, fixture+"/parent_id"))
33-
34-
json, err := MakeImageConfig(v1Compatibility, layerID, parentID)
35-
if err != nil {
36-
t.Fatalf("MakeImageConfig on %s returned error: %v", fixture, err)
37-
}
38-
if !bytes.Equal(json, expectedConfig) {
39-
t.Fatalf("did not get expected JSON for %s\nexpected: %s\ngot: %s", fixture, expectedConfig, json)
40-
}
31+
func TestInvalidJSON(t *testing.T) {
32+
_, err := NewFromJSON([]byte("{}"))
33+
if err == nil {
34+
t.Fatal("Expected JSON parse error")
4135
}
4236
}
4337

44-
// TestGetStrongID makes sure that GetConfigJSON returns the expected
45-
// hash for a reference Image.
46-
func TestGetStrongID(t *testing.T) {
47-
for _, fixture := range fixtures {
48-
expectedConfig := loadFixtureFile(t, fixture+"/expected_config")
49-
expectedComputedID := digest.Digest(loadFixtureFile(t, fixture+"/expected_computed_id"))
38+
func TestMarshalKeyOrder(t *testing.T) {
39+
b, err := json.Marshal(&Image{
40+
V1Image: V1Image{
41+
Comment: "a",
42+
Author: "b",
43+
Architecture: "c",
44+
},
45+
})
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
50+
expectedOrder := []string{"architecture", "author", "comment"}
51+
var indexes []int
52+
for _, k := range expectedOrder {
53+
indexes = append(indexes, strings.Index(string(b), k))
54+
}
5055

51-
if id, err := StrongID(expectedConfig); err != nil || id != expectedComputedID {
52-
t.Fatalf("did not get expected ID for %s\nexpected: %s\ngot: %s\nerror: %v", fixture, expectedComputedID, id, err)
53-
}
56+
if !sort.IntsAreSorted(indexes) {
57+
t.Fatal("invalid key order in JSON: ", string(b))
5458
}
5559
}

‎image/rootfs.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package image
2+
3+
import "github.com/docker/docker/layer"
4+
5+
// Append appends a new diffID to rootfs
6+
func (r *RootFS) Append(id layer.DiffID) {
7+
r.DiffIDs = append(r.DiffIDs, id)
8+
}

‎image/rootfs_unix.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// +build !windows
2+
3+
package image
4+
5+
import "github.com/docker/docker/layer"
6+
7+
// RootFS describes images root filesystem
8+
// This is currently a placeholder that only supports layers. In the future
9+
// this can be made into a interface that supports different implementaions.
10+
type RootFS struct {
11+
Type string `json:"type"`
12+
DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
13+
}
14+
15+
// ChainID returns the ChainID for the top layer in RootFS.
16+
func (r *RootFS) ChainID() layer.ChainID {
17+
return layer.CreateChainID(r.DiffIDs)
18+
}
19+
20+
// NewRootFS returns empty RootFS struct
21+
func NewRootFS() *RootFS {
22+
return &RootFS{Type: "layers"}
23+
}

‎image/rootfs_windows.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// +build windows
2+
3+
package image
4+
5+
import (
6+
"crypto/sha512"
7+
"fmt"
8+
9+
"github.com/docker/distribution/digest"
10+
"github.com/docker/docker/layer"
11+
)
12+
13+
// RootFS describes images root filesystem
14+
// This is currently a placeholder that only supports layers. In the future
15+
// this can be made into a interface that supports different implementaions.
16+
type RootFS struct {
17+
Type string `json:"type"`
18+
DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
19+
BaseLayer string `json:"base_layer,omitempty"`
20+
}
21+
22+
// BaseLayerID returns the 64 byte hex ID for the baselayer name.
23+
func (r *RootFS) BaseLayerID() string {
24+
baseID := sha512.Sum384([]byte(r.BaseLayer))
25+
return fmt.Sprintf("%x", baseID[:32])
26+
}
27+
28+
// ChainID returns the ChainID for the top layer in RootFS.
29+
func (r *RootFS) ChainID() layer.ChainID {
30+
baseDiffID, _ := digest.FromBytes([]byte(r.BaseLayerID())) // can never error
31+
return layer.CreateChainID(append([]layer.DiffID{layer.DiffID(baseDiffID)}, r.DiffIDs...))
32+
}
33+
34+
// NewRootFS returns empty RootFS struct
35+
func NewRootFS() *RootFS {
36+
return &RootFS{Type: "layers+base"}
37+
}

‎image/store.go

+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package image
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"sync"
8+
9+
"github.com/Sirupsen/logrus"
10+
"github.com/docker/distribution/digest"
11+
"github.com/docker/docker/layer"
12+
)
13+
14+
// Store is an interface for creating and accessing images
15+
type Store interface {
16+
Create(config []byte) (ID, error)
17+
Get(id ID) (*Image, error)
18+
Delete(id ID) ([]layer.Metadata, error)
19+
Search(partialID string) (ID, error)
20+
SetParent(id ID, parent ID) error
21+
GetParent(id ID) (ID, error)
22+
Children(id ID) []ID
23+
Map() map[ID]*Image
24+
Heads() map[ID]*Image
25+
}
26+
27+
// LayerGetReleaser is a minimal interface for getting and releasing images.
28+
type LayerGetReleaser interface {
29+
Get(layer.ChainID) (layer.Layer, error)
30+
Release(layer.Layer) ([]layer.Metadata, error)
31+
}
32+
33+
type imageMeta struct {
34+
layer layer.Layer
35+
children map[ID]struct{}
36+
}
37+
38+
type store struct {
39+
sync.Mutex
40+
ls LayerGetReleaser
41+
images map[ID]*imageMeta
42+
fs StoreBackend
43+
digestSet *digest.Set
44+
}
45+
46+
// NewImageStore returns new store object for given layer store
47+
func NewImageStore(fs StoreBackend, ls LayerGetReleaser) (Store, error) {
48+
is := &store{
49+
ls: ls,
50+
images: make(map[ID]*imageMeta),
51+
fs: fs,
52+
digestSet: digest.NewSet(),
53+
}
54+
55+
// load all current images and retain layers
56+
if err := is.restore(); err != nil {
57+
return nil, err
58+
}
59+
60+
return is, nil
61+
}
62+
63+
func (is *store) restore() error {
64+
err := is.fs.Walk(func(id ID) error {
65+
img, err := is.Get(id)
66+
if err != nil {
67+
logrus.Errorf("invalid image %v, %v", id, err)
68+
return nil
69+
}
70+
var l layer.Layer
71+
if chainID := img.RootFS.ChainID(); chainID != "" {
72+
l, err = is.ls.Get(chainID)
73+
if err != nil {
74+
return err
75+
}
76+
}
77+
if err := is.digestSet.Add(digest.Digest(id)); err != nil {
78+
return err
79+
}
80+
81+
imageMeta := &imageMeta{
82+
layer: l,
83+
children: make(map[ID]struct{}),
84+
}
85+
86+
is.images[ID(id)] = imageMeta
87+
88+
return nil
89+
})
90+
if err != nil {
91+
return err
92+
}
93+
94+
// Second pass to fill in children maps
95+
for id := range is.images {
96+
if parent, err := is.GetParent(id); err == nil {
97+
if parentMeta := is.images[parent]; parentMeta != nil {
98+
parentMeta.children[id] = struct{}{}
99+
}
100+
}
101+
}
102+
103+
return nil
104+
}
105+
106+
func (is *store) Create(config []byte) (ID, error) {
107+
var img Image
108+
err := json.Unmarshal(config, &img)
109+
if err != nil {
110+
return "", err
111+
}
112+
113+
// Must reject any config that references diffIDs from the history
114+
// which aren't among the rootfs layers.
115+
rootFSLayers := make(map[layer.DiffID]struct{})
116+
for _, diffID := range img.RootFS.DiffIDs {
117+
rootFSLayers[diffID] = struct{}{}
118+
}
119+
120+
layerCounter := 0
121+
for _, h := range img.History {
122+
if !h.EmptyLayer {
123+
layerCounter++
124+
}
125+
}
126+
if layerCounter > len(img.RootFS.DiffIDs) {
127+
return "", errors.New("too many non-empty layers in History section")
128+
}
129+
130+
dgst, err := is.fs.Set(config)
131+
if err != nil {
132+
return "", err
133+
}
134+
imageID := ID(dgst)
135+
136+
is.Lock()
137+
defer is.Unlock()
138+
139+
if _, exists := is.images[imageID]; exists {
140+
return imageID, nil
141+
}
142+
143+
layerID := img.RootFS.ChainID()
144+
145+
var l layer.Layer
146+
if layerID != "" {
147+
l, err = is.ls.Get(layerID)
148+
if err != nil {
149+
return "", err
150+
}
151+
}
152+
153+
imageMeta := &imageMeta{
154+
layer: l,
155+
children: make(map[ID]struct{}),
156+
}
157+
158+
is.images[imageID] = imageMeta
159+
if err := is.digestSet.Add(digest.Digest(imageID)); err != nil {
160+
delete(is.images, imageID)
161+
return "", err
162+
}
163+
164+
return imageID, nil
165+
}
166+
167+
func (is *store) Search(term string) (ID, error) {
168+
is.Lock()
169+
defer is.Unlock()
170+
171+
dgst, err := is.digestSet.Lookup(term)
172+
if err != nil {
173+
return "", err
174+
}
175+
return ID(dgst), nil
176+
}
177+
178+
func (is *store) Get(id ID) (*Image, error) {
179+
// todo: Check if image is in images
180+
// todo: Detect manual insertions and start using them
181+
config, err := is.fs.Get(id)
182+
if err != nil {
183+
return nil, err
184+
}
185+
186+
img, err := NewFromJSON(config)
187+
if err != nil {
188+
return nil, err
189+
}
190+
img.computedID = id
191+
192+
img.Parent, err = is.GetParent(id)
193+
if err != nil {
194+
img.Parent = ""
195+
}
196+
197+
return img, nil
198+
}
199+
200+
func (is *store) Delete(id ID) ([]layer.Metadata, error) {
201+
is.Lock()
202+
defer is.Unlock()
203+
204+
imageMeta := is.images[id]
205+
if imageMeta == nil {
206+
return nil, fmt.Errorf("unrecognized image ID %s", id.String())
207+
}
208+
for id := range imageMeta.children {
209+
is.fs.DeleteMetadata(id, "parent")
210+
}
211+
if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
212+
delete(is.images[parent].children, id)
213+
}
214+
215+
delete(is.images, id)
216+
is.fs.Delete(id)
217+
218+
if imageMeta.layer != nil {
219+
return is.ls.Release(imageMeta.layer)
220+
}
221+
return nil, nil
222+
}
223+
224+
func (is *store) SetParent(id, parent ID) error {
225+
is.Lock()
226+
defer is.Unlock()
227+
parentMeta := is.images[parent]
228+
if parentMeta == nil {
229+
return fmt.Errorf("unknown parent image ID %s", parent.String())
230+
}
231+
parentMeta.children[id] = struct{}{}
232+
return is.fs.SetMetadata(id, "parent", []byte(parent))
233+
}
234+
235+
func (is *store) GetParent(id ID) (ID, error) {
236+
d, err := is.fs.GetMetadata(id, "parent")
237+
if err != nil {
238+
return "", err
239+
}
240+
return ID(d), nil // todo: validate?
241+
}
242+
243+
func (is *store) Children(id ID) []ID {
244+
is.Lock()
245+
defer is.Unlock()
246+
247+
return is.children(id)
248+
}
249+
250+
func (is *store) children(id ID) []ID {
251+
var ids []ID
252+
if is.images[id] != nil {
253+
for id := range is.images[id].children {
254+
ids = append(ids, id)
255+
}
256+
}
257+
return ids
258+
}
259+
260+
func (is *store) Heads() map[ID]*Image {
261+
return is.imagesMap(false)
262+
}
263+
264+
func (is *store) Map() map[ID]*Image {
265+
return is.imagesMap(true)
266+
}
267+
268+
func (is *store) imagesMap(all bool) map[ID]*Image {
269+
is.Lock()
270+
defer is.Unlock()
271+
272+
images := make(map[ID]*Image)
273+
274+
for id := range is.images {
275+
if !all && len(is.children(id)) > 0 {
276+
continue
277+
}
278+
img, err := is.Get(id)
279+
if err != nil {
280+
logrus.Errorf("invalid image access: %q, error: %q", id, err)
281+
continue
282+
}
283+
images[id] = img
284+
}
285+
return images
286+
}

‎image/store_test.go

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package image
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"testing"
7+
8+
"github.com/docker/distribution/digest"
9+
"github.com/docker/docker/layer"
10+
)
11+
12+
func TestRestore(t *testing.T) {
13+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
14+
if err != nil {
15+
t.Fatal(err)
16+
}
17+
defer os.RemoveAll(tmpdir)
18+
fs, err := NewFSStoreBackend(tmpdir)
19+
if err != nil {
20+
t.Fatal(err)
21+
}
22+
23+
id1, err := fs.Set([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
_, err = fs.Set([]byte(`invalid`))
28+
if err != nil {
29+
t.Fatal(err)
30+
}
31+
id2, err := fs.Set([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
err = fs.SetMetadata(id2, "parent", []byte(id1))
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
40+
is, err := NewImageStore(fs, &mockLayerGetReleaser{})
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
45+
imgs := is.Map()
46+
if actual, expected := len(imgs), 2; actual != expected {
47+
t.Fatalf("invalid images length, expected 2, got %q", len(imgs))
48+
}
49+
50+
img1, err := is.Get(ID(id1))
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
55+
if actual, expected := img1.computedID, ID(id1); actual != expected {
56+
t.Fatalf("invalid image ID: expected %q, got %q", expected, actual)
57+
}
58+
59+
if actual, expected := img1.computedID.String(), string(id1); actual != expected {
60+
t.Fatalf("invalid image ID string: expected %q, got %q", expected, actual)
61+
}
62+
63+
img2, err := is.Get(ID(id2))
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
68+
if actual, expected := img1.Comment, "abc"; actual != expected {
69+
t.Fatalf("invalid comment for image1: expected %q, got %q", expected, actual)
70+
}
71+
72+
if actual, expected := img2.Comment, "def"; actual != expected {
73+
t.Fatalf("invalid comment for image2: expected %q, got %q", expected, actual)
74+
}
75+
76+
p, err := is.GetParent(ID(id1))
77+
if err == nil {
78+
t.Fatal("expected error for getting parent")
79+
}
80+
81+
p, err = is.GetParent(ID(id2))
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
if actual, expected := p, ID(id1); actual != expected {
86+
t.Fatalf("invalid parent: expected %q, got %q", expected, actual)
87+
}
88+
89+
children := is.Children(ID(id1))
90+
if len(children) != 1 {
91+
t.Fatalf("invalid children length: %q", len(children))
92+
}
93+
if actual, expected := children[0], ID(id2); actual != expected {
94+
t.Fatalf("invalid child for id1: expected %q, got %q", expected, actual)
95+
}
96+
97+
heads := is.Heads()
98+
if actual, expected := len(heads), 1; actual != expected {
99+
t.Fatalf("invalid images length: expected %q, got %q", expected, actual)
100+
}
101+
102+
sid1, err := is.Search(string(id1)[:10])
103+
if err != nil {
104+
t.Fatal(err)
105+
}
106+
if actual, expected := sid1, ID(id1); actual != expected {
107+
t.Fatalf("searched ID mismatch: expected %q, got %q", expected, actual)
108+
}
109+
110+
sid1, err = is.Search(digest.Digest(id1).Hex()[:6])
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
if actual, expected := sid1, ID(id1); actual != expected {
115+
t.Fatalf("searched ID mismatch: expected %q, got %q", expected, actual)
116+
}
117+
118+
invalidPattern := digest.Digest(id1).Hex()[1:6]
119+
_, err = is.Search(invalidPattern)
120+
if err == nil {
121+
t.Fatalf("expected search for %q to fail", invalidPattern)
122+
}
123+
124+
}
125+
126+
func TestAddDelete(t *testing.T) {
127+
tmpdir, err := ioutil.TempDir("", "images-fs-store")
128+
if err != nil {
129+
t.Fatal(err)
130+
}
131+
defer os.RemoveAll(tmpdir)
132+
fs, err := NewFSStoreBackend(tmpdir)
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
137+
is, err := NewImageStore(fs, &mockLayerGetReleaser{})
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
142+
id1, err := is.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
143+
if err != nil {
144+
t.Fatal(err)
145+
}
146+
147+
if actual, expected := id1, ID("sha256:8d25a9c45df515f9d0fe8e4a6b1c64dd3b965a84790ddbcc7954bb9bc89eb993"); actual != expected {
148+
t.Fatalf("create ID mismatch: expected %q, got %q", expected, actual)
149+
}
150+
151+
img, err := is.Get(id1)
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
156+
if actual, expected := img.Comment, "abc"; actual != expected {
157+
t.Fatalf("invalid comment in image: expected %q, got %q", expected, actual)
158+
}
159+
160+
id2, err := is.Create([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
161+
if err != nil {
162+
t.Fatal(err)
163+
}
164+
165+
err = is.SetParent(id2, id1)
166+
if err != nil {
167+
t.Fatal(err)
168+
}
169+
170+
pid1, err := is.GetParent(id2)
171+
if err != nil {
172+
t.Fatal(err)
173+
}
174+
if actual, expected := pid1, id1; actual != expected {
175+
t.Fatalf("invalid parent for image: expected %q, got %q", expected, actual)
176+
}
177+
178+
_, err = is.Delete(id1)
179+
if err != nil {
180+
t.Fatal(err)
181+
}
182+
_, err = is.Get(id1)
183+
if err == nil {
184+
t.Fatalf("expected get for deleted image %q to fail", id1)
185+
}
186+
_, err = is.Get(id2)
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
pid1, err = is.GetParent(id2)
191+
if err == nil {
192+
t.Fatalf("expected parent check for image %q to fail, got %q", id2, pid1)
193+
}
194+
195+
}
196+
197+
type mockLayerGetReleaser struct{}
198+
199+
func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) {
200+
return nil, nil
201+
}
202+
203+
func (ls *mockLayerGetReleaser) Release(layer.Layer) ([]layer.Metadata, error) {
204+
return nil, nil
205+
}

‎image/tarexport/load.go

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package tarexport
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"io/ioutil"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/Sirupsen/logrus"
12+
"github.com/docker/distribution/reference"
13+
"github.com/docker/docker/image"
14+
"github.com/docker/docker/image/v1"
15+
"github.com/docker/docker/layer"
16+
"github.com/docker/docker/pkg/archive"
17+
"github.com/docker/docker/pkg/chrootarchive"
18+
"github.com/docker/docker/pkg/symlink"
19+
)
20+
21+
func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer) error {
22+
tmpDir, err := ioutil.TempDir("", "docker-import-")
23+
if err != nil {
24+
return err
25+
}
26+
defer os.RemoveAll(tmpDir)
27+
28+
if err := chrootarchive.Untar(inTar, tmpDir, nil); err != nil {
29+
return err
30+
}
31+
// read manifest, if no file then load in legacy mode
32+
manifestPath, err := safePath(tmpDir, manifestFileName)
33+
if err != nil {
34+
return err
35+
}
36+
manifestFile, err := os.Open(manifestPath)
37+
if err != nil {
38+
if os.IsNotExist(err) {
39+
return l.legacyLoad(tmpDir, outStream)
40+
}
41+
return manifestFile.Close()
42+
}
43+
defer manifestFile.Close()
44+
45+
var manifest []manifestItem
46+
if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
47+
return err
48+
}
49+
50+
for _, m := range manifest {
51+
configPath, err := safePath(tmpDir, m.Config)
52+
if err != nil {
53+
return err
54+
}
55+
config, err := ioutil.ReadFile(configPath)
56+
if err != nil {
57+
return err
58+
}
59+
img, err := image.NewFromJSON(config)
60+
if err != nil {
61+
return err
62+
}
63+
var rootFS image.RootFS
64+
rootFS = *img.RootFS
65+
rootFS.DiffIDs = nil
66+
67+
if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual {
68+
return fmt.Errorf("invalid manifest, layers length mismatch: expected %q, got %q", expected, actual)
69+
}
70+
71+
for i, diffID := range img.RootFS.DiffIDs {
72+
layerPath, err := safePath(tmpDir, m.Layers[i])
73+
if err != nil {
74+
return err
75+
}
76+
newLayer, err := l.loadLayer(layerPath, rootFS)
77+
if err != nil {
78+
return err
79+
}
80+
defer layer.ReleaseAndLog(l.ls, newLayer)
81+
if expected, actual := diffID, newLayer.DiffID(); expected != actual {
82+
return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual)
83+
}
84+
rootFS.Append(diffID)
85+
}
86+
87+
imgID, err := l.is.Create(config)
88+
if err != nil {
89+
return err
90+
}
91+
92+
for _, repoTag := range m.RepoTags {
93+
named, err := reference.ParseNamed(repoTag)
94+
if err != nil {
95+
return err
96+
}
97+
ref, ok := named.(reference.NamedTagged)
98+
if !ok {
99+
return fmt.Errorf("invalid tag %q", repoTag)
100+
}
101+
l.setLoadedTag(ref, imgID, outStream)
102+
}
103+
104+
}
105+
106+
return nil
107+
}
108+
109+
func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS) (layer.Layer, error) {
110+
rawTar, err := os.Open(filename)
111+
if err != nil {
112+
logrus.Debugf("Error reading embedded tar: %v", err)
113+
return nil, err
114+
}
115+
inflatedLayerData, err := archive.DecompressStream(rawTar)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
defer rawTar.Close()
121+
defer inflatedLayerData.Close()
122+
123+
return l.ls.Register(inflatedLayerData, rootFS.ChainID())
124+
}
125+
126+
func (l *tarexporter) setLoadedTag(ref reference.NamedTagged, imgID image.ID, outStream io.Writer) error {
127+
if prevID, err := l.ts.Get(ref); err == nil && prevID != imgID {
128+
fmt.Fprintf(outStream, "The image %s already exists, renaming the old one with ID %s to empty string\n", ref.String(), string(prevID)) // todo: this message is wrong in case of multiple tags
129+
}
130+
131+
if err := l.ts.Add(ref, imgID, true); err != nil {
132+
return err
133+
}
134+
return nil
135+
}
136+
137+
func (l *tarexporter) legacyLoad(tmpDir string, outStream io.Writer) error {
138+
legacyLoadedMap := make(map[string]image.ID)
139+
140+
dirs, err := ioutil.ReadDir(tmpDir)
141+
if err != nil {
142+
return err
143+
}
144+
145+
// every dir represents an image
146+
for _, d := range dirs {
147+
if d.IsDir() {
148+
if err := l.legacyLoadImage(d.Name(), tmpDir, legacyLoadedMap); err != nil {
149+
return err
150+
}
151+
}
152+
}
153+
154+
// load tags from repositories file
155+
repositoriesPath, err := safePath(tmpDir, legacyRepositoriesFileName)
156+
if err != nil {
157+
return err
158+
}
159+
repositoriesFile, err := os.Open(repositoriesPath)
160+
if err != nil {
161+
if !os.IsNotExist(err) {
162+
return err
163+
}
164+
return repositoriesFile.Close()
165+
}
166+
defer repositoriesFile.Close()
167+
168+
repositories := make(map[string]map[string]string)
169+
if err := json.NewDecoder(repositoriesFile).Decode(&repositories); err != nil {
170+
return err
171+
}
172+
173+
for name, tagMap := range repositories {
174+
for tag, oldID := range tagMap {
175+
imgID, ok := legacyLoadedMap[oldID]
176+
if !ok {
177+
return fmt.Errorf("invalid target ID: %v", oldID)
178+
}
179+
named, err := reference.WithName(name)
180+
if err != nil {
181+
return err
182+
}
183+
ref, err := reference.WithTag(named, tag)
184+
if err != nil {
185+
return err
186+
}
187+
l.setLoadedTag(ref, imgID, outStream)
188+
}
189+
}
190+
191+
return nil
192+
}
193+
194+
func (l *tarexporter) legacyLoadImage(oldID, sourceDir string, loadedMap map[string]image.ID) error {
195+
if _, loaded := loadedMap[oldID]; loaded {
196+
return nil
197+
}
198+
configPath, err := safePath(sourceDir, filepath.Join(oldID, legacyConfigFileName))
199+
if err != nil {
200+
return err
201+
}
202+
imageJSON, err := ioutil.ReadFile(configPath)
203+
if err != nil {
204+
logrus.Debugf("Error reading json: %v", err)
205+
return err
206+
}
207+
208+
var img struct{ Parent string }
209+
if err := json.Unmarshal(imageJSON, &img); err != nil {
210+
return err
211+
}
212+
213+
var parentID image.ID
214+
if img.Parent != "" {
215+
for {
216+
var loaded bool
217+
if parentID, loaded = loadedMap[img.Parent]; !loaded {
218+
if err := l.legacyLoadImage(img.Parent, sourceDir, loadedMap); err != nil {
219+
return err
220+
}
221+
} else {
222+
break
223+
}
224+
}
225+
}
226+
227+
// todo: try to connect with migrate code
228+
rootFS := image.NewRootFS()
229+
var history []image.History
230+
231+
if parentID != "" {
232+
parentImg, err := l.is.Get(parentID)
233+
if err != nil {
234+
return err
235+
}
236+
237+
rootFS = parentImg.RootFS
238+
history = parentImg.History
239+
}
240+
241+
layerPath, err := safePath(sourceDir, filepath.Join(oldID, legacyLayerFileName))
242+
if err != nil {
243+
return err
244+
}
245+
newLayer, err := l.loadLayer(layerPath, *rootFS)
246+
if err != nil {
247+
return err
248+
}
249+
rootFS.Append(newLayer.DiffID())
250+
251+
h, err := v1.HistoryFromConfig(imageJSON, false)
252+
if err != nil {
253+
return err
254+
}
255+
history = append(history, h)
256+
257+
config, err := v1.MakeConfigFromV1Config(imageJSON, rootFS, history)
258+
if err != nil {
259+
return err
260+
}
261+
imgID, err := l.is.Create(config)
262+
if err != nil {
263+
return err
264+
}
265+
266+
metadata, err := l.ls.Release(newLayer)
267+
layer.LogReleaseMetadata(metadata)
268+
if err != nil {
269+
return err
270+
}
271+
272+
if parentID != "" {
273+
if err := l.is.SetParent(imgID, parentID); err != nil {
274+
return err
275+
}
276+
}
277+
278+
loadedMap[oldID] = imgID
279+
return nil
280+
}
281+
282+
func safePath(base, path string) (string, error) {
283+
return symlink.FollowSymlinkInScope(filepath.Join(base, path), base)
284+
}

‎image/tarexport/save.go

+303
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
package tarexport
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"io/ioutil"
8+
"os"
9+
"path/filepath"
10+
"time"
11+
12+
"github.com/docker/distribution/digest"
13+
"github.com/docker/distribution/reference"
14+
"github.com/docker/docker/image"
15+
"github.com/docker/docker/image/v1"
16+
"github.com/docker/docker/layer"
17+
"github.com/docker/docker/pkg/archive"
18+
"github.com/docker/docker/registry"
19+
"github.com/docker/docker/tag"
20+
)
21+
22+
type imageDescriptor struct {
23+
refs []reference.NamedTagged
24+
layers []string
25+
}
26+
27+
type saveSession struct {
28+
*tarexporter
29+
outDir string
30+
images map[image.ID]*imageDescriptor
31+
savedLayers map[string]struct{}
32+
}
33+
34+
func (l *tarexporter) Save(names []string, outStream io.Writer) error {
35+
images, err := l.parseNames(names)
36+
if err != nil {
37+
return err
38+
}
39+
40+
return (&saveSession{tarexporter: l, images: images}).save(outStream)
41+
}
42+
43+
func (l *tarexporter) parseNames(names []string) (map[image.ID]*imageDescriptor, error) {
44+
imgDescr := make(map[image.ID]*imageDescriptor)
45+
46+
addAssoc := func(id image.ID, ref reference.Named) {
47+
if _, ok := imgDescr[id]; !ok {
48+
imgDescr[id] = &imageDescriptor{}
49+
}
50+
51+
if ref != nil {
52+
var tagged reference.NamedTagged
53+
if _, ok := ref.(reference.Digested); ok {
54+
return
55+
}
56+
var ok bool
57+
if tagged, ok = ref.(reference.NamedTagged); !ok {
58+
var err error
59+
if tagged, err = reference.WithTag(ref, tag.DefaultTag); err != nil {
60+
return
61+
}
62+
}
63+
64+
for _, t := range imgDescr[id].refs {
65+
if tagged.String() == t.String() {
66+
return
67+
}
68+
}
69+
imgDescr[id].refs = append(imgDescr[id].refs, tagged)
70+
}
71+
}
72+
73+
for _, name := range names {
74+
ref, err := reference.ParseNamed(name)
75+
if err != nil {
76+
return nil, err
77+
}
78+
ref = registry.NormalizeLocalReference(ref)
79+
if ref.Name() == string(digest.Canonical) {
80+
imgID, err := l.is.Search(name)
81+
if err != nil {
82+
return nil, err
83+
}
84+
addAssoc(imgID, nil)
85+
continue
86+
}
87+
if _, ok := ref.(reference.Digested); !ok {
88+
if _, ok := ref.(reference.NamedTagged); !ok {
89+
assocs := l.ts.ReferencesByName(ref)
90+
for _, assoc := range assocs {
91+
addAssoc(assoc.ImageID, assoc.Ref)
92+
}
93+
if len(assocs) == 0 {
94+
imgID, err := l.is.Search(name)
95+
if err != nil {
96+
return nil, err
97+
}
98+
addAssoc(imgID, nil)
99+
}
100+
continue
101+
}
102+
}
103+
var imgID image.ID
104+
if imgID, err = l.ts.Get(ref); err != nil {
105+
return nil, err
106+
}
107+
addAssoc(imgID, ref)
108+
109+
}
110+
return imgDescr, nil
111+
}
112+
113+
func (s *saveSession) save(outStream io.Writer) error {
114+
s.savedLayers = make(map[string]struct{})
115+
116+
// get image json
117+
tempDir, err := ioutil.TempDir("", "docker-export-")
118+
if err != nil {
119+
return err
120+
}
121+
defer os.RemoveAll(tempDir)
122+
123+
s.outDir = tempDir
124+
reposLegacy := make(map[string]map[string]string)
125+
126+
var manifest []manifestItem
127+
128+
for id, imageDescr := range s.images {
129+
if err = s.saveImage(id); err != nil {
130+
return err
131+
}
132+
133+
var repoTags []string
134+
var layers []string
135+
136+
for _, ref := range imageDescr.refs {
137+
if _, ok := reposLegacy[ref.Name()]; !ok {
138+
reposLegacy[ref.Name()] = make(map[string]string)
139+
}
140+
reposLegacy[ref.Name()][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1]
141+
repoTags = append(repoTags, ref.String())
142+
}
143+
144+
for _, l := range imageDescr.layers {
145+
layers = append(layers, filepath.Join(l, legacyLayerFileName))
146+
}
147+
148+
manifest = append(manifest, manifestItem{
149+
Config: digest.Digest(id).Hex() + ".json",
150+
RepoTags: repoTags,
151+
Layers: layers,
152+
})
153+
}
154+
155+
if len(reposLegacy) > 0 {
156+
reposFile := filepath.Join(tempDir, legacyRepositoriesFileName)
157+
f, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
158+
if err != nil {
159+
f.Close()
160+
return err
161+
}
162+
if err := json.NewEncoder(f).Encode(reposLegacy); err != nil {
163+
return err
164+
}
165+
if err := f.Close(); err != nil {
166+
return err
167+
}
168+
if err := os.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
169+
return err
170+
}
171+
}
172+
173+
manifestFileName := filepath.Join(tempDir, manifestFileName)
174+
f, err := os.OpenFile(manifestFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
175+
if err != nil {
176+
f.Close()
177+
return err
178+
}
179+
if err := json.NewEncoder(f).Encode(manifest); err != nil {
180+
return err
181+
}
182+
if err := f.Close(); err != nil {
183+
return err
184+
}
185+
if err := os.Chtimes(manifestFileName, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
186+
return err
187+
}
188+
189+
fs, err := archive.Tar(tempDir, archive.Uncompressed)
190+
if err != nil {
191+
return err
192+
}
193+
defer fs.Close()
194+
195+
if _, err := io.Copy(outStream, fs); err != nil {
196+
return err
197+
}
198+
return nil
199+
}
200+
201+
func (s *saveSession) saveImage(id image.ID) error {
202+
img, err := s.is.Get(id)
203+
if err != nil {
204+
return err
205+
}
206+
207+
if len(img.RootFS.DiffIDs) == 0 {
208+
return fmt.Errorf("empty export - not implemented")
209+
}
210+
211+
var parent digest.Digest
212+
var layers []string
213+
for i := range img.RootFS.DiffIDs {
214+
v1Img := image.V1Image{}
215+
if i == len(img.RootFS.DiffIDs)-1 {
216+
v1Img = img.V1Image
217+
}
218+
rootFS := *img.RootFS
219+
rootFS.DiffIDs = rootFS.DiffIDs[:i+1]
220+
v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent)
221+
if err != nil {
222+
return err
223+
}
224+
225+
v1Img.ID = v1ID.Hex()
226+
if parent != "" {
227+
v1Img.Parent = parent.Hex()
228+
}
229+
230+
if err := s.saveLayer(rootFS.ChainID(), v1Img, img.Created); err != nil {
231+
return err
232+
}
233+
layers = append(layers, v1Img.ID)
234+
parent = v1ID
235+
}
236+
237+
configFile := filepath.Join(s.outDir, digest.Digest(id).Hex()+".json")
238+
if err := ioutil.WriteFile(configFile, img.RawJSON(), 0644); err != nil {
239+
return err
240+
}
241+
if err := os.Chtimes(configFile, img.Created, img.Created); err != nil {
242+
return err
243+
}
244+
245+
s.images[id].layers = layers
246+
return nil
247+
}
248+
249+
func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, createdTime time.Time) error {
250+
if _, exists := s.savedLayers[legacyImg.ID]; exists {
251+
return nil
252+
}
253+
254+
outDir := filepath.Join(s.outDir, legacyImg.ID)
255+
if err := os.Mkdir(outDir, 0755); err != nil {
256+
return err
257+
}
258+
259+
// todo: why is this version file here?
260+
if err := ioutil.WriteFile(filepath.Join(outDir, legacyVersionFileName), []byte("1.0"), 0644); err != nil {
261+
return err
262+
}
263+
264+
imageConfig, err := json.Marshal(legacyImg)
265+
if err != nil {
266+
return err
267+
}
268+
269+
if err := ioutil.WriteFile(filepath.Join(outDir, legacyConfigFileName), imageConfig, 0644); err != nil {
270+
return err
271+
}
272+
273+
// serialize filesystem
274+
tarFile, err := os.Create(filepath.Join(outDir, legacyLayerFileName))
275+
if err != nil {
276+
return err
277+
}
278+
defer tarFile.Close()
279+
280+
l, err := s.ls.Get(id)
281+
if err != nil {
282+
return err
283+
}
284+
defer layer.ReleaseAndLog(s.ls, l)
285+
286+
arch, err := l.TarStream()
287+
if err != nil {
288+
return err
289+
}
290+
if _, err := io.Copy(tarFile, arch); err != nil {
291+
return err
292+
}
293+
294+
for _, fname := range []string{"", legacyVersionFileName, legacyConfigFileName, legacyLayerFileName} {
295+
// todo: maybe save layer created timestamp?
296+
if err := os.Chtimes(filepath.Join(outDir, fname), createdTime, createdTime); err != nil {
297+
return err
298+
}
299+
}
300+
301+
s.savedLayers[legacyImg.ID] = struct{}{}
302+
return nil
303+
}

‎image/tarexport/tarexport.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package tarexport
2+
3+
import (
4+
"github.com/docker/docker/image"
5+
"github.com/docker/docker/layer"
6+
"github.com/docker/docker/tag"
7+
)
8+
9+
const (
10+
manifestFileName = "manifest.json"
11+
legacyLayerFileName = "layer.tar"
12+
legacyConfigFileName = "json"
13+
legacyVersionFileName = "VERSION"
14+
legacyRepositoriesFileName = "repositories"
15+
)
16+
17+
type manifestItem struct {
18+
Config string
19+
RepoTags []string
20+
Layers []string
21+
}
22+
23+
type tarexporter struct {
24+
is image.Store
25+
ls layer.Store
26+
ts tag.Store
27+
}
28+
29+
// NewTarExporter returns new ImageExporter for tar packages
30+
func NewTarExporter(is image.Store, ls layer.Store, ts tag.Store) image.Exporter {
31+
return &tarexporter{
32+
is: is,
33+
ls: ls,
34+
ts: ts,
35+
}
36+
}

‎image/v1/imagev1.go

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package v1
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
9+
"github.com/Sirupsen/logrus"
10+
"github.com/docker/distribution/digest"
11+
"github.com/docker/docker/image"
12+
"github.com/docker/docker/layer"
13+
"github.com/docker/docker/pkg/version"
14+
)
15+
16+
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
17+
18+
// noFallbackMinVersion is the minimum version for which v1compatibility
19+
// information will not be marshaled through the Image struct to remove
20+
// blank fields.
21+
var noFallbackMinVersion = version.Version("1.8.3")
22+
23+
// HistoryFromConfig creates a History struct from v1 configuration JSON
24+
func HistoryFromConfig(imageJSON []byte, emptyLayer bool) (image.History, error) {
25+
h := image.History{}
26+
var v1Image image.V1Image
27+
if err := json.Unmarshal(imageJSON, &v1Image); err != nil {
28+
return h, err
29+
}
30+
31+
return image.History{
32+
Author: v1Image.Author,
33+
Created: v1Image.Created,
34+
CreatedBy: strings.Join(v1Image.ContainerConfig.Cmd.Slice(), " "),
35+
Comment: v1Image.Comment,
36+
EmptyLayer: emptyLayer,
37+
}, nil
38+
}
39+
40+
// CreateID creates an ID from v1 image, layerID and parent ID.
41+
// Used for backwards compatibility with old clients.
42+
func CreateID(v1Image image.V1Image, layerID layer.ChainID, parent digest.Digest) (digest.Digest, error) {
43+
v1Image.ID = ""
44+
v1JSON, err := json.Marshal(v1Image)
45+
if err != nil {
46+
return "", err
47+
}
48+
49+
var config map[string]*json.RawMessage
50+
if err := json.Unmarshal(v1JSON, &config); err != nil {
51+
return "", err
52+
}
53+
54+
// FIXME: note that this is slightly incompatible with RootFS logic
55+
config["layer_id"] = rawJSON(layerID)
56+
if parent != "" {
57+
config["parent"] = rawJSON(parent)
58+
}
59+
60+
configJSON, err := json.Marshal(config)
61+
if err != nil {
62+
return "", err
63+
}
64+
logrus.Debugf("CreateV1ID %s", configJSON)
65+
66+
return digest.FromBytes(configJSON)
67+
}
68+
69+
// MakeConfigFromV1Config creates an image config from the legacy V1 config format.
70+
func MakeConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) ([]byte, error) {
71+
var dver struct {
72+
DockerVersion string `json:"docker_version"`
73+
}
74+
75+
if err := json.Unmarshal(imageJSON, &dver); err != nil {
76+
return nil, err
77+
}
78+
79+
useFallback := version.Version(dver.DockerVersion).LessThan(noFallbackMinVersion)
80+
81+
if useFallback {
82+
var v1Image image.V1Image
83+
err := json.Unmarshal(imageJSON, &v1Image)
84+
if err != nil {
85+
return nil, err
86+
}
87+
imageJSON, err = json.Marshal(v1Image)
88+
if err != nil {
89+
return nil, err
90+
}
91+
}
92+
93+
var c map[string]*json.RawMessage
94+
if err := json.Unmarshal(imageJSON, &c); err != nil {
95+
return nil, err
96+
}
97+
98+
delete(c, "id")
99+
delete(c, "parent")
100+
delete(c, "Size") // Size is calculated from data on disk and is inconsitent
101+
delete(c, "parent_id")
102+
delete(c, "layer_id")
103+
delete(c, "throwaway")
104+
105+
c["rootfs"] = rawJSON(rootfs)
106+
c["history"] = rawJSON(history)
107+
108+
return json.Marshal(c)
109+
}
110+
111+
// MakeV1ConfigFromConfig creates an legacy V1 image config from an Image struct
112+
func MakeV1ConfigFromConfig(img *image.Image, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
113+
// Top-level v1compatibility string should be a modified version of the
114+
// image config.
115+
var configAsMap map[string]*json.RawMessage
116+
if err := json.Unmarshal(img.RawJSON(), &configAsMap); err != nil {
117+
return nil, err
118+
}
119+
120+
// Delete fields that didn't exist in old manifest
121+
delete(configAsMap, "rootfs")
122+
delete(configAsMap, "history")
123+
configAsMap["id"] = rawJSON(v1ID)
124+
if parentV1ID != "" {
125+
configAsMap["parent"] = rawJSON(parentV1ID)
126+
}
127+
if throwaway {
128+
configAsMap["throwaway"] = rawJSON(true)
129+
}
130+
131+
return json.Marshal(configAsMap)
132+
}
133+
134+
func rawJSON(value interface{}) *json.RawMessage {
135+
jsonval, err := json.Marshal(value)
136+
if err != nil {
137+
return nil
138+
}
139+
return (*json.RawMessage)(&jsonval)
140+
}
141+
142+
// ValidateID checks whether an ID string is a valid image ID.
143+
func ValidateID(id string) error {
144+
if ok := validHex.MatchString(id); !ok {
145+
return fmt.Errorf("image ID '%s' is invalid ", id)
146+
}
147+
return nil
148+
}

0 commit comments

Comments
 (0)
Please sign in to comment.