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 }