Skip to content

Commit

Permalink
Add @sourceIndex syntax to oci/layout
Browse files Browse the repository at this point in the history
Images in the index can now be referenced via the @sourceIndex syntax.

Signed-off-by: Miloslav Trmač <[email protected]>
Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
mtrmac authored and vrothberg committed Nov 13, 2024
1 parent 0c42f2f commit 97b145f
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 41 deletions.
4 changes: 3 additions & 1 deletion docs/containers-transports.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@ An image stored in the docker daemon's internal storage.
The image must be specified as a _docker-reference_ or in an alternative _algo_`:`_digest_ format when being used as an image source.
The _algo_`:`_digest_ refers to the image ID reported by docker-inspect(1).

### **oci:**_path_[`:`_reference_]
### **oci:**_path_[`:`_reference_|@source-index}]_

An image in a directory structure compliant with the "Open Container Image Layout Specification" at _path_.

The _path_ value terminates at the first `:` character; any further `:` characters are not separators, but a part of _reference_.
The _reference_ is used to set, or match, the `org.opencontainers.image.ref.name` annotation in the top-level index.
If _reference_ is not specified when reading an image, the directory must contain exactly one image.
For reading images, @_source-index_ is a zero-based index in manifest (to access untagged images).
If neither reference nor @_source_index is specified when reading an image, the path must contain exactly one image.

### **oci-archive:**_path_[`:`_reference_]

Expand Down
29 changes: 29 additions & 0 deletions oci/internal/oci_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
)

Expand Down Expand Up @@ -119,3 +120,31 @@ func validateScopeNonWindows(scope string) error {

return nil
}

// parseOCIReferenceName parses the image from the oci reference.
func parseOCIReferenceName(image string) (img string, index int, err error) {
index = -1
if strings.HasPrefix(image, "@") {
idx, err := strconv.Atoi(image[1:])
if err != nil {
return "", index, fmt.Errorf("Invalid source index @%s: not an integer: %w", image[1:], err)
}
if idx < 0 {
return "", index, fmt.Errorf("Invalid source index @%d: must not be negative", idx)
}
index = idx
} else {
img = image
}
return img, index, nil
}

// ParseReferenceIntoElements splits the oci reference into location, image name and source index if exists
func ParseReferenceIntoElements(reference string) (string, string, int, error) {
dir, image := SplitPathAndImage(reference)
image, index, err := parseOCIReferenceName(image)
if err != nil {
return "", "", -1, err
}
return dir, image, index, nil
}
28 changes: 28 additions & 0 deletions oci/internal/oci_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ type testDataScopeValidation struct {
errMessage string
}

type testOCIReference struct {
ref string
image string
index int
}

func TestSplitReferenceIntoDirAndImageWindows(t *testing.T) {
tests := []testDataSplitReference{
{`C:\foo\bar:busybox:latest`, `C:\foo\bar`, "busybox:latest"},
Expand Down Expand Up @@ -61,3 +67,25 @@ func TestValidateScopeWindows(t *testing.T) {
}
}
}

