Browse code

Cleanup v2 push logic

Manifest is now generated during a v2 push, not relying on previously generated hashes. When pushing a layer, the hash is directly calculated from the tar contents which will be pushed. Computing the hash on push ensures that the hash contents always match what is seen by the registry. This also mitigates issues with tarsum differences and permits using pure SHA digests.
Additionally the new manifest function is moved to the unit tests since it is no longer called outside the tests.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)

Derek McGowan authored on 2015/03/05 05:05:17
Showing 4 changed files
... ...
@@ -4,113 +4,13 @@ import (
4 4
 	"bytes"
5 5
 	"encoding/json"
6 6
 	"fmt"
7
-	"io"
8
-	"io/ioutil"
9 7
 
10 8
 	log "github.com/Sirupsen/logrus"
11 9
 	"github.com/docker/docker/engine"
12
-	"github.com/docker/docker/pkg/tarsum"
13 10
 	"github.com/docker/docker/registry"
14
-	"github.com/docker/docker/runconfig"
15 11
 	"github.com/docker/libtrust"
16 12
 )
17 13
 
18
-func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error) {
19
-	manifest := &registry.ManifestData{
20
-		Name:          remoteName,
21
-		Tag:           tag,
22
-		SchemaVersion: 1,
23
-	}
24
-	localRepo, err := s.Get(localName)
25
-	if err != nil {
26
-		return nil, err
27
-	}
28
-	if localRepo == nil {
29
-		return nil, fmt.Errorf("Repo does not exist: %s", localName)
30
-	}
31
-
32
-	// Get the top-most layer id which the tag points to
33
-	layerId, exists := localRepo[tag]
34
-	if !exists {
35
-		return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag)
36
-	}
37
-	layersSeen := make(map[string]bool)
38
-
39
-	layer, err := s.graph.Get(layerId)
40
-	if err != nil {
41
-		return nil, err
42
-	}
43
-	manifest.Architecture = layer.Architecture
44
-	manifest.FSLayers = make([]*registry.FSLayer, 0, 4)
45
-	manifest.History = make([]*registry.ManifestHistory, 0, 4)
46
-	var metadata runconfig.Config
47
-	if layer.Config != nil {
48
-		metadata = *layer.Config
49
-	}
50
-
51
-	for ; layer != nil; layer, err = layer.GetParent() {
52
-		if err != nil {
53
-			return nil, err
54
-		}
55
-
56
-		if layersSeen[layer.ID] {
57
-			break
58
-		}
59
-		if layer.Config != nil && metadata.Image != layer.ID {
60
-			err = runconfig.Merge(&metadata, layer.Config)
61
-			if err != nil {
62
-				return nil, err
63
-			}
64
-		}
65
-
66
-		checksum, err := layer.GetCheckSum(s.graph.ImageRoot(layer.ID))
67
-		if err != nil {
68
-			return nil, fmt.Errorf("Error getting image checksum: %s", err)
69
-		}
70
-		if tarsum.VersionLabelForChecksum(checksum) != tarsum.Version1.String() {
71
-			archive, err := layer.TarLayer()
72
-			if err != nil {
73
-				return nil, err
74
-			}
75
-
76
-			defer archive.Close()
77
-
78
-			tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1)
79
-			if err != nil {
80
-				return nil, err
81
-			}
82
-			if _, err := io.Copy(ioutil.Discard, tarSum); err != nil {
83
-				return nil, err
84
-			}
85
-
86
-			checksum = tarSum.Sum(nil)
87
-
88
-			// Save checksum value
89
-			if err := layer.SaveCheckSum(s.graph.ImageRoot(layer.ID), checksum); err != nil {
90
-				return nil, err
91
-			}
92
-		}
93
-
94
-		jsonData, err := layer.RawJson()
95
-		if err != nil {
96
-			return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)
97
-		}
98
-
99
-		manifest.FSLayers = append(manifest.FSLayers, &registry.FSLayer{BlobSum: checksum})
100
-
101
-		layersSeen[layer.ID] = true
102
-
103
-		manifest.History = append(manifest.History, &registry.ManifestHistory{V1Compatibility: string(jsonData)})
104
-	}
105
-
106
-	manifestBytes, err := json.MarshalIndent(manifest, "", "   ")
107
-	if err != nil {
108
-		return nil, err
109
-	}
110
-
111
-	return manifestBytes, nil
112
-}
113
-
114 14
 // loadManifest loads a manifest from a byte array and verifies its content.
