1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00
moby--moby/tag/store.go
Aaron Lehmann 6e37b622d3 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>
2015-12-07 18:31:51 -08:00

315 lines
8.3 KiB
Go

package tag
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"sync"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/docker/docker/image"
)
// DefaultTag defines the default tag used when performing images related actions and no tag string is specified
const DefaultTag = "latest"
var (
// ErrDoesNotExist is returned if a reference is not found in the
// store.
ErrDoesNotExist = errors.New("reference does not exist")
)
// An Association is a tuple associating a reference with an image ID.
type Association struct {
Ref reference.Named
ImageID image.ID
}
// Store provides the set of methods which can operate on a tag store.
type Store interface {
References(id image.ID) []reference.Named
ReferencesByName(ref reference.Named) []Association
AddTag(ref reference.Named, id image.ID, force bool) error
AddDigest(ref reference.Canonical, id image.ID, force bool) error
Delete(ref reference.Named) (bool, error)
Get(ref reference.Named) (image.ID, error)
}
type store struct {
mu sync.RWMutex
// jsonPath is the path to the file where the serialized tag data is
// stored.
jsonPath string
// Repositories is a map of repositories, indexed by name.
Repositories map[string]repository
// referencesByIDCache is a cache of references indexed by ID, to speed
// up References.
referencesByIDCache map[image.ID]map[string]reference.Named
}
// Repository maps tags to image IDs. The key is a a stringified Reference,
// including the repository name.
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 {
switch ref.(type) {
case reference.Tagged:
return ref
case reference.Digested:
return ref
default:
// Should never fail
ref, _ = reference.WithTag(ref, DefaultTag)
return ref
}
}
// NewTagStore creates a new tag store, tied to a file path where the set of
// tags is serialized in JSON format.
func NewTagStore(jsonPath string) (Store, error) {
abspath, err := filepath.Abs(jsonPath)
if err != nil {
return nil, err
}
store := &store{
jsonPath: abspath,
Repositories: make(map[string]repository),
referencesByIDCache: make(map[image.ID]map[string]reference.Named),
}
// Load the json file if it exists, otherwise create it.
if err := store.reload(); os.IsNotExist(err) {
if err := store.save(); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
return store, nil
}
// Add adds a tag to the store. If force is set to true, existing
// references can be overwritten. This only works for tags, not digests.
func (store *store) AddTag(ref reference.Named, id image.ID, force bool) error {
if _, isDigested := ref.(reference.Digested); isDigested {
return errors.New("refusing to create a tag with a digest reference")
}
return store.addReference(defaultTagIfNameOnly(ref), id, force)
}
// Add adds a digest reference to the store.
func (store *store) AddDigest(ref reference.Canonical, id image.ID, force bool) error {
return store.addReference(ref, id, force)
}
func (store *store) addReference(ref reference.Named, id image.ID, force bool) error {
if ref.Name() == string(digest.Canonical) {
return errors.New("refusing to create an ambiguous tag using digest algorithm as name")
}
store.mu.Lock()
defer store.mu.Unlock()
repository, exists := store.Repositories[ref.Name()]
if !exists || repository == nil {
repository = make(map[string]image.ID)
store.Repositories[ref.Name()] = repository
}
refStr := ref.String()
oldID, exists := repository[refStr]
if exists {
// force only works for tags
if digested, isDigest := ref.(reference.Digested); isDigest {
return fmt.Errorf("Cannot overwrite digest %s", digested.Digest().String())
}
if !force {
return fmt.Errorf("Conflict: Tag %s is already set to image %s, if you want to replace it, please use -f option", ref.String(), oldID.String())
}
if store.referencesByIDCache[oldID] != nil {
delete(store.referencesByIDCache[oldID], refStr)
if len(store.referencesByIDCache[oldID]) == 0 {
delete(store.referencesByIDCache, oldID)
}
}
}
repository[refStr] = id
if store.referencesByIDCache[id] == nil {
store.referencesByIDCache[id] = make(map[string]reference.Named)
}
store.referencesByIDCache[id][refStr] = ref
return store.save()
}
// Delete deletes a reference from the store. It returns true if a deletion
// happened, or false otherwise.
func (store *store) Delete(ref reference.Named) (bool, error) {
ref = defaultTagIfNameOnly(ref)
store.mu.Lock()
defer store.mu.Unlock()
repoName := ref.Name()
repository, exists := store.Repositories[repoName]
if !exists {
return false, ErrDoesNotExist
}
refStr := ref.String()
if id, exists := repository[refStr]; exists {
delete(repository, refStr)
if len(repository) == 0 {
delete(store.Repositories, repoName)
}
if store.referencesByIDCache[id] != nil {
delete(store.referencesByIDCache[id], refStr)
if len(store.referencesByIDCache[id]) == 0 {
delete(store.referencesByIDCache, id)
}
}
return true, store.save()
}
return false, ErrDoesNotExist
}
// Get retrieves an item from the store by reference.
func (store *store) Get(ref reference.Named) (image.ID, error) {
ref = defaultTagIfNameOnly(ref)
store.mu.RLock()
defer store.mu.RUnlock()
repository, exists := store.Repositories[ref.Name()]
if !exists || repository == nil {
return "", ErrDoesNotExist
}
id, exists := repository[ref.String()]
if !exists {
return "", ErrDoesNotExist
}
return id, nil
}
// References returns a slice of references to the given image ID. The slice
// will be nil if there are no references to this image ID.
func (store *store) References(id image.ID) []reference.Named {
store.mu.RLock()
defer store.mu.RUnlock()
// Convert the internal map to an array for two reasons:
// 1) We must not return a mutable reference.
// 2) It would be ugly to expose the extraneous map keys to callers.
var references []reference.Named
for _, ref := range store.referencesByIDCache[id] {
references = append(references, ref)
}
sort.Sort(lexicalRefs(references))
return references
}
// ReferencesByName returns the references for a given repository name.
// If there are no references known for this repository name,
// ReferencesByName returns nil.
func (store *store) ReferencesByName(ref reference.Named) []Association {
store.mu.RLock()
defer store.mu.RUnlock()
repository, exists := store.Repositories[ref.Name()]
if !exists {
return nil
}
var associations []Association
for refStr, refID := range repository {
ref, err := reference.ParseNamed(refStr)
if err != nil {
// Should never happen
return nil
}
associations = append(associations,
Association{
Ref: ref,
ImageID: refID,
})
}
sort.Sort(lexicalAssociations(associations))
return associations
}
func (store *store) save() error {
// Store the json
jsonData, err := json.Marshal(store)
if err != nil {
return err
}
tempFilePath := store.jsonPath + ".tmp"
if err := ioutil.WriteFile(tempFilePath, jsonData, 0600); err != nil {
return err
}
if err := os.Rename(tempFilePath, store.jsonPath); err != nil {
return err
}
return nil
}
func (store *store) reload() error {
f, err := os.Open(store.jsonPath)
if err != nil {
return err
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&store); err != nil {
return err
}
for _, repository := range store.Repositories {
for refStr, refID := range repository {
ref, err := reference.ParseNamed(refStr)
if err != nil {
// Should never happen
continue
}
if store.referencesByIDCache[refID] == nil {
store.referencesByIDCache[refID] = make(map[string]reference.Named)
}
store.referencesByIDCache[refID][refStr] = ref
}
}
return nil
}