Skip to content

Commit

Permalink
Fix oci to save the full image name
Browse files Browse the repository at this point in the history
Fix oci to save the full image name "image:tag" instead of just the tag in index.json.
Add function to retrieve the image name from index.json when loading or pulling image from oci directory.

Signed-off-by: umohnani8 <umohnani@redhat.com>
  • Loading branch information
umohnani8 committed Aug 31, 2017
1 parent 6cfdd04 commit 4eb8db3
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 77 deletions.
9 changes: 6 additions & 3 deletions oci/layout/oci_dest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ type ociImageDestination struct {
}

// newImageDestination returns an ImageDestination for writing to an existing directory.
func newImageDestination(ref ociReference) types.ImageDestination {
func newImageDestination(ref ociReference) (types.ImageDestination, error) {
if ref.image == "" {
return nil, errors.Errorf("cannot save image with empty image.ref.name")
}
index := imgspecv1.Index{
Versioned: imgspec.Versioned{
SchemaVersion: 2,
},
}
return &ociImageDestination{ref: ref, index: index}
return &ociImageDestination{ref: ref, index: index}, nil
}

// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
Expand Down Expand Up @@ -178,7 +181,7 @@ func (d *ociImageDestination) PutManifest(m []byte) error {
}

annotations := make(map[string]string)
annotations["org.opencontainers.image.ref.name"] = d.ref.tag
annotations["org.opencontainers.image.ref.name"] = d.ref.image
desc.Annotations = annotations
desc.Platform = &imgspecv1.Platform{
Architecture: runtime.GOARCH,
Expand Down
113 changes: 69 additions & 44 deletions oci/layout/oci_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,37 @@ func (t ociTransport) ParseReference(reference string) (types.ImageReference, er
return ParseReference(reference)
}

var refRegexp = regexp.MustCompile(`^([A-Za-z0-9._-]+)+$`)
// annotation spex from https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys
const (
separator = `(?:[-._:@+]|--)`
alphanum = `(?:[A-Za-z0-9]+)`
component = `(?:` + alphanum + `(?:` + separator + alphanum + `)*)`
)

var refRegexp = regexp.MustCompile(`^` + component + `(?:/` + component + `)*$`)

// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t ociTransport) ValidatePolicyConfigurationScope(scope string) error {
var dir string
sep := strings.LastIndex(scope, ":")
if sep == -1 {
dir = scope
} else {
dir = scope[:sep]
tag := scope[sep+1:]
if !refRegexp.MatchString(tag) {
return errors.Errorf("Invalid tag %s", tag)
}
}
sep := strings.SplitN(scope, ":", 2)
dir = sep[0]

if strings.Contains(dir, ":") {
return errors.Errorf("Invalid OCI reference %s: path contains a colon", scope)
if len(sep) == 2 {
image := sep[1]
if !refRegexp.MatchString(image) {
return errors.Errorf("Invalid image %s", image)
}
}

if !strings.HasPrefix(dir, "/") {
return errors.Errorf("Invalid scope %s: must be an absolute path", scope)
}
// Refuse also "/", otherwise "/" and "" would have the same semantics,
// and "" could be unexpectedly shadowed by the "/" entry.
// (Note: we do allow "/:sometag", a bit ridiculous but why refuse it?)
// (Note: we do allow "/:someimage", a bit ridiculous but why refuse it?)
if scope == "/" {
return errors.New(`Invalid scope "/": Use the generic default scope ""`)
}
Expand All @@ -85,41 +87,39 @@ 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.
tag string
image string // If image=="", it means the only image in the index.json is used
}

// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OCI ImageReference.
func ParseReference(reference string) (types.ImageReference, error) {
var dir, tag string
sep := strings.LastIndex(reference, ":")
if sep == -1 {
dir = reference
tag = "latest"
} else {
dir = reference[:sep]
tag = reference[sep+1:]
var dir, image string
sep := strings.SplitN(reference, ":", 2)
dir = sep[0]

if len(sep) == 2 {
image = sep[1]
}
return NewReference(dir, tag)
return NewReference(dir, image)
}

// NewReference returns an OCI reference for a directory and a tag.
// 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, tag string) (types.ImageReference, error) {
func NewReference(dir, image string) (types.ImageReference, error) {
resolved, err := explicitfilepath.ResolvePathToFullyExplicit(dir)
if err != nil {
return nil, err
}
// This is necessary to prevent directory paths returned by PolicyConfigurationNamespaces
// from being ambiguous with values of PolicyConfigurationIdentity.
if strings.Contains(resolved, ":") {
return nil, errors.Errorf("Invalid OCI reference %s:%s: path %s contains a colon", dir, tag, resolved)
return nil, errors.Errorf("Invalid OCI reference %s:%s: path %s contains a colon", dir, image, resolved)
}
if !refRegexp.MatchString(tag) {
return nil, errors.Errorf("Invalid tag %s", tag)
if len(image) > 0 && !refRegexp.MatchString(image) {
return nil, errors.Errorf("Invalid image %s", image)
}
return ociReference{dir: dir, resolvedDir: resolved, tag: tag}, nil
return ociReference{dir: dir, resolvedDir: resolved, image: image}, nil
}

func (ref ociReference) Transport() types.ImageTransport {
Expand All @@ -132,7 +132,7 @@ func (ref ociReference) Transport() types.ImageTransport {
// e.g. default attribute values omitted by the user may be filled in 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.tag)
return fmt.Sprintf("%s:%s", ref.dir, ref.image)
}

// DockerReference returns a Docker reference associated with this reference
Expand All @@ -150,7 +150,10 @@ func (ref ociReference) DockerReference() reference.Named {
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref ociReference) PolicyConfigurationIdentity() string {
return fmt.Sprintf("%s:%s", ref.resolvedDir, ref.tag)
// NOTE: ref.image is not a part of the image identity, because "$dir:$someimage" and "$dir:" may mean the
// same image and the two can’t be statically disambiguated. Using at least the repository directory is
// less granular but hopefully still useful.
return fmt.Sprintf("%s", ref.resolvedDir)
}

// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
Expand Down Expand Up @@ -196,26 +199,48 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) {
if err := json.NewDecoder(indexJSON).Decode(&index); err != nil {
return imgspecv1.Descriptor{}, err
}

var d *imgspecv1.Descriptor
for _, md := range index.Manifests {
if md.MediaType != imgspecv1.MediaTypeImageManifest {
continue
}
refName, ok := md.Annotations["org.opencontainers.image.ref.name"]
if !ok {
continue
if ref.image == "" {
// return manifest if only one image is in the oci directory
if len(index.Manifests) == 1 {
d = &index.Manifests[0]
} else {
// ask user to choose image when more than one image in the oci directory
return imgspecv1.Descriptor{}, errors.Wrapf(err, "more than one image in oci, choose an image")
}
if refName == ref.tag {
d = &md
break
} else {
// if image specified, look through all manifests for a match
for _, md := range index.Manifests {
if md.MediaType != imgspecv1.MediaTypeImageManifest {
continue
}
refName, ok := md.Annotations["org.opencontainers.image.ref.name"]
if !ok {
continue
}
if refName == ref.image {
d = &md
break
}
}
}
if d == nil {
return imgspecv1.Descriptor{}, fmt.Errorf("no descriptor found for reference %q", ref.tag)
return imgspecv1.Descriptor{}, fmt.Errorf("no descriptor found for reference %q", ref.image)
}
return *d, nil
}

// LoadManifestDescriptor loads the manifest descriptor to be used to retrieve the image name
// when pulling an image
func LoadManifestDescriptor(imgRef types.ImageReference) (imgspecv1.Descriptor, error) {
ociRef, ok := imgRef.(ociReference)
if !ok {
return imgspecv1.Descriptor{}, errors.Errorf("error typecasting, need type ociRef")
}
return ociRef.getManifestDescriptor()
}

// NewImageSource returns a types.ImageSource for this reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
Expand All @@ -227,7 +252,7 @@ func (ref ociReference) NewImageSource(ctx *types.SystemContext, requestedManife
// NewImageDestination returns a types.ImageDestination for this reference.
// The caller must call .Close() on the returned ImageDestination.
func (ref ociReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
return newImageDestination(ref), nil
return newImageDestination(ref)
}

// DeleteImage deletes the named image from the registry, if supported.
Expand Down
67 changes: 38 additions & 29 deletions oci/layout/oci_transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
"/has/./dot",
"/has/dot/../dot",
"/trailing/slash/",
"/etc:invalid'tag!value@",
"/path:with/colons",
"/path:with/colons/and:tag",
"/etc:invalid'image!value@",
"/etc_no_image:",
"/etc:multiple_:separators",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.Error(t, err, scope)
Expand All @@ -64,48 +64,57 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err
"relativepath",
tmpDir + "/thisdoesnotexist",
} {
for _, tag := range []struct{ suffix, tag string }{
{":notlatest", "notlatest"},
{"", "latest"},
for _, image := range []struct{ suffix, image string }{
{":notlatest:image", "notlatest:image"},
{":latestimage", "latestimage"},
{":", ""},
{"", ""},
} {
input := path + tag.suffix
input := path + image.suffix
ref, err := fn(input)
require.NoError(t, err, input)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
assert.Equal(t, path, ociRef.dir, input)
assert.Equal(t, tag.tag, ociRef.tag, input)
assert.Equal(t, image.image, ociRef.image, input)
}
}

_, err = fn(tmpDir + "/with:multiple:colons:and:tag")
assert.Error(t, err)

_, err = fn(tmpDir + ":invalid'tag!value@")
_, err = fn(tmpDir + ":invalid'image!value@")
assert.Error(t, err)
}

func TestNewReference(t *testing.T) {
const tagValue = "tagValue"
const (
imageValue = "imageValue"
noImageValue = ""
)

tmpDir, err := ioutil.TempDir("", "oci-transport-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)

ref, err := NewReference(tmpDir, tagValue)
ref, err := NewReference(tmpDir, imageValue)
require.NoError(t, err)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
assert.Equal(t, tmpDir, ociRef.dir)
assert.Equal(t, tagValue, ociRef.tag)
assert.Equal(t, imageValue, ociRef.image)

ref, err = NewReference(tmpDir, noImageValue)
require.NoError(t, err)
ociRef, ok = ref.(ociReference)
require.True(t, ok)
assert.Equal(t, tmpDir, ociRef.dir)
assert.Equal(t, noImageValue, ociRef.image)

_, err = NewReference(tmpDir+"/thisparentdoesnotexist/something", tagValue)
_, err = NewReference(tmpDir+"/thisparentdoesnotexist/something", imageValue)
assert.Error(t, err)

_, err = NewReference(tmpDir+"/has:colon", tagValue)
_, err = NewReference(tmpDir, "invalid'image!value@")
assert.Error(t, err)

_, err = NewReference(tmpDir, "invalid'tag!value@")
_, err = NewReference(tmpDir+"/has:colon", imageValue)
assert.Error(t, err)
}

Expand All @@ -127,14 +136,14 @@ func refToTempOCI(t *testing.T) (ref types.ImageReference, tmpDir string) {
"os": "linux"
},
"annotations": {
"org.opencontainers.image.ref.name": "tagValue"
"org.opencontainers.image.ref.name": "imageValue"
}
}
]
}
`
ioutil.WriteFile(filepath.Join(tmpDir, "index.json"), []byte(m), 0644)
ref, err = NewReference(tmpDir, "tagValue")
ref, err = NewReference(tmpDir, "imageValue")
require.NoError(t, err)
return ref, tmpDir
}
Expand All @@ -151,8 +160,8 @@ func TestReferenceStringWithinTransport(t *testing.T) {
defer os.RemoveAll(tmpDir)

for _, c := range []struct{ input, result string }{
{"/dir1:notlatest", "/dir1:notlatest"}, // Explicit tag
{"/dir2", "/dir2:latest"}, // Default tag
{"/dir1:notlatest:notlatest", "/dir1:notlatest:notlatest"}, // Explicit image
{"/dir3:", "/dir3:"}, // No image
} {
ref, err := ParseReference(tmpDir + c.input)
require.NoError(t, err, c.input)
Expand All @@ -176,17 +185,17 @@ func TestReferencePolicyConfigurationIdentity(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)

assert.Equal(t, tmpDir+":tagValue", ref.PolicyConfigurationIdentity())
assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity())
// A non-canonical path. Test just one, the various other cases are
// tested in explicitfilepath.ResolvePathToFullyExplicit.
ref, err := NewReference(tmpDir+"/.", "tag2")
ref, err := NewReference(tmpDir+"/.", "image2")
require.NoError(t, err)
assert.Equal(t, tmpDir+":tag2", ref.PolicyConfigurationIdentity())
assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity())

// "/" as a corner case.
ref, err = NewReference("/", "tag3")
ref, err = NewReference("/", "image3")
require.NoError(t, err)
assert.Equal(t, "/:tag3", ref.PolicyConfigurationIdentity())
assert.Equal(t, "/", ref.PolicyConfigurationIdentity())
}

func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
Expand All @@ -208,15 +217,15 @@ func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
for _, path := range []string{"/etc/skel", "/etc/skel/./."} {
_, err := os.Lstat(path)
require.NoError(t, err)
ref, err := NewReference(path, "sometag")
ref, err := NewReference(path, "someimage")
require.NoError(t, err)
ns := ref.PolicyConfigurationNamespaces()
require.NotNil(t, ns)
assert.Equal(t, []string{"/etc/skel", "/etc"}, ns)
}

// "/" as a corner case.
ref, err := NewReference("/", "tag3")
ref, err := NewReference("/", "image3")
require.NoError(t, err)
assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces())
}
Expand Down
2 changes: 1 addition & 1 deletion transports/alltransports/alltransports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestImageNameHandling(t *testing.T) {
{"docker-daemon", "busybox:latest", "busybox:latest"},
{"docker-archive", "/var/lib/oci/busybox.tar:busybox:latest", "/var/lib/oci/busybox.tar:docker.io/library/busybox:latest"},
{"docker-archive", "busybox.tar:busybox:latest", "busybox.tar:docker.io/library/busybox:latest"},
{"oci", "/etc:sometag", "/etc:sometag"},
{"oci", "/etc:someimage:latest", "/etc:someimage:latest"},
// "atomic" not tested here because it depends on per-user configuration for the default cluster.
// "containers-storage" not tested here because it needs to initialize various directories on the fs.
} {
Expand Down

0 comments on commit 4eb8db3

Please sign in to comment.