moby/tag/store.go

299 lines
7.6 KiB
Go
Raw Normal View History

package tag
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"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
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)
}
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,
})
}
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
}