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 }