oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/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/image-layout.md
    18  package oci
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"maps"
    27  	"os"
    28  	"path"
    29  	"path/filepath"
    30  	"sync"
    31  
    32  	"github.com/opencontainers/go-digest"
    33  	specs "github.com/opencontainers/image-spec/specs-go"
    34  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    35  	"oras.land/oras-go/v2/content"
    36  	"oras.land/oras-go/v2/errdef"
    37  	"oras.land/oras-go/v2/internal/container/set"
    38  	"oras.land/oras-go/v2/internal/descriptor"
    39  	"oras.land/oras-go/v2/internal/graph"
    40  	"oras.land/oras-go/v2/internal/manifestutil"
    41  	"oras.land/oras-go/v2/internal/resolver"
    42  	"oras.land/oras-go/v2/registry"
    43  )
    44  
    45  // Store implements `oras.Target`, and represents a content store
    46  // based on file system with the OCI-Image layout.
    47  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
    48  type Store struct {
    49  	// AutoSaveIndex controls if the OCI store will automatically save the index
    50  	// file when needed.
    51  	//   - If AutoSaveIndex is set to true, the OCI store will automatically save
    52  	//     the changes to `index.json` when
    53  	//      1. pushing a manifest
    54  	//      2. calling Tag() or Delete()
    55  	//   - If AutoSaveIndex is set to false, it's the caller's responsibility
    56  	//     to manually call SaveIndex() when needed.
    57  	//   - Default value: true.
    58  	AutoSaveIndex bool
    59  
    60  	// AutoGC controls if the OCI store will automatically clean dangling
    61  	// (unreferenced) blobs created by the Delete() operation. This includes the
    62  	// referrers and the unreferenced successor blobs of the deleted content.
    63  	// Tagged manifests will not be deleted.
    64  	//   - Default value: true.
    65  	AutoGC bool
    66  
    67  	root        string
    68  	indexPath   string
    69  	index       *ocispec.Index
    70  	storage     *Storage
    71  	tagResolver *resolver.Memory
    72  	graph       *graph.Memory
    73  
    74  	// sync ensures that most operations can be done concurrently, while Delete
    75  	// has the exclusive access to Store if a delete operation is underway.
    76  	// Operations such as Fetch, Push use sync.RLock(), while Delete uses
    77  	// sync.Lock().
    78  	sync sync.RWMutex
    79  	// indexLock ensures that only one go-routine is writing to the index.
    80  	indexLock sync.Mutex
    81  }
    82  
    83  // New creates a new OCI store with context.Background().
    84  func New(root string) (*Store, error) {
    85  	return NewWithContext(context.Background(), root)
    86  }
    87  
    88  // NewWithContext creates a new OCI store.
    89  func NewWithContext(ctx context.Context, root string) (*Store, error) {
    90  	rootAbs, err := filepath.Abs(root)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err)
    93  	}
    94  	storage, err := NewStorage(rootAbs)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("failed to create storage: %w", err)
    97  	}
    98  
    99  	store := &Store{
   100  		AutoSaveIndex: true,
   101  		AutoGC:        true,
   102  		root:          rootAbs,
   103  		indexPath:     filepath.Join(rootAbs, ocispec.ImageIndexFile),
   104  		storage:       storage,
   105  		tagResolver:   resolver.NewMemory(),
   106  		graph:         graph.NewMemory(),
   107  	}
   108  
   109  	if err := ensureDir(filepath.Join(rootAbs, ocispec.ImageBlobsDir)); err != nil {
   110  		return nil, err
   111  	}
   112  	if err := store.ensureOCILayoutFile(); err != nil {
   113  		return nil, fmt.Errorf("invalid OCI Image Layout: %w", err)
   114  	}
   115  	if err := store.loadIndexFile(ctx); err != nil {
   116  		return nil, fmt.Errorf("invalid OCI Image Index: %w", err)
   117  	}
   118  
   119  	return store, nil
   120  }
   121  
   122  // Fetch fetches the content identified by the descriptor. It returns an io.ReadCloser.
   123  // It's recommended to close the io.ReadCloser before a Delete operation, otherwise
   124  // Delete may fail (for example on NTFS file systems).
   125  func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
   126  	s.sync.RLock()
   127  	defer s.sync.RUnlock()
   128  
   129  	return s.storage.Fetch(ctx, target)
   130  }
   131  
   132  // Push pushes the content, matching the expected descriptor.
   133  func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
   134  	s.sync.RLock()
   135  	defer s.sync.RUnlock()
   136  
   137  	if err := s.storage.Push(ctx, expected, reader); err != nil {
   138  		return err
   139  	}
   140  	if err := s.graph.Index(ctx, s.storage, expected); err != nil {
   141  		return err
   142  	}
   143  	if descriptor.IsManifest(expected) {
   144  		// tag by digest
   145  		return s.tag(ctx, expected, expected.Digest.String())
   146  	}
   147  	return nil
   148  }
   149  
   150  // Exists returns true if the described content exists.
   151  func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   152  	s.sync.RLock()
   153  	defer s.sync.RUnlock()
   154  
   155  	return s.storage.Exists(ctx, target)
   156  }
   157  
   158  // Delete deletes the content matching the descriptor from the store. Delete may
   159  // fail on certain systems (i.e. NTFS), if there is a process (i.e. an unclosed
   160  // Reader) using target. If s.AutoGC is set to true, Delete will recursively
   161  // remove the dangling blobs caused by the current delete. If s.AutoDeleteReferrers
   162  // is set to true, Delete will recursively remove the referrers of the manifests
   163  // being deleted.
   164  func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error {
   165  	s.sync.Lock()
   166  	defer s.sync.Unlock()
   167  
   168  	deleteQueue := []ocispec.Descriptor{target}
   169  	for len(deleteQueue) > 0 {
   170  		head := deleteQueue[0]
   171  		deleteQueue = deleteQueue[1:]
   172  
   173  		// get referrers if applicable
   174  		if s.AutoGC && descriptor.IsManifest(head) {
   175  			referrers, err := registry.Referrers(ctx, &unsafeStore{s}, head, "")
   176  			if err != nil {
   177  				return err
   178  			}
   179  			deleteQueue = append(deleteQueue, referrers...)
   180  		}
   181  
   182  		// delete the head of queue
   183  		danglings, err := s.delete(ctx, head)
   184  		if err != nil {
   185  			return err
   186  		}
   187  		if s.AutoGC {
   188  			for _, d := range danglings {
   189  				// do not delete existing tagged manifests
   190  				if !s.isTagged(d) {
   191  					deleteQueue = append(deleteQueue, d)
   192  				}
   193  			}
   194  		}
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  // delete deletes one node and returns the dangling nodes caused by the delete.
   201  func (s *Store) delete(ctx context.Context, target ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   202  	resolvers := s.tagResolver.Map()
   203  	untagged := false
   204  	for reference, desc := range resolvers {
   205  		if content.Equal(desc, target) {
   206  			s.tagResolver.Untag(reference)
   207  			untagged = true
   208  		}
   209  	}
   210  	danglings := s.graph.Remove(target)
   211  	if untagged && s.AutoSaveIndex {
   212  		err := s.saveIndex()
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  	}
   217  	if err := s.storage.Delete(ctx, target); err != nil {
   218  		return nil, err
   219  	}
   220  	return danglings, nil
   221  }
   222  
   223  // Tag tags a descriptor with a reference string.
   224  // reference should be a valid tag (e.g. "latest").
   225  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md#indexjson-file
   226  func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
   227  	s.sync.RLock()
   228  	defer s.sync.RUnlock()
   229  
   230  	if err := validateReference(reference); err != nil {
   231  		return err
   232  	}
   233  
   234  	exists, err := s.storage.Exists(ctx, desc)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	if !exists {
   239  		return fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrNotFound)
   240  	}
   241  
   242  	return s.tag(ctx, desc, reference)
   243  }
   244  
   245  // tag tags a descriptor with a reference string.
   246  func (s *Store) tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
   247  	dgst := desc.Digest.String()
   248  	if reference != dgst {
   249  		// also tag desc by its digest
   250  		if err := s.tagResolver.Tag(ctx, desc, dgst); err != nil {
   251  			return err
   252  		}
   253  	}
   254  	if err := s.tagResolver.Tag(ctx, desc, reference); err != nil {
   255  		return err
   256  	}
   257  	if s.AutoSaveIndex {
   258  		return s.saveIndex()
   259  	}
   260  	return nil
   261  }
   262  
   263  // Resolve resolves a reference to a descriptor. If the reference to be resolved
   264  // is a tag, the returned descriptor will be a full descriptor declared by
   265  // github.com/opencontainers/image-spec/specs-go/v1. If the reference is a
   266  // digest the returned descriptor will be a plain descriptor (containing only
   267  // the digest, media type and size).
   268  func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
   269  	s.sync.RLock()
   270  	defer s.sync.RUnlock()
   271  
   272  	if reference == "" {
   273  		return ocispec.Descriptor{}, errdef.ErrMissingReference
   274  	}
   275  
   276  	// attempt resolving manifest
   277  	desc, err := s.tagResolver.Resolve(ctx, reference)
   278  	if err != nil {
   279  		if errors.Is(err, errdef.ErrNotFound) {
   280  			// attempt resolving blob
   281  			return resolveBlob(os.DirFS(s.root), reference)
   282  		}
   283  		return ocispec.Descriptor{}, err
   284  	}
   285  
   286  	if reference == desc.Digest.String() {
   287  		return descriptor.Plain(desc), nil
   288  	}
   289  
   290  	return desc, nil
   291  }
   292  
   293  func (s *Store) Untag(ctx context.Context, reference string) error {
   294  	if reference == "" {
   295  		return errdef.ErrMissingReference
   296  	}
   297  
   298  	s.sync.RLock()
   299  	defer s.sync.RUnlock()
   300  
   301  	desc, err := s.tagResolver.Resolve(ctx, reference)
   302  	if err != nil {
   303  		return fmt.Errorf("resolving reference %q: %w", reference, err)
   304  	}
   305  	if reference == desc.Digest.String() {
   306  		return fmt.Errorf("reference %q is a digest and not a tag: %w", reference, errdef.ErrInvalidReference)
   307  	}
   308  
   309  	s.tagResolver.Untag(reference)
   310  	if s.AutoSaveIndex {
   311  		return s.saveIndex()
   312  	}
   313  	return nil
   314  }
   315  
   316  // Predecessors returns the nodes directly pointing to the current node.
   317  // Predecessors returns nil without error if the node does not exists in the
   318  // store.
   319  func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   320  	s.sync.RLock()
   321  	defer s.sync.RUnlock()
   322  
   323  	return s.graph.Predecessors(ctx, node)
   324  }
   325  
   326  // Tags lists the tags presented in the `index.json` file of the OCI layout,
   327  // returned in ascending order.
   328  // If `last` is NOT empty, the entries in the response start after the tag
   329  // specified by `last`. Otherwise, the response starts from the top of the tags
   330  // list.
   331  //
   332  // See also `Tags()` in the package `registry`.
   333  func (s *Store) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
   334  	s.sync.RLock()
   335  	defer s.sync.RUnlock()
   336  
   337  	return listTags(s.tagResolver, last, fn)
   338  }
   339  
   340  // ensureOCILayoutFile ensures the `oci-layout` file.
   341  func (s *Store) ensureOCILayoutFile() error {
   342  	layoutFilePath := filepath.Join(s.root, ocispec.ImageLayoutFile)
   343  	layoutFile, err := os.Open(layoutFilePath)
   344  	if err != nil {
   345  		if !os.IsNotExist(err) {
   346  			return fmt.Errorf("failed to open OCI layout file: %w", err)
   347  		}
   348  
   349  		layout := ocispec.ImageLayout{
   350  			Version: ocispec.ImageLayoutVersion,
   351  		}
   352  		layoutJSON, err := json.Marshal(layout)
   353  		if err != nil {
   354  			return fmt.Errorf("failed to marshal OCI layout file: %w", err)
   355  		}
   356  		return os.WriteFile(layoutFilePath, layoutJSON, 0666)
   357  	}
   358  	defer layoutFile.Close()
   359  
   360  	var layout ocispec.ImageLayout
   361  	err = json.NewDecoder(layoutFile).Decode(&layout)
   362  	if err != nil {
   363  		return fmt.Errorf("failed to decode OCI layout file: %w", err)
   364  	}
   365  	return validateOCILayout(&layout)
   366  }
   367  
   368  // loadIndexFile reads index.json from the file system.
   369  // Create index.json if it does not exist.
   370  func (s *Store) loadIndexFile(ctx context.Context) error {
   371  	indexFile, err := os.Open(s.indexPath)
   372  	if err != nil {
   373  		if !os.IsNotExist(err) {
   374  			return fmt.Errorf("failed to open index file: %w", err)
   375  		}
   376  
   377  		// write index.json if it does not exist
   378  		s.index = &ocispec.Index{
   379  			Versioned: specs.Versioned{
   380  				SchemaVersion: 2, // historical value
   381  			},
   382  			Manifests: []ocispec.Descriptor{},
   383  		}
   384  		return s.writeIndexFile()
   385  	}
   386  	defer indexFile.Close()
   387  
   388  	var index ocispec.Index
   389  	if err := json.NewDecoder(indexFile).Decode(&index); err != nil {
   390  		return fmt.Errorf("failed to decode index file: %w", err)
   391  	}
   392  	s.index = &index
   393  	return loadIndex(ctx, s.index, s.storage, s.tagResolver, s.graph)
   394  }
   395  
   396  // SaveIndex writes the `index.json` file to the file system.
   397  //   - If AutoSaveIndex is set to true (default value),
   398  //     the OCI store will automatically save the changes to `index.json`
   399  //     on Tag() and Delete() calls, and when pushing a manifest.
   400  //   - If AutoSaveIndex is set to false, it's the caller's responsibility
   401  //     to manually call this method when needed.
   402  func (s *Store) SaveIndex() error {
   403  	s.sync.RLock()
   404  	defer s.sync.RUnlock()
   405  
   406  	return s.saveIndex()
   407  }
   408  
   409  func (s *Store) saveIndex() error {
   410  	s.indexLock.Lock()
   411  	defer s.indexLock.Unlock()
   412  
   413  	var manifests []ocispec.Descriptor
   414  	tagged := set.New[digest.Digest]()
   415  	refMap := s.tagResolver.Map()
   416  
   417  	// 1. Add descriptors that are associated with tags
   418  	// Note: One descriptor can be associated with multiple tags.
   419  	for ref, desc := range refMap {
   420  		if ref != desc.Digest.String() {
   421  			annotations := make(map[string]string, len(desc.Annotations)+1)
   422  			maps.Copy(annotations, desc.Annotations)
   423  			annotations[ocispec.AnnotationRefName] = ref
   424  			desc.Annotations = annotations
   425  			manifests = append(manifests, desc)
   426  			// mark the digest as tagged for deduplication in step 2
   427  			tagged.Add(desc.Digest)
   428  		}
   429  	}
   430  	// 2. Add descriptors that are not associated with any tag
   431  	for ref, desc := range refMap {
   432  		if ref == desc.Digest.String() && !tagged.Contains(desc.Digest) {
   433  			// skip tagged ones since they have been added in step 1
   434  			manifests = append(manifests, deleteAnnotationRefName(desc))
   435  		}
   436  	}
   437  
   438  	s.index.Manifests = manifests
   439  	return s.writeIndexFile()
   440  }
   441  
   442  // writeIndexFile writes the `index.json` file.
   443  func (s *Store) writeIndexFile() error {
   444  	indexJSON, err := json.Marshal(s.index)
   445  	if err != nil {
   446  		return fmt.Errorf("failed to marshal index file: %w", err)
   447  	}
   448  	return os.WriteFile(s.indexPath, indexJSON, 0666)
   449  }
   450  
   451  // GC removes garbage from Store. Unsaved index will be lost. To prevent unexpected
   452  // loss, call SaveIndex() before GC or set AutoSaveIndex to true.
   453  // The garbage to be cleaned are:
   454  //   - unreferenced (dangling) blobs in Store which have no predecessors
   455  //   - garbage blobs in the storage whose metadata is not stored in Store
   456  func (s *Store) GC(ctx context.Context) error {
   457  	s.sync.Lock()
   458  	defer s.sync.Unlock()
   459  
   460  	// get reachable nodes by reloading the index
   461  	err := s.gcIndex(ctx)
   462  	if err != nil {
   463  		return fmt.Errorf("unable to reload index: %w", err)
   464  	}
   465  	reachableNodes := s.graph.DigestSet()
   466  
   467  	// clean up garbage blobs in the storage
   468  	rootpath := filepath.Join(s.root, ocispec.ImageBlobsDir)
   469  	algDirs, err := os.ReadDir(rootpath)
   470  	if err != nil {
   471  		return err
   472  	}
   473  	for _, algDir := range algDirs {
   474  		if !algDir.IsDir() {
   475  			continue
   476  		}
   477  		alg := algDir.Name()
   478  		// skip unsupported directories
   479  		if !isKnownAlgorithm(alg) {
   480  			continue
   481  		}
   482  		algPath := path.Join(rootpath, alg)
   483  		digestEntries, err := os.ReadDir(algPath)
   484  		if err != nil {
   485  			return err
   486  		}
   487  		for _, digestEntry := range digestEntries {
   488  			if err := isContextDone(ctx); err != nil {
   489  				return err
   490  			}
   491  			dgst := digestEntry.Name()
   492  			blobDigest := digest.NewDigestFromEncoded(digest.Algorithm(alg), dgst)
   493  			if err := blobDigest.Validate(); err != nil {
   494  				// skip irrelevant content
   495  				continue
   496  			}
   497  			if !reachableNodes.Contains(blobDigest) {
   498  				// remove the blob from storage if it does not exist in Store
   499  				err = os.Remove(path.Join(algPath, dgst))
   500  				if err != nil {
   501  					return err
   502  				}
   503  			}
   504  		}
   505  	}
   506  	return nil
   507  }
   508  
   509  // gcIndex reloads the index and updates metadata. Information of untagged blobs
   510  // are cleaned and only tagged blobs remain.
   511  func (s *Store) gcIndex(ctx context.Context) error {
   512  	tagResolver := resolver.NewMemory()
   513  	graph := graph.NewMemory()
   514  	tagged := set.New[digest.Digest]()
   515  
   516  	// index tagged manifests
   517  	refMap := s.tagResolver.Map()
   518  	for ref, desc := range refMap {
   519  		if ref == desc.Digest.String() {
   520  			continue
   521  		}
   522  		if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil {
   523  			return err
   524  		}
   525  		if err := tagResolver.Tag(ctx, desc, ref); err != nil {
   526  			return err
   527  		}
   528  		plain := descriptor.Plain(desc)
   529  		if err := graph.IndexAll(ctx, s.storage, plain); err != nil {
   530  			return err
   531  		}
   532  		tagged.Add(desc.Digest)
   533  	}
   534  
   535  	// index referrer manifests
   536  	for ref, desc := range refMap {
   537  		if ref != desc.Digest.String() || tagged.Contains(desc.Digest) {
   538  			continue
   539  		}
   540  		// check if the referrers manifest can traverse to the existing graph
   541  		subject := &desc
   542  		for {
   543  			subject, err := manifestutil.Subject(ctx, s.storage, *subject)
   544  			if err != nil {
   545  				return err
   546  			}
   547  			if subject == nil {
   548  				break
   549  			}
   550  			if graph.Exists(*subject) {
   551  				if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil {
   552  					return err
   553  				}
   554  				plain := descriptor.Plain(desc)
   555  				if err := graph.IndexAll(ctx, s.storage, plain); err != nil {
   556  					return err
   557  				}
   558  				break
   559  			}
   560  		}
   561  	}
   562  	s.tagResolver = tagResolver
   563  	s.graph = graph
   564  	return nil
   565  }
   566  
   567  // isTagged checks if the blob given by the descriptor is tagged.
   568  func (s *Store) isTagged(desc ocispec.Descriptor) bool {
   569  	tagSet := s.tagResolver.TagSet(desc)
   570  	if tagSet.Contains(string(desc.Digest)) {
   571  		return len(tagSet) > 1
   572  	}
   573  	return len(tagSet) > 0
   574  }
   575  
   576  // unsafeStore is used to bypass lock restrictions in Delete.
   577  type unsafeStore struct {
   578  	*Store
   579  }
   580  
   581  func (s *unsafeStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
   582  	return s.storage.Fetch(ctx, target)
   583  }
   584  
   585  func (s *unsafeStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   586  	return s.graph.Predecessors(ctx, node)
   587  }
   588  
   589  // isContextDone returns an error if the context is done.
   590  // Reference: https://pkg.go.dev/context#Context
   591  func isContextDone(ctx context.Context) error {
   592  	select {
   593  	case <-ctx.Done():
   594  		return ctx.Err()
   595  	default:
   596  		return nil
   597  	}
   598  }
   599  
   600  // validateReference validates ref.
   601  func validateReference(ref string) error {
   602  	if ref == "" {
   603  		return errdef.ErrMissingReference
   604  	}
   605  
   606  	// TODO: may enforce more strict validation if needed.
   607  	return nil
   608  }
   609  
   610  // isKnownAlgorithm checks is a string is a supported hash algorithm
   611  func isKnownAlgorithm(alg string) bool {
   612  	switch digest.Algorithm(alg) {
   613  	case digest.SHA256, digest.SHA512, digest.SHA384:
   614  		return true
   615  	default:
   616  		return false
   617  	}
   618  }