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

WIP: oci/layout API extensions #2567

Closed
wants to merge 3 commits into from
Closed
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
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
}
30 changes: 30 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 Expand Up @@ -302,6 +305,33 @@ func (d *ociImageDestination) Commit(context.Context, types.UnparsedImage) error
return os.WriteFile(d.ref.indexPath(), indexJSON, 0644)
}

// PutBlobFromLocalFile arranges the data from path to be used as blob with digest.
// It computes, and returns, the digest and size of the used file.
//
// This function can be used instead of dest.PutBlob() where the ImageDestination requires PutBlob() to be called.
func PutBlobFromLocalFile(ctx context.Context, dest types.ImageDestination, file string) (digest.Digest, int64, error) {
vrothberg marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a means to configure the mime type of the blob to serve artifacts. Going forward, I anticipate users to create their custom layer types for LLM etc.

Copy link
Collaborator Author

@mtrmac mtrmac Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the ImageDestination API works, PutBlob doesn’t typically need to worry about the MIME type: the MIME type comes via PutManifest.

While it is true that the PutBlobWithOptions gets a full types.BlobInfo, and

blobInfo, diffIDChan, err := ic.copyLayerFromStream(ctx, srcStream, types.BlobInfo{Digest: srcInfo.Digest, Size: srcBlobSize, MediaType: srcInfo.MediaType, Annotations: srcInfo.Annotations}, diffIDIsNeeded, toEncrypt, bar, layerIndex, emptyLayer)
forwards the MIME type value into the layer copy code, there are things like
stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
just dropping that; so, transports can’t rely on a MIME type being present (~instead, we have PutBlobOptions.IsConfig).

Also, specifically in the OCI transport, the PutBlob* code really doesn’t care about the MIME type. It is going to create a sha256-named file, and as far as the layout definition is concerned, it’s perfectly valid to refer to a single blob in two different manifests using two different MIME types. So, for this OCI-transport-specific function, I don’t think adding a MIME type is right.

d, ok := dest.(*ociImageDestination)
if !ok {
return "", -1, errors.New("internal error: PutBlobFromLocalFile called with a non-oci: destination")
}

reader, err := os.Open(file)
if err != nil {
return "", -1, fmt.Errorf("opening %q: %w", file, err)
}
defer reader.Close()

// This makes a full copy; instead, if possible, we could only digest the file and reflink (hard link?)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: c/storage/drivers/copy.CopyRegular…

uploaded, err := d.PutBlobWithOptions(ctx, reader, types.BlobInfo{
Digest: "",
Size: -1,
}, private.PutBlobOptions{})
if err != nil {
return "", -1, err
}
return uploaded.Digest, uploaded.Size, nil
}

func ensureDirectoryExists(path string) error {
if err := fileutils.Exists(path); err != nil && errors.Is(err, fs.ErrNotExist) {
if err := os.MkdirAll(path, 0755); err != nil {
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
52 changes: 52 additions & 0 deletions oci/layout/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package layout

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/containers/image/v5/types"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
)

// This file is named reader.go for consistency with other transports
// handling of “image containers”, but we don’t actually need a stateful reader object.

// ListResult wraps the image reference and the manifest for loading
type ListResult struct {
Reference types.ImageReference
ManifestDescriptor imgspecv1.Descriptor
}

// List returns a slice of manifests included in the archive
func List(dir string) ([]ListResult, error) {
var res []ListResult

indexJSON, err := os.ReadFile(filepath.Join(dir, imgspecv1.ImageIndexFile))
if err != nil {
return nil, err
}
var index imgspecv1.Index
if err := json.Unmarshal(indexJSON, &index); err != nil {
return nil, err
}

for manifestIndex, md := range index.Manifests {
refName := md.Annotations[imgspecv1.AnnotationRefName]
index := -1
if refName == "" {
index = manifestIndex
}
ref, err := newReference(dir, refName, index)
if err != nil {
return nil, fmt.Errorf("error creating image reference: %w", err)
}
reference := ListResult{
Reference: ref,
ManifestDescriptor: md,
}
res = append(res, reference)
}
return res, nil
}
Loading