Browse code

Calculate hash based image IDs on pull

Generate a hash chain involving the image configuration, layer digests,
and parent image hashes. Use the digests to compute IDs for each image
in a manifest, instead of using the remotely specified IDs.

To avoid breaking users' caches, check for images already in the graph
under old IDs, and avoid repulling an image if the version on disk under
the legacy ID ends up with the same digest that was computed from the
manifest for that image.

When a calculated ID already exists in the graph but can't be verified,
continue trying SHA256(digest) until a suitable ID is found.

"save" and "load" are not changed to use a similar scheme. "load" will
preserve the IDs present in the tar file.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Tonis Tiigi authored on 2015/08/27 06:58:56
Showing 26 changed files
... ...
@@ -27,5 +27,5 @@ type Tagger interface {
27 27
 // functions without needing to import graph.
28 28
 type Recorder interface {
29 29
 	Exists(id string) bool
30
-	Register(img *image.Image, layerData io.Reader) error
30
+	Register(img image.Descriptor, layerData io.Reader) error
31 31
 }
... ...
@@ -40,6 +40,26 @@ const (
40 40
 	filterDriver
41 41
 )
42 42
 
43
+// CustomImageDescriptor is an image descriptor for use by RestoreCustomImages
44
+type customImageDescriptor struct {
45
+	img *image.Image
46
+}
47
+
48
+// ID returns the image ID specified in the image structure.
49
+func (img customImageDescriptor) ID() string {
50
+	return img.img.ID
51
+}
52
+
53
+// Parent returns the parent ID - in this case, none
54
+func (img customImageDescriptor) Parent() string {
55
+	return ""
56
+}
57
+
58
+// MarshalConfig renders the image structure into JSON.
59
+func (img customImageDescriptor) MarshalConfig() ([]byte, error) {
60
+	return json.Marshal(img.img)
61
+}
62
+
43 63
 // Driver represents a windows graph driver.
44 64
 type Driver struct {
45 65
 	// info stores the shim driver information
... ...
@@ -426,7 +446,7 @@ func (d *Driver) RestoreCustomImages(tagger graphdriver.Tagger, recorder graphdr
426 426
 				Size:          imageData.Size,
427 427
 			}
428 428
 
429
-			if err := recorder.Register(img, nil); err != nil {
429
+			if err := recorder.Register(customImageDescriptor{img}, nil); err != nil {
430 430
 				return nil, err
431 431
 			}
432 432
 
... ...
@@ -112,6 +112,11 @@ func (s *TagStore) ImageExport(names []string, outStream io.Writer) error {
112 112
 
113 113
 func (s *TagStore) exportImage(name, tempdir string) error {
114 114
 	for n := name; n != ""; {
115
+		img, err := s.LookupImage(n)
116
+		if err != nil || img == nil {
117
+			return fmt.Errorf("No such image %s", n)
118
+		}
119
+
115 120
 		// temporary directory
116 121
 		tmpImageDir := filepath.Join(tempdir, n)
117 122
 		if err := os.Mkdir(tmpImageDir, os.FileMode(0755)); err != nil {
... ...
@@ -128,19 +133,17 @@ func (s *TagStore) exportImage(name, tempdir string) error {
128 128
 			return err
129 129
 		}
130 130
 
131
-		// serialize json
132
-		json, err := os.Create(filepath.Join(tmpImageDir, "json"))
131
+		imageInspectRaw, err := json.Marshal(img)
133 132
 		if err != nil {
134 133
 			return err
135 134
 		}
136
-		img, err := s.LookupImage(n)
137
-		if err != nil || img == nil {
138
-			return fmt.Errorf("No such image %s", n)
139
-		}
140
-		imageInspectRaw, err := s.graph.RawJSON(img.ID)
135
+
136
+		// serialize json
137
+		json, err := os.Create(filepath.Join(tmpImageDir, "json"))
141 138
 		if err != nil {
142 139
 			return err
143 140
 		}
141
+
144 142
 		written, err := json.Write(imageInspectRaw)
145 143
 		if err != nil {
146 144
 			return err
... ...
@@ -31,6 +31,26 @@ import (
31 31
 	"github.com/vbatts/tar-split/tar/storage"
32 32
 )
33 33
 
34
+// v1Descriptor is a non-content-addressable image descriptor
35
+type v1Descriptor struct {
36
+	img *image.Image
37
+}
38
+
39
+// ID returns the image ID specified in the image structure.
40
+func (img v1Descriptor) ID() string {
41
+	return img.img.ID
42
+}
43
+
44
+// Parent returns the parent ID specified in the image structure.
45
+func (img v1Descriptor) Parent() string {
46
+	return img.img.Parent
47
+}
48
+
49
+// MarshalConfig renders the image structure into JSON.
50
+func (img v1Descriptor) MarshalConfig() ([]byte, error) {
51
+	return json.Marshal(img.img)
52
+}
53
+
34 54
 // The type is used to protect pulling or building related image
35 55
 // layers from deleteing when filtered by dangling=true
36 56
 // The key of layers is the images ID which is pulling or building
... ...
@@ -88,10 +108,12 @@ type Graph struct {
88 88
 
89 89
 // file names for ./graph/<ID>/
90 90
 const (
91
-	jsonFileName      = "json"
92
-	layersizeFileName = "layersize"
93
-	digestFileName    = "checksum"
94
-	tarDataFileName   = "tar-data.json.gz"
91
+	jsonFileName            = "json"
92
+	layersizeFileName       = "layersize"
93
+	digestFileName          = "checksum"
94
+	tarDataFileName         = "tar-data.json.gz"
95
+	v1CompatibilityFileName = "v1Compatibility"
96
+	parentFileName          = "parent"
95 97
 )
96 98
 
97 99
 var (
... ...
@@ -225,7 +247,7 @@ func (graph *Graph) Create(layerData io.Reader, containerID, containerImage, com
225 225
 		img.ContainerConfig = *containerConfig
226 226
 	}
227 227
 
228
-	if err := graph.Register(img, layerData); err != nil {
228
+	if err := graph.Register(v1Descriptor{img}, layerData); err != nil {
229 229
 		return nil, err
230 230
 	}
231 231
 	return img, nil
... ...
@@ -233,19 +255,26 @@ func (graph *Graph) Create(layerData io.Reader, containerID, containerImage, com
233 233
 
234 234
 // Register imports a pre-existing image into the graph.
235 235
 // Returns nil if the image is already registered.
236
-func (graph *Graph) Register(img *image.Image, layerData io.Reader) (err error) {
236
+func (graph *Graph) Register(im image.Descriptor, layerData io.Reader) (err error) {
237
+	imgID := im.ID()
237 238
 
238
-	if err := image.ValidateID(img.ID); err != nil {
239
+	if err := image.ValidateID(imgID); err != nil {
239 240
 		return err
240 241
 	}
241 242
 
242 243
 	// We need this entire operation to be atomic within the engine. Note that
243 244
 	// this doesn't mean Register is fully safe yet.
244
-	graph.imageMutex.Lock(img.ID)
245
-	defer graph.imageMutex.Unlock(img.ID)
245
+	graph.imageMutex.Lock(imgID)
246
+	defer graph.imageMutex.Unlock(imgID)
247
+
248
+	return graph.register(im, layerData)
249
+}
250
+
251
+func (graph *Graph) register(im image.Descriptor, layerData io.Reader) (err error) {
252
+	imgID := im.ID()
246 253
 
247 254
 	// Skip register if image is already registered
248
-	if graph.Exists(img.ID) {
255
+	if graph.Exists(imgID) {
249 256
 		return nil
250 257
 	}
251 258
 
... ...
@@ -255,14 +284,14 @@ func (graph *Graph) Register(img *image.Image, layerData io.Reader) (err error)
255 255
 		// If any error occurs, remove the new dir from the driver.
256 256
 		// Don't check for errors since the dir might not have been created.
257 257
 		if err != nil {
258
-			graph.driver.Remove(img.ID)
258
+			graph.driver.Remove(imgID)
259 259
 		}
260 260
 	}()
261 261
 
262 262
 	// Ensure that the image root does not exist on the filesystem
263 263
 	// when it is not registered in the graph.
264 264
 	// This is common when you switch from one graph driver to another
265
-	if err := os.RemoveAll(graph.imageRoot(img.ID)); err != nil && !os.IsNotExist(err) {
265
+	if err := os.RemoveAll(graph.imageRoot(imgID)); err != nil && !os.IsNotExist(err) {
266 266
 		return err
267 267
 	}
268 268
 
... ...
@@ -270,7 +299,7 @@ func (graph *Graph) Register(img *image.Image, layerData io.Reader) (err error)
270 270
 	// (the graph is the source of truth).
271 271
 	// Ignore errors, since we don't know if the driver correctly returns ErrNotExist.
272 272
 	// (FIXME: make that mandatory for drivers).
273
-	graph.driver.Remove(img.ID)
273
+	graph.driver.Remove(imgID)
274 274
 
275 275
 	tmp, err := graph.mktemp()
276 276
 	defer os.RemoveAll(tmp)
... ...
@@ -278,26 +307,32 @@ func (graph *Graph) Register(img *image.Image, layerData io.Reader) (err error)
278 278
 		return fmt.Errorf("mktemp failed: %s", err)
279 279
 	}
280 280
 
281
+	parent := im.Parent()
282
+
281 283
 	// Create root filesystem in the driver
282
-	if err := createRootFilesystemInDriver(graph, img); err != nil {
284
+	if err := createRootFilesystemInDriver(graph, imgID, parent, layerData); err != nil {
283 285
 		return err
284 286
 	}
285 287
 
286 288
 	// Apply the diff/layer
287
-	if err := graph.storeImage(img, layerData, tmp); err != nil {
289
+	config, err := im.MarshalConfig()
290
+	if err != nil {
291
+		return err
292
+	}
293
+	if err := graph.storeImage(imgID, parent, config, layerData, tmp); err != nil {
288 294
 		return err
289 295
 	}
290 296
 	// Commit
291
-	if err := os.Rename(tmp, graph.imageRoot(img.ID)); err != nil {
297
+	if err := os.Rename(tmp, graph.imageRoot(imgID)); err != nil {
292 298
 		return err
293 299
 	}
294
-	graph.idIndex.Add(img.ID)
300
+	graph.idIndex.Add(imgID)
295 301
 	return nil
296 302
 }
297 303
 
298
-func createRootFilesystemInDriver(graph *Graph, img *image.Image) error {
299
-	if err := graph.driver.Create(img.ID, img.Parent); err != nil {
300
-		return fmt.Errorf("Driver %s failed to create image rootfs %s: %s", graph.driver, img.ID, err)
304
+func createRootFilesystemInDriver(graph *Graph, id, parent string, layerData io.Reader) error {
305
+	if err := graph.driver.Create(id, parent); err != nil {
306
+		return fmt.Errorf("Driver %s failed to create image rootfs %s: %s", graph.driver, id, err)
301 307
 	}
302 308
 	return nil
303 309
 }
... ...
@@ -480,6 +515,21 @@ func (graph *Graph) loadImage(id string) (*image.Image, error) {
480 480
 	if err := dec.Decode(img); err != nil {
481 481
 		return nil, err
482 482
 	}
483
+
484
+	if img.ID == "" {
485
+		img.ID = id
486
+	}
487
+
488
+	if img.Parent == "" && img.ParentID != "" && img.ParentID.Validate() == nil {
489
+		img.Parent = img.ParentID.Hex()
490
+	}
491
+
492
+	// compatibilityID for parent
493
+	parent, err := ioutil.ReadFile(filepath.Join(root, parentFileName))
494
+	if err == nil && len(parent) > 0 {
495
+		img.Parent = string(parent)
496
+	}
497
+
483 498
 	if err := image.ValidateID(img.ID); err != nil {
484 499
 		return nil, err
485 500
 	}
... ...
@@ -513,11 +563,14 @@ func (graph *Graph) saveSize(root string, size int64) error {
513 513
 	return nil
514 514
 }
515 515
 
516
-// SetDigest sets the digest for the image layer to the provided value.
517
-func (graph *Graph) SetDigest(id string, dgst digest.Digest) error {
516
+// SetLayerDigest sets the digest for the image layer to the provided value.
517
+func (graph *Graph) SetLayerDigest(id string, dgst digest.Digest) error {
518 518
 	graph.imageMutex.Lock(id)
519 519
 	defer graph.imageMutex.Unlock(id)
520 520
 
521
+	return graph.setLayerDigest(id, dgst)
522
+}
523
+func (graph *Graph) setLayerDigest(id string, dgst digest.Digest) error {
521 524
 	root := graph.imageRoot(id)
522 525
 	if err := ioutil.WriteFile(filepath.Join(root, digestFileName), []byte(dgst.String()), 0600); err != nil {
523 526
 		return fmt.Errorf("Error storing digest in %s/%s: %s", root, digestFileName, err)
... ...
@@ -525,11 +578,15 @@ func (graph *Graph) SetDigest(id string, dgst digest.Digest) error {
525 525
 	return nil
526 526
 }
527 527
 
528
-// GetDigest gets the digest for the provide image layer id.
529
-func (graph *Graph) GetDigest(id string) (digest.Digest, error) {
528
+// GetLayerDigest gets the digest for the provide image layer id.
529
+func (graph *Graph) GetLayerDigest(id string) (digest.Digest, error) {
530 530
 	graph.imageMutex.Lock(id)
531 531
 	defer graph.imageMutex.Unlock(id)
532 532
 
533
+	return graph.getLayerDigest(id)
534
+}
535
+
536
+func (graph *Graph) getLayerDigest(id string) (digest.Digest, error) {
533 537
 	root := graph.imageRoot(id)
534 538
 	cs, err := ioutil.ReadFile(filepath.Join(root, digestFileName))
535 539
 	if err != nil {
... ...
@@ -541,6 +598,76 @@ func (graph *Graph) GetDigest(id string) (digest.Digest, error) {
541 541
 	return digest.ParseDigest(string(cs))
542 542
 }
543 543
 
544
+// SetV1CompatibilityConfig stores the v1Compatibility JSON data associated
545
+// with the image in the manifest to the disk
546
+func (graph *Graph) SetV1CompatibilityConfig(id string, data []byte) error {
547
+	graph.imageMutex.Lock(id)
548
+	defer graph.imageMutex.Unlock(id)
549
+
550
+	return graph.setV1CompatibilityConfig(id, data)
551
+}
552
+func (graph *Graph) setV1CompatibilityConfig(id string, data []byte) error {
553
+	root := graph.imageRoot(id)
554
+	return ioutil.WriteFile(filepath.Join(root, v1CompatibilityFileName), data, 0600)
555
+}
556
+
557
+// GetV1CompatibilityConfig reads the v1Compatibility JSON data for the image
558
+// from the disk
559
+func (graph *Graph) GetV1CompatibilityConfig(id string) ([]byte, error) {
560
+	graph.imageMutex.Lock(id)
561
+	defer graph.imageMutex.Unlock(id)
562
+
563
+	return graph.getV1CompatibilityConfig(id)
564
+}
565
+
566
+func (graph *Graph) getV1CompatibilityConfig(id string) ([]byte, error) {
567
+	root := graph.imageRoot(id)
568
+	return ioutil.ReadFile(filepath.Join(root, v1CompatibilityFileName))
569
+}
570
+
571
+// GenerateV1CompatibilityChain makes sure v1Compatibility JSON data exists
572
+// for the image. If it doesn't it generates and stores it for the image and
573
+// all of it's parents based on the image config JSON.
574
+func (graph *Graph) GenerateV1CompatibilityChain(id string) ([]byte, error) {
575
+	graph.imageMutex.Lock(id)
576
+	defer graph.imageMutex.Unlock(id)
577
+
578
+	if v1config, err := graph.getV1CompatibilityConfig(id); err == nil {
579
+		return v1config, nil
580
+	}
581
+
582
+	// generate new, store it to disk
583
+	img, err := graph.Get(id)
584
+	if err != nil {
585
+		return nil, err
586
+	}
587
+
588
+	digestPrefix := string(digest.Canonical) + ":"
589
+	img.ID = strings.TrimPrefix(img.ID, digestPrefix)
590
+
591
+	if img.Parent != "" {
592
+		parentConfig, err := graph.GenerateV1CompatibilityChain(img.Parent)
593
+		if err != nil {
594
+			return nil, err
595
+		}
596
+		var parent struct{ ID string }
597
+		err = json.Unmarshal(parentConfig, &parent)
598
+		if err != nil {
599
+			return nil, err
600
+		}
601
+		img.Parent = parent.ID
602
+	}
603
+
604
+	json, err := json.Marshal(img)
605
+	if err != nil {
606
+		return nil, err
607
+	}
608
+	if err := graph.setV1CompatibilityConfig(id, json); err != nil {
609
+		return nil, err
610
+	}
611
+	return json, nil
612
+}
613
+
544 614
 // RawJSON returns the JSON representation for an image as a byte array.
545 615
 func (graph *Graph) RawJSON(id string) ([]byte, error) {
546 616
 	root := graph.imageRoot(id)
... ...
@@ -560,29 +687,38 @@ func jsonPath(root string) string {
560 560
 // storeImage stores file system layer data for the given image to the
561 561
 // graph's storage driver. Image metadata is stored in a file
562 562
 // at the specified root directory.
563
-func (graph *Graph) storeImage(img *image.Image, layerData io.Reader, root string) (err error) {
563
+func (graph *Graph) storeImage(id, parent string, config []byte, layerData io.Reader, root string) (err error) {
564
+	var size int64
564 565
 	// Store the layer. If layerData is not nil, unpack it into the new layer
565 566
 	if layerData != nil {
566
-		if err := graph.disassembleAndApplyTarLayer(img, layerData, root); err != nil {
567
+		if size, err = graph.disassembleAndApplyTarLayer(id, parent, layerData, root); err != nil {
567 568
 			return err
568 569
 		}
569 570
 	}
570 571
 
571
-	if err := graph.saveSize(root, img.Size); err != nil {
572
+	if err := graph.saveSize(root, size); err != nil {
572 573
 		return err
573 574
 	}
574 575
 
575
-	f, err := os.OpenFile(jsonPath(root), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
576
-	if err != nil {
576
+	if err := ioutil.WriteFile(jsonPath(root), config, 0600); err != nil {
577 577
 		return err
578 578
 	}
579 579
 
580
-	defer f.Close()
580
+	// If image is pointing to a parent via CompatibilityID write the reference to disk
581
+	img, err := image.NewImgJSON(config)
582
+	if err != nil {
583
+		return err
584
+	}
581 585
 
582
-	return json.NewEncoder(f).Encode(img)
586
+	if img.ParentID.Validate() == nil && parent != img.ParentID.Hex() {
587
+		if err := ioutil.WriteFile(filepath.Join(root, parentFileName), []byte(parent), 0600); err != nil {
588
+			return err
589
+		}
590
+	}
591
+	return nil
583 592
 }
584 593
 
585
-func (graph *Graph) disassembleAndApplyTarLayer(img *image.Image, layerData io.Reader, root string) (err error) {
594
+func (graph *Graph) disassembleAndApplyTarLayer(id, parent string, layerData io.Reader, root string) (size int64, err error) {
586 595
 	var ar io.Reader
587 596
 
588 597
 	if graph.tarSplitDisabled {
... ...
@@ -591,7 +727,7 @@ func (graph *Graph) disassembleAndApplyTarLayer(img *image.Image, layerData io.R
591 591
 		// this is saving the tar-split metadata
592 592
 		mf, err := os.OpenFile(filepath.Join(root, tarDataFileName), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
593 593
 		if err != nil {
594
-			return err
594
+			return 0, err
595 595
 		}
596 596
 
597 597
 		mfz := gzip.NewWriter(mf)
... ...
@@ -601,24 +737,24 @@ func (graph *Graph) disassembleAndApplyTarLayer(img *image.Image, layerData io.R
601 601
 
602 602
 		inflatedLayerData, err := archive.DecompressStream(layerData)
603 603
 		if err != nil {
604
-			return err
604
+			return 0, err
605 605
 		}
606 606
 
607 607
 		// we're passing nil here for the file putter, because the ApplyDiff will
608 608
 		// handle the extraction of the archive
609 609
 		rdr, err := asm.NewInputTarStream(inflatedLayerData, metaPacker, nil)
610 610
 		if err != nil {
611
-			return err
611
+			return 0, err
612 612
 		}
613 613
 
614 614
 		ar = archive.Reader(rdr)
615 615
 	}
616 616
 
617
-	if img.Size, err = graph.driver.ApplyDiff(img.ID, img.Parent, ar); err != nil {
618
-		return err
617
+	if size, err = graph.driver.ApplyDiff(id, parent, ar); err != nil {
618
+		return 0, err
619 619
 	}
620 620
 
621
-	return nil
621
+	return
622 622
 }
623 623
 
624 624
 func (graph *Graph) assembleTarLayer(img *image.Image) (io.ReadCloser, error) {
... ...
@@ -73,7 +73,7 @@ func TestInterruptedRegister(t *testing.T) {
73 73
 		Created: time.Now(),
74 74
 	}
75 75
 	w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling)
76
-	graph.Register(image, badArchive)
76
+	graph.Register(v1Descriptor{image}, badArchive)
77 77
 	if _, err := graph.Get(image.ID); err == nil {
78 78
 		t.Fatal("Image should not exist after Register is interrupted")
79 79
 	}
... ...
@@ -82,7 +82,7 @@ func TestInterruptedRegister(t *testing.T) {
82 82
 	if err != nil {
83 83
 		t.Fatal(err)
84 84
 	}
85
-	if err := graph.Register(image, goodArchive); err != nil {
85
+	if err := graph.Register(v1Descriptor{image}, goodArchive); err != nil {
86 86
 		t.Fatal(err)
87 87
 	}
88 88
 }
... ...
@@ -130,7 +130,7 @@ func TestRegister(t *testing.T) {
130 130
 		Comment: "testing",
131 131
 		Created: time.Now(),
132 132
 	}
133
-	err = graph.Register(image, archive)
133
+	err = graph.Register(v1Descriptor{image}, archive)
134 134
 	if err != nil {
135 135
 		t.Fatal(err)
136 136
 	}
... ...
@@ -212,7 +212,7 @@ func TestDelete(t *testing.T) {
212 212
 		t.Fatal(err)
213 213
 	}
214 214
 	// Test delete twice (pull -> rm -> pull -> rm)
215
-	if err := graph.Register(img1, archive); err != nil {
215
+	if err := graph.Register(v1Descriptor{img1}, archive); err != nil {
216 216
 		t.Fatal(err)
217 217
 	}
218 218
 	if err := graph.Delete(img1.ID); err != nil {
... ...
@@ -246,9 +246,19 @@ func TestByParent(t *testing.T) {
246 246
 		Created: time.Now(),
247 247
 		Parent:  parentImage.ID,
248 248
 	}
249
-	_ = graph.Register(parentImage, archive1)
250
-	_ = graph.Register(childImage1, archive2)
251
-	_ = graph.Register(childImage2, archive3)
249
+
250
+	err := graph.Register(v1Descriptor{parentImage}, archive1)
251
+	if err != nil {
252
+		t.Fatal(err)
253
+	}
254
+	err = graph.Register(v1Descriptor{childImage1}, archive2)
255
+	if err != nil {
256
+		t.Fatal(err)
257
+	}
258
+	err = graph.Register(v1Descriptor{childImage2}, archive3)
259
+	if err != nil {
260
+		t.Fatal(err)
261
+	}
252 262
 
253 263
 	byParent := graph.ByParent()
254 264
 	numChildren := len(byParent[parentImage.ID])
... ...
@@ -122,7 +122,7 @@ func (s *TagStore) recursiveLoad(address, tmpImageDir string) error {
122 122
 				}
123 123
 			}
124 124
 		}
125
-		if err := s.graph.Register(img, layer); err != nil {
125
+		if err := s.graph.Register(v1Descriptor{img}, layer); err != nil {
126 126
 			return err
127 127
 		}
128 128
 	}
... ...
@@ -127,7 +127,7 @@ func (p *v1Puller) pullRepository(askedTag string) error {
127 127
 	defer func() {
128 128
 		p.graph.Release(sessionID, imgIDs...)
129 129
 	}()
130
-	for _, image := range repoData.ImgList {
130
+	for _, imgData := range repoData.ImgList {
131 131
 		downloadImage := func(img *registry.ImgData) {
132 132
 			if askedTag != "" && img.Tag != askedTag {
133 133
 				errors <- nil
... ...
@@ -140,6 +140,11 @@ func (p *v1Puller) pullRepository(askedTag string) error {
140 140
 				return
141 141
 			}
142 142
 
143
+			if err := image.ValidateID(img.ID); err != nil {
144
+				errors <- err
145
+				return
146
+			}
147
+
143 148
 			// ensure no two downloads of the same image happen at the same time
144 149
 			poolKey := "img:" + img.ID
145 150
 			broadcaster, found := p.poolAdd("pull", poolKey)
... ...
@@ -197,7 +202,7 @@ func (p *v1Puller) pullRepository(askedTag string) error {
197 197
 			errors <- nil
198 198
 		}
199 199
 
200
-		go downloadImage(image)
200
+		go downloadImage(imgData)
201 201
 	}
202 202
 
203 203
 	var lastError error
... ...
@@ -317,7 +322,7 @@ func (p *v1Puller) pullImage(out io.Writer, imgID, endpoint string) (layersDownl
317 317
 				layersDownloaded = true
318 318
 				defer layer.Close()
319 319
 
320
-				err = p.graph.Register(img,
320
+				err = p.graph.Register(v1Descriptor{img},
321 321
 					progressreader.New(progressreader.Config{
322 322
 						In:        layer,
323 323
 						Out:       broadcaster,
... ...
@@ -1,10 +1,12 @@
1 1
 package graph
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
5 6
 	"io"
6 7
 	"io/ioutil"
7 8
 	"os"
9
+	"sync"
8 10
 
9 11
 	"github.com/Sirupsen/logrus"
10 12
 	"github.com/docker/distribution"
... ...
@@ -73,7 +75,8 @@ func (p *v2Puller) pullV2Repository(tag string) (err error) {
73 73
 
74 74
 	}
75 75
 
76
-	broadcaster, found := p.poolAdd("pull", taggedName)
76
+	poolKey := "v2:" + taggedName
77
+	broadcaster, found := p.poolAdd("pull", poolKey)
77 78
 	broadcaster.Add(p.config.OutStream)
78 79
 	if found {
79 80
 		// Another pull of the same repository is already taking place; just wait for it to finish
... ...
@@ -83,7 +86,7 @@ func (p *v2Puller) pullV2Repository(tag string) (err error) {
83 83
 	// This must use a closure so it captures the value of err when the
84 84
 	// function returns, not when the 'defer' is evaluated.
85 85
 	defer func() {
86
-		p.poolRemoveWithError("pull", taggedName, err)
86
+		p.poolRemoveWithError("pull", poolKey, err)
87 87
 	}()
88 88
 
89 89
 	var layersDownloaded bool
... ...
@@ -104,7 +107,8 @@ func (p *v2Puller) pullV2Repository(tag string) (err error) {
104 104
 
105 105
 // downloadInfo is used to pass information from download to extractor
106 106
 type downloadInfo struct {
107
-	img         *image.Image
107
+	img         contentAddressableDescriptor
108
+	imgIndex    int
108 109
 	tmpFile     *os.File
109 110
 	digest      digest.Digest
110 111
 	layer       distribution.ReadSeekCloser
... ...
@@ -114,12 +118,66 @@ type downloadInfo struct {
114 114
 	broadcaster *broadcaster.Buffered
115 115
 }
116 116
 
117
+// contentAddressableDescriptor is used to pass image data from a manifest to the
118
+// graph.
119
+type contentAddressableDescriptor struct {
120
+	id              string
121
+	parent          string
122
+	strongID        digest.Digest
123
+	compatibilityID string
124
+	config          []byte
125
+	v1Compatibility []byte
126
+}
127
+
128
+func newContentAddressableImage(v1Compatibility []byte, blobSum digest.Digest, parent digest.Digest) (contentAddressableDescriptor, error) {
129
+	img := contentAddressableDescriptor{
130
+		v1Compatibility: v1Compatibility,
131
+	}
132
+
133
+	var err error
134
+	img.config, err = image.MakeImageConfig(v1Compatibility, blobSum, parent)
135
+	if err != nil {
136
+		return img, err
137
+	}
138
+	img.strongID, err = image.StrongID(img.config)
139
+	if err != nil {
140
+		return img, err
141
+	}
142
+
143
+	unmarshalledConfig, err := image.NewImgJSON(v1Compatibility)
144
+	if err != nil {
145
+		return img, err
146
+	}
147
+
148
+	img.compatibilityID = unmarshalledConfig.ID
149
+	img.id = img.strongID.Hex()
150
+
151
+	return img, nil
152
+}
153
+
154
+// ID returns the actual ID to be used for the downloaded image. This may be
155
+// a computed ID.
156
+func (img contentAddressableDescriptor) ID() string {
157
+	return img.id
158
+}
159
+
160
+// Parent returns the parent ID to be used for the image. This may be a
161
+// computed ID.
162
+func (img contentAddressableDescriptor) Parent() string {
163
+	return img.parent
164
+}
165
+
166
+// MarshalConfig renders the image structure into JSON.
167
+func (img contentAddressableDescriptor) MarshalConfig() ([]byte, error) {
168
+	return img.config, nil
169
+}
170
+
117 171
 type errVerification struct{}
118 172
 
119 173
 func (errVerification) Error() string { return "verification failed" }
120 174
 
121 175
 func (p *v2Puller) download(di *downloadInfo) {
122
-	logrus.Debugf("pulling blob %q to %s", di.digest, di.img.ID)
176
+	logrus.Debugf("pulling blob %q to %s", di.digest, di.img.id)
123 177
 
124 178
 	blobs := p.repo.Blobs(context.Background())
125 179
 
... ...
@@ -151,12 +209,12 @@ func (p *v2Puller) download(di *downloadInfo) {
151 151
 		Formatter: p.sf,
152 152
 		Size:      di.size,
153 153
 		NewLines:  false,
154
-		ID:        stringid.TruncateID(di.img.ID),
154
+		ID:        stringid.TruncateID(di.img.id),
155 155
 		Action:    "Downloading",
156 156
 	})
157 157
 	io.Copy(di.tmpFile, reader)
158 158
 
159
-	di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Verifying Checksum", nil))
159
+	di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.id), "Verifying Checksum", nil))
160 160
 
161 161
 	if !verifier.Verified() {
162 162
 		err = fmt.Errorf("filesystem layer verification failed for digest %s", di.digest)
... ...
@@ -165,9 +223,9 @@ func (p *v2Puller) download(di *downloadInfo) {
165 165
 		return
166 166
 	}
167 167
 
168
-	di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Download complete", nil))
168
+	di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.id), "Download complete", nil))
169 169
 
170
-	logrus.Debugf("Downloaded %s to tempfile %s", di.img.ID, di.tmpFile.Name())
170
+	logrus.Debugf("Downloaded %s to tempfile %s", di.img.id, di.tmpFile.Name())
171 171
 	di.layer = layerDownload
172 172
 
173 173
 	di.err <- nil
... ...
@@ -193,6 +251,17 @@ func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (verified bo
193 193
 		logrus.Printf("Image manifest for %s has been verified", taggedName)
194 194
 	}
195 195
 
196
+	// remove duplicate layers and check parent chain validity
197
+	err = fixManifestLayers(&manifest.Manifest)
198
+	if err != nil {
199
+		return false, err
200
+	}
201
+
202
+	imgs, err := p.getImageInfos(manifest.Manifest)
203
+	if err != nil {
204
+		return false, err
205
+	}
206
+
196 207
 	out.Write(p.sf.FormatStatus(tag, "Pulling from %s", p.repo.Name()))
197 208
 
198 209
 	var downloads []*downloadInfo
... ...
@@ -213,26 +282,32 @@ func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (verified bo
213 213
 	}()
214 214
 
215 215
 	for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
216
-		img, err := image.NewImgJSON([]byte(manifest.History[i].V1Compatibility))
217
-		if err != nil {
218
-			logrus.Debugf("error getting image v1 json: %v", err)
219
-			return false, err
220
-		}
221
-		p.graph.Retain(p.sessionID, img.ID)
222
-		layerIDs = append(layerIDs, img.ID)
216
+		img := imgs[i]
217
+
218
+		p.graph.Retain(p.sessionID, img.id)
219
+		layerIDs = append(layerIDs, img.id)
220
+
221
+		p.graph.imageMutex.Lock(img.id)
223 222
 
224 223
 		// Check if exists
225
-		if p.graph.Exists(img.ID) {
226
-			logrus.Debugf("Image already exists: %s", img.ID)
227
-			out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Already exists", nil))
224
+		if p.graph.Exists(img.id) {
225
+			if err := p.validateImageInGraph(img.id, imgs, i); err != nil {
226
+				p.graph.imageMutex.Unlock(img.id)
227
+				return false, fmt.Errorf("image validation failed: %v", err)
228
+			}
229
+			logrus.Debugf("Image already exists: %s", img.id)
230
+			p.graph.imageMutex.Unlock(img.id)
228 231
 			continue
229 232
 		}
230
-		out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pulling fs layer", nil))
233
+		p.graph.imageMutex.Unlock(img.id)
234
+
235
+		out.Write(p.sf.FormatProgress(stringid.TruncateID(img.id), "Pulling fs layer", nil))
231 236
 
232 237
 		d := &downloadInfo{
233
-			img:     img,
234
-			poolKey: "layer:" + img.ID,
235
-			digest:  manifest.FSLayers[i].BlobSum,
238
+			img:      img,
239
+			imgIndex: i,
240
+			poolKey:  "v2layer:" + img.id,
241
+			digest:   manifest.FSLayers[i].BlobSum,
236 242
 			// TODO: seems like this chan buffer solved hanging problem in go1.5,
237 243
 			// this can indicate some deeper problem that somehow we never take
238 244
 			// error from channel in loop below
... ...
@@ -274,26 +349,49 @@ func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (verified bo
274 274
 		}
275 275
 
276 276
 		d.tmpFile.Seek(0, 0)
277
-		reader := progressreader.New(progressreader.Config{
278
-			In:        d.tmpFile,
279
-			Out:       d.broadcaster,
280
-			Formatter: p.sf,
281
-			Size:      d.size,
282
-			NewLines:  false,
283
-			ID:        stringid.TruncateID(d.img.ID),
284
-			Action:    "Extracting",
285
-		})
286
-
287
-		err = p.graph.Register(d.img, reader)
288
-		if err != nil {
289
-			return false, err
290
-		}
277
+		err := func() error {
278
+			reader := progressreader.New(progressreader.Config{
279
+				In:        d.tmpFile,
280
+				Out:       d.broadcaster,
281
+				Formatter: p.sf,
282
+				Size:      d.size,
283
+				NewLines:  false,
284
+				ID:        stringid.TruncateID(d.img.id),
285
+				Action:    "Extracting",
286
+			})
287
+
288
+			p.graph.imageMutex.Lock(d.img.id)
289
+			defer p.graph.imageMutex.Unlock(d.img.id)
290
+
291
+			// Must recheck the data on disk if any exists.
292
+			// This protects against races where something
293
+			// else is written to the graph under this ID
294
+			// after attemptIDReuse.
295
+			if p.graph.Exists(d.img.id) {
296
+				if err := p.validateImageInGraph(d.img.id, imgs, d.imgIndex); err != nil {
297
+					return fmt.Errorf("image validation failed: %v", err)
298
+				}
299
+			}
300
+
301
+			if err := p.graph.register(d.img, reader); err != nil {
302
+				return err
303
+			}
304
+
305
+			if err := p.graph.setLayerDigest(d.img.id, d.digest); err != nil {
306
+				return err
307
+			}
308
+
309
+			if err := p.graph.setV1CompatibilityConfig(d.img.id, d.img.v1Compatibility); err != nil {
310
+				return err
311
+			}
291 312
 
292
-		if err := p.graph.SetDigest(d.img.ID, d.digest); err != nil {
313
+			return nil
314
+		}()
315
+		if err != nil {
293 316
 			return false, err
294 317
 		}
295 318
 
296
-		d.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(d.img.ID), "Pull complete", nil))
319
+		d.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(d.img.id), "Pull complete", nil))
297 320
 		d.broadcaster.Close()
298 321
 		tagUpdated = true
299 322
 	}
... ...
@@ -424,3 +522,217 @@ func (p *v2Puller) validateManifest(m *manifest.SignedManifest, tag string) (ver
424 424
 	}
425 425
 	return verified, nil
426 426
 }
427
+
428
+// fixManifestLayers removes repeated layers from the manifest and checks the
429
+// correctness of the parent chain.
430
+func fixManifestLayers(m *manifest.Manifest) error {
431
+	images := make([]*image.Image, len(m.FSLayers))
432
+	for i := range m.FSLayers {
433
+		img, err := image.NewImgJSON([]byte(m.History[i].V1Compatibility))
434
+		if err != nil {
435
+			return err
436
+		}
437
+		images[i] = img
438
+		if err := image.ValidateID(img.ID); err != nil {
439
+			return err
440
+		}
441
+	}
442
+
443
+	if images[len(images)-1].Parent != "" {
444
+		return errors.New("Invalid parent ID in the base layer of the image.")
445
+	}
446
+
447
+	// check general duplicates to error instead of a deadlock
448
+	idmap := make(map[string]struct{})
449
+
450
+	var lastID string
451
+	for _, img := range images {
452
+		// skip IDs that appear after each other, we handle those later
453
+		if _, exists := idmap[img.ID]; img.ID != lastID && exists {
454
+			return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
455
+		}
456
+		lastID = img.ID
457
+		idmap[lastID] = struct{}{}
458
+	}
459
+
460
+	// backwards loop so that we keep the remaining indexes after removing items
461
+	for i := len(images) - 2; i >= 0; i-- {
462
+		if images[i].ID == images[i+1].ID { // repeated ID. remove and continue
463
+			m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...)
464
+			m.History = append(m.History[:i], m.History[i+1:]...)
465
+		} else if images[i].Parent != images[i+1].ID {
466
+			return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", images[i+1].ID, images[i].Parent)
467
+		}
468
+	}
469
+
470
+	return nil
471
+}
472
+
473
+// getImageInfos returns an imageinfo struct for every image in the manifest.
474
+// These objects contain both calculated strongIDs and compatibilityIDs found
475
+// in v1Compatibility object.
476
+func (p *v2Puller) getImageInfos(m manifest.Manifest) ([]contentAddressableDescriptor, error) {
477
+	imgs := make([]contentAddressableDescriptor, len(m.FSLayers))
478
+
479
+	var parent digest.Digest
480
+	for i := len(imgs) - 1; i >= 0; i-- {
481
+		var err error
482
+		imgs[i], err = newContentAddressableImage([]byte(m.History[i].V1Compatibility), m.FSLayers[i].BlobSum, parent)
483
+		if err != nil {
484
+			return nil, err
485
+		}
486
+		parent = imgs[i].strongID
487
+	}
488
+
489
+	p.attemptIDReuse(imgs)
490
+
491
+	return imgs, nil
492
+}
493
+
494
+var idReuseLock sync.Mutex
495
+
496
+// attemptIDReuse does a best attempt to match verified compatibilityIDs
497
+// already in the graph with the computed strongIDs so we can keep using them.
498
+// This process will never fail but may just return the strongIDs if none of
499
+// the compatibilityIDs exists or can be verified. If the strongIDs themselves
500
+// fail verification, we deterministically generate alternate IDs to use until
501
+// we find one that's available or already exists with the correct data.
502
+func (p *v2Puller) attemptIDReuse(imgs []contentAddressableDescriptor) {
503
+	// This function needs to be protected with a global lock, because it
504
+	// locks multiple IDs at once, and there's no good way to make sure
505
+	// the locking happens a deterministic order.
506
+	idReuseLock.Lock()
507
+	defer idReuseLock.Unlock()
508
+
509
+	idMap := make(map[string]struct{})
510
+	for _, img := range imgs {
511
+		idMap[img.id] = struct{}{}
512
+		idMap[img.compatibilityID] = struct{}{}
513
+
514
+		if p.graph.Exists(img.compatibilityID) {
515
+			if _, err := p.graph.GenerateV1CompatibilityChain(img.compatibilityID); err != nil {
516
+				logrus.Debugf("Migration v1Compatibility generation error: %v", err)
517
+				return
518
+			}
519
+		}
520
+	}
521
+	for id := range idMap {
522
+		p.graph.imageMutex.Lock(id)
523
+		defer p.graph.imageMutex.Unlock(id)
524
+	}
525
+
526
+	// continueReuse controls whether the function will try to find
527
+	// existing layers on disk under the old v1 IDs, to avoid repulling
528
+	// them. The hashes are checked to ensure these layers are okay to
529
+	// use. continueReuse starts out as true, but is set to false if
530
+	// the code encounters something that doesn't match the expected hash.
531
+	continueReuse := true
532
+
533
+	for i := len(imgs) - 1; i >= 0; i-- {
534
+		if p.graph.Exists(imgs[i].id) {
535
+			// Found an image in the graph under the strongID. Validate the
536
+			// image before using it.
537
+			if err := p.validateImageInGraph(imgs[i].id, imgs, i); err != nil {
538
+				continueReuse = false
539
+				logrus.Debugf("not using existing strongID: %v", err)
540
+
541
+				// The strong ID existed in the graph but didn't
542
+				// validate successfully. We can't use the strong ID
543
+				// because it didn't validate successfully. Treat the
544
+				// graph like a hash table with probing... compute
545
+				// SHA256(id) until we find an ID that either doesn't
546
+				// already exist in the graph, or has existing content
547
+				// that validates successfully.
548
+				for {
549
+					if err := p.tryNextID(imgs, i, idMap); err != nil {
550
+						logrus.Debug(err.Error())
551
+					} else {
552
+						break
553
+					}
554
+				}
555
+			}
556
+			continue
557
+		}
558
+
559
+		if continueReuse {
560
+			compatibilityID := imgs[i].compatibilityID
561
+			if err := p.validateImageInGraph(compatibilityID, imgs, i); err != nil {
562
+				logrus.Debugf("stopping ID reuse: %v", err)
563
+				continueReuse = false
564
+			} else {
565
+				// The compatibility ID exists in the graph and was
566
+				// validated. Use it.
567
+				imgs[i].id = compatibilityID
568
+			}
569
+		}
570
+	}
571
+
572
+	// fix up the parents of the images
573
+	for i := 0; i < len(imgs); i++ {
574
+		if i == len(imgs)-1 { // Base layer
575
+			imgs[i].parent = ""
576
+		} else {
577
+			imgs[i].parent = imgs[i+1].id
578
+		}
579
+	}
580
+}
581
+
582
+// validateImageInGraph checks that an image in the graph has the expected
583
+// strongID. id is the entry in the graph to check, imgs is the slice of
584
+// images being processed (for access to the parent), and i is the index
585
+// into this slice which the graph entry should be checked against.
586
+func (p *v2Puller) validateImageInGraph(id string, imgs []contentAddressableDescriptor, i int) error {
587
+	img, err := p.graph.Get(id)
588
+	if err != nil {
589
+		return fmt.Errorf("missing: %v", err)
590
+	}
591
+	layerID, err := p.graph.getLayerDigest(id)
592
+	if err != nil {
593
+		return fmt.Errorf("digest: %v", err)
594
+	}
595
+	var parentID digest.Digest
596
+	if i != len(imgs)-1 {
597
+		if img.Parent != imgs[i+1].id { // comparing that graph points to validated ID
598
+			return fmt.Errorf("parent: %v %v", img.Parent, imgs[i+1].id)
599
+		}
600
+		parentID = imgs[i+1].strongID
601
+	} else if img.Parent != "" {
602
+		return fmt.Errorf("unexpected parent: %v", img.Parent)
603
+	}
604
+
605
+	v1Config, err := p.graph.getV1CompatibilityConfig(img.ID)
606
+	if err != nil {
607
+		return fmt.Errorf("v1Compatibility: %v %v", img.ID, err)
608
+	}
609
+
610
+	json, err := image.MakeImageConfig(v1Config, layerID, parentID)
611
+	if err != nil {
612
+		return fmt.Errorf("make config: %v", err)
613
+	}
614
+
615
+	if dgst, err := image.StrongID(json); err == nil && dgst == imgs[i].strongID {
616
+		logrus.Debugf("Validated %v as %v", dgst, id)
617
+	} else {
618
+		return fmt.Errorf("digest mismatch: %v %v, error: %v", dgst, imgs[i].strongID, err)
619
+	}
620
+
621
+	// All clear
622
+	return nil
623
+}
624
+
625
+func (p *v2Puller) tryNextID(imgs []contentAddressableDescriptor, i int, idMap map[string]struct{}) error {
626
+	nextID, _ := digest.FromBytes([]byte(imgs[i].id))
627
+	imgs[i].id = nextID.Hex()
628
+
629
+	if _, exists := idMap[imgs[i].id]; !exists {
630
+		p.graph.imageMutex.Lock(imgs[i].id)
631
+		defer p.graph.imageMutex.Unlock(imgs[i].id)
632
+	}
633
+
634
+	if p.graph.Exists(imgs[i].id) {
635
+		if err := p.validateImageInGraph(imgs[i].id, imgs, i); err != nil {
636
+			return fmt.Errorf("not using existing strongID permutation %s: %v", imgs[i].id, err)
637
+		}
638
+	}
639
+	return nil
640
+}
427 641
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package graph
1
+
2
+import (
3
+	"reflect"
4
+	"strings"
5
+	"testing"
6
+
7
+	"github.com/docker/distribution/digest"
8
+	"github.com/docker/distribution/manifest"
9
+)
10
+
11
+// TestFixManifestLayers checks that fixManifestLayers removes a duplicate
12
+// layer, and that it makes no changes to the manifest when called a second
13
+// time, after the duplicate is removed.
14
+func TestFixManifestLayers(t *testing.T) {
15
+	duplicateLayerManifest := manifest.Manifest{
16
+		FSLayers: []manifest.FSLayer{
17
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
18
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
19
+			{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
20
+		},
21
+		History: []manifest.History{
22
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
23
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
24
+			{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026     go get -v github.com/tools/godep \\u0026\\u0026     godep restore \\u0026\\u0026     go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
25
+		},
26
+	}
27
+
28
+	duplicateLayerManifestExpectedOutput := manifest.Manifest{
29
+		FSLayers: []manifest.FSLayer{
30
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
31
+			{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
32
+		},
33
+		History: []manifest.History{
34
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
35
+			{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026     go get -v github.com/tools/godep \\u0026\\u0026     godep restore \\u0026\\u0026     go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
36
+		},
37
+	}
38
+
39
+	if err := fixManifestLayers(&duplicateLayerManifest); err != nil {
40
+		t.Fatalf("unexpected error from fixManifestLayers: %v", err)
41
+	}
42
+
43
+	if !reflect.DeepEqual(duplicateLayerManifest, duplicateLayerManifestExpectedOutput) {
44
+		t.Fatal("incorrect output from fixManifestLayers on duplicate layer manifest")
45
+	}
46
+
47
+	// Run fixManifestLayers again and confirm that it doesn't change the
48
+	// manifest (which no longer has duplicate layers).
49
+	if err := fixManifestLayers(&duplicateLayerManifest); err != nil {
50
+		t.Fatalf("unexpected error from fixManifestLayers: %v", err)
51
+	}
52
+
53
+	if !reflect.DeepEqual(duplicateLayerManifest, duplicateLayerManifestExpectedOutput) {
54
+		t.Fatal("incorrect output from fixManifestLayers on duplicate layer manifest (second pass)")
55
+	}
56
+}
57
+
58
+// TestFixManifestLayersBaseLayerParent makes sure that fixManifestLayers fails
59
+// if the base layer configuration specifies a parent.
60
+func TestFixManifestLayersBaseLayerParent(t *testing.T) {
61
+	duplicateLayerManifest := manifest.Manifest{
62
+		FSLayers: []manifest.FSLayer{
63
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
64
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
65
+			{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
66
+		},
67
+		History: []manifest.History{
68
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
69
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
70
+			{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"parent\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026     go get -v github.com/tools/godep \\u0026\\u0026     godep restore \\u0026\\u0026     go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
71
+		},
72
+	}
73
+
74
+	if err := fixManifestLayers(&duplicateLayerManifest); err == nil || !strings.Contains(err.Error(), "Invalid parent ID in the base layer of the image.") {
75
+		t.Fatalf("expected an invalid parent ID error from fixManifestLayers")
76
+	}
77
+}
78
+
79
+// TestFixManifestLayersBadParent makes sure that fixManifestLayers fails
80
+// if an image configuration specifies a parent that doesn't directly follow
81
+// that (deduplicated) image in the image history.
82
+func TestFixManifestLayersBadParent(t *testing.T) {
83
+	duplicateLayerManifest := manifest.Manifest{
84
+		FSLayers: []manifest.FSLayer{
85
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
86
+			{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
87
+			{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
88
+		},
89
+		History: []manifest.History{
90
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ac3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
91
+			{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ac3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
92
+			{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026     go get -v github.com/tools/godep \\u0026\\u0026     godep restore \\u0026\\u0026     go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
93
+		},
94
+	}
95
+
96
+	if err := fixManifestLayers(&duplicateLayerManifest); err == nil || !strings.Contains(err.Error(), "Invalid parent ID.") {
97
+		t.Fatalf("expected an invalid parent ID error from fixManifestLayers")
98
+	}
99
+}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 
9 9
 	"github.com/Sirupsen/logrus"
10 10
 	"github.com/docker/distribution/registry/client/transport"
11
+	"github.com/docker/docker/image"
11 12
 	"github.com/docker/docker/pkg/ioutils"
12 13
 	"github.com/docker/docker/pkg/progressreader"
13 14
 	"github.com/docker/docker/pkg/streamformatter"
... ...
@@ -127,7 +128,7 @@ func (s *TagStore) createImageIndex(images []string, tags map[string][]string) [
127 127
 			continue
128 128
 		}
129 129
 		// If the image does not have a tag it still needs to be sent to the
130
-		// registry with an empty tag so that it is accociated with the repository
130
+		// registry with an empty tag so that it is associated with the repository
131 131
 		imageIndex = append(imageIndex, &registry.ImgData{
132 132
 			ID:  id,
133 133
 			Tag: "",
... ...
@@ -137,8 +138,9 @@ func (s *TagStore) createImageIndex(images []string, tags map[string][]string) [
137 137
 }
138 138
 
139 139
 type imagePushData struct {
140
-	id       string
141
-	endpoint string
140
+	id              string
141
+	compatibilityID string
142
+	endpoint        string
142 143
 }
143 144
 
144 145
 // lookupImageOnEndpoint checks the specified endpoint to see if an image exists
... ...
@@ -146,7 +148,7 @@ type imagePushData struct {
146 146
 func (p *v1Pusher) lookupImageOnEndpoint(wg *sync.WaitGroup, images chan imagePushData, imagesToPush chan string) {
147 147
 	defer wg.Done()
148 148
 	for image := range images {
149
-		if err := p.session.LookupRemoteImage(image.id, image.endpoint); err != nil {
149
+		if err := p.session.LookupRemoteImage(image.compatibilityID, image.endpoint); err != nil {
150 150
 			logrus.Errorf("Error in LookupRemoteImage: %s", err)
151 151
 			imagesToPush <- image.id
152 152
 			continue
... ...
@@ -180,9 +182,14 @@ func (p *v1Pusher) pushImageToEndpoint(endpoint string, imageIDs []string, tags
180 180
 		pushes <- shouldPush
181 181
 	}()
182 182
 	for _, id := range imageIDs {
183
+		compatibilityID, err := p.getV1ID(id)
184
+		if err != nil {
185
+			return err
186
+		}
183 187
 		imageData <- imagePushData{
184
-			id:       id,
185
-			endpoint: endpoint,
188
+			id:              id,
189
+			compatibilityID: compatibilityID,
190
+			endpoint:        endpoint,
186 191
 		}
187 192
 	}
188 193
 	// close the channel to notify the workers that there will be no more images to check.
... ...
@@ -202,7 +209,11 @@ func (p *v1Pusher) pushImageToEndpoint(endpoint string, imageIDs []string, tags
202 202
 		}
203 203
 		for _, tag := range tags[id] {
204 204
 			p.out.Write(p.sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", stringid.TruncateID(id), endpoint+"repositories/"+p.repoInfo.RemoteName+"/tags/"+tag))
205
-			if err := p.session.PushRegistryTag(p.repoInfo.RemoteName, id, tag, endpoint); err != nil {
205
+			compatibilityID, err := p.getV1ID(id)
206
+			if err != nil {
207
+				return err
208
+			}
209
+			if err := p.session.PushRegistryTag(p.repoInfo.RemoteName, compatibilityID, tag, endpoint); err != nil {
206 210
 				return err
207 211
 			}
208 212
 		}
... ...
@@ -224,6 +235,12 @@ func (p *v1Pusher) pushRepository(tag string) error {
224 224
 	logrus.Debugf("Preparing to push %s with the following images and tags", p.localRepo)
225 225
 	for _, data := range imageIndex {
226 226
 		logrus.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag)
227
+
228
+		// convert IDs to compatibilityIDs, imageIndex only used in registry calls
229
+		data.ID, err = p.getV1ID(data.ID)
230
+		if err != nil {
231
+			return err
232
+		}
227 233
 	}
228 234
 
229 235
 	if _, found := p.poolAdd("push", p.repoInfo.LocalName); found {
... ...
@@ -253,20 +270,27 @@ func (p *v1Pusher) pushRepository(tag string) error {
253 253
 }
254 254
 
255 255
 func (p *v1Pusher) pushImage(imgID, ep string) (checksum string, err error) {
256
-	jsonRaw, err := p.graph.RawJSON(imgID)
256
+	jsonRaw, err := p.getV1Config(imgID)
257 257
 	if err != nil {
258 258
 		return "", fmt.Errorf("Cannot retrieve the path for {%s}: %s", imgID, err)
259 259
 	}
260 260
 	p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Pushing", nil))
261 261
 
262
+	compatibilityID, err := p.getV1ID(imgID)
263
+	if err != nil {
264
+		return "", err
265
+	}
266
+
267
+	// General rule is to use ID for graph accesses and compatibilityID for
268
+	// calls to session.registry()
262 269
 	imgData := &registry.ImgData{
263
-		ID: imgID,
270
+		ID: compatibilityID,
264 271
 	}
265 272
 
266 273
 	// Send the json
267 274
 	if err := p.session.PushImageJSONRegistry(imgData, jsonRaw, ep); err != nil {
268 275
 		if err == registry.ErrAlreadyExists {
269
-			p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgData.ID), "Image already pushed, skipping", nil))
276
+			p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Image already pushed, skipping", nil))
270 277
 			return "", nil
271 278
 		}
272 279
 		return "", err
... ...
@@ -279,7 +303,7 @@ func (p *v1Pusher) pushImage(imgID, ep string) (checksum string, err error) {
279 279
 	defer os.RemoveAll(layerData.Name())
280 280
 
281 281
 	// Send the layer
282
-	logrus.Debugf("rendered layer for %s of [%d] size", imgData.ID, layerData.Size)
282
+	logrus.Debugf("rendered layer for %s of [%d] size", imgID, layerData.Size)
283 283
 
284 284
 	checksum, checksumPayload, err := p.session.PushImageLayerRegistry(imgData.ID,
285 285
 		progressreader.New(progressreader.Config{
... ...
@@ -288,7 +312,7 @@ func (p *v1Pusher) pushImage(imgID, ep string) (checksum string, err error) {
288 288
 			Formatter: p.sf,
289 289
 			Size:      layerData.Size,
290 290
 			NewLines:  false,
291
-			ID:        stringid.TruncateID(imgData.ID),
291
+			ID:        stringid.TruncateID(imgID),
292 292
 			Action:    "Pushing",
293 293
 		}), ep, jsonRaw)
294 294
 	if err != nil {
... ...
@@ -301,6 +325,30 @@ func (p *v1Pusher) pushImage(imgID, ep string) (checksum string, err error) {
301 301
 		return "", err
302 302
 	}
303 303
 
304
-	p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgData.ID), "Image successfully pushed", nil))
304
+	p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Image successfully pushed", nil))
305 305
 	return imgData.Checksum, nil
306 306
 }
307
+
308
+// getV1ID returns the compatibilityID for the ID in the graph. compatibilityID
309
+// is read from from the v1Compatibility config file in the disk.
310
+func (p *v1Pusher) getV1ID(id string) (string, error) {
311
+	jsonData, err := p.getV1Config(id)
312
+	if err != nil {
313
+		return "", err
314
+	}
315
+	img, err := image.NewImgJSON(jsonData)
316
+	if err != nil {
317
+		return "", err
318
+	}
319
+	return img.ID, nil
320
+}
321
+
322
+// getV1Config returns v1Compatibility config for the image in the graph. If
323
+// there is no v1Compatibility file on disk for the image
324
+func (p *v1Pusher) getV1Config(id string) ([]byte, error) {
325
+	jsonData, err := p.graph.GenerateV1CompatibilityChain(id)
326
+	if err != nil {
327
+		return nil, err
328
+	}
329
+	return jsonData, nil
330
+}
... ...
@@ -138,13 +138,8 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
138 138
 			}
139 139
 		}
140 140
 
141
-		jsonData, err := p.graph.RawJSON(layer.ID)
142
-		if err != nil {
143
-			return fmt.Errorf("cannot retrieve the path for %s: %s", layer.ID, err)
144
-		}
145
-
146 141
 		var exists bool
147
-		dgst, err := p.graph.GetDigest(layer.ID)
142
+		dgst, err := p.graph.GetLayerDigest(layer.ID)
148 143
 		switch err {
149 144
 		case nil:
150 145
 			if p.layersPushed[dgst] {
... ...
@@ -178,13 +173,19 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
178 178
 				return err
179 179
 			} else if pushDigest != dgst {
180 180
 				// Cache new checksum
181
-				if err := p.graph.SetDigest(layer.ID, pushDigest); err != nil {
181
+				if err := p.graph.SetLayerDigest(layer.ID, pushDigest); err != nil {
182 182
 					return err
183 183
 				}
184 184
 				dgst = pushDigest
185 185
 			}
186 186
 		}
187 187
 
188
+		// read v1Compatibility config, generate new if needed
189
+		jsonData, err := p.graph.GenerateV1CompatibilityChain(layer.ID)
190
+		if err != nil {
191
+			return err
192
+		}
193
+
188 194
 		m.FSLayers = append(m.FSLayers, manifest.FSLayer{BlobSum: dgst})
189 195
 		m.History = append(m.History, manifest.History{V1Compatibility: string(jsonData)})
190 196
 
... ...
@@ -82,7 +82,7 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
82 82
 		t.Fatal(err)
83 83
 	}
84 84
 	img := &image.Image{ID: testOfficialImageID}
85
-	if err := graph.Register(img, officialArchive); err != nil {
85
+	if err := graph.Register(v1Descriptor{img}, officialArchive); err != nil {
86 86
 		t.Fatal(err)
87 87
 	}
88 88
 	if err := store.Tag(testOfficialImageName, "", testOfficialImageID, false); err != nil {
... ...
@@ -93,7 +93,7 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
93 93
 		t.Fatal(err)
94 94
 	}
95 95
 	img = &image.Image{ID: testPrivateImageID}
96
-	if err := graph.Register(img, privateArchive); err != nil {
96
+	if err := graph.Register(v1Descriptor{img}, privateArchive); err != nil {
97 97
 		t.Fatal(err)
98 98
 	}
99 99
 	if err := store.Tag(testPrivateImageName, "", testPrivateImageID, false); err != nil {
100 100
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+sha256:f2722a8ec6926e02fa9f2674072cbc2a25cf0f449f27350f613cd843b02c9105
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{"architecture":"amd64","config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":null,"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"fb1f7270da9519308361b99dc8e0d30f12c24dfd28537c2337ece995ac853a16","container_config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":["/bin/sh","-c","#(nop) ADD file:11998b2a4d664a75cd0c3f4e4cb1837434e0f997ba157a0ac1d3c68a07aa2f4f in /"],"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2015-09-08T21:30:30.807853054Z","docker_version":"1.9.0-dev","layer_id":"sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a","os":"linux","parent_id":"sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02"}
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{"id":"8dfb96b5d09e6cf6f376d81f1e2770ee5ede309f9bd9e079688c9782649ab326","parent":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","created":"2015-09-08T21:30:30.807853054Z","container":"fb1f7270da9519308361b99dc8e0d30f12c24dfd28537c2337ece995ac853a16","container_config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":["/bin/sh","-c","#(nop) ADD file:11998b2a4d664a75cd0c3f4e4cb1837434e0f997ba157a0ac1d3c68a07aa2f4f in /"],"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"docker_version":"1.9.0-dev","config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":null,"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"architecture":"amd64","os":"linux"}
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+sha256:fd6ebfedda8ea140a9380767e15bd32c6e899303cfe34bc4580c931f2f816f89
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{"architecture":"amd64","config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":null,"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"container":"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253","container_config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":["/bin/sh","-c","#(nop) ENTRYPOINT [\"/go/bin/dnsdock\"]"],"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"created":"2015-08-19T16:49:11.368300679Z","docker_version":"1.6.2","layer_id":"sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a","os":"linux","parent_id":"sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02"}
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02
0 1
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+{"id":"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9","parent":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","created":"2015-08-19T16:49:11.368300679Z","container":"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253","container_config":{"Hostname":"03797203757d","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"Cpuset":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":["/bin/sh","-c","#(nop) ENTRYPOINT [\"/go/bin/dnsdock\"]"],"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"NetworkDisabled":false,"MacAddress":"","OnBuild":[],"Labels":{}},"docker_version":"1.6.2","config":{"Hostname":"03797203757d","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"Cpuset":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":null,"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"NetworkDisabled":false,"MacAddress":"","OnBuild":[],"Labels":{}},"architecture":"amd64","os":"linux","Size":0}
... ...
@@ -2,19 +2,38 @@ package image
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"fmt"
5 6
 	"regexp"
6 7
 	"time"
7 8
 
9
+	"github.com/Sirupsen/logrus"
10
+	"github.com/docker/distribution/digest"
8 11
 	derr "github.com/docker/docker/errors"
12
+	"github.com/docker/docker/pkg/version"
9 13
 	"github.com/docker/docker/runconfig"
10 14
 )
11 15
 
12 16
 var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
13 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
+// 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)
29
+}
30
+
14 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.
15 34
 type Image struct {
16 35
 	// ID a unique 64 character identifier of the image
17
-	ID string `json:"id"`
36
+	ID string `json:"id,omitempty"`
18 37
 	// Parent id of the image
19 38
 	Parent string `json:"parent,omitempty"`
20 39
 	// Comment user added comment
... ...
@@ -36,7 +55,11 @@ type Image struct {
36 36
 	// OS is the operating system used to build and run the image
37 37
 	OS string `json:"os,omitempty"`
38 38
 	// Size is the total size of the image including all layers it is composed of
39
-	Size int64
39
+	Size int64 `json:",omitempty"` // capitalized for backwards compatibility
40
+	// ParentID specifies the strong, content address of the parent configuration.
41
+	ParentID digest.Digest `json:"parent_id,omitempty"`
42
+	// LayerID provides the content address of the associated layer.
43
+	LayerID digest.Digest `json:"layer_id,omitempty"`
40 44
 }
41 45
 
42 46
 // NewImgJSON creates an Image configuration from json.
... ...
@@ -57,3 +80,70 @@ func ValidateID(id string) error {
57 57
 	}
58 58
 	return nil
59 59
 }
60
+
61
+// MakeImageConfig returns immutable configuration JSON for image based on the
62
+// v1Compatibility object, layer digest and parent StrongID. SHA256() of this
63
+// config is the new image ID (strongID).
64
+func MakeImageConfig(v1Compatibility []byte, layerID, parentID digest.Digest) ([]byte, error) {
65
+
66
+	// Detect images created after 1.8.3
67
+	img, err := NewImgJSON(v1Compatibility)
68
+	if err != nil {
69
+		return nil, err
70
+	}
71
+	useFallback := version.Version(img.DockerVersion).LessThan(noFallbackMinVersion)
72
+
73
+	if useFallback {
74
+		// Fallback for pre-1.8.3. Calculate base config based on Image struct
75
+		// so that fields with default values added by Docker will use same ID
76
+		logrus.Debugf("Using fallback hash for %v", layerID)
77
+
78
+		v1Compatibility, err = json.Marshal(img)
79
+		if err != nil {
80
+			return nil, err
81
+		}
82
+	}
83
+
84
+	var c map[string]*json.RawMessage
85
+	if err := json.Unmarshal(v1Compatibility, &c); err != nil {
86
+		return nil, err
87
+	}
88
+
89
+	if err := layerID.Validate(); err != nil {
90
+		return nil, fmt.Errorf("invalid layerID: %v", err)
91
+	}
92
+
93
+	c["layer_id"] = rawJSON(layerID)
94
+
95
+	if parentID != "" {
96
+		if err := parentID.Validate(); err != nil {
97
+			return nil, fmt.Errorf("invalid parentID %v", err)
98
+		}
99
+		c["parent_id"] = rawJSON(parentID)
100
+	}
101
+
102
+	delete(c, "id")
103
+	delete(c, "parent")
104
+	delete(c, "Size") // Size is calculated from data on disk and is inconsitent
105
+
106
+	return json.Marshal(c)
107
+}
108
+
109
+// StrongID returns image ID for the config JSON.
110
+func StrongID(configJSON []byte) (digest.Digest, error) {
111
+	digester := digest.Canonical.New()
112
+	if _, err := digester.Hash().Write(configJSON); err != nil {
113
+		return "", err
114
+	}
115
+	dgst := digester.Digest()
116
+	logrus.Debugf("H(%v) = %v", string(configJSON), dgst)
117
+	return dgst, nil
118
+}
119
+
120
+func rawJSON(value interface{}) *json.RawMessage {
121
+	jsonval, err := json.Marshal(value)
122
+	if err != nil {
123
+		return nil
124
+	}
125
+	return (*json.RawMessage)(&jsonval)
126
+}
60 127
new file mode 100644
... ...
@@ -0,0 +1,55 @@
0
+package image
1
+
2
+import (
3
+	"bytes"
4
+	"io/ioutil"
5
+	"testing"
6
+
7
+	"github.com/docker/distribution/digest"
8
+)
9
+
10
+var fixtures = []string{
11
+	"fixtures/pre1.9",
12
+	"fixtures/post1.9",
13
+}
14
+
15
+func loadFixtureFile(t *testing.T, path string) []byte {
16
+	fileData, err := ioutil.ReadFile(path)
17
+	if err != nil {
18
+		t.Fatalf("error opening %s: %v", path, err)
19
+	}
20
+
21
+	return bytes.TrimSpace(fileData)
22
+}
23
+
24
+// TestMakeImageConfig makes sure that MakeImageConfig returns the expected
25
+// canonical JSON for a reference Image.
26
+func TestMakeImageConfig(t *testing.T) {
27
+	for _, fixture := range fixtures {
28
+		v1Compatibility := loadFixtureFile(t, fixture+"/v1compatibility")
29
+		expectedConfig := loadFixtureFile(t, fixture+"/expected_config")
30
+		layerID := digest.Digest(loadFixtureFile(t, fixture+"/layer_id"))
31
+		parentID := digest.Digest(loadFixtureFile(t, fixture+"/parent_id"))
32
+
33
+		json, err := MakeImageConfig(v1Compatibility, layerID, parentID)
34
+		if err != nil {
35
+			t.Fatalf("MakeImageConfig on %s returned error: %v", fixture, err)
36
+		}
37
+		if !bytes.Equal(json, expectedConfig) {
38
+			t.Fatalf("did not get expected JSON for %s\nexpected: %s\ngot: %s", fixture, expectedConfig, json)
39
+		}
40
+	}
41
+}
42
+
43
+// TestGetStrongID makes sure that GetConfigJSON returns the expected
44
+// hash for a reference Image.
45
+func TestGetStrongID(t *testing.T) {
46
+	for _, fixture := range fixtures {
47
+		expectedConfig := loadFixtureFile(t, fixture+"/expected_config")
48
+		expectedComputedID := digest.Digest(loadFixtureFile(t, fixture+"/expected_computed_id"))
49
+
50
+		if id, err := StrongID(expectedConfig); err != nil || id != expectedComputedID {
51
+			t.Fatalf("did not get expected ID for %s\nexpected: %s\ngot: %s\nerror: %v", fixture, expectedComputedID, id, err)
52
+		}
53
+	}
54
+}
... ...
@@ -1,7 +1,11 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"encoding/json"
4 5
 	"fmt"
6
+	"io/ioutil"
7
+	"os"
8
+	"path/filepath"
5 9
 	"regexp"
6 10
 	"strings"
7 11
 	"time"
... ...
@@ -159,3 +163,254 @@ func (s *DockerHubPullSuite) TestPullClientDisconnect(c *check.C) {
159 159
 		time.Sleep(500 * time.Millisecond)
160 160
 	}
161 161
 }
162
+
163
+type idAndParent struct {
164
+	ID     string
165
+	Parent string
166
+}
167
+
168
+func inspectImage(c *check.C, imageRef string) idAndParent {
169
+	out, _ := dockerCmd(c, "inspect", imageRef)
170
+	var inspectOutput []idAndParent
171
+	err := json.Unmarshal([]byte(out), &inspectOutput)
172
+	if err != nil {
173
+		c.Fatal(err)
174
+	}
175
+
176
+	return inspectOutput[0]
177
+}
178
+
179
+func imageID(c *check.C, imageRef string) string {
180
+	return inspectImage(c, imageRef).ID
181
+}
182
+
183
+func imageParent(c *check.C, imageRef string) string {
184
+	return inspectImage(c, imageRef).Parent
185
+}
186
+
187
+// TestPullMigration verifies that pulling an image based on layers
188
+// that already exists locally will reuse those existing layers.
189
+func (s *DockerRegistrySuite) TestPullMigration(c *check.C) {
190
+	repoName := privateRegistryURL + "/dockercli/migration"
191
+
192
+	baseImage := repoName + ":base"
193
+	_, err := buildImage(baseImage, fmt.Sprintf(`
194
+	    FROM scratch
195
+	    ENV IMAGE base
196
+	    CMD echo %s
197
+	`, baseImage), true)
198
+	if err != nil {
199
+		c.Fatal(err)
200
+	}
201
+
202
+	baseIDBeforePush := imageID(c, baseImage)
203
+	baseParentBeforePush := imageParent(c, baseImage)
204
+
205
+	derivedImage := repoName + ":derived"
206
+	_, err = buildImage(derivedImage, fmt.Sprintf(`
207
+	    FROM %s
208
+	    CMD echo %s
209
+	`, baseImage, derivedImage), true)
210
+	if err != nil {
211
+		c.Fatal(err)
212
+	}
213
+
214
+	derivedIDBeforePush := imageID(c, derivedImage)
215
+
216
+	dockerCmd(c, "push", derivedImage)
217
+
218
+	// Remove derived image from the local store
219
+	dockerCmd(c, "rmi", derivedImage)
220
+
221
+	// Repull
222
+	dockerCmd(c, "pull", derivedImage)
223
+
224
+	// Check that the parent of this pulled image is the original base
225
+	// image
226
+	derivedIDAfterPull1 := imageID(c, derivedImage)
227
+	derivedParentAfterPull1 := imageParent(c, derivedImage)
228
+
229
+	if derivedIDAfterPull1 == derivedIDBeforePush {
230
+		c.Fatal("image's ID should have changed on after deleting and pulling")
231
+	}
232
+
233
+	if derivedParentAfterPull1 != baseIDBeforePush {
234
+		c.Fatalf("pulled image's parent ID (%s) does not match base image's ID (%s)", derivedParentAfterPull1, baseIDBeforePush)
235
+	}
236
+
237
+	// Confirm that repushing and repulling does not change the computed ID
238
+	dockerCmd(c, "push", derivedImage)
239
+	dockerCmd(c, "rmi", derivedImage)
240
+	dockerCmd(c, "pull", derivedImage)
241
+
242
+	derivedIDAfterPull2 := imageID(c, derivedImage)
243
+	derivedParentAfterPull2 := imageParent(c, derivedImage)
244
+
245
+	if derivedIDAfterPull2 != derivedIDAfterPull1 {
246
+		c.Fatal("image's ID unexpectedly changed after a repush/repull")
247
+	}
248
+
249
+	if derivedParentAfterPull2 != baseIDBeforePush {
250
+		c.Fatalf("pulled image's parent ID (%s) does not match base image's ID (%s)", derivedParentAfterPull2, baseIDBeforePush)
251
+	}
252
+
253
+	// Remove everything, repull, and make sure everything uses computed IDs
254
+	dockerCmd(c, "rmi", baseImage, derivedImage)
255
+	dockerCmd(c, "pull", derivedImage)
256
+
257
+	derivedIDAfterPull3 := imageID(c, derivedImage)
258
+	derivedParentAfterPull3 := imageParent(c, derivedImage)
259
+	derivedGrandparentAfterPull3 := imageParent(c, derivedParentAfterPull3)
260
+
261
+	if derivedIDAfterPull3 != derivedIDAfterPull1 {
262
+		c.Fatal("image's ID unexpectedly changed after a second repull")
263
+	}
264
+
265
+	if derivedParentAfterPull3 == baseIDBeforePush {
266
+		c.Fatalf("pulled image's parent ID (%s) should not match base image's original ID (%s)", derivedParentAfterPull3, derivedIDBeforePush)
267
+	}
268
+
269
+	if derivedGrandparentAfterPull3 == baseParentBeforePush {
270
+		c.Fatal("base image's parent ID should have been rewritten on pull")
271
+	}
272
+}
273
+
274
+// TestPullMigrationRun verifies that pulling an image based on layers
275
+// that already exists locally will result in an image that runs properly.
276
+func (s *DockerRegistrySuite) TestPullMigrationRun(c *check.C) {
277
+	type idAndParent struct {
278
+		ID     string
279
+		Parent string
280
+	}
281
+
282
+	derivedImage := privateRegistryURL + "/dockercli/migration-run"
283
+	baseImage := "busybox"
284
+
285
+	_, err := buildImage(derivedImage, fmt.Sprintf(`
286
+	    FROM %s
287
+	    RUN dd if=/dev/zero of=/file bs=1024 count=1024
288
+	    CMD echo %s
289
+	`, baseImage, derivedImage), true)
290
+	if err != nil {
291
+		c.Fatal(err)
292
+	}
293
+
294
+	baseIDBeforePush := imageID(c, baseImage)
295
+	derivedIDBeforePush := imageID(c, derivedImage)
296
+
297
+	dockerCmd(c, "push", derivedImage)
298
+
299
+	// Remove derived image from the local store
300
+	dockerCmd(c, "rmi", derivedImage)
301
+
302
+	// Repull
303
+	dockerCmd(c, "pull", derivedImage)
304
+
305
+	// Check that this pulled image is based on the original base image
306
+	derivedIDAfterPull1 := imageID(c, derivedImage)
307
+	derivedParentAfterPull1 := imageParent(c, imageParent(c, derivedImage))
308
+
309
+	if derivedIDAfterPull1 == derivedIDBeforePush {
310
+		c.Fatal("image's ID should have changed on after deleting and pulling")
311
+	}
312
+
313
+	if derivedParentAfterPull1 != baseIDBeforePush {
314
+		c.Fatalf("pulled image's parent ID (%s) does not match base image's ID (%s)", derivedParentAfterPull1, baseIDBeforePush)
315
+	}
316
+
317
+	// Make sure the image runs correctly
318
+	out, _ := dockerCmd(c, "run", "--rm", derivedImage)
319
+	if strings.TrimSpace(out) != derivedImage {
320
+		c.Fatalf("expected %s; got %s", derivedImage, out)
321
+	}
322
+
323
+	// Confirm that repushing and repulling does not change the computed ID
324
+	dockerCmd(c, "push", derivedImage)
325
+	dockerCmd(c, "rmi", derivedImage)
326
+	dockerCmd(c, "pull", derivedImage)
327
+
328
+	derivedIDAfterPull2 := imageID(c, derivedImage)
329
+	derivedParentAfterPull2 := imageParent(c, imageParent(c, derivedImage))
330
+
331
+	if derivedIDAfterPull2 != derivedIDAfterPull1 {
332
+		c.Fatal("image's ID unexpectedly changed after a repush/repull")
333
+	}
334
+
335
+	if derivedParentAfterPull2 != baseIDBeforePush {
336
+		c.Fatalf("pulled image's parent ID (%s) does not match base image's ID (%s)", derivedParentAfterPull2, baseIDBeforePush)
337
+	}
338
+
339
+	// Make sure the image still runs
340
+	out, _ = dockerCmd(c, "run", "--rm", derivedImage)
341
+	if strings.TrimSpace(out) != derivedImage {
342
+		c.Fatalf("expected %s; got %s", derivedImage, out)
343
+	}
344
+}
345
+
346
+// TestPullConflict provides coverage of the situation where a computed
347
+// strongID conflicts with some unverifiable data in the graph.
348
+func (s *DockerRegistrySuite) TestPullConflict(c *check.C) {
349
+	repoName := privateRegistryURL + "/dockercli/conflict"
350
+
351
+	_, err := buildImage(repoName, `
352
+	    FROM scratch
353
+	    ENV IMAGE conflict
354
+	    CMD echo conflict
355
+	`, true)
356
+	if err != nil {
357
+		c.Fatal(err)
358
+	}
359
+
360
+	dockerCmd(c, "push", repoName)
361
+
362
+	// Pull to make it content-addressable
363
+	dockerCmd(c, "rmi", repoName)
364
+	dockerCmd(c, "pull", repoName)
365
+
366
+	IDBeforeLoad := imageID(c, repoName)
367
+
368
+	// Load/save to turn this into an unverified image with the same ID
369
+	tmpDir, err := ioutil.TempDir("", "conflict-save-output")
370
+	if err != nil {
371
+		c.Errorf("failed to create temporary directory: %s", err)
372
+	}
373
+	defer os.RemoveAll(tmpDir)
374
+
375
+	tarFile := filepath.Join(tmpDir, "repo.tar")
376
+
377
+	dockerCmd(c, "save", "-o", tarFile, repoName)
378
+	dockerCmd(c, "rmi", repoName)
379
+	dockerCmd(c, "load", "-i", tarFile)
380
+
381
+	// Check that the the ID is the same after save/load.
382
+	IDAfterLoad := imageID(c, repoName)
383
+
384
+	if IDAfterLoad != IDBeforeLoad {
385
+		c.Fatal("image's ID should be the same after save/load")
386
+	}
387
+
388
+	// Repull
389
+	dockerCmd(c, "pull", repoName)
390
+
391
+	// Check that the ID is now different because of the conflict.
392
+	IDAfterPull1 := imageID(c, repoName)
393
+
394
+	// Expect the new ID to be SHA256(oldID)
395
+	expectedIDDigest, err := digest.FromBytes([]byte(IDBeforeLoad))
396
+	if err != nil {
397
+		c.Fatalf("digest error: %v", err)
398
+	}
399
+	expectedID := expectedIDDigest.Hex()
400
+	if IDAfterPull1 != expectedID {
401
+		c.Fatalf("image's ID should have changed on pull to %s (got %s)", expectedID, IDAfterPull1)
402
+	}
403
+
404
+	// A second pull should use the new ID again.
405
+	dockerCmd(c, "pull", repoName)
406
+
407
+	IDAfterPull2 := imageID(c, repoName)
408
+
409
+	if IDAfterPull2 != IDAfterPull1 {
410
+		c.Fatal("image's ID unexpectedly changed after a repull")
411
+	}
412
+}
... ...
@@ -12,6 +12,8 @@ import (
12 12
 // It should hold only portable information about the container.
13 13
 // Here, "portable" means "independent from the host we are running on".
14 14
 // Non-portable information *should* appear in HostConfig.
15
+// All fields added to this struct must be marked `omitempty` to keep getting
16
+// predictable hashes from the old `v1Compatibility` configuration.
15 17
 type Config struct {
16 18
 	Hostname        string                // Hostname
17 19
 	Domainname      string                // Domainname
... ...
@@ -19,7 +21,8 @@ type Config struct {
19 19
 	AttachStdin     bool                  // Attach the standard input, makes possible user interaction
20 20
 	AttachStdout    bool                  // Attach the standard output
21 21
 	AttachStderr    bool                  // Attach the standard error
22
-	ExposedPorts    map[nat.Port]struct{} // List of exposed ports
22
+	ExposedPorts    map[nat.Port]struct{} `json:",omitempty"` // List of exposed ports
23
+	PublishService  string                `json:",omitempty"` // Name of the network service exposed by the container
23 24
 	Tty             bool                  // Attach standard streams to a tty, including stdin if it is not closed.
24 25
 	OpenStdin       bool                  // Open stdin
25 26
 	StdinOnce       bool                  // If true, close stdin after the 1 attached client disconnects.
... ...
@@ -29,11 +32,11 @@ type Config struct {
29 29
 	Volumes         map[string]struct{}   // List of volumes (mounts) used for the container
30 30
 	WorkingDir      string                // Current directory (PWD) in the command will be launched
31 31
 	Entrypoint      *stringutils.StrSlice // Entrypoint to run when starting the container
32
-	NetworkDisabled bool                  // Is network disabled
33
-	MacAddress      string                // Mac Address of the container
32
+	NetworkDisabled bool                  `json:",omitempty"` // Is network disabled
33
+	MacAddress      string                `json:",omitempty"` // Mac Address of the container
34 34
 	OnBuild         []string              // ONBUILD metadata that were defined on the image Dockerfile
35 35
 	Labels          map[string]string     // List of labels set to this container
36
-	StopSignal      string                // Signal to stop a container
36
+	StopSignal      string                `json:",omitempty"` // Signal to stop a container
37 37
 }
38 38
 
39 39
 // DecodeContainerConfig decodes a json encoded config into a ContainerConfigWrapper