Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image.UnparsedInstanceWithReference and storage.ResolveReference #2056

Merged
merged 2 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions image/unparsed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package image

import (
"github.com/containers/image/v5/internal/image"
"github.com/containers/image/v5/internal/private"
"github.com/containers/image/v5/internal/unparsedimage"
"github.com/containers/image/v5/types"
"github.com/opencontainers/go-digest"
)
Expand All @@ -17,3 +19,23 @@ type UnparsedImage = image.UnparsedImage
func UnparsedInstance(src types.ImageSource, instanceDigest *digest.Digest) *UnparsedImage {
return image.UnparsedInstance(src, instanceDigest)
}

// unparsedWithRef wraps a private.UnparsedImage, claiming another replacementRef
type unparsedWithRef struct {
private.UnparsedImage
ref types.ImageReference
}

func (uwr *unparsedWithRef) Reference() types.ImageReference {
return uwr.ref
}

// UnparsedInstanceWithReference returns a types.UnparsedImage for wrappedInstance which claims to be a replacementRef.
// This is useful for combining image data with other reference values, e.g. to check signatures on a locally-pulled image
// based on a remote-registry policy.
func UnparsedInstanceWithReference(wrappedInstance types.UnparsedImage, replacementRef types.ImageReference) types.UnparsedImage {
return &unparsedWithRef{
UnparsedImage: unparsedimage.FromPublic(wrappedInstance),
ref: replacementRef,
}
}
27 changes: 27 additions & 0 deletions storage/storage_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"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"
Expand Down Expand Up @@ -283,3 +284,29 @@ func (s storageReference) NewImageSource(ctx context.Context, sys *types.SystemC
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).
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
}
58 changes: 55 additions & 3 deletions storage/storage_reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"testing"

"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/pkg/blobinfocache/memory"
"github.com/containers/storage"
"github.com/containers/storage/pkg/archive"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -97,14 +100,19 @@ func TestStorageReferenceDockerReference(t *testing.T) {
}
}

func TestStorageReferenceStringWithinTransport(t *testing.T) {
store := newStore(t)
// The […] part of references created for store
func storeSpecForStringWithinTransport(store storage.Store) string {
optionsList := ""
options := store.GraphOptions()
if len(options) > 0 {
optionsList = ":" + strings.Join(options, ",")
}
storeSpec := fmt.Sprintf("[%s@%s+%s%s]", store.GraphDriverName(), store.GraphRoot(), store.RunRoot(), optionsList)
return fmt.Sprintf("[%s@%s+%s%s]", store.GraphDriverName(), store.GraphRoot(), store.RunRoot(), optionsList)
}

func TestStorageReferenceStringWithinTransport(t *testing.T) {
store := newStore(t)
storeSpec := storeSpecForStringWithinTransport(store)

for _, c := range validReferenceTestCases {
ref, err := Transport.ParseReference(c.input)
Expand Down Expand Up @@ -142,3 +150,47 @@ func TestStorageReferencePolicyConfigurationNamespaces(t *testing.T) {
}

// NewImage, NewImageSource, NewImageDestination, DeleteImage tested in storage_test.go

func TestResolveReference(t *testing.T) {
// This is, so far, only a minimal smoke test

ensureTestCanCreateImages(t)

store := newStore(t)
storeSpec := storeSpecForStringWithinTransport(store)
cache := memory.New()

id := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

// Create an image with a known name and ID
ref, err := Transport.ParseStoreReference(store, "test@"+id)
require.NoError(t, err)
createImage(t, ref, cache, []testBlob{makeLayer(t, archive.Gzip)}, nil)

for _, c := range []struct {
input string
expected string // "" on error
}{
{ // No ID match
"@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "",
},
{"@" + id, "@" + id}, // ID-only lookup
{"test", "docker.io/library/test:latest@" + id}, // Name is resolved to include ID
{"nottest", ""}, // No name match
{"test@" + id, "docker.io/library/test:latest@" + id}, // Name+ID works, and is unchanged
{"nottest@" + id, ""}, // Name mismatch is rejected even with an ID
} {
input, err := Transport.ParseStoreReference(store, c.input)
require.NoError(t, err, c.input)
inputClone := *input
resolved, img, err := ResolveReference(input)
if c.expected == "" {
assert.Error(t, err, c.input)
} else {
require.NoError(t, err, c.input)
require.Equal(t, &inputClone, input) // input was not modified in-place
assert.Equal(t, id, img.ID, c.input)
assert.Equal(t, storeSpec+c.expected, resolved.StringWithinTransport(), c.input)
}
}
}
29 changes: 29 additions & 0 deletions storage/storage_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,24 @@ type StoreTransport interface {
GetStoreIfSet() storage.Store
// GetImage retrieves the image from the transport's store that's named
// by the reference.
// Deprecated: Surprisingly, with a StoreTransport reference which contains an ID,
// this ignores that ID; and repeated calls of GetStoreImage with the same named reference
// can return different images, with no way for the caller to "freeze" the storage.Image identity
// without discarding the name entirely.
//
// Use storage.ResolveReference instead.
GetImage(types.ImageReference) (*storage.Image, error)
// GetStoreImage retrieves the image from a specified store that's named
// by the reference.
//
// Deprecated: Surprisingly, with a StoreTransport reference which contains an ID,
// this ignores that ID; and repeated calls of GetStoreImage with the same named reference
// can return different images, with no way for the caller to "freeze" the storage.Image identity
// without discarding the name entirely.
//
// Also, a StoreTransport reference already contains a store, so providing another one is redundant.
//
// Use storage.ResolveReference instead.
GetStoreImage(storage.Store, types.ImageReference) (*storage.Image, error)
// ParseStoreReference parses a reference, overriding any store
// specification that it may contain.
Expand Down Expand Up @@ -290,6 +305,14 @@ func (s *storageTransport) ParseReference(reference string) (types.ImageReferenc
return s.ParseStoreReference(store, reference)
}

// Deprecated: Surprisingly, with a StoreTransport reference which contains an ID,
// this ignores that ID; and repeated calls of GetStoreImage with the same named reference
// can return different images, with no way for the caller to "freeze" the storage.Image identity
// without discarding the name entirely.
//
// Also, a StoreTransport reference already contains a store, so providing another one is redundant.
//
// Use storage.ResolveReference instead.
func (s storageTransport) GetStoreImage(store storage.Store, ref types.ImageReference) (*storage.Image, error) {
dref := ref.DockerReference()
if dref != nil {
Expand All @@ -306,6 +329,12 @@ func (s storageTransport) GetStoreImage(store storage.Store, ref types.ImageRefe
return nil, storage.ErrImageUnknown
}

// Deprecated: Surprisingly, with a StoreTransport reference which contains an ID,
// this ignores that ID; and repeated calls of GetStoreImage with the same named reference
// can return different images, with no way for the caller to "freeze" the storage.Image identity
// without discarding the name entirely.
//
// Use storage.ResolveReference instead.
func (s *storageTransport) GetImage(ref types.ImageReference) (*storage.Image, error) {
store, err := s.GetStore()
if err != nil {
Expand Down