...

Source file src/oras.land/oras-go/pkg/content/oci.go

Documentation: oras.land/oras-go/pkg/content

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  package content
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/containerd/containerd/content"
    29  	"github.com/containerd/containerd/content/local"
    30  	"github.com/containerd/containerd/remotes"
    31  	"github.com/opencontainers/go-digest"
    32  	specs "github.com/opencontainers/image-spec/specs-go"
    33  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    34  )
    35  
    36  // OCI provides content from the file system with the OCI-Image layout.
    37  // Reference: https://github.com/opencontainers/image-spec/blob/master/image-layout.md
    38  type OCI struct {
    39  	content.Store
    40  
    41  	root    string
    42  	index   *ocispec.Index
    43  	nameMap map[string]ocispec.Descriptor
    44  }
    45  
    46  // NewOCI creates a new OCI store
    47  func NewOCI(rootPath string) (*OCI, error) {
    48  	fileStore, err := local.NewStore(rootPath)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	store := &OCI{
    54  		Store: fileStore,
    55  		root:  rootPath,
    56  	}
    57  	if err := store.validateOCILayoutFile(); err != nil {
    58  		return nil, err
    59  	}
    60  	if err := store.LoadIndex(); err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	return store, nil
    65  }
    66  
    67  // LoadIndex reads the index.json from the file system
    68  func (s *OCI) LoadIndex() error {
    69  	path := filepath.Join(s.root, OCIImageIndexFile)
    70  	indexFile, err := os.Open(path)
    71  	if err != nil {
    72  		if !os.IsNotExist(err) {
    73  			return err
    74  		}
    75  		s.index = &ocispec.Index{
    76  			Versioned: specs.Versioned{
    77  				SchemaVersion: 2, // historical value
    78  			},
    79  		}
    80  		s.nameMap = make(map[string]ocispec.Descriptor)
    81  
    82  		return nil
    83  	}
    84  	defer indexFile.Close()
    85  
    86  	if err := json.NewDecoder(indexFile).Decode(&s.index); err != nil {
    87  		return err
    88  	}
    89  
    90  	s.nameMap = make(map[string]ocispec.Descriptor)
    91  	for _, desc := range s.index.Manifests {
    92  		if name := desc.Annotations[ocispec.AnnotationRefName]; name != "" {
    93  			s.nameMap[name] = desc
    94  		}
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  // SaveIndex writes the index.json to the file system
   101  func (s *OCI) SaveIndex() error {
   102  	// first need to update the index
   103  	var descs []ocispec.Descriptor
   104  	for name, desc := range s.nameMap {
   105  		if desc.Annotations == nil {
   106  			desc.Annotations = map[string]string{}
   107  		}
   108  		desc.Annotations[ocispec.AnnotationRefName] = name
   109  		descs = append(descs, desc)
   110  	}
   111  	s.index.Manifests = descs
   112  	indexJSON, err := json.Marshal(s.index)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	path := filepath.Join(s.root, OCIImageIndexFile)
   118  	return ioutil.WriteFile(path, indexJSON, 0644)
   119  }
   120  
   121  func (s *OCI) Resolver() remotes.Resolver {
   122  	return s
   123  }
   124  
   125  func (s *OCI) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) {
   126  	if err := s.LoadIndex(); err != nil {
   127  		return "", ocispec.Descriptor{}, err
   128  	}
   129  	desc, ok := s.nameMap[ref]
   130  	if !ok {
   131  		return "", ocispec.Descriptor{}, fmt.Errorf("reference %s not in store", ref)
   132  	}
   133  	return ref, desc, nil
   134  }
   135  
   136  func (s *OCI) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
   137  	if err := s.LoadIndex(); err != nil {
   138  		return nil, err
   139  	}
   140  	if _, ok := s.nameMap[ref]; !ok {
   141  		return nil, fmt.Errorf("reference %s not in store", ref)
   142  	}
   143  	return s, nil
   144  }
   145  
   146  // Fetch get an io.ReadCloser for the specific content
   147  func (s *OCI) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
   148  	readerAt, err := s.Store.ReaderAt(ctx, desc)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	// just wrap the ReaderAt with a Reader
   153  	return ioutil.NopCloser(&ReaderAtWrapper{readerAt: readerAt}), nil
   154  }
   155  
   156  // Pusher get a remotes.Pusher for the given ref
   157  func (s *OCI) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
   158  	// separate the tag based ref from the hash
   159  	var (
   160  		baseRef, hash string
   161  	)
   162  	parts := strings.SplitN(ref, "@", 2)
   163  	baseRef = parts[0]
   164  	if len(parts) > 1 {
   165  		hash = parts[1]
   166  	}
   167  	return &ociPusher{oci: s, ref: baseRef, digest: hash}, nil
   168  }
   169  
   170  // AddReference adds or updates an reference to index.
   171  func (s *OCI) AddReference(name string, desc ocispec.Descriptor) {
   172  	if desc.Annotations == nil {
   173  		desc.Annotations = map[string]string{
   174  			ocispec.AnnotationRefName: name,
   175  		}
   176  	} else {
   177  		desc.Annotations[ocispec.AnnotationRefName] = name
   178  	}
   179  
   180  	if _, ok := s.nameMap[name]; ok {
   181  		s.nameMap[name] = desc
   182  
   183  		for i, ref := range s.index.Manifests {
   184  			if name == ref.Annotations[ocispec.AnnotationRefName] {
   185  				s.index.Manifests[i] = desc
   186  				return
   187  			}
   188  		}
   189  
   190  		// Process should not reach here.
   191  		// Fallthrough to `Add` scenario and recover.
   192  		s.index.Manifests = append(s.index.Manifests, desc)
   193  		return
   194  	}
   195  
   196  	s.index.Manifests = append(s.index.Manifests, desc)
   197  	s.nameMap[name] = desc
   198  }
   199  
   200  // DeleteReference deletes an reference from index.
   201  func (s *OCI) DeleteReference(name string) {
   202  	if _, ok := s.nameMap[name]; !ok {
   203  		return
   204  	}
   205  
   206  	delete(s.nameMap, name)
   207  	for i, desc := range s.index.Manifests {
   208  		if name == desc.Annotations[ocispec.AnnotationRefName] {
   209  			s.index.Manifests[i] = s.index.Manifests[len(s.index.Manifests)-1]
   210  			s.index.Manifests = s.index.Manifests[:len(s.index.Manifests)-1]
   211  			return
   212  		}
   213  	}
   214  }
   215  
   216  // ListReferences lists all references in index.
   217  func (s *OCI) ListReferences() map[string]ocispec.Descriptor {
   218  	return s.nameMap
   219  }
   220  
   221  // validateOCILayoutFile ensures the `oci-layout` file
   222  func (s *OCI) validateOCILayoutFile() error {
   223  	layoutFilePath := filepath.Join(s.root, ocispec.ImageLayoutFile)
   224  	layoutFile, err := os.Open(layoutFilePath)
   225  	if err != nil {
   226  		if !os.IsNotExist(err) {
   227  			return err
   228  		}
   229  
   230  		layout := ocispec.ImageLayout{
   231  			Version: ocispec.ImageLayoutVersion,
   232  		}
   233  		layoutJSON, err := json.Marshal(layout)
   234  		if err != nil {
   235  			return err
   236  		}
   237  
   238  		return ioutil.WriteFile(layoutFilePath, layoutJSON, 0644)
   239  	}
   240  	defer layoutFile.Close()
   241  
   242  	var layout *ocispec.ImageLayout
   243  	err = json.NewDecoder(layoutFile).Decode(&layout)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	if layout.Version != ocispec.ImageLayoutVersion {
   248  		return ErrUnsupportedVersion
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  // Info will return metadata about content available in the content store.
   255  // Abort completely cancels the ingest operation targeted by ref.
   256  func (s *OCI) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
   257  	return s.Store.Info(ctx, dgst)
   258  }
   259  
   260  // TODO: implement (needed to create a content.Store)
   261  // Update updates mutable information related to content.
   262  // If one or more fieldpaths are provided, only those
   263  // fields will be updated.
   264  // Mutable fields:
   265  //  labels.*
   266  func (s *OCI) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   267  	return content.Info{}, errors.New("not yet implemented: Update (content.Store interface)")
   268  }
   269  
   270  // TODO: implement (needed to create a content.Store)
   271  // Walk will call fn for each item in the content store which
   272  // match the provided filters. If no filters are given all
   273  // items will be walked.
   274  func (s *OCI) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   275  	return errors.New("not yet implemented: Walk (content.Store interface)")
   276  }
   277  
   278  // Delete removes the content from the store.
   279  func (s *OCI) Delete(ctx context.Context, dgst digest.Digest) error {
   280  	return s.Store.Delete(ctx, dgst)
   281  }
   282  
   283  // TODO: implement (needed to create a content.Store)
   284  func (s *OCI) Status(ctx context.Context, ref string) (content.Status, error) {
   285  	// Status returns the status of the provided ref.
   286  	return content.Status{}, errors.New("not yet implemented: Status (content.Store interface)")
   287  }
   288  
   289  // TODO: implement (needed to create a content.Store)
   290  // ListStatuses returns the status of any active ingestions whose ref match the
   291  // provided regular expression. If empty, all active ingestions will be
   292  // returned.
   293  func (s *OCI) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
   294  	return []content.Status{}, errors.New("not yet implemented: ListStatuses (content.Store interface)")
   295  }
   296  
   297  // TODO: implement (needed to create a content.Store)
   298  // Abort completely cancels the ingest operation targeted by ref.
   299  func (s *OCI) Abort(ctx context.Context, ref string) error {
   300  	return errors.New("not yet implemented: Abort (content.Store interface)")
   301  }
   302  
   303  // ReaderAt provides contents
   304  func (s *OCI) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   305  	return s.Store.ReaderAt(ctx, desc)
   306  }
   307  
   308  // ociPusher to push content for a single referencem can handle multiple descriptors.
   309  // Needs to be able to recognize when a root manifest is being pushed and to create the tag
   310  // for it.
   311  type ociPusher struct {
   312  	oci    *OCI
   313  	ref    string
   314  	digest string
   315  }
   316  
   317  // Push get a writer for a single Descriptor
   318  func (p *ociPusher) Push(ctx context.Context, desc ocispec.Descriptor) (content.Writer, error) {
   319  	// do we need to create a tag?
   320  	switch desc.MediaType {
   321  	case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
   322  		// if the hash of the content matches that which was provided as the hash for the root, mark it
   323  		if p.digest != "" && p.digest == desc.Digest.String() {
   324  			if err := p.oci.LoadIndex(); err != nil {
   325  				return nil, err
   326  			}
   327  			p.oci.nameMap[p.ref] = desc
   328  			if err := p.oci.SaveIndex(); err != nil {
   329  				return nil, err
   330  			}
   331  		}
   332  	}
   333  
   334  	return p.oci.Store.Writer(ctx, content.WithDescriptor(desc), content.WithRef(p.ref))
   335  }
   336  

View as plain text