115 15
 // The signature must be verified or an error is returned. If the manifest
116 16
 // contains no signatures by a trusted key for the name in the manifest, the
... ...
@@ -2,11 +2,16 @@ package graph
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"io/ioutil"
5 8
 	"os"
6 9
 	"testing"
7 10
 
8 11
 	"github.com/docker/docker/image"
12
+	"github.com/docker/docker/pkg/tarsum"
9 13
 	"github.com/docker/docker/registry"
14
+	"github.com/docker/docker/runconfig"
10 15
 	"github.com/docker/docker/utils"
11 16
 )
12 17
 
... ...
@@ -17,6 +22,102 @@ const (
17 17
 	testManifestTag          = "manifesttest"
18 18
 )
19 19
 
20
+func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error) {
21
+	manifest := &registry.ManifestData{
22
+		Name:          remoteName,
23
+		Tag:           tag,
24
+		SchemaVersion: 1,
25
+	}
26
+	localRepo, err := s.Get(localName)
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+	if localRepo == nil {
31
+		return nil, fmt.Errorf("Repo does not exist: %s", localName)
32
+	}
33
+
34
+	// Get the top-most layer id which the tag points to
35
+	layerId, exists := localRepo[tag]
36
+	if !exists {
37
+		return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag)
38
+	}
39
+	layersSeen := make(map[string]bool)
40
+
41
+	layer, err := s.graph.Get(layerId)
42
+	if err != nil {
43
+		return nil, err
44
+	}
45
+	manifest.Architecture = layer.Architecture
46
+	manifest.FSLayers = make([]*registry.FSLayer, 0, 4)
47
+	manifest.History = make([]*registry.ManifestHistory, 0, 4)
48
+	var metadata runconfig.Config
49
+	if layer.Config != nil {
50
+		metadata = *layer.Config
51
+	}
52
+
53
+	for ; layer != nil; layer, err = layer.GetParent() {
54
+		if err != nil {
55
+			return nil, err
56
+		}
57
+
58
+		if layersSeen[layer.ID] {
59
+			break
60
+		}
61
+		if layer.Config != nil && metadata.Image != layer.ID {
62
+			err = runconfig.Merge(&metadata, layer.Config)
63
+			if err != nil {
64
+				return nil, err
65
+			}
66
+		}
67
+
68
+		checksum, err := layer.GetCheckSum(s.graph.ImageRoot(layer.ID))
69
+		if err != nil {
70
+			return nil, fmt.Errorf("Error getting image checksum: %s", err)
71
+		}
72
+		if tarsum.VersionLabelForChecksum(checksum) != tarsum.Version1.String() {
73
+			archive, err := layer.TarLayer()
74
+			if err != nil {
75
+				return nil, err
76
+			}
77
+
78
+			defer archive.Close()
79
+
80
+			tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1)
81
+			if err != nil {
82
+				return nil, err
83
+			}
84
+			if _, err := io.Copy(ioutil.Discard, tarSum); err != nil {
85
+				return nil, err
86
+			}
87
+
88
+			checksum = tarSum.Sum(nil)
89
+
90
+			// Save checksum value
91
+			if err := layer.SaveCheckSum(s.graph.ImageRoot(layer.ID), checksum); err != nil {
92
+				return nil, err
93
+			}
94
+		}
95
+
96
+		jsonData, err := layer.RawJson()
97
+		if err != nil {
98
+			return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)
99
+		}
100
+
101
+		manifest.FSLayers = append(manifest.FSLayers, &registry.FSLayer{BlobSum: checksum})
102
+
103
+		layersSeen[layer.ID] = true
104
+
105
+		manifest.History = append(manifest.History, &registry.ManifestHistory{V1Compatibility: string(jsonData)})
106
+	}
107
+
108
+	manifestBytes, err := json.MarshalIndent(manifest, "", "   ")
109
+	if err != nil {
110
+		return nil, err
111
+	}
112
+
113
+	return manifestBytes, nil
114
+}
115
+
20 116
 func TestManifestTarsumCache(t *testing.T) {
21 117
 	tmp, err := utils.TestDirectory("")
22 118
 	if err != nil {
... ...
@@ -2,6 +2,7 @@ package graph
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"encoding/json"
5 6
 	"errors"
6 7
 	"fmt"
7 8
 	"io"
... ...
@@ -15,7 +16,9 @@ import (
15 15
 	"github.com/docker/docker/engine"
16 16
 	"github.com/docker/docker/image"
17 17
 	"github.com/docker/docker/pkg/common"
18
+	"github.com/docker/docker/pkg/tarsum"
18 19
 	"github.com/docker/docker/registry"
20
+	"github.com/docker/docker/runconfig"
19 21
 	"github.com/docker/docker/utils"
20 22
 	"github.com/docker/libtrust"
21 23
 )
... ...
@@ -69,15 +72,11 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string
69 69
 	return imageList, tagsByImage, nil
70 70
 }
71 71
 
72
-func (s *TagStore) getImageTags(localName, askedTag string) ([]string, error) {
73
-	localRepo, err := s.Get(localName)
74
-	if err != nil {
75
-		return nil, err
76
-	}
72
+func (s *TagStore) getImageTags(localRepo map[string]string, askedTag string) ([]string, error) {
77 73
 	log.Debugf("Checking %s against %#v", askedTag, localRepo)
78 74
 	if len(askedTag) > 0 {
79 75
 		if _, ok := localRepo[askedTag]; !ok {
80
-			return nil, fmt.Errorf("Tag does not exist for %s:%s", localName, askedTag)
76
+			return nil, fmt.Errorf("Tag does not exist: %s", askedTag)
81 77
 		}
82 78
 		return []string{askedTag}, nil
83 79
 	}
... ...
@@ -274,14 +273,7 @@ func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep strin
274 274
 	return imgData.Checksum, nil
275 275
 }
276 276
 
277
-func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter) error {
278
-	if repoInfo.Official {
279
-		j := eng.Job("trust_update_base")
280
-		if err := j.Run(); err != nil {
281
-			log.Errorf("error updating trust base graph: %s", err)
282
-		}
283
-	}
284
-
277
+func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter) error {
285 278
 	endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
286 279
 	if err != nil {
287 280
 		if repoInfo.Index.Official {
... ...
@@ -291,7 +283,7 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out
291 291
 		return fmt.Errorf("error getting registry endpoint: %s", err)
292 292
 	}
293 293
 
294
-	tags, err := s.getImageTags(repoInfo.LocalName, tag)
294
+	tags, err := s.getImageTags(localRepo, tag)
295 295
 	if err != nil {
296 296
 		return err
297 297
 	}
... ...
@@ -305,76 +297,122 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out
305 305
 	}
306 306
 
307 307
 	for _, tag := range tags {
308
-		log.Debugf("Pushing %s:%s to v2 repository", repoInfo.LocalName, tag)
309
-		mBytes, err := s.newManifest(repoInfo.LocalName, repoInfo.RemoteName, tag)
310
-		if err != nil {
311
-			return err
312
-		}
313
-		js, err := libtrust.NewJSONSignature(mBytes)
314
-		if err != nil {
315
-			return err
316
-		}
308
+		log.Debugf("Pushing repository: %s:%s", repoInfo.CanonicalName, tag)
317 309
 
318
-		if err = js.Sign(s.trustKey); err != nil {
319
-			return err
310
+		layerId, exists := localRepo[tag]
311
+		if !exists {
312
+			return fmt.Errorf("tag does not exist: %s", tag)
320 313
 		}
321 314
 
322
-		signedBody, err := js.PrettySignature("signatures")
315
+		layer, err := s.graph.Get(layerId)
323 316
 		if err != nil {
324 317
 			return err
325 318
 		}
326
-		log.Infof("Signed manifest for %s:%s using daemon's key: %s", repoInfo.LocalName, tag, s.trustKey.KeyID())
327
-
328
-		manifestBytes := string(signedBody)
329 319
 
330
-		manifest, verified, err := s.loadManifest(eng, signedBody)
331
-		if err != nil {
332
-			return fmt.Errorf("error verifying manifest: %s", err)
320
+		m := &registry.ManifestData{
321
+			SchemaVersion: 1,
322
+			Name:          repoInfo.RemoteName,
323
+			Tag:           tag,
324
+			Architecture:  layer.Architecture,
333 325
 		}
334
-
335
-		if err := checkValidManifest(manifest); err != nil {
336
-			return fmt.Errorf("invalid manifest: %s", err)
326
+		var metadata runconfig.Config
327
+		if layer.Config != nil {
328
+			metadata = *layer.Config
337 329
 		}
338 330
 
339
-		if verified {
340
-			log.Infof("Pushing verified image, key %s is registered for %q", s.trustKey.KeyID(), repoInfo.RemoteName)
331
+		layersSeen := make(map[string]bool)
332
+		layers := []*image.Image{layer}
333
+		for ; layer != nil; layer, err = layer.GetParent() {
334
+			if err != nil {
335
+				return err
336
+			}
337
+
338
+			if layersSeen[layer.ID] {
339
+				break
340
+			}
341
+			layers = append(layers, layer)
342
+			layersSeen[layer.ID] = true
341 343
 		}
344
+		m.FSLayers = make([]*registry.FSLayer, len(layers))
345
+		m.History = make([]*registry.ManifestHistory, len(layers))
342 346
 
343
-		for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
344
-			var (
345
-				sumStr  = manifest.FSLayers[i].BlobSum
346
-				imgJSON = []byte(manifest.History[i].V1Compatibility)
347
-			)
347
+		// Schema version 1 requires layer ordering from top to root
348
+		for i, layer := range layers {
349
+			log.Debugf("Pushing layer: %s", layer.ID)
348 350
 
349
-			sumParts := strings.SplitN(sumStr, ":", 2)
350
-			if len(sumParts) < 2 {
351
-				return fmt.Errorf("Invalid checksum: %s", sumStr)
351
+			if layer.Config != nil && metadata.Image != layer.ID {
352
+				err = runconfig.Merge(&metadata, layer.Config)
353
+				if err != nil {
354
+					return err
355
+				}
352 356
 			}
353
-			manifestSum := sumParts[1]
354
-
355
-			img, err := image.NewImgJSON(imgJSON)
357
+			jsonData, err := layer.RawJson()
356 358
 			if err != nil {
357
-				return fmt.Errorf("Failed to parse json: %s", err)
359
+				return fmt.Errorf("cannot retrieve the path for %s: %s", layer.ID, err)
358 360
 			}
359 361
 
360
-			// Call mount blob
361
-			exists, err := r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, auth)
362
+			checksum, err := layer.GetCheckSum(s.graph.ImageRoot(layer.ID))
362 363
 			if err != nil {
363
-				out.Write(sf.FormatProgress(common.TruncateID(img.ID), "Image push failed", nil))
364
-				return err
364
+				return fmt.Errorf("error getting image checksum: %s", err)
365 365
 			}
366 366
 
367
+			var exists bool
368
+			if len(checksum) > 0 {
369
+				sumParts := strings.SplitN(checksum, ":", 2)
370
+				if len(sumParts) < 2 {
371
+					return fmt.Errorf("Invalid checksum: %s", checksum)
372
+				}
373
+
374
+				// Call mount blob
375
+				exists, err = r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], sumParts[1], auth)
376
+				if err != nil {
377
+					out.Write(sf.FormatProgress(common.TruncateID(layer.ID), "Image push failed", nil))
378
+					return err
379
+				}
380
+			}
367 381
 			if !exists {
368
-				if err := s.pushV2Image(r, img, endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, sf, out, auth); err != nil {
382
+				if cs, err := s.pushV2Image(r, layer, endpoint, repoInfo.RemoteName, sf, out, auth); err != nil {
369 383
 					return err
384
+				} else if cs != checksum {
385
+					// Cache new checksum
386
+					if err := layer.SaveCheckSum(s.graph.ImageRoot(layer.ID), cs); err != nil {
387
+						return err
388
+					}
389
+					checksum = cs
370 390
 				}
371 391
 			} else {
372
-				out.Write(sf.FormatProgress(common.TruncateID(img.ID), "Image already exists", nil))
392
+				out.Write(sf.FormatProgress(common.TruncateID(layer.ID), "Image already exists", nil))
373 393
 			}
394
+			m.FSLayers[i] = &registry.FSLayer{BlobSum: checksum}
395
+			m.History[i] = &registry.ManifestHistory{V1Compatibility: string(jsonData)}
396
+		}
397
+
398
+		if err := checkValidManifest(m); err != nil {
399
+			return fmt.Errorf("invalid manifest: %s", err)
400
+		}
401
+
402
+		log.Debugf("Pushing %s:%s to v2 repository", repoInfo.LocalName, tag)
403
+		mBytes, err := json.MarshalIndent(m, "", "   ")
404
+		if err != nil {
405
+			return err
406
+		}
407
+		js, err := libtrust.NewJSONSignature(mBytes)
408
+		if err != nil {
409
+			return err
410
+		}
411
+
412
+		if err = js.Sign(s.trustKey); err != nil {
413
+			return err
414
+		}
415
+
416
+		signedBody, err := js.PrettySignature("signatures")
417
+		if err != nil {
418
+			return err
374 419
 		}
420
+		log.Infof("Signed manifest for %s:%s using daemon's key: %s", repoInfo.LocalName, tag, s.trustKey.KeyID())
375 421
 
376 422
 		// push the manifest
377
-		if err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth); err != nil {
423
+		if err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader(signedBody), auth); err != nil {
378 424
 			return err
379 425
 		}
380 426
 	}
... ...
@@ -382,42 +420,51 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out
382 382
 }
383 383
 
384 384
 // PushV2Image pushes the image content to the v2 registry, first buffering the contents to disk
385
-func (s *TagStore) pushV2Image(r *registry.Session, img *image.Image, endpoint *registry.Endpoint, imageName, sumType, sumStr string, sf *utils.StreamFormatter, out io.Writer, auth *registry.RequestAuthorization) error {
385
+func (s *TagStore) pushV2Image(r *registry.Session, img *image.Image, endpoint *registry.Endpoint, imageName string, sf *utils.StreamFormatter, out io.Writer, auth *registry.RequestAuthorization) (string, error) {
386 386
 	out.Write(sf.FormatProgress(common.TruncateID(img.ID), "Buffering to Disk", nil))
387 387
 
388 388
 	image, err := s.graph.Get(img.ID)
389 389
 	if err != nil {
390
-		return err
390
+		return "", err
391 391
 	}
392 392
 	arch, err := image.TarLayer()
393 393
 	if err != nil {
394
-		return err
394
+		return "", err
395 395
 	}
396 396
 	defer arch.Close()
397 397
 
398 398
 	tf, err := s.graph.newTempFile()
399 399
 	if err != nil {
400
-		return err
400
+		return "", err
401 401
 	}
402 402
 	defer func() {
403 403
 		tf.Close()
404 404
 		os.Remove(tf.Name())
405 405
 	}()
406 406
 
407
-	size, err := bufferToFile(tf, arch)
407
+	ts, err := tarsum.NewTarSum(arch, true, tarsum.Version1)
408 408
 	if err != nil {
409
-		return err
409
+		return "", err
410
+	}
411
+	size, err := bufferToFile(tf, ts)
412
+	if err != nil {
413
+		return "", err
414
+	}
415
+	checksum := ts.Sum(nil)
416
+	sumParts := strings.SplitN(checksum, ":", 2)
417
+	if len(sumParts) < 2 {
418
+		return "", fmt.Errorf("Invalid checksum: %s", checksum)
410 419
 	}
411 420
 
412 421
 	// Send the layer
413 422
 	log.Debugf("rendered layer for %s of [%d] size", img.ID, size)
414 423
 
415
-	if err := r.PutV2ImageBlob(endpoint, imageName, sumType, sumStr, utils.ProgressReader(tf, int(size), out, sf, false, common.TruncateID(img.ID), "Pushing"), auth); err != nil {
424
+	if err := r.PutV2ImageBlob(endpoint, imageName, sumParts[0], sumParts[1], utils.ProgressReader(tf, int(size), out, sf, false, common.TruncateID(img.ID), "Pushing"), auth); err != nil {
416 425
 		out.Write(sf.FormatProgress(common.TruncateID(img.ID), "Image push failed", nil))
417
-		return err
426
+		return "", err
418 427
 	}
419 428
 	out.Write(sf.FormatProgress(common.TruncateID(img.ID), "Image successfully pushed", nil))
420
-	return nil
429
+	return checksum, nil
421 430
 }
422 431
 
423 432
 // FIXME: Allow to interrupt current push when new push of same image is done.
... ...
@@ -457,17 +504,6 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
457 457
 		return job.Error(err)
458 458
 	}
459 459
 
460
-	if endpoint.Version == registry.APIVersion2 {
461
-		err := s.pushV2Repository(r, job.Eng, job.Stdout, repoInfo, tag, sf)
462
-		if err == nil {
463
-			return engine.StatusOK
464
-		}
465
-
466
-		if err != ErrV2RegistryUnavailable {
467
-			return job.Errorf("Error pushing to registry: %s", err)
468
-		}
469
-	}
470
-
471 460
 	reposLen := 1
472 461
 	if tag == "" {
473 462
 		reposLen = len(s.Repositories[repoInfo.LocalName])
... ...
@@ -478,6 +514,18 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
478 478
 	if !exists {
479 479
 		return job.Errorf("Repository does not exist: %s", repoInfo.LocalName)
480 480
 	}
481
+
482
+	if endpoint.Version == registry.APIVersion2 {
483
+		err := s.pushV2Repository(r, localRepo, job.Stdout, repoInfo, tag, sf)
484
+		if err == nil {
485
+			return engine.StatusOK
486
+		}
487
+
488
+		if err != ErrV2RegistryUnavailable {
489
+			return job.Errorf("Error pushing to registry: %s", err)
490
+		}
491
+	}
492
+
481 493
 	if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil {
482 494
 		return job.Error(err)
483 495
 	}
... ...
@@ -45,7 +45,7 @@ func TestPushUntagged(t *testing.T) {
45 45
 
46 46
 	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
47 47
 
48
-	expected := "No tags to push"
48
+	expected := "Repository does not exist"
49 49
 	pushCmd := exec.Command(dockerBinary, "push", repoName)
50 50
 	if out, _, err := runCommandWithOutput(pushCmd); err == nil {
51 51
 		t.Fatalf("pushing the image to the private registry should have failed: outuput %q", out)