2655954c |
package reference |
7de380c5 |
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath" |
6e37b622 |
"sort" |
7de380c5 |
"sync"
|
3a127939 |
"github.com/docker/distribution/reference" |
ea3cbd32 |
"github.com/docker/docker/pkg/ioutils" |
7a855799 |
"github.com/opencontainers/go-digest" |
7de380c5 |
)
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 { |
3a127939 |
Ref reference.Named |
80522398 |
ID digest.Digest |
7de380c5 |
}
// Store provides the set of methods which can operate on a tag store.
type Store interface { |
3a127939 |
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) |
7de380c5 |
}
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. |
3a127939 |
referencesByIDCache map[digest.Digest]map[string]reference.Named |
3aa4a007 |
// platform is the container target platform for this store (which may be
// different to the host operating system
platform string |
7de380c5 |
}
|
80522398 |
// Repository maps tags to digests. The key is a stringified Reference, |
7de380c5 |
// including the repository name. |
80522398 |
type repository map[string]digest.Digest |
7de380c5 |
|
3a127939 |
type lexicalRefs []reference.Named |
6e37b622 |
|
3a127939 |
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()
} |
6e37b622 |
type lexicalAssociations []Association
|
3a127939 |
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()
} |
6e37b622 |
|
2655954c |
// NewReferenceStore creates a new reference store, tied to a file path where
// the set of references are serialized in JSON format. |
3aa4a007 |
func NewReferenceStore(jsonPath, platform string) (Store, error) { |
7de380c5 |
abspath, err := filepath.Abs(jsonPath)
if err != nil {
return nil, err
}
store := &store{
jsonPath: abspath,
Repositories: make(map[string]repository), |
3a127939 |
referencesByIDCache: make(map[digest.Digest]map[string]reference.Named), |
3aa4a007 |
platform: platform, |
7de380c5 |
}
// 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
}
|
2655954c |
// AddTag adds a tag reference to the store. If force is set to true, existing |
7de380c5 |
// references can be overwritten. This only works for tags, not digests. |
3a127939 |
func (store *store) AddTag(ref reference.Named, id digest.Digest, force bool) error {
if _, isCanonical := ref.(reference.Canonical); isCanonical { |
20e759ab |
return errors.New("refusing to create a tag with a digest reference")
} |
3a127939 |
return store.addReference(reference.TagNameOnly(ref), id, force) |
20e759ab |
}
|
2655954c |
// AddDigest adds a digest reference to the store. |
3a127939 |
func (store *store) AddDigest(ref reference.Canonical, id digest.Digest, force bool) error { |
20e759ab |
return store.addReference(ref, id, force)
}
|
0fbfeb17 |
func favorDigest(originalRef reference.Named) (reference.Named, error) {
ref := originalRef |
b7a2d853 |
// If the reference includes a digest and a tag, we must store only the
// digest. |
0fbfeb17 |
canonical, isCanonical := originalRef.(reference.Canonical)
_, isNamedTagged := originalRef.(reference.NamedTagged) |
b7a2d853 |
if isCanonical && isNamedTagged {
trimmed, err := reference.WithDigest(reference.TrimNamed(canonical), canonical.Digest())
if err != nil {
// should never happen |
0fbfeb17 |
return originalRef, err |
b7a2d853 |
}
ref = trimmed
} |
0fbfeb17 |
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
} |
b7a2d853 |
|
3a127939 |
refName := reference.FamiliarName(ref)
refStr := reference.FamiliarString(ref)
if refName == string(digest.Canonical) { |
20e759ab |
return errors.New("refusing to create an ambiguous tag using digest algorithm as name")
} |
7de380c5 |
store.mu.Lock()
defer store.mu.Unlock()
|
3a127939 |
repository, exists := store.Repositories[refName] |
7de380c5 |
if !exists || repository == nil { |
80522398 |
repository = make(map[string]digest.Digest) |
3a127939 |
store.Repositories[refName] = repository |
7de380c5 |
}
oldID, exists := repository[refStr]
if exists {
// force only works for tags |
3a127939 |
if digested, isDigest := ref.(reference.Canonical); isDigest { |
7de380c5 |
return fmt.Errorf("Cannot overwrite digest %s", digested.Digest().String())
}
if !force { |
3a127939 |
return fmt.Errorf("Conflict: Tag %s is already set to image %s, if you want to replace it, please use -f option", refStr, oldID.String()) |
7de380c5 |
}
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 { |
3a127939 |
store.referencesByIDCache[id] = make(map[string]reference.Named) |
7de380c5 |
}
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. |
3a127939 |
func (store *store) Delete(ref reference.Named) (bool, error) { |
0fbfeb17 |
ref, err := favorDigest(ref)
if err != nil {
return false, err
}
|
3a127939 |
ref = reference.TagNameOnly(ref)
refName := reference.FamiliarName(ref)
refStr := reference.FamiliarString(ref) |
7de380c5 |
store.mu.Lock()
defer store.mu.Unlock()
|
3a127939 |
repository, exists := store.Repositories[refName] |
7de380c5 |
if !exists {
return false, ErrDoesNotExist
}
if id, exists := repository[refStr]; exists {
delete(repository, refStr)
if len(repository) == 0 { |
3a127939 |
delete(store.Repositories, refName) |
7de380c5 |
}
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
}
|
80522398 |
// Get retrieves an item from the store by reference |
3a127939 |
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 |
39bcaee4 |
// lookup by digest as it takes precedence over |
3a127939 |
// 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) |
7de380c5 |
store.mu.RLock()
defer store.mu.RUnlock()
|
3a127939 |
repository, exists := store.Repositories[refName] |
7de380c5 |
if !exists || repository == nil {
return "", ErrDoesNotExist
}
|
3a127939 |
id, exists := repository[refStr] |
7de380c5 |
if !exists {
return "", ErrDoesNotExist
}
return id, nil
}
|
80522398 |
// References returns a slice of references to the given ID. The slice
// will be nil if there are no references to this ID. |
3a127939 |
func (store *store) References(id digest.Digest) []reference.Named { |
7de380c5 |
store.mu.RLock()
defer store.mu.RUnlock()
// Convert the internal map to an array for two reasons: |
2655954c |
// 1) We must not return a mutable |
7de380c5 |
// 2) It would be ugly to expose the extraneous map keys to callers.
|
3a127939 |
var references []reference.Named |
7de380c5 |
for _, ref := range store.referencesByIDCache[id] {
references = append(references, ref)
}
|
6e37b622 |
sort.Sort(lexicalRefs(references))
|
7de380c5 |
return references
}
// ReferencesByName returns the references for a given repository name.
// If there are no references known for this repository name,
// ReferencesByName returns nil. |
3a127939 |
func (store *store) ReferencesByName(ref reference.Named) []Association {
refName := reference.FamiliarName(ref)
|
7de380c5 |
store.mu.RLock()
defer store.mu.RUnlock()
|
3a127939 |
repository, exists := store.Repositories[refName] |
7de380c5 |
if !exists {
return nil
}
var associations []Association
for refStr, refID := range repository { |
3a127939 |
ref, err := reference.ParseNormalizedNamed(refStr) |
7de380c5 |
if err != nil {
// Should never happen
return nil
}
associations = append(associations,
Association{ |
80522398 |
Ref: ref,
ID: refID, |
7de380c5 |
})
}
|
6e37b622 |
sort.Sort(lexicalAssociations(associations))
|
7de380c5 |
return associations
}
func (store *store) save() error {
// Store the json
jsonData, err := json.Marshal(store)
if err != nil {
return err
} |
ea3cbd32 |
return ioutils.AtomicWriteFile(store.jsonPath, jsonData, 0600) |
7de380c5 |
}
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 { |
3a127939 |
ref, err := reference.ParseNormalizedNamed(refStr) |
7de380c5 |
if err != nil {
// Should never happen
continue
}
if store.referencesByIDCache[refID] == nil { |
3a127939 |
store.referencesByIDCache[refID] = make(map[string]reference.Named) |
7de380c5 |
}
store.referencesByIDCache[refID][refStr] = ref
}
}
return nil
} |