251 lines
6.5 KiB
Go
251 lines
6.5 KiB
Go
// Package oci provides basic operations for manipulating OCI images.
|
|
package oci
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/opencontainers/image-spec/specs-go"
|
|
spec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// Init initializes the img directory as an OCI image.
|
|
// i.e. Creates oci-layout, index.json, and blobs.
|
|
//
|
|
// img directory must not exist before calling this function.
|
|
//
|
|
// imageLayoutVersion can be an empty string for specifying the default version.
|
|
func Init(img, imageLayoutVersion string) error {
|
|
if imageLayoutVersion == "" {
|
|
imageLayoutVersion = spec.ImageLayoutVersion
|
|
}
|
|
if _, err := os.Stat(img); err == nil {
|
|
return os.ErrExist
|
|
}
|
|
// Create the directory
|
|
if err := os.MkdirAll(img, 0755); err != nil {
|
|
return err
|
|
}
|
|
// Create blobs/sha256
|
|
if err := os.MkdirAll(
|
|
filepath.Join(img, "blobs", string(digest.Canonical)),
|
|
0755); err != nil {
|
|
return nil
|
|
}
|
|
// Create oci-layout
|
|
if err := WriteImageLayout(img, spec.ImageLayout{Version: imageLayoutVersion}); err != nil {
|
|
return err
|
|
}
|
|
// Create index.json
|
|
return WriteIndex(img, spec.Index{Versioned: specs.Versioned{SchemaVersion: 2}})
|
|
}
|
|
|
|
func blobPath(img string, d digest.Digest) string {
|
|
return filepath.Join(img, "blobs", d.Algorithm().String(), d.Hex())
|
|
}
|
|
|
|
func indexPath(img string) string {
|
|
return filepath.Join(img, "index.json")
|
|
}
|
|
|
|
// GetBlobReader returns io.ReadCloser for a blob.
|
|
func GetBlobReader(img string, d digest.Digest) (io.ReadCloser, error) {
|
|
// we return a reader rather than the full *os.File here so as to prohibit write operations.
|
|
return os.Open(blobPath(img, d))
|
|
}
|
|
|
|
// ReadBlob reads an OCI blob.
|
|
func ReadBlob(img string, d digest.Digest) ([]byte, error) {
|
|
r, err := GetBlobReader(img, d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
return ioutil.ReadAll(r)
|
|
}
|
|
|
|
// WriteBlob writes bytes as an OCI blob and returns its digest using the canonical digest algorithm.
|
|
// If you need to specify certain algorithm, you can use NewBlobWriter(img string, algo digest.Algorithm).
|
|
func WriteBlob(img string, b []byte) (digest.Digest, error) {
|
|
d := digest.FromBytes(b)
|
|
return d, ioutil.WriteFile(blobPath(img, d), b, 0444)
|
|
}
|
|
|
|
// BlobWriter writes an OCI blob and returns a digest when closed.
|
|
type BlobWriter interface {
|
|
io.Writer
|
|
io.Closer
|
|
// Digest returns the digest when closed.
|
|
// Digest panics when the writer is not closed.
|
|
Digest() digest.Digest
|
|
}
|
|
|
|
// blobWriter implements BlobWriter.
|
|
type blobWriter struct {
|
|
img string
|
|
digester digest.Digester
|
|
f *os.File
|
|
closed bool
|
|
}
|
|
|
|
// NewBlobWriter returns a BlobWriter.
|
|
func NewBlobWriter(img string, algo digest.Algorithm) (BlobWriter, error) {
|
|
// use img rather than the default tmp, so as to make sure rename(2) can be applied
|
|
f, err := ioutil.TempFile(img, "tmp.blobwriter")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &blobWriter{
|
|
img: img,
|
|
digester: algo.Digester(),
|
|
f: f,
|
|
}, nil
|
|
}
|
|
|
|
// Write implements io.Writer.
|
|
func (bw *blobWriter) Write(b []byte) (int, error) {
|
|
n, err := bw.f.Write(b)
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
return bw.digester.Hash().Write(b)
|
|
}
|
|
|
|
// Close implements io.Closer.
|
|
func (bw *blobWriter) Close() error {
|
|
oldPath := bw.f.Name()
|
|
if err := bw.f.Close(); err != nil {
|
|
return err
|
|
}
|
|
newPath := blobPath(bw.img, bw.digester.Digest())
|
|
if err := os.MkdirAll(filepath.Dir(newPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(oldPath, 0444); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
bw.closed = true
|
|
return nil
|
|
}
|
|
|
|
// Digest returns the digest when closed.
|
|
func (bw *blobWriter) Digest() digest.Digest {
|
|
if !bw.closed {
|
|
panic("blobWriter is unclosed")
|
|
}
|
|
return bw.digester.Digest()
|
|
}
|
|
|
|
// DeleteBlob deletes an OCI blob.
|
|
func DeleteBlob(img string, d digest.Digest) error {
|
|
return os.Remove(blobPath(img, d))
|
|
}
|
|
|
|
// ReadImageLayout returns the image layout.
|
|
func ReadImageLayout(img string) (spec.ImageLayout, error) {
|
|
b, err := ioutil.ReadFile(filepath.Join(img, spec.ImageLayoutFile))
|
|
if err != nil {
|
|
return spec.ImageLayout{}, err
|
|
}
|
|
var layout spec.ImageLayout
|
|
if err := json.Unmarshal(b, &layout); err != nil {
|
|
return spec.ImageLayout{}, err
|
|
}
|
|
return layout, nil
|
|
}
|
|
|
|
// WriteImageLayout writes the image layout.
|
|
func WriteImageLayout(img string, layout spec.ImageLayout) error {
|
|
b, err := json.Marshal(layout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(filepath.Join(img, spec.ImageLayoutFile), b, 0644)
|
|
}
|
|
|
|
// ReadIndex returns the index.
|
|
func ReadIndex(img string) (spec.Index, error) {
|
|
b, err := ioutil.ReadFile(indexPath(img))
|
|
if err != nil {
|
|
return spec.Index{}, err
|
|
}
|
|
var idx spec.Index
|
|
if err := json.Unmarshal(b, &idx); err != nil {
|
|
return spec.Index{}, err
|
|
}
|
|
return idx, nil
|
|
}
|
|
|
|
// WriteIndex writes the index.
|
|
func WriteIndex(img string, idx spec.Index) error {
|
|
b, err := json.Marshal(idx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(indexPath(img), b, 0644)
|
|
}
|
|
|
|
// RemoveManifestDescriptorFromIndex removes the manifest descriptor from the index.
|
|
// Returns nil error when the entry not found.
|
|
func RemoveManifestDescriptorFromIndex(img string, refName string) error {
|
|
if refName == "" {
|
|
return errors.New("empty refName specified")
|
|
}
|
|
src, err := ReadIndex(img)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dst := src
|
|
dst.Manifests = nil
|
|
for _, m := range src.Manifests {
|
|
mRefName, ok := m.Annotations[spec.AnnotationRefName]
|
|
if ok && mRefName == refName {
|
|
continue
|
|
}
|
|
dst.Manifests = append(dst.Manifests, m)
|
|
}
|
|
return WriteIndex(img, dst)
|
|
}
|
|
|
|
// PutManifestDescriptorToIndex puts a manifest descriptor to the index.
|
|
// If ref name is set and conflicts with the existing descriptors, the old ones are removed.
|
|
func PutManifestDescriptorToIndex(img string, desc spec.Descriptor) error {
|
|
refName, ok := desc.Annotations[spec.AnnotationRefName]
|
|
if ok && refName != "" {
|
|
if err := RemoveManifestDescriptorFromIndex(img, refName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
idx, err := ReadIndex(img)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
idx.Manifests = append(idx.Manifests, desc)
|
|
return WriteIndex(img, idx)
|
|
}
|
|
|
|
// WriteJSONBlob is an utility function that writes x as a JSON blob with the specified media type, and returns the descriptor.
|
|
func WriteJSONBlob(img string, x interface{}, mediaType string) (spec.Descriptor, error) {
|
|
b, err := json.Marshal(x)
|
|
if err != nil {
|
|
return spec.Descriptor{}, err
|
|
}
|
|
d, err := WriteBlob(img, b)
|
|
if err != nil {
|
|
return spec.Descriptor{}, err
|
|
}
|
|
return spec.Descriptor{
|
|
MediaType: mediaType,
|
|
Digest: d,
|
|
Size: int64(len(b)),
|
|
}, nil
|
|
}
|