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  }