| package reference // import "github.com/docker/docker/reference" |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "sort" |
| "sync" |
| |
| "github.com/docker/distribution/reference" |
| "github.com/docker/docker/pkg/ioutils" |
| digest "github.com/opencontainers/go-digest" |
| "github.com/pkg/errors" |
| ) |
| |
| var ( |
| // ErrDoesNotExist is returned if a reference is not found in the |
| // store. |
| ErrDoesNotExist notFoundError = "reference does not exist" |
| ) |
| |
| // An Association is a tuple associating a reference with an image ID. |
| type Association struct { |
| Ref reference.Named |
| ID digest.Digest |
| } |
| |
| // Store provides the set of methods which can operate on a reference store. |
| type Store interface { |
| References(id digest.Digest) []reference.Named |
| ReferencesByName(ref reference.Named) []Association |
| AddTag(ref reference.Named, id digest.Digest, force bool) error |
| AddDigest(ref reference.Canonical, id digest.Digest, force bool) error |
| Delete(ref reference.Named) (bool, error) |
| Get(ref reference.Named) (digest.Digest, 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[digest.Digest]map[string]reference.Named |
| } |
| |
| // Repository maps tags to digests. The key is a stringified Reference, |
| // including the repository name. |
| type repository map[string]digest.Digest |
| |
| 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() |
| } |
| |
| // NewReferenceStore creates a new reference store, tied to a file path where |
| // the set of references are serialized in JSON format. |
| func NewReferenceStore(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[digest.Digest]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 |
| } |
| |
| // AddTag adds a tag reference 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 digest.Digest, force bool) error { |
| if _, isCanonical := ref.(reference.Canonical); isCanonical { |
| return errors.WithStack(invalidTagError("refusing to create a tag with a digest reference")) |
| } |
| return store.addReference(reference.TagNameOnly(ref), id, force) |
| } |
| |
| // AddDigest adds a digest reference to the store. |
| func (store *store) AddDigest(ref reference.Canonical, id digest.Digest, force bool) error { |
| return store.addReference(ref, id, force) |
| } |
| |
| func favorDigest(originalRef reference.Named) (reference.Named, error) { |
| ref := originalRef |
| // If the reference includes a digest and a tag, we must store only the |
| // digest. |
| canonical, isCanonical := originalRef.(reference.Canonical) |
| _, isNamedTagged := originalRef.(reference.NamedTagged) |
| |
| if isCanonical && isNamedTagged { |
| trimmed, err := reference.WithDigest(reference.TrimNamed(canonical), canonical.Digest()) |
| if err != nil { |
| // should never happen |
| return originalRef, err |
| } |
| ref = trimmed |
| } |
| return ref, nil |
| } |
| |
| func (store *store) addReference(ref reference.Named, id digest.Digest, force bool) error { |
| ref, err := favorDigest(ref) |
| if err != nil { |
| return err |
| } |
| |
| refName := reference.FamiliarName(ref) |
| refStr := reference.FamiliarString(ref) |
| |
| if refName == string(digest.Canonical) { |
| return errors.WithStack(invalidTagError("refusing to create an ambiguous tag using digest algorithm as name")) |
| } |
| |
| store.mu.Lock() |
| defer store.mu.Unlock() |
| |
| repository, exists := store.Repositories[refName] |
| if !exists || repository == nil { |
| repository = make(map[string]digest.Digest) |
| store.Repositories[refName] = repository |
| } |
| |
| oldID, exists := repository[refStr] |
| |
| if exists { |
| if oldID == id { |
| // Nothing to do. The caller may have checked for this using store.Get in advance, but store.mu was unlocked in the meantime, so this can legitimately happen nevertheless. |
| return nil |
| } |
| |
| // force only works for tags |
| if digested, isDigest := ref.(reference.Canonical); isDigest { |
| return errors.WithStack(conflictingTagError("Cannot overwrite digest " + digested.Digest().String())) |
| } |
| |
| if !force { |
| return errors.WithStack( |
| conflictingTagError( |
| fmt.Sprintf("Conflict: Tag %s is already set to image %s, if you want to replace it, please use the force option", refStr, 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, err := favorDigest(ref) |
| if err != nil { |
| return false, err |
| } |
| |
| ref = reference.TagNameOnly(ref) |
| |
| refName := reference.FamiliarName(ref) |
| refStr := reference.FamiliarString(ref) |
| |
| store.mu.Lock() |
| defer store.mu.Unlock() |
| |
| repository, exists := store.Repositories[refName] |
| if !exists { |
| return false, ErrDoesNotExist |
| } |
| |
| if id, exists := repository[refStr]; exists { |
| delete(repository, refStr) |
| if len(repository) == 0 { |
| delete(store.Repositories, refName) |
| } |
| 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) (digest.Digest, error) { |
| if canonical, ok := ref.(reference.Canonical); ok { |
| // If reference contains both tag and digest, only |
| // lookup by digest as it takes precedence over |
| // tag, until tag/digest combos are stored. |
| if _, ok := ref.(reference.Tagged); ok { |
| var err error |
| ref, err = reference.WithDigest(reference.TrimNamed(canonical), canonical.Digest()) |
| if err != nil { |
| return "", err |
| } |
| } |
| } else { |
| ref = reference.TagNameOnly(ref) |
| } |
| |
| refName := reference.FamiliarName(ref) |
| refStr := reference.FamiliarString(ref) |
| |
| store.mu.RLock() |
| defer store.mu.RUnlock() |
| |
| repository, exists := store.Repositories[refName] |
| if !exists || repository == nil { |
| return "", ErrDoesNotExist |
| } |
| |
| id, exists := repository[refStr] |
| if !exists { |
| return "", ErrDoesNotExist |
| } |
| |
| return id, nil |
| } |
| |
| // References returns a slice of references to the given ID. The slice |
| // will be nil if there are no references to this ID. |
| func (store *store) References(id digest.Digest) []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 |
| // 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 { |
| refName := reference.FamiliarName(ref) |
| |
| store.mu.RLock() |
| defer store.mu.RUnlock() |
| |
| repository, exists := store.Repositories[refName] |
| if !exists { |
| return nil |
| } |
| |
| var associations []Association |
| for refStr, refID := range repository { |
| ref, err := reference.ParseNormalizedNamed(refStr) |
| if err != nil { |
| // Should never happen |
| return nil |
| } |
| associations = append(associations, |
| Association{ |
| Ref: ref, |
| ID: 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 |
| } |
| return ioutils.AtomicWriteFile(store.jsonPath, jsonData, 0600) |
| } |
| |
| 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.ParseNormalizedNamed(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 |
| } |