diff --git a/cmd/lifecycle/creator.go b/cmd/lifecycle/creator.go index 4eaa465c6..7e84a60a3 100644 --- a/cmd/lifecycle/creator.go +++ b/cmd/lifecycle/creator.go @@ -124,7 +124,7 @@ func (c *createCmd) Exec() error { plan files.Plan ) cmd.DefaultLogger.Phase("ANALYZING") - analyzerFactory := phase.NewConnectedFactory( + connectedFactory := phase.NewConnectedFactory( c.PlatformAPI, &cmd.BuildpackAPIVerifier{}, NewCacheHandler(c.keychain), @@ -132,7 +132,7 @@ func (c *createCmd) Exec() error { image.NewHandler(c.docker, c.keychain, c.LayoutDir, c.UseLayout, c.InsecureRegistries), image.NewRegistryHandler(c.keychain, c.InsecureRegistries), ) - analyzer, err := analyzerFactory.NewAnalyzer(c.Inputs(), cmd.DefaultLogger) + analyzer, err := connectedFactory.NewAnalyzer(c.Inputs(), cmd.DefaultLogger) if err != nil { return unwrapErrorFailWithMessage(err, "initialize analyzer") } @@ -146,13 +146,13 @@ func (c *createCmd) Exec() error { // Detect cmd.DefaultLogger.Phase("DETECTING") - detectorFactory := phase.NewHermeticFactory( + hermeticFactory := phase.NewHermeticFactory( c.PlatformAPI, &cmd.BuildpackAPIVerifier{}, files.NewHandler(), dirStore, ) - detector, err := detectorFactory.NewDetector(c.Inputs(), cmd.DefaultLogger) + detector, err := hermeticFactory.NewDetector(c.Inputs(), cmd.DefaultLogger) if err != nil { return unwrapErrorFailWithMessage(err, "initialize detector") } @@ -171,12 +171,12 @@ func (c *createCmd) Exec() error { // Restore if !c.SkipLayers || c.PlatformAPI.AtLeast("0.10") { cmd.DefaultLogger.Phase("RESTORING") - restoreCmd := &restoreCmd{ - Platform: c.Platform, - keychain: c.keychain, + restorer, err := connectedFactory.NewRestorer(c.Inputs(), cmd.DefaultLogger, buildpack.Group{}) + if err != nil { + return unwrapErrorFailWithMessage(err, "initialize restorer") } - if err := restoreCmd.restore(analyzedMD.LayersMetadata, group, cacheStore); err != nil { - return err + if err = restorer.RestoreCache(); err != nil { + return cmd.FailErrCode(err, c.CodeFor(platform.RestoreError), "restore") } } diff --git a/cmd/lifecycle/restorer.go b/cmd/lifecycle/restorer.go index dc2bfc65e..761339820 100644 --- a/cmd/lifecycle/restorer.go +++ b/cmd/lifecycle/restorer.go @@ -3,13 +3,7 @@ package main import ( "errors" "fmt" - "os" - "path/filepath" - "github.com/buildpacks/imgutil" - "github.com/buildpacks/imgutil/layout" - "github.com/buildpacks/imgutil/layout/sparse" - "github.com/buildpacks/imgutil/remote" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" @@ -18,16 +12,12 @@ import ( "github.com/buildpacks/lifecycle/cmd" "github.com/buildpacks/lifecycle/cmd/lifecycle/cli" "github.com/buildpacks/lifecycle/image" - "github.com/buildpacks/lifecycle/internal/encoding" - "github.com/buildpacks/lifecycle/internal/layer" "github.com/buildpacks/lifecycle/phase" "github.com/buildpacks/lifecycle/platform" "github.com/buildpacks/lifecycle/platform/files" "github.com/buildpacks/lifecycle/priv" ) -const kanikoDir = "/kaniko" - type restoreCmd struct { *platform.Platform @@ -95,196 +85,25 @@ func (r *restoreCmd) Privileges() error { } func (r *restoreCmd) Exec() error { - group, err := files.Handler.ReadGroup(r.GroupPath) - if err != nil { - return err - } - if err = verifyBuildpackApis(group); err != nil { - return err - } - - var analyzedMD files.Analyzed - if analyzedMD, err = files.Handler.ReadAnalyzed(r.AnalyzedPath, cmd.DefaultLogger); err == nil { - if r.supportsBuildImageExtension() && r.BuildImageRef != "" { - cmd.DefaultLogger.Debugf("Pulling builder image metadata for %s...", r.BuildImageRef) - remoteBuildImage, err := r.pullSparse(r.BuildImageRef) - if err != nil { - return cmd.FailErr(err, fmt.Sprintf("pull builder image %s", r.BuildImageRef)) - } - digestRef, err := remoteBuildImage.Identifier() - if err != nil { - return cmd.FailErr(err, "get digest reference for builder image") - } - analyzedMD.BuildImage = &files.ImageIdentifier{Reference: digestRef.String()} - cmd.DefaultLogger.Debugf("Adding build image info to analyzed metadata: ") - cmd.DefaultLogger.Debugf(encoding.ToJSONMaybe(analyzedMD.BuildImage)) - } - var ( - runImage imgutil.Image - ) - runImageName := analyzedMD.RunImageImage() // FIXME: if we have a digest reference available in `Reference` (e.g., in the non-daemon case) we should use it - if r.supportsRunImageExtension() && needsPulling(analyzedMD.RunImage) { - cmd.DefaultLogger.Debugf("Pulling run image metadata for %s...", runImageName) - runImage, err = r.pullSparse(runImageName) - if err != nil { - return cmd.FailErr(err, fmt.Sprintf("pull run image %s", runImageName)) - } - // update analyzed metadata, even if we only needed to pull the image metadata, because - // the extender needs a digest reference in analyzed.toml, - // and daemon images will only have a daemon image ID - if err = r.updateAnalyzedMD(&analyzedMD, runImage); err != nil { - return cmd.FailErr(err, "update analyzed metadata") - } - } else if r.needsUpdating(analyzedMD.RunImage, group) { - cmd.DefaultLogger.Debugf("Updating run image info in analyzed metadata...") - h := image.NewHandler(r.docker, r.keychain, r.LayoutDir, r.UseLayout, r.InsecureRegistries) - runImage, err = h.InitImage(runImageName) - if err != nil || !runImage.Found() { - return cmd.FailErr(err, fmt.Sprintf("get run image %s", runImageName)) - } - if err = r.updateAnalyzedMD(&analyzedMD, runImage); err != nil { - return cmd.FailErr(err, "update analyzed metadata") - } - } - if err = files.Handler.WriteAnalyzed(r.AnalyzedPath, &analyzedMD, cmd.DefaultLogger); err != nil { - return cmd.FailErr(err, "write analyzed metadata") - } - } else { - cmd.DefaultLogger.Warnf("Not using analyzed data, usable file not found: %s", err) - } - - cacheStore, err := initCache(r.CacheImageRef, r.CacheDir, r.keychain, r.PlatformAPI.LessThan("0.13")) - if err != nil { - return err - } - return r.restore(analyzedMD.LayersMetadata, group, cacheStore) -} - -func (r *restoreCmd) updateAnalyzedMD(analyzedMD *files.Analyzed, runImage imgutil.Image) error { - if r.PlatformAPI.LessThan("0.10") { - return nil - } - digestRef, err := runImage.Identifier() - if err != nil { - return cmd.FailErr(err, "get digest reference for run image") - } - var targetData *files.TargetMetadata - if r.PlatformAPI.AtLeast("0.12") { - targetData, err = platform.GetTargetMetadata(runImage) - if err != nil { - return cmd.FailErr(err, "read target data from run image") - } - } - cmd.DefaultLogger.Debugf("Run image info in analyzed metadata was: ") - cmd.DefaultLogger.Debugf(encoding.ToJSONMaybe(analyzedMD.RunImage)) - analyzedMD.RunImage.Reference = digestRef.String() - analyzedMD.RunImage.TargetMetadata = targetData - cmd.DefaultLogger.Debugf("Run image info in analyzed metadata is: ") - cmd.DefaultLogger.Debugf(encoding.ToJSONMaybe(analyzedMD.RunImage)) - return nil -} - -func needsPulling(runImage *files.RunImage) bool { - if runImage == nil { - // sanity check to prevent panic, should be unreachable - return false - } - return runImage.Extend -} - -func (r *restoreCmd) needsUpdating(runImage *files.RunImage, group buildpack.Group) bool { - if r.PlatformAPI.LessThan("0.10") { - return false - } - if !group.HasExtensions() { - return false - } - if runImage == nil { - // sanity check to prevent panic, should be unreachable - return false - } - if isPopulated(runImage.TargetMetadata) { - return false - } - return true -} - -func isPopulated(metadata *files.TargetMetadata) bool { - return metadata != nil && metadata.OS != "" -} - -func (r *restoreCmd) supportsBuildImageExtension() bool { - return r.PlatformAPI.AtLeast("0.10") -} - -func (r *restoreCmd) supportsRunImageExtension() bool { - return r.PlatformAPI.AtLeast("0.12") && !r.UseLayout // FIXME: add layout support as part of https://github.com/buildpacks/lifecycle/issues/1102 -} - -func (r *restoreCmd) supportsTargetData() bool { - return r.PlatformAPI.AtLeast("0.12") -} - -func (r *restoreCmd) pullSparse(imageRef string) (imgutil.Image, error) { - baseCacheDir := filepath.Join(kanikoDir, "cache", "base") - if err := os.MkdirAll(baseCacheDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create cache directory: %w", err) - } - - var opts []imgutil.ImageOption - opts = append(opts, append(image.GetInsecureOptions(r.InsecureRegistries), remote.FromBaseImage(imageRef))...) - - // get remote image - remoteImage, err := remote.NewImage(imageRef, r.keychain, opts...) - if err != nil { - return nil, fmt.Errorf("failed to initialize remote image: %w", err) - } - if !remoteImage.Found() { - return nil, fmt.Errorf("failed to get remote image") - } - // check for usable kaniko dir - if _, err := os.Stat(kanikoDir); err != nil { - if !os.IsNotExist(err) { - return nil, fmt.Errorf("failed to read kaniko directory: %w", err) - } - return nil, nil - } - // save to disk - h, err := remoteImage.UnderlyingImage().Digest() - if err != nil { - return nil, fmt.Errorf("failed to get remote image digest: %w", err) - } - path := filepath.Join(baseCacheDir, h.String()) - cmd.DefaultLogger.Debugf("Saving image metadata to %s...", path) - sparseImage, err := sparse.NewImage( - path, - remoteImage.UnderlyingImage(), - layout.WithMediaTypes(imgutil.DefaultTypes), + factory := phase.NewConnectedFactory( + r.PlatformAPI, + &cmd.BuildpackAPIVerifier{}, + NewCacheHandler(r.keychain), + files.Handler, + image.NewHandler(r.docker, r.keychain, r.LayoutDir, r.UseLayout, r.InsecureRegistries), + image.NewRegistryHandler(r.keychain, r.InsecureRegistries), ) + restorer, err := factory.NewRestorer(r.Inputs(), cmd.DefaultLogger, buildpack.Group{}) if err != nil { - return nil, fmt.Errorf("failed to initialize sparse image: %w", err) + return unwrapErrorFailWithMessage(err, "initialize restorer") } - if err = sparseImage.Save(); err != nil { - return nil, fmt.Errorf("failed to save sparse image: %w", err) + if err = restorer.RestoreAnalyzed(); err != nil { + return cmd.FailErrCode(err, r.CodeFor(platform.RestoreError), "restore") } - return remoteImage, nil -} - -func (r *restoreCmd) restore(layerMetadata files.LayersMetadata, group buildpack.Group, cacheStore phase.Cache) error { - restorer := &phase.Restorer{ - LayersDir: r.LayersDir, - Buildpacks: group.Group, - Logger: cmd.DefaultLogger, - PlatformAPI: r.PlatformAPI, - LayerMetadataRestorer: layer.NewDefaultMetadataRestorer(r.LayersDir, r.SkipLayers, cmd.DefaultLogger), - LayersMetadata: layerMetadata, - SBOMRestorer: layer.NewSBOMRestorer(layer.SBOMRestorerOpts{ - LayersDir: r.LayersDir, - Logger: cmd.DefaultLogger, - Nop: r.SkipLayers, - }, r.PlatformAPI), + if err = files.Handler.WriteAnalyzed(r.AnalyzedPath, &restorer.AnalyzedMD, cmd.DefaultLogger); err != nil { + return cmd.FailErr(err, "write analyzed metadata") } - if err := restorer.Restore(cacheStore); err != nil { + if err = restorer.RestoreCache(); err != nil { return cmd.FailErrCode(err, r.CodeFor(platform.RestoreError), "restore") } return nil diff --git a/image/handler.go b/image/handler.go index 2705102ed..811e7a0fb 100644 --- a/image/handler.go +++ b/image/handler.go @@ -14,6 +14,7 @@ import ( //go:generate mockgen -package testmock -destination ../phase/testmock/image_handler.go github.com/buildpacks/lifecycle/image Handler type Handler interface { InitImage(imageRef string) (imgutil.Image, error) + InitRemoteImage(imageRef string) (imgutil.Image, error) Kind() string } @@ -22,15 +23,25 @@ type Handler interface { // - WHEN a docker client is provided then it returns a LocalHandler // - WHEN an auth.Keychain is provided then it returns a RemoteHandler // - Otherwise nil is returned -func NewHandler(docker client.CommonAPIClient, keychain authn.Keychain, layoutDir string, useLayout bool, insecureRegistries []string) Handler { +func NewHandler( + docker client.CommonAPIClient, + keychain authn.Keychain, + layoutDir string, + useLayout bool, + insecureRegistries []string, +) Handler { if layoutDir != "" && useLayout { return &LayoutHandler{ - layoutDir: layoutDir, + layoutDir: layoutDir, + keychain: keychain, + insecureRegistries: insecureRegistries, } } if docker != nil { return &LocalHandler{ - docker: docker, + docker: docker, + keychain: keychain, + insecureRegistries: insecureRegistries, } } if keychain != nil { diff --git a/image/layout_handler.go b/image/layout_handler.go index 820a17a76..1ce672060 100644 --- a/image/layout_handler.go +++ b/image/layout_handler.go @@ -8,13 +8,17 @@ import ( "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/layout" + "github.com/buildpacks/imgutil/remote" + "github.com/google/go-containerregistry/pkg/authn" v1 "github.com/google/go-containerregistry/pkg/v1" ) const LayoutKind = "layout" type LayoutHandler struct { - layoutDir string + layoutDir string + keychain authn.Keychain + insecureRegistries []string } func (h *LayoutHandler) InitImage(imageRef string) (imgutil.Image, error) { @@ -29,6 +33,25 @@ func (h *LayoutHandler) InitImage(imageRef string) (imgutil.Image, error) { return layout.NewImage(path, layout.FromBaseImagePath(path)) } +// InitRemoteImage TODO +func (h *LayoutHandler) InitRemoteImage(imageRef string) (imgutil.Image, error) { + if imageRef == "" { + return nil, nil + } + + options := []imgutil.ImageOption{ + remote.FromBaseImage(imageRef), + } + + options = append(options, GetInsecureOptions(h.insecureRegistries)...) + + return remote.NewImage( + imageRef, + h.keychain, + options..., + ) +} + func (h *LayoutHandler) Kind() string { return LayoutKind } diff --git a/image/local_handler.go b/image/local_handler.go index d3f650d5b..c699a2047 100644 --- a/image/local_handler.go +++ b/image/local_handler.go @@ -3,13 +3,17 @@ package image import ( "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/local" + "github.com/buildpacks/imgutil/remote" "github.com/docker/docker/client" + "github.com/google/go-containerregistry/pkg/authn" ) const LocalKind = "docker" type LocalHandler struct { - docker client.CommonAPIClient + docker client.CommonAPIClient + keychain authn.Keychain + insecureRegistries []string } func (h *LocalHandler) InitImage(imageRef string) (imgutil.Image, error) { @@ -24,6 +28,25 @@ func (h *LocalHandler) InitImage(imageRef string) (imgutil.Image, error) { ) } +// InitRemoteImage TODO +func (h *LocalHandler) InitRemoteImage(imageRef string) (imgutil.Image, error) { + if imageRef == "" { + return nil, nil + } + + options := []imgutil.ImageOption{ + remote.FromBaseImage(imageRef), + } + + options = append(options, GetInsecureOptions(h.insecureRegistries)...) + + return remote.NewImage( + imageRef, + h.keychain, + options..., + ) +} + func (h *LocalHandler) Kind() string { return LocalKind } diff --git a/image/remote_handler.go b/image/remote_handler.go index 752fa7a10..5beea758e 100644 --- a/image/remote_handler.go +++ b/image/remote_handler.go @@ -31,6 +31,11 @@ func (h *RemoteHandler) InitImage(imageRef string) (imgutil.Image, error) { ) } +// InitRemoteImage TODO +func (h *RemoteHandler) InitRemoteImage(imageRef string) (imgutil.Image, error) { + return h.InitImage(imageRef) +} + func (h *RemoteHandler) Kind() string { return RemoteKind } diff --git a/phase/analyzer.go b/phase/analyzer.go index 0217411bf..c211bba4a 100644 --- a/phase/analyzer.go +++ b/phase/analyzer.go @@ -16,32 +16,35 @@ import ( // Analyzer reads metadata from the previous image (if it exists) and the run image, // and additionally restores the SBOM layer from the previous image for use later in the build. type Analyzer struct { + // images PreviousImage imgutil.Image RunImage imgutil.Image - Logger log.Logger - SBOMRestorer layer.SBOMRestorer - PlatformAPI *api.Version + // services + SBOMRestorer layer.SBOMRestorer + // common + Logger log.Logger + PlatformAPI *api.Version } // NewAnalyzer configures a new Analyzer according to the provided Platform API version. func (f *ConnectedFactory) NewAnalyzer(inputs platform.LifecycleInputs, logger log.Logger) (*Analyzer, error) { analyzer := &Analyzer{ - Logger: logger, - SBOMRestorer: &layer.NopSBOMRestorer{}, - PlatformAPI: f.platformAPI, + Logger: logger, + SBOMRestorer: layer.NewSBOMRestorer( + layer.SBOMRestorerOpts{ + LayersDir: inputs.LayersDir, + Nop: inputs.SkipLayers, + Logger: logger, + }, + f.platformAPI, // FIXME: this should probably be inputs.PlatformAPI, although they are the same + ), + PlatformAPI: f.platformAPI, // FIXME: this should probably be inputs.PlatformAPI, although they are the same } if err := f.ensureRegistryAccess(inputs); err != nil { return nil, err } - if f.platformAPI.AtLeast("0.8") && !inputs.SkipLayers { - analyzer.SBOMRestorer = &layer.DefaultSBOMRestorer{ - LayersDir: inputs.LayersDir, - Logger: logger, - } - } - var err error if analyzer.PreviousImage, err = f.getPreviousImage(inputs.PreviousImageRef, inputs.LaunchCacheDir); err != nil { return nil, err diff --git a/phase/connected_factory.go b/phase/connected_factory.go index 3514938a2..8f9de4c6e 100644 --- a/phase/connected_factory.go +++ b/phase/connected_factory.go @@ -6,9 +6,12 @@ import ( "github.com/buildpacks/imgutil" "github.com/buildpacks/lifecycle/api" + "github.com/buildpacks/lifecycle/buildpack" "github.com/buildpacks/lifecycle/cache" "github.com/buildpacks/lifecycle/image" + "github.com/buildpacks/lifecycle/log" "github.com/buildpacks/lifecycle/platform" + "github.com/buildpacks/lifecycle/platform/files" ) // ConnectedFactory is used to construct lifecycle phases that require access to an image repository @@ -58,6 +61,36 @@ func (f *ConnectedFactory) ensureRegistryAccess(inputs platform.LifecycleInputs) return nil } +func (f *ConnectedFactory) getAnalyzed(analyzedPath string, logger log.Logger) (files.Analyzed, error) { + return f.configHandler.ReadAnalyzed(analyzedPath, logger) +} + +func (f *ConnectedFactory) getBuildpacks(groupPath string, withOptionalGroup buildpack.Group, logger log.Logger) ([]buildpack.GroupElement, error) { + group := withOptionalGroup + var err error + if withOptionalGroup.Group == nil { + group, err = f.configHandler.ReadGroup(groupPath) + if err != nil { + return nil, err + } + } + if err = f.verifyGroup(group.Group, logger); err != nil { + return nil, err + } + return group.Group, nil +} + +func (f *ConnectedFactory) getExtensions(groupPath string, logger log.Logger) ([]buildpack.GroupElement, error) { + group, err := f.configHandler.ReadGroup(groupPath) + if err != nil { + return nil, fmt.Errorf("reading group: %w", err) + } + if err = f.verifyGroup(group.GroupExtensions, logger); err != nil { + return nil, err + } + return group.GroupExtensions, nil +} + func (f *ConnectedFactory) getPreviousImage(imageRef string, launchCacheDir string) (imgutil.Image, error) { if imageRef == "" { return nil, nil @@ -86,3 +119,12 @@ func (f *ConnectedFactory) getRunImage(imageRef string) (imgutil.Image, error) { } return runImage, nil } + +func (f *ConnectedFactory) verifyGroup(group []buildpack.GroupElement, logger log.Logger) error { + for _, groupEl := range group { + if err := f.apiVerifier.VerifyBuildpackAPI(groupEl.Kind(), groupEl.String(), groupEl.API, logger); err != nil { + return err + } + } + return nil +} diff --git a/phase/detector.go b/phase/detector.go index 4324fdc67..ab0fb5aba 100644 --- a/phase/detector.go +++ b/phase/detector.go @@ -49,7 +49,7 @@ type Detector struct { PlatformDir string Resolver DetectResolver Runs *sync.Map - AnalyzeMD files.Analyzed + AnalyzeMD files.Analyzed // FIXME: this should be AnalyzedMD PlatformAPI *api.Version // If detect fails, we want to print debug statements as info level. diff --git a/phase/restorer.go b/phase/restorer.go index 48b6da59e..e717493ff 100644 --- a/phase/restorer.go +++ b/phase/restorer.go @@ -1,13 +1,20 @@ package phase import ( + "fmt" + "os" "path/filepath" + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/layout" + "github.com/buildpacks/imgutil/layout/sparse" "github.com/pkg/errors" "golang.org/x/sync/errgroup" "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/buildpack" + "github.com/buildpacks/lifecycle/cmd" + "github.com/buildpacks/lifecycle/internal/encoding" "github.com/buildpacks/lifecycle/internal/layer" "github.com/buildpacks/lifecycle/layers" "github.com/buildpacks/lifecycle/log" @@ -15,22 +22,220 @@ import ( "github.com/buildpacks/lifecycle/platform/files" ) -type Restorer struct { - LayersDir string - Logger log.Logger +const kanikoDir = "/kaniko" - Buildpacks []buildpack.GroupElement +// Restorer TODO +type Restorer struct { + LayersDir string + AnalyzedMD files.Analyzed + LayersMetadata files.LayersMetadata // deprecated, use AnalyzedMD instead + Buildpacks []buildpack.GroupElement + Extensions []buildpack.GroupElement + UseLayout bool + // images + BuilderImage imgutil.Image + RunImage imgutil.Image + // services + Cache Cache LayerMetadataRestorer layer.MetadataRestorer - LayersMetadata files.LayersMetadata - PlatformAPI *api.Version SBOMRestorer layer.SBOMRestorer + // common + Logger log.Logger + PlatformAPI *api.Version } -// Restore restores metadata for launch and cache layers into the layers directory and attempts to restore layer data for cache=true layers, removing the layer when unsuccessful. -// If a usable cache is not provided, Restore will not restore any cache=true layer metadata. -func (r *Restorer) Restore(cache Cache) error { +// NewRestorer configures a new Restorer according to the provided Platform API version. +func (f *ConnectedFactory) NewRestorer(inputs platform.LifecycleInputs, logger log.Logger, withOptionalGroup buildpack.Group) (*Restorer, error) { + cache, err := f.cacheHandler.InitCache( + inputs.CacheImageRef, + inputs.CacheDir, + inputs.PlatformAPI.LessThan("0.13"), + ) + if err != nil { + return nil, err + } + restorer := &Restorer{ + LayersDir: inputs.LayersDir, + UseLayout: inputs.UseLayout, + Cache: cache, + LayerMetadataRestorer: layer.NewDefaultMetadataRestorer( + inputs.LayersDir, + inputs.SkipLayers, + logger, + ), + SBOMRestorer: layer.NewSBOMRestorer( + layer.SBOMRestorerOpts{ + LayersDir: inputs.LayersDir, + Nop: inputs.SkipLayers, + Logger: logger, + }, + inputs.PlatformAPI, + ), + Logger: logger, + PlatformAPI: inputs.PlatformAPI, + } + + if restorer.AnalyzedMD, err = f.getAnalyzed(inputs.AnalyzedPath, logger); err != nil { + return nil, err + } + restorer.LayersMetadata = restorer.AnalyzedMD.LayersMetadata // for backwards compatibility with library callers that might expect LayersMetadata + if restorer.Buildpacks, err = f.getBuildpacks(inputs.GroupPath, withOptionalGroup, logger); err != nil { + return nil, err + } + if restorer.Extensions, err = f.getExtensions(inputs.GroupPath, logger); err != nil { + return nil, err + } + + if restorer.supportsBuildImageExtension() && inputs.BuildImageRef != "" { + restorer.BuilderImage, err = f.imageHandler.InitRemoteImage(inputs.BuildImageRef) + if err != nil || !restorer.BuilderImage.Found() { + return nil, fmt.Errorf("failed to initialize builder image %s", inputs.BuildImageRef) + } + } + if restorer.shouldPullRunImage() { + restorer.RunImage, err = f.imageHandler.InitRemoteImage(restorer.AnalyzedMD.RunImageImage()) // FIXME: if we have a digest reference available in `Reference` (e.g., in the non-daemon case) we should use it) + if err != nil || !restorer.RunImage.Found() { + return nil, fmt.Errorf("failed to initialize run image %s", restorer.AnalyzedMD.RunImageImage()) + } + } else if restorer.shouldUpdateAnalyzed() { + restorer.RunImage, err = f.imageHandler.InitImage(restorer.AnalyzedMD.RunImageImage()) + if err != nil || !restorer.RunImage.Found() { + return nil, fmt.Errorf("failed to initialize run image %s", restorer.AnalyzedMD.RunImageImage()) + } + } + + return restorer, nil +} + +func (r *Restorer) supportsBuildImageExtension() bool { + return r.PlatformAPI.AtLeast("0.10") +} + +// RestoreAnalyzed TODO +func (r *Restorer) RestoreAnalyzed() error { + if r.BuilderImage != nil { + r.Logger.Debugf("Pulling manifest and config for builder image %s...", r.BuilderImage.Name()) + if err := r.pullSparse(r.BuilderImage); err != nil { + return err + } + digestRef, err := r.BuilderImage.Identifier() + if err != nil { + return fmt.Errorf("failed to get digest reference for builder image %s", r.BuilderImage.Name()) + } + r.AnalyzedMD.BuildImage = &files.ImageIdentifier{Reference: digestRef.String()} + r.Logger.Debugf("Adding build image info to analyzed metadata: ") + r.Logger.Debugf(encoding.ToJSONMaybe(r.AnalyzedMD.BuildImage)) + } + if r.RunImage != nil { + if r.shouldPullRunImage() { + r.Logger.Debugf("Pulling manifest and config for run image %s...", r.RunImage.Name()) + if err := r.pullSparse(r.RunImage); err != nil { + return err + } + } + // update analyzed metadata, even if we only needed to pull the image, because + // the extender needs a digest reference in analyzed.toml, + // and daemon images will only have a daemon image ID + if err := r.updateAnalyzedMD(); err != nil { + return cmd.FailErr(err, "update analyzed metadata") + } + } + return nil +} + +func (r *Restorer) shouldPullRunImage() bool { + if r.PlatformAPI.LessThan("0.12") { + return false + } + if r.AnalyzedMD.RunImage == nil { + return false + } + return r.AnalyzedMD.RunImage.Extend +} + +func (r *Restorer) pullSparse(image imgutil.Image) error { + baseCacheDir := filepath.Join(kanikoDir, "cache", "base") + if err := os.MkdirAll(baseCacheDir, 0750); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + // check for usable kaniko dir + if _, err := os.Stat(kanikoDir); err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("failed to read kaniko directory: %w", err) + } + return nil + } + + // save to disk + h, err := image.UnderlyingImage().Digest() + if err != nil { + return fmt.Errorf("failed to get remote image digest: %w", err) + } + path := filepath.Join(baseCacheDir, h.String()) + r.Logger.Debugf("Saving image metadata to %s...", path) + + sparseImage, err := sparse.NewImage( + path, + image.UnderlyingImage(), + layout.WithMediaTypes(imgutil.DefaultTypes), + ) + if err != nil { + return fmt.Errorf("failed to initialize sparse image: %w", err) + } + if err = sparseImage.Save(); err != nil { + return fmt.Errorf("failed to save sparse image: %w", err) + } + return nil +} + +func (r *Restorer) shouldUpdateAnalyzed() bool { + if r.PlatformAPI.LessThan("0.10") { + return false + } + if len(r.Extensions) == 0 { + return false + } + if r.AnalyzedMD.RunImage == nil { + return false + } + return !isPopulated(r.AnalyzedMD.RunImage.TargetMetadata) +} + +func isPopulated(metadata *files.TargetMetadata) bool { + return metadata != nil && metadata.OS != "" +} + +func (r *Restorer) updateAnalyzedMD() error { + if r.PlatformAPI.LessThan("0.10") { + return nil + } + if r.RunImage == nil { + return nil + } + digestRef, err := r.RunImage.Identifier() + if err != nil { + return errors.New("failed to get digest reference for run image") + } + var targetData *files.TargetMetadata + if r.PlatformAPI.AtLeast("0.12") { + targetData, err = platform.GetTargetMetadata(r.RunImage) + if err != nil { + return errors.New("failed to read target data from run image") + } + } + r.Logger.Debugf("Run image info in analyzed metadata was: ") + r.Logger.Debugf(encoding.ToJSONMaybe(r.AnalyzedMD.RunImage)) + r.AnalyzedMD.RunImage.Reference = digestRef.String() + r.AnalyzedMD.RunImage.TargetMetadata = targetData + r.Logger.Debugf("Run image info in analyzed metadata is: ") + r.Logger.Debugf(encoding.ToJSONMaybe(r.AnalyzedMD.RunImage)) + return nil +} + +// RestoreCache TODO +func (r *Restorer) RestoreCache() error { defer log.NewMeasurement("Restorer", r.Logger)() - cacheMeta, err := retrieveCacheMetadata(cache, r.Logger) + cacheMeta, err := retrieveCacheMetadata(r.Cache, r.Logger) if err != nil { return err } @@ -49,7 +254,11 @@ func (r *Restorer) Restore(cache Cache) error { layerSHAStore := layer.NewSHAStore() r.Logger.Debug("Restoring Layer Metadata") - if err := r.LayerMetadataRestorer.Restore(r.Buildpacks, r.LayersMetadata, cacheMeta, layerSHAStore); err != nil { + layersMD := r.AnalyzedMD.LayersMetadata + if len(layersMD.Buildpacks) == 0 { + layersMD = r.LayersMetadata // for backwards compatibility with library callers that do not set AnalyzedMD + } + if err := r.LayerMetadataRestorer.Restore(r.Buildpacks, layersMD, cacheMeta, layerSHAStore); err != nil { return err } @@ -57,11 +266,10 @@ func (r *Restorer) Restore(cache Cache) error { for _, bp := range r.Buildpacks { cachedLayers := cacheMeta.MetadataForBuildpack(bp.ID).Layers - var cachedFn func(buildpack.Layer) bool // At this point in the build, .toml files never contain layer types information // (this information is added by buildpacks during the `build` phase). // The cache metadata is the only way to identify cache=true layers. - cachedFn = func(l buildpack.Layer) bool { + cachedFn := func(l buildpack.Layer) bool { bpLayer, ok := cachedLayers[filepath.Base(l.Path())] return ok && bpLayer.Cache } @@ -98,7 +306,7 @@ func (r *Restorer) Restore(cache Cache) error { } else { r.Logger.Infof("Restoring data for %q from cache", bpLayer.Identifier()) g.Go(func() error { - return r.restoreCacheLayer(cache, cachedLayer.SHA) + return r.restoreCacheLayer(r.Cache, cachedLayer.SHA) }) } } @@ -108,7 +316,7 @@ func (r *Restorer) Restore(cache Cache) error { g.Go(func() error { if cacheMeta.BOM.SHA != "" { r.Logger.Infof("Restoring data for SBOM from cache") - if err := r.SBOMRestorer.RestoreFromCache(cache, cacheMeta.BOM.SHA); err != nil { + if err := r.SBOMRestorer.RestoreFromCache(r.Cache, cacheMeta.BOM.SHA); err != nil { return err } } @@ -123,6 +331,15 @@ func (r *Restorer) Restore(cache Cache) error { return nil } +// Restore restores metadata for launch and cache layers into the layers directory and attempts to restore layer data for cache=true layers, removing the layer when unsuccessful. +// If a usable cache is not provided, Restore will not restore any cache=true layer metadata. +// +// Deprecated: use RestoreCache instead. +func (r *Restorer) Restore(cache Cache) error { + r.Cache = cache + return r.RestoreCache() +} + func (r *Restorer) restoreCacheLayer(cache Cache, sha string) error { // Sanity check to prevent panic. if cache == nil { @@ -133,7 +350,9 @@ func (r *Restorer) restoreCacheLayer(cache Cache, sha string) error { if err != nil { return err } - defer rc.Close() + defer func() { + _ = rc.Close() + }() return layers.Extract(rc, "") } diff --git a/phase/testmock/image_handler.go b/phase/testmock/image_handler.go index 517c0b3c2..dac701627 100644 --- a/phase/testmock/image_handler.go +++ b/phase/testmock/image_handler.go @@ -49,6 +49,21 @@ func (mr *MockHandlerMockRecorder) InitImage(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitImage", reflect.TypeOf((*MockHandler)(nil).InitImage), arg0) } +// InitRemoteImage mocks base method. +func (m *MockHandler) InitRemoteImage(arg0 string) (imgutil.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InitRemoteImage", arg0) + ret0, _ := ret[0].(imgutil.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InitRemoteImage indicates an expected call of InitRemoteImage. +func (mr *MockHandlerMockRecorder) InitRemoteImage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitRemoteImage", reflect.TypeOf((*MockHandler)(nil).InitRemoteImage), arg0) +} + // Kind mocks base method. func (m *MockHandler) Kind() string { m.ctrl.T.Helper()