func TestParseOCIReferenceName(t *testing.T) {
validTests := []testOCIReference{
{"@0", "", 0},
{"notlatest@1", "notlatest@1", -1},
}
for _, test := range validTests {
img, idx, err := parseOCIReferenceName(test.ref)
assert.NoError(t, err)
assert.Equal(t, img, test.image)
assert.Equal(t, idx, test.index)
}

invalidTests := []string{
"@-5",
"@invalidIndex",
}
for _, test := range invalidTests {
_, _, err := parseOCIReferenceName(test)
assert.Error(t, err)
}
}
3 changes: 3 additions & 0 deletions oci/layout/oci_dest.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type ociImageDestination struct {

// newImageDestination returns an ImageDestination for writing to an existing directory.
func newImageDestination(sys *types.SystemContext, ref ociReference) (private.ImageDestination, error) {
if ref.sourceIndex != -1 {
return nil, fmt.Errorf("Destination reference must not contain a manifest index @%d", ref.sourceIndex)
}
var index *imgspecv1.Index
if indexExists(ref) {
var err error
Expand Down
8 changes: 4 additions & 4 deletions oci/layout/oci_dest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestPutBlobDigestFailure(t *testing.T) {
const digestErrorString = "Simulated digest error"
const blobDigest = "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"

ref, _ := refToTempOCI(t)
ref, _ := refToTempOCI(t, false)
dirRef, ok := ref.(ociReference)
require.True(t, ok)
blobPath, err := dirRef.blobPath(blobDigest, "")
Expand Down Expand Up @@ -71,7 +71,7 @@ func TestPutBlobDigestFailure(t *testing.T) {

// TestPutManifestAppendsToExistingManifest tests that new manifests are getting added to existing index.
func TestPutManifestAppendsToExistingManifest(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
ref, tmpDir := refToTempOCI(t, false)

ociRef, ok := ref.(ociReference)
require.True(t, ok)
Expand All @@ -94,7 +94,7 @@ func TestPutManifestAppendsToExistingManifest(t *testing.T) {

// TestPutManifestTwice tests that existing manifest gets updated and not appended.
func TestPutManifestTwice(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
ref, tmpDir := refToTempOCI(t, false)

ociRef, ok := ref.(ociReference)
require.True(t, ok)
Expand All @@ -109,7 +109,7 @@ func TestPutManifestTwice(t *testing.T) {
}

func TestPutTwoDifferentTags(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
ref, tmpDir := refToTempOCI(t, false)

ociRef, ok := ref.(ociReference)
require.True(t, ok)
Expand Down
76 changes: 60 additions & 16 deletions oci/layout/oci_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,32 @@ type ociReference struct {
// (But in general, we make no attempt to be completely safe against concurrent hostile filesystem modifications.)
dir string // As specified by the user. May be relative, contain symlinks, etc.
resolvedDir string // Absolute path with no symlinks, at least at the time of its creation. Primarily used for policy namespaces.
// If image=="", it means the "only image" in the index.json is used in the case it is a source
// for destinations, the image name annotation "image.ref.name" is not added to the index.json
// If image=="" && sourceIndex==-1, it means the "only image" in the index.json is used in the case it is a source
// for destinations, the image name annotation "image.ref.name" is not added to the index.json.
//
// Must not be set if sourceIndex is set (the value is not -1).
image string
// If not -1, a zero-based index of an image in the manifest index. Valid only for sources.
// Must not be set if image is set.
sourceIndex int
}

// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OCI ImageReference.
func ParseReference(reference string) (types.ImageReference, error) {
dir, image := internal.SplitPathAndImage(reference)
return NewReference(dir, image)
dir, image, index, err := internal.ParseReferenceIntoElements(reference)
if err != nil {
return nil, err
}
return newReference(dir, image, index)
}

// NewReference returns an OCI reference for a directory and a image.
// newReference returns an OCI reference for a directory, and an image name annotation or sourceIndex.
//
// If sourceIndex==-1, the index will not be valid to point out the source image, only image will be used.
// NewReference returns an OCI reference for a directory and a image.
// We do not expose an API supplying the resolvedDir; we could, but recomputing it
// is generally cheap enough that we prefer being confident about the properties of resolvedDir.
func NewReference(dir, image string) (types.ImageReference, error) {
func newReference(dir, image string, sourceIndex int) (types.ImageReference, error) {
resolved, err := explicitfilepath.ResolvePathToFullyExplicit(dir)
if err != nil {
return nil, err
Expand All @@ -90,7 +100,26 @@ func NewReference(dir, image string) (types.ImageReference, error) {
return nil, err
}

return ociReference{dir: dir, resolvedDir: resolved, image: image}, nil
if sourceIndex != -1 && sourceIndex < 0 {
return nil, fmt.Errorf("Invalid oci: layout reference: index @%d must not be negative", sourceIndex)
}
if sourceIndex != -1 && image != "" {
return nil, fmt.Errorf("Invalid oci: layout reference: cannot use both an image %s and a source index @%d", image, sourceIndex)
}
return ociReference{dir: dir, resolvedDir: resolved, image: image, sourceIndex: sourceIndex}, nil
}

// NewIndexReference returns an OCI reference for a path and a zero-based source manifest index.
func NewIndexReference(dir string, sourceIndex int) (types.ImageReference, error) {
return newReference(dir, "", sourceIndex)
}

// NewReference returns an OCI reference for a directory and a image.
//
// We do not expose an API supplying the resolvedDir; we could, but recomputing it
// is generally cheap enough that we prefer being confident about the properties of resolvedDir.
func NewReference(dir, image string) (types.ImageReference, error) {
return newReference(dir, image, -1)
}

func (ref ociReference) Transport() types.ImageTransport {
Expand All @@ -103,7 +132,10 @@ func (ref ociReference) Transport() types.ImageTransport {
// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref ociReference) StringWithinTransport() string {
return fmt.Sprintf("%s:%s", ref.dir, ref.image)
if ref.sourceIndex == -1 {
return fmt.Sprintf("%s:%s", ref.dir, ref.image)
}
return fmt.Sprintf("%s:@%d", ref.dir, ref.sourceIndex)
}

// DockerReference returns a Docker reference associated with this reference
Expand Down Expand Up @@ -187,14 +219,18 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, int, erro
return imgspecv1.Descriptor{}, -1, err
}

if ref.image == "" {
// return manifest if only one image is in the oci directory
if len(index.Manifests) != 1 {
// ask user to choose image when more than one image in the oci directory
return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage
switch {
case ref.image != "" && ref.sourceIndex != -1:
return imgspecv1.Descriptor{}, -1, fmt.Errorf("Internal error: Cannot have both ref %s and source index @%d",
ref.image, ref.sourceIndex)

case ref.sourceIndex != -1:
if ref.sourceIndex >= len(index.Manifests) {
return imgspecv1.Descriptor{}, -1, fmt.Errorf("index %d is too large, only %d entries available", ref.sourceIndex, len(index.Manifests))
}
return index.Manifests[0], 0, nil
} else {
return index.Manifests[ref.sourceIndex], ref.sourceIndex, nil

case ref.image != "":
// if image specified, look through all manifests for a match
var unsupportedMIMETypes []string
for i, md := range index.Manifests {
Expand All @@ -208,8 +244,16 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, int, erro
if len(unsupportedMIMETypes) != 0 {
return imgspecv1.Descriptor{}, -1, fmt.Errorf("reference %q matches unsupported manifest MIME types %q", ref.image, unsupportedMIMETypes)
}
return imgspecv1.Descriptor{}, -1, ImageNotFoundError{ref}

default:
// return manifest if only one image is in the oci directory
if len(index.Manifests) != 1 {
// ask user to choose image when more than one image in the oci directory
return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage
}
return index.Manifests[0], 0, nil
}
return imgspecv1.Descriptor{}, -1, ImageNotFoundError{ref}
}

// LoadManifestDescriptor loads the manifest descriptor to be used to retrieve the image name
Expand Down
Loading

0 comments on commit 97b145f

Please sign in to comment.