Browse Source

Make order of items in "docker images" deterministic

The server-side portion of "docker images" sorts the images it returns
by creation timestamp so the list keeps a consistent order. However, it
does not sort the RepoTags and RepoDigests lists that each image carries
along with it. Since items in these lists are populated from a map,
their order will vary. If the user has a collection of tags which point
to overlapping IDs, for example tags that point to the same images on
different registries, the order will fluctuate between invocations of
"docker images". This can be disorienting with a long list of images.

Sort these references at the tag store level, so that the tag store's
References call always returns references in a lexically sorted order.
As well as giving the tag store more deterministic behavior, doing it at
this level simplifies the tag store unit tests.

Do the same for the ReferencesByName call. This will make push-all-tags
iterate over the tags in a consistent order.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Aaron Lehmann 9 years ago
parent
commit
6e37b622d3
2 changed files with 21 additions and 15 deletions
  1. 17 0
      tag/store.go
  2. 4 15
      tag/store_test.go

+ 17 - 0
tag/store.go

@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"sort"
 	"sync"
 	"sync"
 
 
 	"github.com/docker/distribution/digest"
 	"github.com/docker/distribution/digest"
@@ -55,6 +56,18 @@ type store struct {
 // including the repository name.
 // including the repository name.
 type repository map[string]image.ID
 type repository map[string]image.ID
 
 
+type lexicalRefs []reference.Named
+
+func (a lexicalRefs) Len() int           { return len(a) }
+func (a lexicalRefs) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a lexicalRefs) Less(i, j int) bool { return a[i].String() < a[j].String() }
+
+type lexicalAssociations []Association
+
+func (a lexicalAssociations) Len() int           { return len(a) }
+func (a lexicalAssociations) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a lexicalAssociations) Less(i, j int) bool { return a[i].Ref.String() < a[j].Ref.String() }
+
 func defaultTagIfNameOnly(ref reference.Named) reference.Named {
 func defaultTagIfNameOnly(ref reference.Named) reference.Named {
 	switch ref.(type) {
 	switch ref.(type) {
 	case reference.Tagged:
 	case reference.Tagged:
@@ -218,6 +231,8 @@ func (store *store) References(id image.ID) []reference.Named {
 		references = append(references, ref)
 		references = append(references, ref)
 	}
 	}
 
 
+	sort.Sort(lexicalRefs(references))
+
 	return references
 	return references
 }
 }
 
 
@@ -247,6 +262,8 @@ func (store *store) ReferencesByName(ref reference.Named) []Association {
 			})
 			})
 	}
 	}
 
 
+	sort.Sort(lexicalAssociations(associations))
+
 	return associations
 	return associations
 }
 }
 
 

+ 4 - 15
tag/store_test.go

@@ -5,7 +5,6 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
-	"sort"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 
 
@@ -103,18 +102,6 @@ func TestSave(t *testing.T) {
 	}
 	}
 }
 }
 
 
-type LexicalRefs []reference.Named
-
-func (a LexicalRefs) Len() int           { return len(a) }
-func (a LexicalRefs) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a LexicalRefs) Less(i, j int) bool { return a[i].String() < a[j].String() }
-
-type LexicalAssociations []Association
-
-func (a LexicalAssociations) Len() int           { return len(a) }
-func (a LexicalAssociations) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a LexicalAssociations) Less(i, j int) bool { return a[i].Ref.String() < a[j].Ref.String() }
-
 func TestAddDeleteGet(t *testing.T) {
 func TestAddDeleteGet(t *testing.T) {
 	jsonFile, err := ioutil.TempFile("", "tag-store-test")
 	jsonFile, err := ioutil.TempFile("", "tag-store-test")
 	if err != nil {
 	if err != nil {
@@ -261,10 +248,11 @@ func TestAddDeleteGet(t *testing.T) {
 
 
 	// Check References
 	// Check References
 	refs := store.References(testImageID1)
 	refs := store.References(testImageID1)
-	sort.Sort(LexicalRefs(refs))
 	if len(refs) != 3 {
 	if len(refs) != 3 {
 		t.Fatal("unexpected number of references")
 		t.Fatal("unexpected number of references")
 	}
 	}
+	// Looking for the references in this order verifies that they are
+	// returned lexically sorted.
 	if refs[0].String() != ref3.String() {
 	if refs[0].String() != ref3.String() {
 		t.Fatalf("unexpected reference: %v", refs[0].String())
 		t.Fatalf("unexpected reference: %v", refs[0].String())
 	}
 	}
@@ -281,10 +269,11 @@ func TestAddDeleteGet(t *testing.T) {
 		t.Fatalf("could not parse reference: %v", err)
 		t.Fatalf("could not parse reference: %v", err)
 	}
 	}
 	associations := store.ReferencesByName(repoName)
 	associations := store.ReferencesByName(repoName)
-	sort.Sort(LexicalAssociations(associations))
 	if len(associations) != 3 {
 	if len(associations) != 3 {
 		t.Fatal("unexpected number of associations")
 		t.Fatal("unexpected number of associations")
 	}
 	}
+	// Looking for the associations in this order verifies that they are
+	// returned lexically sorted.
 	if associations[0].Ref.String() != ref3.String() {
 	if associations[0].Ref.String() != ref3.String() {
 		t.Fatalf("unexpected reference: %v", associations[0].Ref.String())
 		t.Fatalf("unexpected reference: %v", associations[0].Ref.String())
 	}
 	}