oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/extendedcopy.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 oras
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"regexp"
    23  
    24  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    25  	"golang.org/x/sync/semaphore"
    26  	"oras.land/oras-go/v2/content"
    27  	"oras.land/oras-go/v2/internal/cas"
    28  	"oras.land/oras-go/v2/internal/container/set"
    29  	"oras.land/oras-go/v2/internal/copyutil"
    30  	"oras.land/oras-go/v2/internal/descriptor"
    31  	"oras.land/oras-go/v2/internal/docker"
    32  	"oras.land/oras-go/v2/internal/spec"
    33  	"oras.land/oras-go/v2/internal/status"
    34  	"oras.land/oras-go/v2/internal/syncutil"
    35  	"oras.land/oras-go/v2/registry"
    36  )
    37  
    38  // DefaultExtendedCopyOptions provides the default ExtendedCopyOptions.
    39  var DefaultExtendedCopyOptions ExtendedCopyOptions = ExtendedCopyOptions{
    40  	ExtendedCopyGraphOptions: DefaultExtendedCopyGraphOptions,
    41  }
    42  
    43  // ExtendedCopyOptions contains parameters for [oras.ExtendedCopy].
    44  type ExtendedCopyOptions struct {
    45  	ExtendedCopyGraphOptions
    46  }
    47  
    48  // DefaultExtendedCopyGraphOptions provides the default ExtendedCopyGraphOptions.
    49  var DefaultExtendedCopyGraphOptions ExtendedCopyGraphOptions = ExtendedCopyGraphOptions{
    50  	CopyGraphOptions: DefaultCopyGraphOptions,
    51  }
    52  
    53  // ExtendedCopyGraphOptions contains parameters for [oras.ExtendedCopyGraph].
    54  type ExtendedCopyGraphOptions struct {
    55  	CopyGraphOptions
    56  	// Depth limits the maximum depth of the directed acyclic graph (DAG) that
    57  	// will be extended-copied.
    58  	// If Depth is no specified, or the specified value is less than or
    59  	// equal to 0, the depth limit will be considered as infinity.
    60  	Depth int
    61  	// FindPredecessors finds the predecessors of the current node.
    62  	// If FindPredecessors is nil, src.Predecessors will be adapted and used.
    63  	FindPredecessors func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error)
    64  }
    65  
    66  // ExtendedCopy copies the directed acyclic graph (DAG) that are reachable from
    67  // the given tagged node from the source GraphTarget to the destination Target.
    68  // In other words, it copies a tagged artifact along with its referrers or
    69  // other predecessor manifests referencing it.
    70  //
    71  // The tagged node (e.g. a tagged manifest of the artifact) is identified by the
    72  // source reference.
    73  // The destination reference will be the same as the source reference if the
    74  // destination reference is left blank.
    75  //
    76  // Returns the descriptor of the tagged node on successful copy.
    77  func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, dst Target, dstRef string, opts ExtendedCopyOptions) (ocispec.Descriptor, error) {
    78  	if src == nil {
    79  		return ocispec.Descriptor{}, errors.New("nil source graph target")
    80  	}
    81  	if dst == nil {
    82  		return ocispec.Descriptor{}, errors.New("nil destination target")
    83  	}
    84  	if dstRef == "" {
    85  		dstRef = srcRef
    86  	}
    87  
    88  	node, err := src.Resolve(ctx, srcRef)
    89  	if err != nil {
    90  		return ocispec.Descriptor{}, err
    91  	}
    92  
    93  	if err := ExtendedCopyGraph(ctx, src, dst, node, opts.ExtendedCopyGraphOptions); err != nil {
    94  		return ocispec.Descriptor{}, err
    95  	}
    96  
    97  	if err := dst.Tag(ctx, node, dstRef); err != nil {
    98  		return ocispec.Descriptor{}, err
    99  	}
   100  
   101  	return node, nil
   102  }
   103  
   104  // ExtendedCopyGraph copies the directed acyclic graph (DAG) that are reachable
   105  // from the given node from the source GraphStorage to the destination Storage.
   106  // In other words, it copies an artifact along with its referrers or other
   107  // predecessor manifests referencing it.
   108  // The node (e.g. a manifest of the artifact) is identified by a descriptor.
   109  func ExtendedCopyGraph(ctx context.Context, src content.ReadOnlyGraphStorage, dst content.Storage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) error {
   110  	roots, err := findRoots(ctx, src, node, opts)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// if Concurrency is not set or invalid, use the default concurrency
   116  	if opts.Concurrency <= 0 {
   117  		opts.Concurrency = defaultConcurrency
   118  	}
   119  	limiter := semaphore.NewWeighted(int64(opts.Concurrency))
   120  	// use caching proxy on non-leaf nodes
   121  	if opts.MaxMetadataBytes <= 0 {
   122  		opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes
   123  	}
   124  	proxy := cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes)
   125  	// track content status
   126  	tracker := status.NewTracker()
   127  
   128  	// copy the sub-DAGs rooted by the root nodes
   129  	return syncutil.Go(ctx, limiter, func(ctx context.Context, region *syncutil.LimitedRegion, root ocispec.Descriptor) error {
   130  		// As a root can be a predecessor of other roots, release the limit here
   131  		// for dispatching, to avoid dead locks where predecessor roots are
   132  		// handled first and are waiting for its successors to complete.
   133  		region.End()
   134  		if err := copyGraph(ctx, src, dst, root, proxy, limiter, tracker, opts.CopyGraphOptions); err != nil {
   135  			return err
   136  		}
   137  		return region.Start()
   138  	}, roots...)
   139  }
   140  
   141  // findRoots finds the root nodes reachable from the given node through a
   142  // depth-first search.
   143  func findRoots(ctx context.Context, storage content.ReadOnlyGraphStorage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) ([]ocispec.Descriptor, error) {
   144  	visited := set.New[descriptor.Descriptor]()
   145  	rootMap := make(map[descriptor.Descriptor]ocispec.Descriptor)
   146  	addRoot := func(key descriptor.Descriptor, val ocispec.Descriptor) {
   147  		if _, exists := rootMap[key]; !exists {
   148  			rootMap[key] = val
   149  		}
   150  	}
   151  
   152  	// if FindPredecessors is not provided, use the default one
   153  	if opts.FindPredecessors == nil {
   154  		opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   155  			return src.Predecessors(ctx, desc)
   156  		}
   157  	}
   158  
   159  	var stack copyutil.Stack
   160  	// push the initial node to the stack, set the depth to 0
   161  	stack.Push(copyutil.NodeInfo{Node: node, Depth: 0})
   162  	for {
   163  		current, ok := stack.Pop()
   164  		if !ok {
   165  			// empty stack
   166  			break
   167  		}
   168  		currentNode := current.Node
   169  		currentKey := descriptor.FromOCI(currentNode)
   170  
   171  		if visited.Contains(currentKey) {
   172  			// skip the current node if it has been visited
   173  			continue
   174  		}
   175  		visited.Add(currentKey)
   176  
   177  		// stop finding predecessors if the target depth is reached
   178  		if opts.Depth > 0 && current.Depth == opts.Depth {
   179  			addRoot(currentKey, currentNode)
   180  			continue
   181  		}
   182  
   183  		predecessors, err := opts.FindPredecessors(ctx, storage, currentNode)
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  
   188  		// The current node has no predecessor node,
   189  		// which means it is a root node of a sub-DAG.
   190  		if len(predecessors) == 0 {
   191  			addRoot(currentKey, currentNode)
   192  			continue
   193  		}
   194  
   195  		// The current node has predecessor nodes, which means it is NOT a root node.
   196  		// Push the predecessor nodes to the stack and keep finding from there.
   197  		for _, predecessor := range predecessors {
   198  			predecessorKey := descriptor.FromOCI(predecessor)
   199  			if !visited.Contains(predecessorKey) {
   200  				// push the predecessor node with increased depth
   201  				stack.Push(copyutil.NodeInfo{Node: predecessor, Depth: current.Depth + 1})
   202  			}
   203  		}
   204  	}
   205  
   206  	roots := make([]ocispec.Descriptor, 0, len(rootMap))
   207  	for _, root := range rootMap {
   208  		roots = append(roots, root)
   209  	}
   210  	return roots, nil
   211  }
   212  
   213  // FilterAnnotation configures opts.FindPredecessors to filter the predecessors
   214  // whose annotation matches a given regex pattern.
   215  //
   216  // A predecessor is kept if key is in its annotations and the annotation value
   217  // matches regex.
   218  // If regex is nil, predecessors whose annotations contain key will be kept,
   219  // no matter of the annotation value.
   220  //
   221  // For performance consideration, when using both FilterArtifactType and
   222  // FilterAnnotation, it's recommended to call FilterArtifactType first.
   223  func (opts *ExtendedCopyGraphOptions) FilterAnnotation(key string, regex *regexp.Regexp) {
   224  	keep := func(desc ocispec.Descriptor) bool {
   225  		value, ok := desc.Annotations[key]
   226  		return ok && (regex == nil || regex.MatchString(value))
   227  	}
   228  
   229  	fp := opts.FindPredecessors
   230  	opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   231  		var predecessors []ocispec.Descriptor
   232  		var err error
   233  		if fp == nil {
   234  			if rf, ok := src.(registry.ReferrerLister); ok {
   235  				// if src is a ReferrerLister, use Referrers() for possible memory saving
   236  				if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
   237  					// for each page of the results, filter the referrers
   238  					for _, r := range referrers {
   239  						if keep(r) {
   240  							predecessors = append(predecessors, r)
   241  						}
   242  					}
   243  					return nil
   244  				}); err != nil {
   245  					return nil, err
   246  				}
   247  				return predecessors, nil
   248  			}
   249  			predecessors, err = src.Predecessors(ctx, desc)
   250  		} else {
   251  			predecessors, err = fp(ctx, src, desc)
   252  		}
   253  		if err != nil {
   254  			return nil, err
   255  		}
   256  
   257  		// Predecessor descriptors that are not from Referrers API are not
   258  		// guaranteed to include the annotations of the corresponding manifests.
   259  		var kept []ocispec.Descriptor
   260  		for _, p := range predecessors {
   261  			if p.Annotations == nil {
   262  				// If the annotations are not present in the descriptors,
   263  				// fetch it from the manifest content.
   264  				switch p.MediaType {
   265  				case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest,
   266  					docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex,
   267  					spec.MediaTypeArtifactManifest:
   268  					annotations, err := fetchAnnotations(ctx, src, p)
   269  					if err != nil {
   270  						return nil, err
   271  					}
   272  					p.Annotations = annotations
   273  				}
   274  			}
   275  			if keep(p) {
   276  				kept = append(kept, p)
   277  			}
   278  		}
   279  		return kept, nil
   280  	}
   281  }
   282  
   283  // fetchAnnotations fetches the annotations of the manifest described by desc.
   284  func fetchAnnotations(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (map[string]string, error) {
   285  	rc, err := src.Fetch(ctx, desc)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	defer rc.Close()
   290  
   291  	var manifest struct {
   292  		Annotations map[string]string `json:"annotations"`
   293  	}
   294  	if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
   295  		return nil, err
   296  	}
   297  	if manifest.Annotations == nil {
   298  		// to differentiate with nil
   299  		return make(map[string]string), nil
   300  	}
   301  	return manifest.Annotations, nil
   302  }
   303  
   304  // FilterArtifactType configures opts.FindPredecessors to filter the
   305  // predecessors whose artifact type matches a given regex pattern.
   306  //
   307  // A predecessor is kept if its artifact type matches regex.
   308  // If regex is nil, all predecessors will be kept.
   309  //
   310  // For performance consideration, when using both FilterArtifactType and
   311  // FilterAnnotation, it's recommended to call FilterArtifactType first.
   312  func (opts *ExtendedCopyGraphOptions) FilterArtifactType(regex *regexp.Regexp) {
   313  	if regex == nil {
   314  		return
   315  	}
   316  	keep := func(desc ocispec.Descriptor) bool {
   317  		return regex.MatchString(desc.ArtifactType)
   318  	}
   319  
   320  	fp := opts.FindPredecessors
   321  	opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   322  		var predecessors []ocispec.Descriptor
   323  		var err error
   324  		if fp == nil {
   325  			if rf, ok := src.(registry.ReferrerLister); ok {
   326  				// if src is a ReferrerLister, use Referrers() for possible memory saving
   327  				if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
   328  					// for each page of the results, filter the referrers
   329  					for _, r := range referrers {
   330  						if keep(r) {
   331  							predecessors = append(predecessors, r)
   332  						}
   333  					}
   334  					return nil
   335  				}); err != nil {
   336  					return nil, err
   337  				}
   338  				return predecessors, nil
   339  			}
   340  			predecessors, err = src.Predecessors(ctx, desc)
   341  		} else {
   342  			predecessors, err = fp(ctx, src, desc)
   343  		}
   344  		if err != nil {
   345  			return nil, err
   346  		}
   347  
   348  		// predecessor descriptors that are not from Referrers API are not
   349  		// guaranteed to include the artifact type of the corresponding
   350  		// manifests.
   351  		var kept []ocispec.Descriptor
   352  		for _, p := range predecessors {
   353  			if p.ArtifactType == "" {
   354  				// if the artifact type is not present in the descriptors,
   355  				// fetch it from the manifest content.
   356  				switch p.MediaType {
   357  				case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest:
   358  					artifactType, err := fetchArtifactType(ctx, src, p)
   359  					if err != nil {
   360  						return nil, err
   361  					}
   362  					p.ArtifactType = artifactType
   363  				}
   364  			}
   365  			if keep(p) {
   366  				kept = append(kept, p)
   367  			}
   368  		}
   369  		return kept, nil
   370  	}
   371  }
   372  
   373  // fetchArtifactType fetches the artifact type of the manifest described by desc.
   374  func fetchArtifactType(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (string, error) {
   375  	rc, err := src.Fetch(ctx, desc)
   376  	if err != nil {
   377  		return "", err
   378  	}
   379  	defer rc.Close()
   380  
   381  	switch desc.MediaType {
   382  	case spec.MediaTypeArtifactManifest:
   383  		var manifest spec.Artifact
   384  		if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
   385  			return "", err
   386  		}
   387  		return manifest.ArtifactType, nil
   388  	case ocispec.MediaTypeImageManifest:
   389  		var manifest ocispec.Manifest
   390  		if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
   391  			return "", err
   392  		}
   393  		return manifest.Config.MediaType, nil
   394  	default:
   395  		return "", nil
   396  	}
   397  }