diff --git a/acceptance/exporter_test.go b/acceptance/exporter_test.go index ba57caac8..1d4234eb8 100644 --- a/acceptance/exporter_test.go +++ b/acceptance/exporter_test.go @@ -314,8 +314,11 @@ func testExporterFunc(platformAPI string) func(t *testing.T, when spec.G, it spe ) h.AssertStringContains(t, output, "Saving "+exportedImageName) + // To detect whether the export of cacheImage and exportedImage is successful h.Run(t, exec.Command("docker", "pull", exportedImageName)) assertImageOSAndArchAndCreatedAt(t, exportedImageName, exportTest, imgutil.NormalizedDateTime) + + h.Run(t, exec.Command("docker", "pull", cacheImageName)) }) it("is created with empty layer", func() { diff --git a/cmd/lifecycle/cli/flags.go b/cmd/lifecycle/cli/flags.go index a469a60bd..d384fd9f6 100644 --- a/cmd/lifecycle/cli/flags.go +++ b/cmd/lifecycle/cli/flags.go @@ -108,6 +108,11 @@ func FlagPlanPath(planPath *string) { flagSet.StringVar(planPath, "plan", *planPath, "path to plan.toml") } +// FlagParallelExport parses `parallel` flag +func FlagParallelExport(parallelExport *bool) { + flagSet.BoolVar(parallelExport, "parallel", *parallelExport, "export app image and cache image in parallel") +} + func FlagPlatformDir(platformDir *string) { flagSet.StringVar(platformDir, "platform", *platformDir, "path to platform directory") } diff --git a/cmd/lifecycle/creator.go b/cmd/lifecycle/creator.go index 708525131..57fadff4d 100644 --- a/cmd/lifecycle/creator.go +++ b/cmd/lifecycle/creator.go @@ -48,6 +48,7 @@ func (c *createCmd) DefineFlags() { cli.FlagLauncherPath(&c.LauncherPath) cli.FlagLayersDir(&c.LayersDir) cli.FlagOrderPath(&c.OrderPath) + cli.FlagParallelExport(&c.ParallelExport) cli.FlagPlatformDir(&c.PlatformDir) cli.FlagPreviousImage(&c.PreviousImageRef) cli.FlagProcessType(&c.DefaultProcessType) @@ -75,6 +76,11 @@ func (c *createCmd) Args(nargs int, args []string) error { return err } } + if c.ParallelExport { + if c.CacheImageRef == "" { + cmd.DefaultLogger.Warn("parallel export has been enabled, but it has not taken effect because cache image (-cache-image) has not been specified.") + } + } return nil } diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go index f45d2be8c..131ecec8d 100644 --- a/cmd/lifecycle/exporter.go +++ b/cmd/lifecycle/exporter.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strconv" + "sync" "time" "github.com/BurntSushi/toml" @@ -69,6 +70,7 @@ func (e *exportCmd) DefineFlags() { cli.FlagLaunchCacheDir(&e.LaunchCacheDir) cli.FlagLauncherPath(&e.LauncherPath) cli.FlagLayersDir(&e.LayersDir) + cli.FlagParallelExport(&e.ParallelExport) cli.FlagProcessType(&e.DefaultProcessType) cli.FlagProjectMetadataPath(&e.ProjectMetadataPath) cli.FlagReportPath(&e.ReportPath) @@ -205,31 +207,52 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore lifecycle.Cache, an return err } - report, err := exporter.Export(lifecycle.ExportOptions{ - AdditionalNames: e.AdditionalTags, - AppDir: e.AppDir, - DefaultProcessType: e.DefaultProcessType, - ExtendedDir: e.ExtendedDir, - LauncherConfig: launcherConfig(e.LauncherPath, e.LauncherSBOMDir), - LayersDir: e.LayersDir, - OrigMetadata: analyzedMD.LayersMetadata, - Project: projectMD, - RunImageRef: runImageID, - RunImageForExport: runImageForExport, - WorkingImage: appImage, - }) - if err != nil { - return cmd.FailErrCode(err, e.CodeFor(platform.ExportError), "export") + var exportWaitGroup sync.WaitGroup + var report files.Report + var appErr error + appErr = nil + + exportWaitGroup.Add(1) + go func() { + defer exportWaitGroup.Done() + report, appErr = exporter.Export(lifecycle.ExportOptions{ + AdditionalNames: e.AdditionalTags, + AppDir: e.AppDir, + DefaultProcessType: e.DefaultProcessType, + ExtendedDir: e.ExtendedDir, + LauncherConfig: launcherConfig(e.LauncherPath, e.LauncherSBOMDir), + LayersDir: e.LayersDir, + OrigMetadata: analyzedMD.LayersMetadata, + Project: projectMD, + RunImageRef: runImageID, + RunImageForExport: runImageForExport, + WorkingImage: appImage, + }) + }() + + // waiting here if parallel export is not enabled + if !e.ParallelExport { + exportWaitGroup.Wait() + } + + exportWaitGroup.Add(1) + go func() { + defer exportWaitGroup.Done() + if cacheStore != nil { + if cacheErr := exporter.Cache(e.LayersDir, cacheStore); cacheErr != nil { + cmd.DefaultLogger.Warnf("Failed to export cache: %v\n", cacheErr) + } + } + }() + + exportWaitGroup.Wait() + if appErr != nil { + return cmd.FailErrCode(appErr, e.CodeFor(platform.ExportError), "export") } if err = encoding.WriteTOML(e.ReportPath, &report); err != nil { return cmd.FailErrCode(err, e.CodeFor(platform.ExportError), "write export report") } - if cacheStore != nil { - if cacheErr := exporter.Cache(e.LayersDir, cacheStore); cacheErr != nil { - cmd.DefaultLogger.Warnf("Failed to export cache: %v\n", cacheErr) - } - } return nil } diff --git a/layers/factory.go b/layers/factory.go index d73922518..4d97162e0 100644 --- a/layers/factory.go +++ b/layers/factory.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "strings" + "sync" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -27,7 +28,8 @@ type Factory struct { UID, GID int // UID and GID are used to normalize layer entries Logger log.Logger - tarHashes map[string]string // tarHases Stores hashes of layer tarballs for reuse between the export and cache steps. + // tarHashes stores hashes of layer tarballs for reuse between the export and cache steps. + tarHashes sync.Map } type Layer struct { @@ -39,15 +41,13 @@ type Layer struct { func (f *Factory) writeLayer(id, createdBy string, addEntries func(tw *archive.NormalizingTarWriter) error) (layer Layer, err error) { tarPath := filepath.Join(f.ArtifactsDir, escape(id)+".tar") - if f.tarHashes == nil { - f.tarHashes = make(map[string]string) - } - if sha, ok := f.tarHashes[tarPath]; ok { - f.Logger.Debugf("Reusing tarball for layer %q with SHA: %s\n", id, sha) + if sha, ok := f.tarHashes.Load(tarPath); ok { + shaString := sha.(string) + f.Logger.Debugf("Reusing tarball for layer %q with SHA: %s\n", id, shaString) return Layer{ ID: id, TarPath: tarPath, - Digest: sha, + Digest: shaString, History: v1.History{CreatedBy: createdBy}, }, nil } @@ -69,7 +69,7 @@ func (f *Factory) writeLayer(id, createdBy string, addEntries func(tw *archive.N return Layer{}, err } digest := lw.Digest() - f.tarHashes[tarPath] = digest + f.tarHashes.Store(tarPath, digest) return Layer{ ID: id, Digest: digest, diff --git a/platform/defaults.go b/platform/defaults.go index cfd1d60c9..603a7b191 100644 --- a/platform/defaults.go +++ b/platform/defaults.go @@ -186,6 +186,9 @@ const ( // EnvKanikoCacheTTL is the amount of time to persist layers cached by kaniko during the `extend` phase. EnvKanikoCacheTTL = "CNB_KANIKO_CACHE_TTL" + + // EnvParallelExport is a flag used to instruct the lifecycle to export of application image and cache image in parallel, if true. + EnvParallelExport = "CNB_PARALLEL_EXPORT" ) // DefaultKanikoCacheTTL is the default kaniko cache TTL (2 weeks). diff --git a/platform/lifecycle_inputs.go b/platform/lifecycle_inputs.go index e7cbde28e..dae5234d8 100644 --- a/platform/lifecycle_inputs.go +++ b/platform/lifecycle_inputs.go @@ -54,6 +54,7 @@ type LifecycleInputs struct { UID int GID int ForceRebase bool + ParallelExport bool SkipLayers bool UseDaemon bool UseLayout bool @@ -129,6 +130,7 @@ func NewLifecycleInputs(platformAPI *api.Version) *LifecycleInputs { KanikoDir: "/kaniko", LaunchCacheDir: os.Getenv(EnvLaunchCacheDir), SkipLayers: skipLayers, + ParallelExport: boolEnv(EnvParallelExport), // Images used by the lifecycle during the build @@ -147,6 +149,7 @@ func NewLifecycleInputs(platformAPI *api.Version) *LifecycleInputs { ProjectMetadataPath: envOrDefault(EnvProjectMetadataPath, filepath.Join(PlaceholderLayers, DefaultProjectMetadataFile)), // Configuration options for rebasing + ForceRebase: boolEnv(EnvForceRebase), }