diff --git a/cmd/lifecycle/cli/flags.go b/cmd/lifecycle/cli/flags.go index d1aa30299..61eb9628c 100644 --- a/cmd/lifecycle/cli/flags.go +++ b/cmd/lifecycle/cli/flags.go @@ -156,6 +156,10 @@ func FlagVersion(showVersion *bool) { flagSet.BoolVar(showVersion, "version", false, "show version") } +func FlagForceRebase(force *bool) { + flagSet.BoolVar(force, "force", *force, "execute rebase even if operation is unsafe") +} + // deprecated func DeprecatedFlagRunImage(deprecatedRunImage *string) { diff --git a/cmd/lifecycle/rebaser.go b/cmd/lifecycle/rebaser.go index a51a08ed8..861da216f 100644 --- a/cmd/lifecycle/rebaser.go +++ b/cmd/lifecycle/rebaser.go @@ -42,6 +42,10 @@ func (r *rebaseCmd) DefineFlags() { if r.PlatformAPI.AtLeast("0.11") { cli.FlagPreviousImage(&r.PreviousImageRef) } + + if r.PlatformAPI.AtLeast("0.12") { + cli.FlagForceRebase(&r.ForceRebase) + } } // Args validates arguments and flags, and fills in default values. @@ -103,6 +107,7 @@ func (r *rebaseCmd) Exec() error { rebaser := &lifecycle.Rebaser{ Logger: cmd.DefaultLogger, PlatformAPI: r.PlatformAPI, + Force: r.ForceRebase, } report, err := rebaser.Rebase(r.appImage, newBaseImage, r.OutputImageRef, r.AdditionalTags) if err != nil { @@ -157,9 +162,20 @@ func (r *rebaseCmd) setAppImage() error { } if r.RunImageRef == "" { + if r.PlatformAPI.AtLeast("0.12") { + r.RunImageRef = md.RunImage.Reference + if r.RunImageRef != "" { + return nil + } + } + + // for backwards compatibility, we need to fallback to the stack metadata + // fail if there is no run image metadata available from either location if md.Stack.RunImage.Image == "" { - return cmd.FailErrCode(errors.New("-run-image is required when there is no stack metadata available"), cmd.CodeForInvalidArgs, "parse arguments") + return cmd.FailErrCode(errors.New("-run-image is required when there is no run image metadata available"), cmd.CodeForInvalidArgs, "parse arguments") } + + // for older platforms, we find the best mirror for the run image as this point r.RunImageRef, err = md.Stack.BestRunImageMirror(registry) if err != nil { return err diff --git a/platform/defaults.go b/platform/defaults.go index d88d089fd..b3c6f8616 100644 --- a/platform/defaults.go +++ b/platform/defaults.go @@ -207,6 +207,12 @@ const ( DefaultProjectMetadataFile = "project-metadata.toml" ) +// The following are configuration options for rebaser. +const ( + // EnvForceRebase is used to force the rebaser to rebase the app image even if the operation is unsafe. + EnvForceRebase = "CNB_FORCE_REBASE" +) + var ( // DefaultLauncherPath is the default location of the launcher executable during the build. // The launcher is exported in the output application image and is used to start application processes at runtime. diff --git a/platform/labels.go b/platform/labels.go index 63de14bf0..97eca5e52 100644 --- a/platform/labels.go +++ b/platform/labels.go @@ -3,7 +3,8 @@ package platform const ( BuildMetadataLabel = "io.buildpacks.build.metadata" LayerMetadataLabel = "io.buildpacks.lifecycle.metadata" + MixinsLabel = "io.buildpacks.stack.mixins" ProjectMetadataLabel = "io.buildpacks.project.metadata" + RebaseableLabel = "io.buildpacks.rebasable" StackIDLabel = "io.buildpacks.stack.id" - MixinsLabel = "io.buildpacks.stack.mixins" ) diff --git a/platform/lifecycle_inputs.go b/platform/lifecycle_inputs.go index 0ec043bb8..d135aceb4 100644 --- a/platform/lifecycle_inputs.go +++ b/platform/lifecycle_inputs.go @@ -47,6 +47,7 @@ type LifecycleInputs struct { StackPath string UID int GID int + ForceRebase bool SkipLayers bool UseDaemon bool UseLayout bool @@ -131,6 +132,9 @@ func NewLifecycleInputs(platformAPI *api.Version) *LifecycleInputs { LauncherPath: DefaultLauncherPath, LauncherSBOMDir: DefaultBuildpacksioSBOMDir, ProjectMetadataPath: envOrDefault(EnvProjectMetadataPath, filepath.Join(PlaceholderLayers, DefaultProjectMetadataFile)), + + // Configuration options for rebasing + ForceRebase: boolEnv(EnvForceRebase), } if platformAPI.LessThan("0.6") { diff --git a/platform/lifecycle_inputs_test.go b/platform/lifecycle_inputs_test.go index 0cba1726f..a1c525758 100644 --- a/platform/lifecycle_inputs_test.go +++ b/platform/lifecycle_inputs_test.go @@ -40,6 +40,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, inputs.DefaultProcessType, "") h.AssertEq(t, inputs.DeprecatedRunImageRef, "") h.AssertEq(t, inputs.ExtensionsDir, platform.DefaultExtensionsDir) + h.AssertEq(t, inputs.ForceRebase, false) h.AssertEq(t, inputs.GID, 0) h.AssertEq(t, inputs.KanikoCacheTTL, platform.DefaultKanikoCacheTTL) h.AssertEq(t, inputs.KanikoDir, "/kaniko") @@ -72,6 +73,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, os.Setenv(platform.EnvCacheImage, "some-cache-image")) h.AssertNil(t, os.Setenv(platform.EnvExtensionsDir, "some-extensions-dir")) h.AssertNil(t, os.Setenv(platform.EnvGID, "5678")) + h.AssertNil(t, os.Setenv(platform.EnvForceRebase, "true")) h.AssertNil(t, os.Setenv(platform.EnvGeneratedDir, "some-generated-dir")) h.AssertNil(t, os.Setenv(platform.EnvGroupPath, "some-group-path")) h.AssertNil(t, os.Setenv(platform.EnvKanikoCacheTTL, "1h0m0s")) @@ -103,6 +105,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, os.Unsetenv(platform.EnvCacheDir)) h.AssertNil(t, os.Unsetenv(platform.EnvCacheImage)) h.AssertNil(t, os.Unsetenv(platform.EnvExtensionsDir)) + h.AssertNil(t, os.Unsetenv(platform.EnvForceRebase)) h.AssertNil(t, os.Unsetenv(platform.EnvGID)) h.AssertNil(t, os.Unsetenv(platform.EnvGeneratedDir)) h.AssertNil(t, os.Unsetenv(platform.EnvGroupPath)) @@ -139,6 +142,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, inputs.DefaultProcessType, "some-process-type") h.AssertEq(t, inputs.DeprecatedRunImageRef, "") h.AssertEq(t, inputs.ExtensionsDir, "some-extensions-dir") + h.AssertEq(t, inputs.ForceRebase, true) h.AssertEq(t, inputs.GID, 5678) h.AssertEq(t, inputs.KanikoCacheTTL, 1*time.Hour) h.AssertEq(t, inputs.KanikoDir, "/kaniko") diff --git a/rebaser.go b/rebaser.go index d89a9ca49..67560b38e 100644 --- a/rebaser.go +++ b/rebaser.go @@ -19,6 +19,7 @@ import ( type Rebaser struct { Logger log.Logger PlatformAPI *api.Version + Force bool } type RebaseReport struct { @@ -53,6 +54,10 @@ func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image, return RebaseReport{}, fmt.Errorf("incompatible stack: '%s' is not compatible with '%s'", newBaseStackID, appStackID) } + if err := r.validateRebaseable(workingImage); err != nil { + return RebaseReport{}, err + } + if err := validateMixins(workingImage, newBaseImage); err != nil { return RebaseReport{}, err } @@ -123,6 +128,19 @@ func validateMixins(appImg, newBaseImg imgutil.Image) error { return nil } +func (r *Rebaser) validateRebaseable(appImg imgutil.Image) error { + if r.PlatformAPI.AtLeast("0.12") { + rebaseable, err := appImg.Label(platform.RebaseableLabel) + if err != nil { + return errors.Wrap(err, "get app image rebaseable label") + } + if !r.Force && rebaseable == "false" { + return fmt.Errorf("app image is not marked as rebaseable") + } + } + return nil +} + func (r *Rebaser) supportsManifestSize() bool { return r.PlatformAPI.AtLeast("0.6") } diff --git a/rebaser_test.go b/rebaser_test.go index bbab40772..7065b6b51 100644 --- a/rebaser_test.go +++ b/rebaser_test.go @@ -250,6 +250,55 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) { }) }) + when("platform API is >= 0.12", func() { + it.Before(func() { + rebaser.PlatformAPI = api.MustParse("0.12") + }) + + when("validating rebasable", func() { + when("rebaseable label is false", func() { + it.Before(func() { + h.AssertNil(t, fakeAppImage.SetLabel(platform.RebaseableLabel, "false")) + }) + + it("returns an error", func() { + _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) + h.AssertError(t, err, "app image is not marked as rebaseable") + }) + + when("force is true", func() { + it("allows rebase", func() { + rebaser.Force = true + _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) + h.AssertNil(t, err) + }) + }) + }) + + when("rebaseable label is not false", func() { + it.Before(func() { + h.AssertNil(t, fakeAppImage.SetLabel(platform.RebaseableLabel, "true")) + }) + + it("allows rebase", func() { + _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) + h.AssertNil(t, err) + }) + }) + + when("rebaseable label is empty", func() { + it.Before(func() { + h.AssertNil(t, fakeAppImage.SetLabel(platform.RebaseableLabel, "")) + }) + + it("allows rebase", func() { + _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) + h.AssertNil(t, err) + }) + }) + }) + }) + when("validating mixins", func() { when("there are no mixin labels", func() { it("allows rebase", func() {