github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/zoci/pull.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package zoci contains functions for interacting with Jackal packages stored in OCI registries. 5 package zoci 6 7 import ( 8 "context" 9 "fmt" 10 "path/filepath" 11 12 "github.com/Racer159/jackal/src/pkg/layout" 13 "github.com/Racer159/jackal/src/pkg/transform" 14 "github.com/Racer159/jackal/src/pkg/utils" 15 "github.com/Racer159/jackal/src/types" 16 "github.com/defenseunicorns/pkg/helpers" 17 "github.com/defenseunicorns/pkg/oci" 18 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 19 "oras.land/oras-go/v2/content/file" 20 ) 21 22 var ( 23 // PackageAlwaysPull is a list of paths that will always be pulled from the remote repository. 24 PackageAlwaysPull = []string{layout.JackalYAML, layout.Checksums, layout.Signature} 25 ) 26 27 // PullPackage pulls the package from the remote repository and saves it to the given path. 28 // 29 // layersToPull is an optional parameter that allows the caller to specify which layers to pull. 30 // 31 // The following layers will ALWAYS be pulled if they exist: 32 // - jackal.yaml 33 // - checksums.txt 34 // - jackal.yaml.sig 35 func (r *Remote) PullPackage(ctx context.Context, destinationDir string, concurrency int, layersToPull ...ocispec.Descriptor) ([]ocispec.Descriptor, error) { 36 isPartialPull := len(layersToPull) > 0 37 r.Log().Debug(fmt.Sprintf("Pulling %s", r.Repo().Reference)) 38 39 manifest, err := r.FetchRoot(ctx) 40 if err != nil { 41 return nil, err 42 } 43 44 if isPartialPull { 45 for _, path := range PackageAlwaysPull { 46 desc := manifest.Locate(path) 47 layersToPull = append(layersToPull, desc) 48 } 49 } else { 50 layersToPull = append(layersToPull, manifest.Layers...) 51 } 52 layersToPull = append(layersToPull, manifest.Config) 53 54 // Create a thread to update a progress bar as we save the package to disk 55 doneSaving := make(chan error) 56 successText := fmt.Sprintf("Pulling %q", helpers.OCIURLPrefix+r.Repo().Reference.String()) 57 58 layerSize := oci.SumDescsSize(layersToPull) 59 go utils.RenderProgressBarForLocalDirWrite(destinationDir, layerSize, doneSaving, "Pulling", successText) 60 61 dst, err := file.New(destinationDir) 62 if err != nil { 63 return nil, err 64 } 65 defer dst.Close() 66 67 copyOpts := r.GetDefaultCopyOpts() 68 copyOpts.Concurrency = concurrency 69 70 err = r.CopyToTarget(ctx, layersToPull, dst, copyOpts) 71 doneSaving <- err 72 <-doneSaving 73 return layersToPull, err 74 } 75 76 // LayersFromRequestedComponents returns the descriptors for the given components from the root manifest. 77 // 78 // It also retrieves the descriptors for all image layers that are required by the components. 79 func (r *Remote) LayersFromRequestedComponents(ctx context.Context, requestedComponents []types.JackalComponent) (layers []ocispec.Descriptor, err error) { 80 root, err := r.FetchRoot(ctx) 81 if err != nil { 82 return nil, err 83 } 84 85 pkg, err := r.FetchJackalYAML(ctx) 86 if err != nil { 87 return nil, err 88 } 89 tarballFormat := "%s.tar" 90 images := map[string]bool{} 91 for _, rc := range requestedComponents { 92 component := helpers.Find(pkg.Components, func(component types.JackalComponent) bool { 93 return component.Name == rc.Name 94 }) 95 if component.Name == "" { 96 return nil, fmt.Errorf("component %s does not exist in this package", rc.Name) 97 } 98 for _, image := range component.Images { 99 images[image] = true 100 } 101 layers = append(layers, root.Locate(filepath.Join(layout.ComponentsDir, fmt.Sprintf(tarballFormat, component.Name)))) 102 } 103 // Append the sboms.tar layer if it exists 104 // 105 // Since sboms.tar is not a heavy addition 99% of the time, we'll just always pull it 106 sbomsDescriptor := root.Locate(layout.SBOMTar) 107 if !oci.IsEmptyDescriptor(sbomsDescriptor) { 108 layers = append(layers, sbomsDescriptor) 109 } 110 if len(images) > 0 { 111 // Add the image index and the oci-layout layers 112 layers = append(layers, root.Locate(layout.IndexPath), root.Locate(layout.OCILayoutPath)) 113 index, err := r.FetchImagesIndex(ctx) 114 if err != nil { 115 return nil, err 116 } 117 for image := range images { 118 // use docker's transform lib to parse the image ref 119 // this properly mirrors the logic within create 120 refInfo, err := transform.ParseImageRef(image) 121 if err != nil { 122 return nil, fmt.Errorf("failed to parse image ref %q: %w", image, err) 123 } 124 125 manifestDescriptor := helpers.Find(index.Manifests, func(layer ocispec.Descriptor) bool { 126 return layer.Annotations[ocispec.AnnotationBaseImageName] == refInfo.Reference || 127 // A backwards compatibility shim for older Jackal versions that would leave docker.io off of image annotations 128 (layer.Annotations[ocispec.AnnotationBaseImageName] == refInfo.Path+refInfo.TagOrDigest && refInfo.Host == "docker.io") 129 }) 130 131 // even though these are technically image manifests, we store them as Jackal blobs 132 manifestDescriptor.MediaType = JackalLayerMediaTypeBlob 133 134 manifest, err := r.FetchManifest(ctx, manifestDescriptor) 135 if err != nil { 136 return nil, err 137 } 138 // Add the manifest and the manifest config layers 139 layers = append(layers, root.Locate(filepath.Join(layout.ImagesBlobsDir, manifestDescriptor.Digest.Encoded()))) 140 layers = append(layers, root.Locate(filepath.Join(layout.ImagesBlobsDir, manifest.Config.Digest.Encoded()))) 141 142 // Add all the layers from the manifest 143 for _, layer := range manifest.Layers { 144 layerPath := filepath.Join(layout.ImagesBlobsDir, layer.Digest.Encoded()) 145 layers = append(layers, root.Locate(layerPath)) 146 } 147 } 148 } 149 return layers, nil 150 } 151 152 // PullPackageMetadata pulls the package metadata from the remote repository and saves it to `destinationDir`. 153 func (r *Remote) PullPackageMetadata(ctx context.Context, destinationDir string) ([]ocispec.Descriptor, error) { 154 return r.PullPaths(ctx, destinationDir, PackageAlwaysPull) 155 } 156 157 // PullPackageSBOM pulls the package's sboms.tar from the remote repository and saves it to `destinationDir`. 158 func (r *Remote) PullPackageSBOM(ctx context.Context, destinationDir string) ([]ocispec.Descriptor, error) { 159 return r.PullPaths(ctx, destinationDir, []string{layout.SBOMTar}) 160 }