github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/content/oci/readonlyoci.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
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/fs"
    25  	"slices"
    26  
    27  	"github.com/opencontainers/go-digest"
    28  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    29  	"oras.land/oras-go/v2/content"
    30  	"oras.land/oras-go/v2/errdef"
    31  	"oras.land/oras-go/v2/internal/descriptor"
    32  	"oras.land/oras-go/v2/internal/fs/tarfs"
    33  	"oras.land/oras-go/v2/internal/graph"
    34  	"oras.land/oras-go/v2/internal/resolver"
    35  )
    36  
    37  // ReadOnlyStore implements `oras.ReadonlyTarget`, and represents a read-only
    38  // content store based on file system with the OCI-Image layout.
    39  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
    40  type ReadOnlyStore struct {
    41  	fsys        fs.FS
    42  	storage     content.ReadOnlyStorage
    43  	tagResolver *resolver.Memory
    44  	graph       *graph.Memory
    45  }
    46  
    47  // NewFromFS creates a new read-only OCI store from fsys.
    48  func NewFromFS(ctx context.Context, fsys fs.FS) (*ReadOnlyStore, error) {
    49  	store := &ReadOnlyStore{
    50  		fsys:        fsys,
    51  		storage:     NewStorageFromFS(fsys),
    52  		tagResolver: resolver.NewMemory(),
    53  		graph:       graph.NewMemory(),
    54  	}
    55  
    56  	if err := store.validateOCILayoutFile(); err != nil {
    57  		return nil, fmt.Errorf("invalid OCI Image Layout: %w", err)
    58  	}
    59  	if err := store.loadIndexFile(ctx); err != nil {
    60  		return nil, fmt.Errorf("invalid OCI Image Index: %w", err)
    61  	}
    62  
    63  	return store, nil
    64  }
    65  
    66  // NewFromTar creates a new read-only OCI store from a tar archive located at
    67  // path.
    68  func NewFromTar(ctx context.Context, path string) (*ReadOnlyStore, error) {
    69  	tfs, err := tarfs.New(path)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	return NewFromFS(ctx, tfs)
    74  }
    75  
    76  // Fetch fetches the content identified by the descriptor.
    77  func (s *ReadOnlyStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
    78  	return s.storage.Fetch(ctx, target)
    79  }
    80  
    81  // Exists returns true if the described content exists.
    82  func (s *ReadOnlyStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
    83  	return s.storage.Exists(ctx, target)
    84  }
    85  
    86  // Resolve resolves a reference to a descriptor. If the reference to be resolved
    87  // is a tag, the returned descriptor will be a full descriptor declared by
    88  // github.com/opencontainers/image-spec/specs-go/v1. If the reference is a
    89  // digest the returned descriptor will be a plain descriptor (containing only
    90  // the digest, media type and size).
    91  func (s *ReadOnlyStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
    92  	if reference == "" {
    93  		return ocispec.Descriptor{}, errdef.ErrMissingReference
    94  	}
    95  
    96  	// attempt resolving manifest
    97  	desc, err := s.tagResolver.Resolve(ctx, reference)
    98  	if err != nil {
    99  		if errors.Is(err, errdef.ErrNotFound) {
   100  			// attempt resolving blob
   101  			return resolveBlob(s.fsys, reference)
   102  		}
   103  		return ocispec.Descriptor{}, err
   104  	}
   105  
   106  	if reference == desc.Digest.String() {
   107  		return descriptor.Plain(desc), nil
   108  	}
   109  
   110  	return desc, nil
   111  }
   112  
   113  // Predecessors returns the nodes directly pointing to the current node.
   114  // Predecessors returns nil without error if the node does not exists in the
   115  // store.
   116  func (s *ReadOnlyStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   117  	return s.graph.Predecessors(ctx, node)
   118  }
   119  
   120  // Tags lists the tags presented in the `index.json` file of the OCI layout,
   121  // returned in ascending order.
   122  // If `last` is NOT empty, the entries in the response start after the tag
   123  // specified by `last`. Otherwise, the response starts from the top of the tags
   124  // list.
   125  //
   126  // See also `Tags()` in the package `registry`.
   127  func (s *ReadOnlyStore) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
   128  	return listTags(s.tagResolver, last, fn)
   129  }
   130  
   131  // validateOCILayoutFile validates the `oci-layout` file.
   132  func (s *ReadOnlyStore) validateOCILayoutFile() error {
   133  	layoutFile, err := s.fsys.Open(ocispec.ImageLayoutFile)
   134  	if err != nil {
   135  		return fmt.Errorf("failed to open OCI layout file: %w", err)
   136  	}
   137  	defer layoutFile.Close()
   138  
   139  	var layout ocispec.ImageLayout
   140  	err = json.NewDecoder(layoutFile).Decode(&layout)
   141  	if err != nil {
   142  		return fmt.Errorf("failed to decode OCI layout file: %w", err)
   143  	}
   144  	return validateOCILayout(&layout)
   145  }
   146  
   147  // validateOCILayout validates layout.
   148  func validateOCILayout(layout *ocispec.ImageLayout) error {
   149  	if layout.Version != ocispec.ImageLayoutVersion {
   150  		return errdef.ErrUnsupportedVersion
   151  	}
   152  	return nil
   153  }
   154  
   155  // loadIndexFile reads index.json from s.fsys.
   156  func (s *ReadOnlyStore) loadIndexFile(ctx context.Context) error {
   157  	indexFile, err := s.fsys.Open(ocispec.ImageIndexFile)
   158  	if err != nil {
   159  		return fmt.Errorf("failed to open index file: %w", err)
   160  	}
   161  	defer indexFile.Close()
   162  
   163  	var index ocispec.Index
   164  	if err := json.NewDecoder(indexFile).Decode(&index); err != nil {
   165  		return fmt.Errorf("failed to decode index file: %w", err)
   166  	}
   167  	return loadIndex(ctx, &index, s.storage, s.tagResolver, s.graph)
   168  }
   169  
   170  // loadIndex loads index into memory.
   171  func loadIndex(ctx context.Context, index *ocispec.Index, fetcher content.Fetcher, tagger content.Tagger, graph *graph.Memory) error {
   172  	for _, desc := range index.Manifests {
   173  		if err := tagger.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil {
   174  			return err
   175  		}
   176  		if ref := desc.Annotations[ocispec.AnnotationRefName]; ref != "" {
   177  			if err := tagger.Tag(ctx, desc, ref); err != nil {
   178  				return err
   179  			}
   180  		}
   181  		plain := descriptor.Plain(desc)
   182  		if err := graph.IndexAll(ctx, fetcher, plain); err != nil {
   183  			return err
   184  		}
   185  	}
   186  	return nil
   187  }
   188  
   189  // resolveBlob returns a descriptor describing the blob identified by dgst.
   190  func resolveBlob(fsys fs.FS, dgst string) (ocispec.Descriptor, error) {
   191  	path, err := blobPath(digest.Digest(dgst))
   192  	if err != nil {
   193  		if errors.Is(err, errdef.ErrInvalidDigest) {
   194  			return ocispec.Descriptor{}, errdef.ErrNotFound
   195  		}
   196  		return ocispec.Descriptor{}, err
   197  	}
   198  	fi, err := fs.Stat(fsys, path)
   199  	if err != nil {
   200  		if errors.Is(err, fs.ErrNotExist) {
   201  			return ocispec.Descriptor{}, errdef.ErrNotFound
   202  		}
   203  		return ocispec.Descriptor{}, err
   204  	}
   205  
   206  	return ocispec.Descriptor{
   207  		MediaType: descriptor.DefaultMediaType,
   208  		Size:      fi.Size(),
   209  		Digest:    digest.Digest(dgst),
   210  	}, nil
   211  }
   212  
   213  // listTags returns the tags in ascending order.
   214  // If `last` is NOT empty, the entries in the response start after the tag
   215  // specified by `last`. Otherwise, the response starts from the top of the tags
   216  // list.
   217  //
   218  // See also `Tags()` in the package `registry`.
   219  func listTags(tagResolver *resolver.Memory, last string, fn func(tags []string) error) error {
   220  	var tags []string
   221  
   222  	tagMap := tagResolver.Map()
   223  	for tag, desc := range tagMap {
   224  		if tag == desc.Digest.String() {
   225  			continue
   226  		}
   227  		if last != "" && tag <= last {
   228  			continue
   229  		}
   230  		tags = append(tags, tag)
   231  	}
   232  	slices.Sort(tags)
   233  
   234  	return fn(tags)
   235  }
   236  
   237  // deleteAnnotationRefName deletes the AnnotationRefName from the annotation map
   238  // of desc.
   239  func deleteAnnotationRefName(desc ocispec.Descriptor) ocispec.Descriptor {
   240  	if _, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok {
   241  		// no ops
   242  		return desc
   243  	}
   244  
   245  	size := len(desc.Annotations) - 1
   246  	if size == 0 {
   247  		desc.Annotations = nil
   248  		return desc
   249  	}
   250  
   251  	annotations := make(map[string]string, size)
   252  	for k, v := range desc.Annotations {
   253  		if k != ocispec.AnnotationRefName {
   254  			annotations[k] = v
   255  		}
   256  	}
   257  	desc.Annotations = annotations
   258  	return desc
   259  }