github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/content/oci/oci.go (about)

     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  
    16  // Package oci provides access to an OCI content store.
    17  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/image-layout.md
    18  package oci
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  	"sync"
    29  
    30  	"github.com/opcr-io/oras-go/v2/content"
    31  	"github.com/opcr-io/oras-go/v2/errdef"
    32  	"github.com/opcr-io/oras-go/v2/internal/container/set"
    33  	"github.com/opcr-io/oras-go/v2/internal/descriptor"
    34  	"github.com/opcr-io/oras-go/v2/internal/graph"
    35  	"github.com/opcr-io/oras-go/v2/internal/resolver"
    36  	"github.com/opencontainers/go-digest"
    37  	specs "github.com/opencontainers/image-spec/specs-go"
    38  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    39  )
    40  
    41  // ociImageIndexFile is the file name of the index
    42  // from the OCI Image Layout Specification.
    43  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/image-layout.md#indexjson-file
    44  const ociImageIndexFile = "index.json"
    45  
    46  // Store implements `oras.Target`, and represents a content store
    47  // based on file system with the OCI-Image layout.
    48  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/image-layout.md
    49  type Store struct {
    50  	// AutoSaveIndex controls if the OCI store will automatically save the index
    51  	// file on each Tag() call.
    52  	//   - If AutoSaveIndex is set to true, the OCI store will automatically call
    53  	//     this method on each Tag() call.
    54  	//   - If AutoSaveIndex is set to false, it's the caller's responsibility
    55  	//     to manually call SaveIndex() when needed.
    56  	//   - Default value: true.
    57  	AutoSaveIndex bool
    58  	root          string
    59  	indexPath     string
    60  	index         *ocispec.Index
    61  	indexLock     sync.Mutex
    62  
    63  	storage     content.DeleteStorage
    64  	tagResolver *resolver.Memory
    65  	graph       *graph.Memory
    66  }
    67  
    68  // New creates a new OCI store with context.Background().
    69  func New(root string) (*Store, error) {
    70  	return NewWithContext(context.Background(), root)
    71  }
    72  
    73  // NewWithContext creates a new OCI store.
    74  func NewWithContext(ctx context.Context, root string) (*Store, error) {
    75  	rootAbs, err := filepath.Abs(root)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err)
    78  	}
    79  
    80  	storage, err := NewStorage(rootAbs)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("failed to create storage: %w", err)
    83  	}
    84  
    85  	store := &Store{
    86  		AutoSaveIndex: true,
    87  		root:          rootAbs,
    88  		indexPath:     filepath.Join(rootAbs, ociImageIndexFile),
    89  		storage:       storage,
    90  		tagResolver:   resolver.NewMemory(),
    91  		graph:         graph.NewMemory(),
    92  	}
    93  
    94  	if err := ensureDir(rootAbs); err != nil {
    95  		return nil, err
    96  	}
    97  	if err := store.ensureOCILayoutFile(); err != nil {
    98  		return nil, fmt.Errorf("invalid OCI Image Layout: %w", err)
    99  	}
   100  	if err := store.loadIndexFile(ctx); err != nil {
   101  		return nil, fmt.Errorf("invalid OCI Image Layout: %w", err)
   102  	}
   103  
   104  	return store, nil
   105  }
   106  
   107  // Fetch fetches the content identified by the descriptor.
   108  func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
   109  	return s.storage.Fetch(ctx, target)
   110  }
   111  
   112  // Push pushes the content, matching the expected descriptor.
   113  func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
   114  	if err := s.storage.Push(ctx, expected, reader); err != nil {
   115  		return err
   116  	}
   117  	if err := s.graph.Index(ctx, s.storage, expected); err != nil {
   118  		return err
   119  	}
   120  	if descriptor.IsManifest(expected) {
   121  		// tag by digest
   122  		return s.tag(ctx, expected, expected.Digest.String())
   123  	}
   124  	return nil
   125  }
   126  
   127  // Exists returns true if the described content exists.
   128  func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   129  	return s.storage.Exists(ctx, target)
   130  }
   131  
   132  // Tag tags a descriptor with a reference string.
   133  // reference should be a valid tag (e.g. "latest").
   134  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/image-layout.md#indexjson-file
   135  func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
   136  	if err := validateReference(reference); err != nil {
   137  		return err
   138  	}
   139  
   140  	exists, err := s.storage.Exists(ctx, desc)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	if !exists {
   145  		return fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrNotFound)
   146  	}
   147  
   148  	return s.tag(ctx, desc, reference)
   149  }
   150  
   151  // tag tags a descriptor with a reference string.
   152  func (s *Store) tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
   153  	dgst := desc.Digest.String()
   154  	if reference != dgst {
   155  		// also tag desc by its digest
   156  		if err := s.tagResolver.Tag(ctx, desc, dgst); err != nil {
   157  			return err
   158  		}
   159  	}
   160  	if err := s.tagResolver.Tag(ctx, desc, reference); err != nil {
   161  		return err
   162  	}
   163  	if s.AutoSaveIndex {
   164  		return s.SaveIndex()
   165  	}
   166  	return nil
   167  }
   168  
   169  // Resolve resolves a reference to a descriptor. If the reference to be resolved
   170  // is a tag, the returned descriptor will be a full descriptor declared by
   171  // github.com/opencontainers/image-spec/specs-go/v1. If the reference is a
   172  // digest the returned descriptor will be a plain descriptor (containing only
   173  // the digest, media type and size).
   174  func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
   175  	if reference == "" {
   176  		return ocispec.Descriptor{}, errdef.ErrMissingReference
   177  	}
   178  
   179  	// attempt resolving manifest
   180  	desc, err := s.tagResolver.Resolve(ctx, reference)
   181  	if err != nil {
   182  		if errors.Is(err, errdef.ErrNotFound) {
   183  			// attempt resolving blob
   184  			return resolveBlob(os.DirFS(s.root), reference)
   185  		}
   186  		return ocispec.Descriptor{}, err
   187  	}
   188  
   189  	if reference == desc.Digest.String() {
   190  		return descriptor.Plain(desc), nil
   191  	}
   192  
   193  	return desc, nil
   194  }
   195  
   196  // Untag removes a reference string from index.
   197  // reference should be a valid tag (e.g. "latest").
   198  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/image-layout.md#indexjson-file
   199  func (s *Store) Untag(ctx context.Context, descr ocispec.Descriptor, reference string) error {
   200  	if err := validateReference(reference); err != nil {
   201  		return err
   202  	}
   203  
   204  	s.tagResolver.Delete((reference))
   205  	s.tagResolver.Delete(descr.Digest.String())
   206  
   207  	if s.AutoSaveIndex {
   208  		err := s.SaveIndex()
   209  		if err != nil {
   210  			return err
   211  		}
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  // Delete removed a target descriptor from index and storage.
   218  func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error {
   219  	resolvers := s.tagResolver.Map()
   220  	for reference, desc := range resolvers {
   221  		if content.Equal(desc, target) {
   222  			s.tagResolver.Delete(reference)
   223  		}
   224  	}
   225  	if s.AutoSaveIndex {
   226  		err := s.SaveIndex()
   227  		if err != nil {
   228  			return err
   229  		}
   230  	}
   231  	return s.storage.Delete(ctx, target)
   232  }
   233  
   234  // Predecessors returns the nodes directly pointing to the current node.
   235  // Predecessors returns nil without error if the node does not exists in the
   236  // store.
   237  func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   238  	return s.graph.Predecessors(ctx, node)
   239  }
   240  
   241  // Tags lists the tags presented in the `index.json` file of the OCI layout,
   242  // returned in ascending order.
   243  // If `last` is NOT empty, the entries in the response start after the tag
   244  // specified by `last`. Otherwise, the response starts from the top of the tags
   245  // list.
   246  //
   247  // See also `Tags()` in the package `registry`.
   248  func (s *Store) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
   249  	return listTags(ctx, s.tagResolver, last, fn)
   250  }
   251  
   252  // ensureOCILayoutFile ensures the `oci-layout` file.
   253  func (s *Store) ensureOCILayoutFile() error {
   254  	layoutFilePath := filepath.Join(s.root, ocispec.ImageLayoutFile)
   255  	layoutFile, err := os.Open(layoutFilePath)
   256  	if err != nil {
   257  		if !os.IsNotExist(err) {
   258  			return fmt.Errorf("failed to open OCI layout file: %w", err)
   259  		}
   260  
   261  		layout := ocispec.ImageLayout{
   262  			Version: ocispec.ImageLayoutVersion,
   263  		}
   264  		layoutJSON, err := json.Marshal(layout)
   265  		if err != nil {
   266  			return fmt.Errorf("failed to marshal OCI layout file: %w", err)
   267  		}
   268  		return os.WriteFile(layoutFilePath, layoutJSON, 0666)
   269  	}
   270  	defer layoutFile.Close()
   271  
   272  	var layout ocispec.ImageLayout
   273  	err = json.NewDecoder(layoutFile).Decode(&layout)
   274  	if err != nil {
   275  		return fmt.Errorf("failed to decode OCI layout file: %w", err)
   276  	}
   277  	return validateOCILayout(&layout)
   278  }
   279  
   280  // loadIndexFile reads index.json from the file system.
   281  // Create index.json if it does not exist.
   282  func (s *Store) loadIndexFile(ctx context.Context) error {
   283  	indexFile, err := os.Open(s.indexPath)
   284  	if err != nil {
   285  		if !os.IsNotExist(err) {
   286  			return fmt.Errorf("failed to open index file: %w", err)
   287  		}
   288  
   289  		// write index.json if it does not exist
   290  		s.index = &ocispec.Index{
   291  			Versioned: specs.Versioned{
   292  				SchemaVersion: 2, // historical value
   293  			},
   294  			Manifests: []ocispec.Descriptor{},
   295  		}
   296  		return s.writeIndexFile()
   297  	}
   298  	defer indexFile.Close()
   299  
   300  	var index ocispec.Index
   301  	if err := json.NewDecoder(indexFile).Decode(&index); err != nil {
   302  		return fmt.Errorf("failed to decode index file: %w", err)
   303  	}
   304  	s.index = &index
   305  	return loadIndex(ctx, s.index, s.storage, s.tagResolver, s.graph)
   306  }
   307  
   308  // SaveIndex writes the `index.json` file to the file system.
   309  //   - If AutoSaveIndex is set to true (default value),
   310  //     the OCI store will automatically call this method on each Tag() call.
   311  //   - If AutoSaveIndex is set to false, it's the caller's responsibility
   312  //     to manually call this method when needed.
   313  func (s *Store) SaveIndex() error {
   314  	s.indexLock.Lock()
   315  	defer s.indexLock.Unlock()
   316  
   317  	var manifests []ocispec.Descriptor
   318  	tagged := set.New[digest.Digest]()
   319  	refMap := s.tagResolver.Map()
   320  
   321  	// 1. Add descriptors that are associated with tags
   322  	// Note: One descriptor can be associated with multiple tags.
   323  	for ref, desc := range refMap {
   324  		if ref != desc.Digest.String() {
   325  			annotations := make(map[string]string, len(desc.Annotations)+1)
   326  			for k, v := range desc.Annotations {
   327  				annotations[k] = v
   328  			}
   329  			annotations[ocispec.AnnotationRefName] = ref
   330  			desc.Annotations = annotations
   331  			manifests = append(manifests, desc)
   332  			// mark the digest as tagged for deduplication in step 2
   333  			tagged.Add(desc.Digest)
   334  		}
   335  	}
   336  	// 2. Add descriptors that are not associated with any tag
   337  	for ref, desc := range refMap {
   338  		if ref == desc.Digest.String() && !tagged.Contains(desc.Digest) {
   339  			// skip tagged ones since they have been added in step 1
   340  			manifests = append(manifests, deleteAnnotationRefName(desc))
   341  		}
   342  	}
   343  
   344  	s.index.Manifests = manifests
   345  	return s.writeIndexFile()
   346  }
   347  
   348  // writeIndexFile writes the `index.json` file.
   349  func (s *Store) writeIndexFile() error {
   350  	indexJSON, err := json.Marshal(s.index)
   351  	if err != nil {
   352  		return fmt.Errorf("failed to marshal index file: %w", err)
   353  	}
   354  	return os.WriteFile(s.indexPath, indexJSON, 0666)
   355  }
   356  
   357  // validateReference validates ref.
   358  func validateReference(ref string) error {
   359  	if ref == "" {
   360  		return errdef.ErrMissingReference
   361  	}
   362  
   363  	// TODO: may enforce more strict validation if needed.
   364  	return nil
   365  }