github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/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 "sort" 26 27 "github.com/opcr-io/oras-go/v2/content" 28 "github.com/opcr-io/oras-go/v2/errdef" 29 "github.com/opcr-io/oras-go/v2/internal/descriptor" 30 "github.com/opcr-io/oras-go/v2/internal/fs/tarfs" 31 "github.com/opcr-io/oras-go/v2/internal/graph" 32 "github.com/opcr-io/oras-go/v2/internal/resolver" 33 "github.com/opencontainers/go-digest" 34 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 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-rc2/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 Layout: %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(ctx, 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(ociImageIndexFile) 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(ctx context.Context, 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 sort.Strings(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 }