mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
324 lines
12 KiB
Go
324 lines
12 KiB
Go
//go:build !containers_image_storage_stub
|
|
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/containers/image/v5/docker/reference"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/transports"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/storage"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// A storageReference holds an arbitrary name and/or an ID, which is a 32-byte
|
|
// value hex-encoded into a 64-character string, and a reference to a Store
|
|
// where an image is, or would be, kept.
|
|
// Either "named" or "id" must be set.
|
|
type storageReference struct {
|
|
transport storageTransport
|
|
named reference.Named // may include a tag and/or a digest
|
|
id string
|
|
}
|
|
|
|
func newReference(transport storageTransport, named reference.Named, id string) (*storageReference, error) {
|
|
if named == nil && id == "" {
|
|
return nil, ErrInvalidReference
|
|
}
|
|
if named != nil && reference.IsNameOnly(named) {
|
|
return nil, fmt.Errorf("reference %s has neither a tag nor a digest: %w", named.String(), ErrInvalidReference)
|
|
}
|
|
if id != "" {
|
|
if err := validateImageID(id); err != nil {
|
|
return nil, fmt.Errorf("invalid ID value %q: %v: %w", id, err.Error(), ErrInvalidReference)
|
|
}
|
|
}
|
|
// We take a copy of the transport, which contains a pointer to the
|
|
// store that it used for resolving this reference, so that the
|
|
// transport that we'll return from Transport() won't be affected by
|
|
// further calls to the original transport's SetStore() method.
|
|
return &storageReference{
|
|
transport: transport,
|
|
named: named,
|
|
id: id,
|
|
}, nil
|
|
}
|
|
|
|
// imageMatchesRepo returns true iff image.Names contains an element with the same repo as ref
|
|
func imageMatchesRepo(image *storage.Image, ref reference.Named) bool {
|
|
repo := ref.Name()
|
|
return slices.ContainsFunc(image.Names, func(name string) bool {
|
|
if named, err := reference.ParseNormalizedNamed(name); err == nil && named.Name() == repo {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
// multiArchImageMatchesSystemContext returns true if the passed-in image both contains a
|
|
// multi-arch manifest that matches the passed-in digest, and the image is the per-platform
|
|
// image instance that matches sys.
|
|
//
|
|
// See the comment in storageReference.ResolveImage explaining why
|
|
// this check is necessary.
|
|
func multiArchImageMatchesSystemContext(store storage.Store, img *storage.Image, manifestDigest digest.Digest, sys *types.SystemContext) bool {
|
|
// Load the manifest that matches the specified digest.
|
|
// We don't need to care about storage.ImageDigestBigDataKey because
|
|
// manifests lists are only stored into storage by c/image versions
|
|
// that know about manifestBigDataKey, and only using that key.
|
|
key, err := manifestBigDataKey(manifestDigest)
|
|
if err != nil {
|
|
return false // This should never happen, manifestDigest comes from a reference.Digested, and that validates the format.
|
|
}
|
|
manifestBytes, err := store.ImageBigData(img.ID, key)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// The manifest is either a list, or not a list. If it's a list, find
|
|
// the digest of the instance that matches the current system, and try
|
|
// to load that manifest from the image record, and use it.
|
|
manifestType := manifest.GuessMIMEType(manifestBytes)
|
|
if !manifest.MIMETypeIsMultiImage(manifestType) {
|
|
// manifestDigest directly specifies a per-platform image, so we aren't
|
|
// choosing among different variants.
|
|
return false
|
|
}
|
|
list, err := manifest.ListFromBlob(manifestBytes, manifestType)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
chosenInstance, err := list.ChooseInstance(sys)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
key, err = manifestBigDataKey(chosenInstance)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = store.ImageBigData(img.ID, key)
|
|
return err == nil // true if img.ID is based on chosenInstance.
|
|
}
|
|
|
|
// Resolve the reference's name to an image ID in the store, if there's already
|
|
// one present with the same name or ID, and return the image.
|
|
//
|
|
// Returns an error matching ErrNoSuchImage if an image matching ref was not found.
|
|
func (s *storageReference) resolveImage(sys *types.SystemContext) (*storage.Image, error) {
|
|
var loadedImage *storage.Image
|
|
if s.id == "" && s.named != nil {
|
|
// Look for an image that has the expanded reference name as an explicit Name value.
|
|
image, err := s.transport.store.Image(s.named.String())
|
|
if image != nil && err == nil {
|
|
loadedImage = image
|
|
s.id = image.ID
|
|
}
|
|
}
|
|
if s.id == "" && s.named != nil {
|
|
if digested, ok := s.named.(reference.Digested); ok {
|
|
// Look for an image with the specified digest that has the same name,
|
|
// though possibly with a different tag or digest, as a Name value, so
|
|
// that the canonical reference can be implicitly resolved to the image.
|
|
//
|
|
// Typically there should be at most one such image, because the same
|
|
// manifest digest implies the same config, and we choose the storage ID
|
|
// based on the config (deduplicating images), except:
|
|
// - the user can explicitly specify an ID when creating the image.
|
|
// In this case we don't have a preference among the alternatives.
|
|
// - when pulling an image from a multi-platform manifest list, we also
|
|
// store the manifest list in the image; this allows referencing a
|
|
// per-platform image using the manifest list digest, but that also
|
|
// means that we can have multiple genuinely different images in the
|
|
// storage matching the same manifest list digest (if pulled using different
|
|
// SystemContext.{OS,Architecture,Variant}Choice to the same storage).
|
|
// In this case we prefer the image matching the current SystemContext.
|
|
images, err := s.transport.store.ImagesByDigest(digested.Digest())
|
|
if err == nil && len(images) > 0 {
|
|
for _, image := range images {
|
|
if imageMatchesRepo(image, s.named) {
|
|
if loadedImage == nil || multiArchImageMatchesSystemContext(s.transport.store, image, digested.Digest(), sys) {
|
|
loadedImage = image
|
|
s.id = image.ID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if s.id == "" {
|
|
logrus.Debugf("reference %q does not resolve to an image ID", s.StringWithinTransport())
|
|
// %.0w makes the error visible to error.Unwrap() without including any text.
|
|
// ErrNoSuchImage ultimately is “identifier is not an image”, which is not helpful for identifying the root cause.
|
|
return nil, fmt.Errorf("reference %q does not resolve to an image ID%.0w", s.StringWithinTransport(), ErrNoSuchImage)
|
|
}
|
|
if loadedImage == nil {
|
|
img, err := s.transport.store.Image(s.id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading image %q: %w", s.id, err)
|
|
}
|
|
loadedImage = img
|
|
}
|
|
if s.named != nil {
|
|
if !imageMatchesRepo(loadedImage, s.named) {
|
|
logrus.Errorf("no image matching reference %q found", s.StringWithinTransport())
|
|
return nil, ErrNoSuchImage
|
|
}
|
|
}
|
|
// Default to having the image digest that we hand back match the most recently
|
|
// added manifest...
|
|
if digest, ok := loadedImage.BigDataDigests[storage.ImageDigestBigDataKey]; ok {
|
|
loadedImage.Digest = digest
|
|
}
|
|
// ... unless the named reference says otherwise, and it matches one of the digests
|
|
// in the image. For those cases, set the Digest field to that value, for the
|
|
// sake of older consumers that don't know there's a whole list in there now.
|
|
if s.named != nil {
|
|
if digested, ok := s.named.(reference.Digested); ok {
|
|
digest := digested.Digest()
|
|
if slices.Contains(loadedImage.Digests, digest) {
|
|
loadedImage.Digest = digest
|
|
}
|
|
}
|
|
}
|
|
return loadedImage, nil
|
|
}
|
|
|
|
// Return a Transport object that defaults to using the same store that we used
|
|
// to build this reference object.
|
|
func (s storageReference) Transport() types.ImageTransport {
|
|
return &storageTransport{
|
|
store: s.transport.store,
|
|
defaultUIDMap: s.transport.defaultUIDMap,
|
|
defaultGIDMap: s.transport.defaultGIDMap,
|
|
}
|
|
}
|
|
|
|
// Return a name with a tag or digest, if we have either, else return it bare.
|
|
func (s storageReference) DockerReference() reference.Named {
|
|
return s.named
|
|
}
|
|
|
|
// Return a name with a tag, prefixed with the graph root and driver name, to
|
|
// disambiguate between images which may be present in multiple stores and
|
|
// share only their names.
|
|
func (s storageReference) StringWithinTransport() string {
|
|
optionsList := ""
|
|
options := s.transport.store.GraphOptions()
|
|
if len(options) > 0 {
|
|
optionsList = ":" + strings.Join(options, ",")
|
|
}
|
|
res := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "+" + s.transport.store.RunRoot() + optionsList + "]"
|
|
if s.named != nil {
|
|
res += s.named.String()
|
|
}
|
|
if s.id != "" {
|
|
res += "@" + s.id
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (s storageReference) PolicyConfigurationIdentity() string {
|
|
res := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "]"
|
|
if s.named != nil {
|
|
res += s.named.String()
|
|
}
|
|
if s.id != "" {
|
|
res += "@" + s.id
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Also accept policy that's tied to the combination of the graph root and
|
|
// driver name, to apply to all images stored in the Store, and to just the
|
|
// graph root, in case we're using multiple drivers in the same directory for
|
|
// some reason.
|
|
func (s storageReference) PolicyConfigurationNamespaces() []string {
|
|
storeSpec := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "]"
|
|
driverlessStoreSpec := "[" + s.transport.store.GraphRoot() + "]"
|
|
namespaces := []string{}
|
|
if s.named != nil {
|
|
if s.id != "" {
|
|
// The reference without the ID is also a valid namespace.
|
|
namespaces = append(namespaces, storeSpec+s.named.String())
|
|
}
|
|
tagged, isTagged := s.named.(reference.Tagged)
|
|
_, isDigested := s.named.(reference.Digested)
|
|
if isTagged && isDigested { // s.named is "name:tag@digest"; add a "name:tag" parent namespace.
|
|
namespaces = append(namespaces, storeSpec+s.named.Name()+":"+tagged.Tag())
|
|
}
|
|
components := strings.Split(s.named.Name(), "/")
|
|
for len(components) > 0 {
|
|
namespaces = append(namespaces, storeSpec+strings.Join(components, "/"))
|
|
components = components[:len(components)-1]
|
|
}
|
|
}
|
|
namespaces = append(namespaces, storeSpec)
|
|
namespaces = append(namespaces, driverlessStoreSpec)
|
|
return namespaces
|
|
}
|
|
|
|
// NewImage returns a types.ImageCloser for this reference, possibly specialized for this ImageTransport.
|
|
// The caller must call .Close() on the returned ImageCloser.
|
|
// NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
|
|
// verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
|
|
// WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
|
|
func (s storageReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
|
|
return newImage(ctx, sys, s)
|
|
}
|
|
|
|
func (s storageReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error {
|
|
img, err := s.resolveImage(sys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
layers, err := s.transport.store.DeleteImage(img.ID, true)
|
|
if err == nil {
|
|
logrus.Debugf("deleted image %q", img.ID)
|
|
for _, layer := range layers {
|
|
logrus.Debugf("deleted layer %q", layer)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s storageReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) {
|
|
return newImageSource(sys, s)
|
|
}
|
|
|
|
func (s storageReference) NewImageDestination(ctx context.Context, sys *types.SystemContext) (types.ImageDestination, error) {
|
|
return newImageDestination(sys, s)
|
|
}
|
|
|
|
// ResolveReference finds the underlying storage image for a storage.Transport reference.
|
|
// It returns that image, and an updated reference which can be used to refer back to the _same_
|
|
// image again.
|
|
//
|
|
// This matters if the input reference contains a tagged name; the destination of the tag can
|
|
// move in local storage. The updated reference returned by this function contains the resolved
|
|
// image ID, so later uses of that updated reference will either continue to refer to the same
|
|
// image, or fail.
|
|
//
|
|
// Note that it _is_ possible for the later uses to fail, either because the image was removed
|
|
// completely, or because the name used in the reference was untaged (even if the underlying image
|
|
// ID still exists in local storage).
|
|
//
|
|
// Returns an error matching ErrNoSuchImage if an image matching ref was not found.
|
|
func ResolveReference(ref types.ImageReference) (types.ImageReference, *storage.Image, error) {
|
|
sref, ok := ref.(*storageReference)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("trying to resolve a non-%s: reference %q", Transport.Name(),
|
|
transports.ImageName(ref))
|
|
}
|
|
clone := *sref // A shallow copy we can update
|
|
img, err := clone.resolveImage(nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return clone, img, nil
|
|
}
|