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 }