-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
Signed-off-by: Aleksa Sarai <asarai@suse.com>
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
### `umoci/image/cas` ### | ||
|
||
This is a reimplemented version of the currently in-flight [`image-tools` CAS | ||
PR][cas-pr], which combines the `cas` and `refs` interfaces into a single | ||
`Engine` that represents the image. In addition, I've implemented more | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
cyphar
Author
Member
|
||
auto-detection and creature comforts. | ||
|
||
When the PR is merged, these changes will probably go upstream as well. | ||
|
||
[cas-pr]: https://github.com/opencontainers/image-tools/pull/5 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* umoci: Umoci Modifies Open Containers' Images | ||
* Copyright (C) 2016 SUSE LLC. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package cas | ||
|
||
import ( | ||
"encoding/json" | ||
"io" | ||
|
||
"github.com/opencontainers/image-spec/specs-go/v1" | ||
"golang.org/x/net/context" | ||
) | ||
|
||
// Blob represents a "parsed" blob in an OCI image's blob store. MediaType | ||
// offers a type-safe way of checking what the type of Data is. | ||
type Blob struct { | ||
// MediaType is the OCI media type of Data. | ||
MediaType string | ||
|
||
// Digest is the digest of the parsed image. Note that this does not update | ||
// if Data is changed (it is the digest that this blob was parsed *from*). | ||
Digest string | ||
This comment has been minimized.
Sorry, something went wrong.
wking
Contributor
|
||
|
||
// Data is the "parsed" blob taken from the OCI image's blob store, and is | ||
// typed according to the media type. The mapping from MIME => type is as | ||
// follows. | ||
// | ||
// v1.MediaTypeDescriptor => *v1.Descriptor | ||
// v1.MediaTypeImageManifest => *v1.Manifest | ||
// v1.MediaTypeImageManifestList => *v1.ManifestList | ||
// v1.MediaTypeImageLayer => io.ReadCloser | ||
// v1.MediaTypeImageLayerNonDistributable => io.ReadCloser | ||
// v1.MediaTypeImageConfig => *v1.Image | ||
Data interface{} | ||
} | ||
|
||
func (b *Blob) load(ctx context.Context, engine Engine) error { | ||
reader, err := engine.GetBlob(ctx, b.Digest) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// The layer media types are special, we don't want to do any parsing (or | ||
// close the blob reference). | ||
switch b.MediaType { | ||
// v1.MediaTypeImageLayer => io.ReadCloser | ||
// v1.MediaTypeImageLayerNonDistributable => io.ReadCloser | ||
case v1.MediaTypeImageLayer, v1.MediaTypeImageLayerNonDistributable: | ||
// There isn't anything else we can practically do here. | ||
b.Data = reader | ||
return nil | ||
} | ||
|
||
defer reader.Close() | ||
|
||
var parsed interface{} | ||
switch b.MediaType { | ||
// v1.MediaTypeDescriptor => *v1.Descriptor | ||
case v1.MediaTypeDescriptor: | ||
parsed = &v1.Descriptor{} | ||
// v1.MediaTypeImageManifest => *v1.Manifest | ||
case v1.MediaTypeImageManifest: | ||
parsed = &v1.Manifest{} | ||
// v1.MediaTypeImageManifestList => *v1.ManifestList | ||
case v1.MediaTypeImageManifestList: | ||
parsed = &v1.ManifestList{} | ||
// v1.MediaTypeImageConfig => *v1.ImageConfig | ||
case v1.MediaTypeImageConfig: | ||
parsed = &v1.Image{} | ||
} | ||
|
||
if err := json.NewDecoder(reader).Decode(parsed); err != nil { | ||
return err | ||
} | ||
|
||
b.Data = parsed | ||
return nil | ||
} | ||
|
||
func (b *Blob) Close() { | ||
switch b.MediaType { | ||
case v1.MediaTypeImageLayer, v1.MediaTypeImageLayerNonDistributable: | ||
if b.Data != nil { | ||
b.Data.(io.Closer).Close() | ||
} | ||
} | ||
} | ||
|
||
// FromDescriptor parses the blob referenced by the given descriptor. | ||
func FromDescriptor(ctx context.Context, engine Engine, descriptor *v1.Descriptor) (*Blob, error) { | ||
blob := &Blob{ | ||
MediaType: descriptor.MediaType, | ||
Digest: descriptor.Digest, | ||
Data: nil, | ||
} | ||
|
||
if err := blob.load(ctx, engine); err != nil { | ||
return nil, err | ||
} | ||
|
||
return blob, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
/* | ||
* umoci: Umoci Modifies Open Containers' Images | ||
* Copyright (C) 2016 SUSE LLC. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package cas | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/opencontainers/image-spec/specs-go/v1" | ||
"golang.org/x/net/context" | ||
) | ||
|
||
const ( | ||
// BlobAlgorithm is the name of the only supported digest algorithm for blobs. | ||
// FIXME: We can make this a list. | ||
BlobAlgorithm = "sha256" | ||
|
||
// refDirectory is the directory inside an OCI image that contains references. | ||
refDirectory = "refs" | ||
|
||
// blobDirectory is the directory inside an OCI image that contains blobs. | ||
blobDirectory = "blobs" | ||
|
||
// layoutFile is the file in side an OCI image the indicates what version | ||
// of the OCI spec the image is. | ||
layoutFile = "oci-layout" | ||
) | ||
|
||
// Exposed errors. | ||
var ( | ||
// ErrInvalid is returned when an image was detected as being invalid. | ||
ErrInvalid = errors.New("invalid image detected") | ||
|
||
// ErrNotImplemented is returned when a requested operation has not been | ||
// implementing the backing image store. | ||
ErrNotImplemented = errors.New("operation not implemented") | ||
|
||
// ErrClobber is returned when a requested operation would require clobbering a | ||
// reference or blob which already exists. | ||
ErrClobber = errors.New("operation would clobber existing object") | ||
) | ||
|
||
// Engine is an interface that provides methods for accessing and modifying an | ||
// OCI image, namely allowing access to reference descriptors and blobs. | ||
type Engine interface { | ||
// PutBlob adds a new blob to the image. This is idempotent; a nil error | ||
// means that "the content is stored at DIGEST" without implying "because | ||
// of this PutBlob() call". | ||
PutBlob(ctx context.Context, reader io.Reader) (digest string, size int64, err error) | ||
|
||
// PutBlobJSON adds a new JSON blob to the image (marshalled from the given | ||
// interface). This is equivalent to calling PutBlob() with a JSON payload | ||
// as the reader. Note that due to intricacies in the Go JSON | ||
// implementation, we cannot guarantee that two calls to PutBlobJSON() will | ||
// return the same digest. | ||
PutBlobJSON(ctx context.Context, data interface{}) (digest string, size int64, err error) | ||
|
||
// PutReference adds a new reference descriptor blob to the image. This is | ||
// idempotent; a nil error means that "the descriptor is stored at NAME" | ||
// without implying "because of this PutReference() call". ErrClobber is | ||
// returned if there is already a descriptor stored at NAME, but does not | ||
// match the descriptor requested to be stored. | ||
PutReference(ctx context.Context, name string, descriptor *v1.Descriptor) (err error) | ||
|
||
// GetBlob returns a reader for retrieving a blob from the image, which the | ||
// caller must Close(). Returns os.ErrNotExist if the digest is not found. | ||
GetBlob(ctx context.Context, digest string) (reader io.ReadCloser, err error) | ||
|
||
// GetReference returns a reference from the image. Returns os.ErrNotExist | ||
// if the name was not found. | ||
GetReference(ctx context.Context, name string) (descriptor *v1.Descriptor, err error) | ||
|
||
// DeleteBlob removes a blob from the image. This is idempotent; a nil | ||
// error means "the content is not in the store" without implying "because | ||
// of this DeleteBlob() call". | ||
DeleteBlob(ctx context.Context, digest string) (err error) | ||
|
||
// DeleteReference removes a reference from the image. This is idempotent; | ||
// a nil error means "the content is not in the store" without implying | ||
// "because of this DeleteReference() call". | ||
DeleteReference(ctx context.Context, name string) (err error) | ||
|
||
// ListBlobs returns the set of blob digests stored in the image. | ||
ListBlobs(ctx context.Context) (digests []string, err error) | ||
This comment has been minimized.
Sorry, something went wrong.
wking
Contributor
|
||
|
||
// ListReferences returns the set of reference names stored in the image. | ||
ListReferences(ctx context.Context) (names []string, err error) | ||
This comment has been minimized.
Sorry, something went wrong.
wking
Contributor
|
||
|
||
// Close releases all references held by the engine. Subsequent operations | ||
// may fail. | ||
Close() (err error) | ||
} | ||
|
||
// Open will create an Engine reference to the OCI image at the provided | ||
// path. If the image format is not supported, ErrNotImplemented will be | ||
// returned. If the path does not exist, os.ErrNotExist will be returned. | ||
func Open(path string) (Engine, error) { | ||
fi, err := os.Stat(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if fi.IsDir() { | ||
return newDirEngine(path) | ||
} | ||
|
||
return nil, ErrNotImplemented | ||
} | ||
|
||
// blobPath returns the path to a blob given its digest, relative to the root | ||
// of the OCI image. The digest must be of the form algorithm:hex. | ||
func blobPath(digest string) (string, error) { | ||
fields := strings.SplitN(digest, ":", 2) | ||
if len(fields) != 2 { | ||
return "", fmt.Errorf("invalid digest: %q", digest) | ||
} | ||
|
||
algo := fields[0] | ||
hash := fields[1] | ||
|
||
if algo != BlobAlgorithm { | ||
return "", fmt.Errorf("unsupported algorithm: %q", algo) | ||
} | ||
|
||
return filepath.Join(blobDirectory, algo, hash), nil | ||
} | ||
|
||
// refPath returns the path to a reference given its name, relative to the r | ||
// oot of the OCI image. | ||
This comment has been minimized.
Sorry, something went wrong. |
||
func refPath(name string) (string, error) { | ||
return filepath.Join(refDirectory, name), nil | ||
} |
Any particular reason to combine refs and CAs? They seem orthogonal to me, but having them separate does mean two engines to open, pass around, and close.