diff --git a/acceptance/analyzer_test.go b/acceptance/analyzer_test.go index e66a6d9a9..3038fbcff 100644 --- a/acceptance/analyzer_test.go +++ b/acceptance/analyzer_test.go @@ -178,8 +178,8 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) }) - when("group path is provided", func() { - it("uses the provided group path", func() { + when("called with group (on older platforms)", func() { + it("uses the provided group.toml path", func() { h.SkipIf(t, api.MustParse(platformAPI).AtLeast("0.7"), "Platform API >= 0.7 does not accept a -group flag") h.DockerSeedRunAndCopy(t, @@ -228,8 +228,8 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) }) - when("analyzed path is provided", func() { - it("uses the provided analyzed path", func() { + when("called with analyzed", func() { + it("uses the provided analyzed.toml path", func() { analyzeFlags := []string{"-analyzed", ctrPath("/some-dir/some-analyzed.toml")} if api.MustParse(platformAPI).AtLeast("0.7") { analyzeFlags = append(analyzeFlags, "-run-image", analyzeRegFixtures.ReadOnlyRunImage) @@ -256,6 +256,27 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) }) + when("called with run", func() { + it("uses the provided run.toml path", func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "Platform API < 0.12 does not accept -run") + cmd := exec.Command( + "docker", "run", "--rm", + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_REGISTRY_AUTH="+analyzeRegAuthConfig, + "--network", analyzeRegNetwork, + analyzeImage, + ctrPath(analyzerPath), + "-run", "/cnb/run.toml", + analyzeRegFixtures.SomeAppImage, + ) // #nosec G204 + output, err := cmd.CombinedOutput() + + h.AssertNotNil(t, err) + expected := "ensure registry read access to some-run-image-from-run-toml" + h.AssertStringContains(t, string(output), expected) + }) + }) + it("drops privileges", func() { h.SkipIf(t, runtime.GOOS == "windows", "Not relevant on Windows") @@ -357,7 +378,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) when("app image exists", func() { - it("does not restore app metadata", func() { + it("does not restore app metadata to the layers directory", func() { h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "Platform API < 0.7 restores app metadata") analyzeFlags := []string{"-daemon", "-run-image", "some-run-image"} @@ -381,7 +402,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe assertNoRestoreOfAppMetadata(t, copyDir, output) }) - it("restores app metadata", func() { + it("restores app metadata to the layers directory (on older platforms)", func() { h.SkipIf(t, api.MustParse(platformAPI).AtLeast("0.7"), "Platform API >= 0.7 does not restore app metadata") output := h.DockerRunAndCopy(t, containerName, @@ -428,7 +449,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) }) - when("cache is provided", func() { + when("cache is provided (on older platforms)", func() { when("cache image case", func() { when("cache image is in a daemon", func() { it("ignores the cache", func() { @@ -830,7 +851,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) }) - when("cache is provided", func() { + when("cache is provided (on older platforms)", func() { when("cache image case", func() { when("auth registry", func() { when("registry creds are provided in CNB_REGISTRY_AUTH", func() { diff --git a/acceptance/creator_test.go b/acceptance/creator_test.go index 40b98966c..4d67635ef 100644 --- a/acceptance/creator_test.go +++ b/acceptance/creator_test.go @@ -56,6 +56,27 @@ func testCreatorFunc(platformAPI string) func(t *testing.T, when spec.G, it spec return func(t *testing.T, when spec.G, it spec.S) { var createdImageName string + when("called with run", func() { + it("uses the provided run.toml path", func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "Platform API < 0.12 does not accept -run") + cmd := exec.Command( + "docker", "run", "--rm", + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_REGISTRY_AUTH="+createRegAuthConfig, + "--network", createRegNetwork, + createImage, + ctrPath(creatorPath), + "-run", "/cnb/run.toml", + createRegFixtures.SomeAppImage, + ) // #nosec G204 + output, err := cmd.CombinedOutput() + + h.AssertNotNil(t, err) + expected := "ensure registry read access to some-run-image-from-run-toml" + h.AssertStringContains(t, string(output), expected) + }) + }) + when("daemon case", func() { it.After(func() { h.DockerImageRemove(t, createdImageName) diff --git a/acceptance/testdata/analyzer/container/cnb/run.toml b/acceptance/testdata/analyzer/container/cnb/run.toml new file mode 100644 index 000000000..0bdc66f05 --- /dev/null +++ b/acceptance/testdata/analyzer/container/cnb/run.toml @@ -0,0 +1,5 @@ +[[image]] + image = "some-run-image-from-run-toml" + +[[image]] + image = "some-other-run-image" diff --git a/acceptance/testdata/creator/container/cnb/run.toml b/acceptance/testdata/creator/container/cnb/run.toml new file mode 100644 index 000000000..0bdc66f05 --- /dev/null +++ b/acceptance/testdata/creator/container/cnb/run.toml @@ -0,0 +1,5 @@ +[[image]] + image = "some-run-image-from-run-toml" + +[[image]] + image = "some-other-run-image" diff --git a/api/apis.go b/api/apis.go index b0a8ba82a..91c22d805 100644 --- a/api/apis.go +++ b/api/apis.go @@ -8,7 +8,7 @@ import ( ) var ( - Platform = newApisMustParse([]string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10", "0.11"}, []string{"0.3", "0.4", "0.5", "0.6"}) + Platform = newApisMustParse([]string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10", "0.11", "0.12"}, []string{"0.3", "0.4", "0.5", "0.6"}) Buildpack = newApisMustParse([]string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9"}, []string{"0.2", "0.3", "0.4", "0.5", "0.6"}) ) diff --git a/cmd/lifecycle/analyzer.go b/cmd/lifecycle/analyzer.go index 0d4df0bf8..82aa57985 100644 --- a/cmd/lifecycle/analyzer.go +++ b/cmd/lifecycle/analyzer.go @@ -25,20 +25,14 @@ type analyzeCmd struct { // DefineFlags defines the flags that are considered valid and reads their values (if provided). func (a *analyzeCmd) DefineFlags() { - switch { - case a.PlatformAPI.AtLeast("0.9"): - cli.FlagAnalyzedPath(&a.AnalyzedPath) - cli.FlagCacheImage(&a.CacheImageRef) - cli.FlagGID(&a.GID) + if a.PlatformAPI.AtLeast("0.12") { + cli.FlagRunPath(&a.RunPath) + } + if a.PlatformAPI.AtLeast("0.9") { cli.FlagLaunchCacheDir(&a.LaunchCacheDir) - cli.FlagLayersDir(&a.LayersDir) - cli.FlagPreviousImage(&a.PreviousImageRef) - cli.FlagRunImage(&a.RunImageRef) cli.FlagSkipLayers(&a.SkipLayers) - cli.FlagStackPath(&a.StackPath) - cli.FlagTags(&a.AdditionalTags) - cli.FlagUID(&a.UID) - cli.FlagUseDaemon(&a.UseDaemon) + } + switch { case a.PlatformAPI.AtLeast("0.7"): cli.FlagAnalyzedPath(&a.AnalyzedPath) cli.FlagCacheImage(&a.CacheImageRef) diff --git a/cmd/lifecycle/cli/flags.go b/cmd/lifecycle/cli/flags.go index 8c0dd6e78..b7349b1b2 100644 --- a/cmd/lifecycle/cli/flags.go +++ b/cmd/lifecycle/cli/flags.go @@ -116,6 +116,10 @@ func FlagRunImage(runImage *string) { flagSet.StringVar(runImage, "run-image", *runImage, "reference to run image") } +func FlagRunPath(runPath *string) { + flagSet.StringVar(runPath, "run", *runPath, "path to run.toml") +} + func FlagSkipLayers(skipLayers *bool) { flagSet.BoolVar(skipLayers, "skip-layers", *skipLayers, "do not provide layer metadata to buildpacks") } diff --git a/cmd/lifecycle/creator.go b/cmd/lifecycle/creator.go index 508e1f667..cd70ea3b2 100644 --- a/cmd/lifecycle/creator.go +++ b/cmd/lifecycle/creator.go @@ -27,6 +27,9 @@ type createCmd struct { // DefineFlags defines the flags that are considered valid and reads their values (if provided). func (c *createCmd) DefineFlags() { + if c.PlatformAPI.AtLeast("0.12") { + cli.FlagRunPath(&c.RunPath) + } if c.PlatformAPI.AtLeast("0.11") { cli.FlagBuildConfigDir(&c.BuildConfigDir) cli.FlagLauncherSBOMDir(&c.LauncherSBOMDir) diff --git a/exporter_test.go b/exporter_test.go index 5a422884e..1e6ee37d8 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -395,7 +395,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { it("saves run image metadata to the resulting image", func() { opts.Stack = platform.StackMetadata{ - RunImage: platform.StackRunImageMetadata{ + RunImage: platform.RunImageMetadata{ Image: "some/run", Mirrors: []string{"registry.example.com/some/run", "other.example.com/some/run"}, }, diff --git a/platform/analyze_inputs.go b/platform/analyze_inputs.go index 2dc8e579d..58dc08586 100644 --- a/platform/analyze_inputs.go +++ b/platform/analyze_inputs.go @@ -13,10 +13,12 @@ import ( func DefaultAnalyzeInputs(platformAPI *api.Version) LifecycleInputs { var inputs LifecycleInputs switch { - case platformAPI.AtLeast("0.9"): + case platformAPI.AtLeast("0.12"): inputs = defaultAnalyzeInputs() + case platformAPI.AtLeast("0.9"): + inputs = defaultAnalyzeInputs09To011() case platformAPI.AtLeast("0.7"): - inputs = defaultAnalyzeInputs07() + inputs = defaultAnalyzeInputs07To08() case platformAPI.AtLeast("0.5"): inputs = defaultAnalyzeInputs05To06() default: @@ -27,12 +29,18 @@ func DefaultAnalyzeInputs(platformAPI *api.Version) LifecycleInputs { } func defaultAnalyzeInputs() LifecycleInputs { - ai := defaultAnalyzeInputs07() + ai := defaultAnalyzeInputs09To011() + ai.RunPath = envOrDefault(EnvRunPath, DefaultRunPath) + return ai +} + +func defaultAnalyzeInputs09To011() LifecycleInputs { + ai := defaultAnalyzeInputs07To08() ai.LaunchCacheDir = os.Getenv(EnvLaunchCacheDir) return ai } -func defaultAnalyzeInputs07() LifecycleInputs { +func defaultAnalyzeInputs07To08() LifecycleInputs { ai := defaultAnalyzeInputs05To06() ai.AdditionalTags = str.Slice{} ai.CacheDir = "" // removed @@ -72,5 +80,8 @@ func FillAnalyzeImages(i *LifecycleInputs, logger log.Logger) error { if i.PlatformAPI.LessThan("0.7") { return nil } - return fillRunImageFromStackTOMLIfNeeded(i, logger) + if i.PlatformAPI.LessThan("0.12") { + return fillRunImageFromStackTOMLIfNeeded(i, logger) + } + return fillRunImageFromRunTOMLIfNeeded(i, logger) } diff --git a/platform/analyze_inputs_test.go b/platform/analyze_inputs_test.go index aab19c255..3bcb2a828 100644 --- a/platform/analyze_inputs_test.go +++ b/platform/analyze_inputs_test.go @@ -10,6 +10,7 @@ import ( "github.com/sclevine/spec/report" "github.com/buildpacks/lifecycle/api" + "github.com/buildpacks/lifecycle/internal/path" "github.com/buildpacks/lifecycle/internal/str" llog "github.com/buildpacks/lifecycle/log" "github.com/buildpacks/lifecycle/platform" @@ -38,8 +39,51 @@ func testAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G, it sp }) when("latest Platform API(s)", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "") + inputs.RunImageRef = "some-run-image" // satisfy validation + }) + + when("run image", func() { + when("not provided", func() { + it.Before(func() { + inputs.RunImageRef = "" + }) + + it("falls back to run.toml", func() { + inputs.RunPath = filepath.Join("testdata", "cnb", "run.toml") + err := platform.ResolveInputs(platform.Analyze, &inputs, logger) + h.AssertNil(t, err) + h.AssertEq(t, inputs.RunImageRef, "some-run-image") + }) + + when("run.toml", func() { + when("not provided", func() { + it("defaults to /cnb/run.toml", func() { + _ = platform.ResolveInputs(platform.Analyze, &inputs, logger) + h.AssertEq(t, inputs.RunPath, filepath.Join(path.RootDir, "cnb", "run.toml")) + }) + }) + + when("not exists", func() { + it("errors", func() { + inputs.RunImageRef = "" + inputs.RunPath = "not-exist-run.toml" + err := platform.ResolveInputs(platform.Analyze, &inputs, logger) + h.AssertNotNil(t, err) + expected := "-run-image is required when there is no run metadata available" + h.AssertStringContains(t, err.Error(), expected) + }) + }) + }) + }) + }) + }) + + when("Platform API 0.7 to 0.11", func() { it.Before(func() { h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "") + h.SkipIf(t, api.MustParse(platformAPI).AtLeast("0.12"), "") inputs.RunImageRef = "some-run-image" // satisfy validation }) @@ -53,18 +97,34 @@ func testAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G, it sp h.AssertEq(t, inputs.RunImageRef, "some-run-image") }) - when("stack.toml not present", func() { - it("errors", func() { - inputs.RunImageRef = "" - inputs.StackPath = "not-exist-stack.toml" - err := platform.ResolveInputs(platform.Analyze, &inputs, logger) - h.AssertNotNil(t, err) - expected := "-run-image is required when there is no stack metadata available" - h.AssertStringContains(t, err.Error(), expected) + when("stack.toml", func() { + when("not provided", func() { + it("defaults to /cnb/stack.toml", func() { + _ = platform.ResolveInputs(platform.Analyze, &inputs, logger) + h.AssertEq(t, inputs.StackPath, filepath.Join(path.RootDir, "cnb", "stack.toml")) + }) + }) + + when("not exists", func() { + it("errors", func() { + inputs.RunImageRef = "" + inputs.StackPath = "not-exist-stack.toml" + err := platform.ResolveInputs(platform.Analyze, &inputs, logger) + h.AssertNotNil(t, err) + expected := "-run-image is required when there is no stack metadata available" + h.AssertStringContains(t, err.Error(), expected) + }) }) }) }) }) + }) + + when("Platform API >= 0.7", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "") + inputs.RunImageRef = "some-run-image" // satisfy validation + }) when("provided destination tags are on different registries", func() { it("errors", func() { @@ -79,15 +139,6 @@ func testAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G, it sp h.AssertStringContains(t, err.Error(), expected) }) }) - - when("layers directory is provided", func() { - it("writes analyzed.toml at the layers directory", func() { - inputs.LayersDir = "some-layers-dir" - err := platform.ResolveInputs(platform.Analyze, &inputs, logger) - h.AssertNil(t, err) - h.AssertEq(t, inputs.AnalyzedPath, filepath.Join("some-layers-dir", "analyzed.toml")) - }) - }) }) when("Platform API < 0.7", func() { @@ -119,13 +170,12 @@ func testAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G, it sp }) }) - when("Platform API 0.5 to 0.6", func() { + when("Platform API >= 0.5", func() { it.Before(func() { - h.SkipIf( - t, - !(api.MustParse(platformAPI).Equal(api.MustParse("0.5")) || api.MustParse(platformAPI).Equal(api.MustParse("0.6"))), - "", - ) + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.5"), "") + if api.MustParse(platformAPI).AtLeast("0.7") { + inputs.RunImageRef = "some-run-image" // satisfy validation + } }) when("layers directory is provided", func() { diff --git a/platform/create_inputs.go b/platform/create_inputs.go index 684a120f7..4c8d61fc5 100644 --- a/platform/create_inputs.go +++ b/platform/create_inputs.go @@ -14,8 +14,10 @@ import ( func DefaultCreateInputs(platformAPI *api.Version) LifecycleInputs { var inputs LifecycleInputs switch { - case platformAPI.AtLeast("0.11"): + case platformAPI.AtLeast("0.12"): inputs = defaultCreateInputs() + case platformAPI.AtLeast("0.11"): + inputs = defaultCreateInputs011() case platformAPI.AtLeast("0.6"): inputs = defaultCreateInputs06To010() case platformAPI.AtLeast("0.5"): @@ -28,6 +30,12 @@ func DefaultCreateInputs(platformAPI *api.Version) LifecycleInputs { } func defaultCreateInputs() LifecycleInputs { + ci := defaultCreateInputs011() + ci.RunPath = envOrDefault(EnvRunPath, DefaultRunPath) + return ci +} + +func defaultCreateInputs011() LifecycleInputs { ci := defaultCreateInputs06To010() ci.BuildConfigDir = envOrDefault(EnvBuildConfigDir, DefaultBuildConfigDir) ci.LauncherSBOMDir = DefaultBuildpacksioSBOMDir @@ -84,7 +92,9 @@ func FillCreateImages(i *LifecycleInputs, logger log.Logger) error { case i.DeprecatedRunImageRef != "": i.RunImageRef = i.DeprecatedRunImageRef return nil - default: + case i.PlatformAPI.LessThan("0.12"): return fillRunImageFromStackTOMLIfNeeded(i, logger) + default: + return fillRunImageFromRunTOMLIfNeeded(i, logger) } } diff --git a/platform/create_inputs_test.go b/platform/create_inputs_test.go new file mode 100644 index 000000000..d4e2e6db1 --- /dev/null +++ b/platform/create_inputs_test.go @@ -0,0 +1,203 @@ +package platform_test + +import ( + "path/filepath" + "testing" + + "github.com/apex/log" + "github.com/apex/log/handlers/memory" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/lifecycle/api" + "github.com/buildpacks/lifecycle/internal/path" + "github.com/buildpacks/lifecycle/internal/str" + llog "github.com/buildpacks/lifecycle/log" + "github.com/buildpacks/lifecycle/platform" + h "github.com/buildpacks/lifecycle/testhelpers" +) + +func TestCreateInputs(t *testing.T) { + for _, api := range api.Platform.Supported { + spec.Run(t, "unit-create-inputs/"+api.String(), testCreateInputs(api.String()), spec.Parallel(), spec.Report(report.Terminal{})) + } +} + +func testCreateInputs(platformAPI string) func(t *testing.T, when spec.G, it spec.S) { + return func(t *testing.T, when spec.G, it spec.S) { + var ( + inputs platform.LifecycleInputs + logHandler *memory.Handler + logger llog.Logger + ) + + it.Before(func() { + inputs = platform.DefaultAnalyzeInputs(api.MustParse(platformAPI)) + inputs.OutputImageRef = "some-output-image" // satisfy validation + inputs.RunImageRef = "some-run-image" // satisfy validation + logHandler = memory.New() + logger = &log.Logger{Handler: logHandler} + }) + + when("latest Platform API(s)", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "") + }) + + when("run image", func() { + when("not provided", func() { + it.Before(func() { + inputs.RunImageRef = "" + }) + + it("falls back to run.toml", func() { + inputs.RunPath = filepath.Join("testdata", "cnb", "run.toml") + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNil(t, err) + h.AssertEq(t, inputs.RunImageRef, "some-run-image") + }) + + when("run.toml", func() { + when("not provided", func() { + it("defaults to /cnb/run.toml", func() { + _ = platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertEq(t, inputs.RunPath, filepath.Join(path.RootDir, "cnb", "run.toml")) + }) + }) + + when("not exists", func() { + it("errors", func() { + inputs.RunImageRef = "" + inputs.RunPath = "not-exist-run.toml" + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNotNil(t, err) + expected := "-run-image is required when there is no run metadata available" + h.AssertStringContains(t, err.Error(), expected) + }) + }) + }) + }) + }) + }) + + when("Platform API 0.7 to 0.11", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "") + h.SkipIf(t, api.MustParse(platformAPI).AtLeast("0.12"), "") + }) + + when("run image", func() { + when("not provided", func() { + it("falls back to stack.toml", func() { + inputs.RunImageRef = "" + inputs.StackPath = filepath.Join("testdata", "layers", "stack.toml") + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNil(t, err) + h.AssertEq(t, inputs.RunImageRef, "some-run-image") + }) + + when("stack.toml", func() { + when("not provided", func() { + it("defaults to /cnb/stack.toml", func() { + _ = platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertEq(t, inputs.StackPath, filepath.Join(path.RootDir, "cnb", "stack.toml")) + }) + }) + + when("not exists", func() { + it("errors", func() { + inputs.RunImageRef = "" + inputs.StackPath = "not-exist-stack.toml" + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNotNil(t, err) + expected := "-run-image is required when there is no stack metadata available" + h.AssertStringContains(t, err.Error(), expected) + }) + }) + }) + }) + }) + }) + + when("Platform API >= 0.7", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "") + }) + + when("provided destination tags are on different registries", func() { + it("errors", func() { + inputs.AdditionalTags = str.Slice{ + "some-registry.io/some-namespace/some-image:tag", + "some-other-registry.io/some-namespace/some-image", + } + inputs.OutputImageRef = "some-registry.io/some-namespace/some-image" + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNotNil(t, err) + expected := "writing to multiple registries is unsupported" + h.AssertStringContains(t, err.Error(), expected) + }) + }) + }) + + when("Platform API < 0.7", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).AtLeast("0.7"), "") + }) + + when("cache image tag and cache directory are both blank", func() { + it("warns", func() { + inputs.CacheImageRef = "" + inputs.CacheDir = "" + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNil(t, err) + expected := "No cached data will be used, no cache specified." + h.AssertLogEntry(t, logHandler, expected) + }) + }) + + when("run image", func() { + when("not provided", func() { + it("does not warn", func() { + inputs.StackPath = "not-exist-stack.toml" + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNil(t, err) + h.AssertNoLogEntry(t, logHandler, `no stack metadata found at path ''`) + h.AssertNoLogEntry(t, logHandler, `Previous image with name "" not found`) + }) + }) + }) + }) + + when("Platform API >= 0.5", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.5"), "") + }) + + when("layers directory is provided", func() { + it("uses group.toml at the layers directory and writes analyzed.toml at the layers directory", func() { + inputs.LayersDir = "some-layers-dir" + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNil(t, err) + h.AssertEq(t, inputs.GroupPath, filepath.Join("some-layers-dir", "group.toml")) + h.AssertEq(t, inputs.AnalyzedPath, filepath.Join("some-layers-dir", "analyzed.toml")) + }) + }) + }) + + when("Platform API < 0.5", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).AtLeast("0.5"), "") + }) + + when("layers directory is provided", func() { + it("uses group.toml at the working directory and writes analyzed.toml at the working directory", func() { + inputs.LayersDir = filepath.Join("testdata", "other-layers") + err := platform.ResolveInputs(platform.Create, &inputs, logger) + h.AssertNil(t, err) + h.AssertEq(t, inputs.GroupPath, filepath.Join(".", "group.toml")) + h.AssertEq(t, inputs.AnalyzedPath, filepath.Join(".", "analyzed.toml")) + }) + }) + }) + } +} diff --git a/platform/defaults.go b/platform/defaults.go index 3a48dc8de..6bf629221 100644 --- a/platform/defaults.go +++ b/platform/defaults.go @@ -77,7 +77,9 @@ const ( EnvOrderPath = "CNB_ORDER_PATH" DefaultOrderFile = "order.toml" - // EnvStackPath is the location of the stack file, which contains information about the runtime base image. + // EnvRunPath is the location of the run file, which contains information about the runtime base image. + EnvRunPath = "CNB_RUN_PATH" + // EnvStackPath is the location of the (deprecated) stack file, which contains information about the runtime base image. EnvStackPath = "CNB_STACK_PATH" ) @@ -89,6 +91,8 @@ var ( // DefaultOrderPath is the default order path. DefaultOrderPath = filepath.Join(path.RootDir, "cnb", "order.toml") + // DefaultRunPath is the default run path. + DefaultRunPath = filepath.Join(path.RootDir, "cnb", "run.toml") // DefaultStackPath is the default stack path. DefaultStackPath = filepath.Join(path.RootDir, "cnb", "stack.toml") ) diff --git a/platform/files.go b/platform/files.go index 736d275a6..98a9ca26c 100644 --- a/platform/files.go +++ b/platform/files.go @@ -45,28 +45,28 @@ func ReadAnalyzed(analyzedPath string, logger log.Logger) (AnalyzedMetadata, err // NOTE: This struct MUST be kept in sync with `LayersMetadataCompat` type LayersMetadata struct { - App []LayerMetadata `json:"app" toml:"app"` - BOM *LayerMetadata `json:"sbom,omitempty" toml:"sbom,omitempty"` - Buildpacks []buildpack.LayersMetadata `json:"buildpacks" toml:"buildpacks"` - Config LayerMetadata `json:"config" toml:"config"` - Launcher LayerMetadata `json:"launcher" toml:"launcher"` - ProcessTypes LayerMetadata `json:"process-types" toml:"process-types"` - RunImage RunImageMetadata `json:"runImage" toml:"run-image"` - Stack StackMetadata `json:"stack" toml:"stack"` + App []LayerMetadata `json:"app" toml:"app"` + BOM *LayerMetadata `json:"sbom,omitempty" toml:"sbom,omitempty"` + Buildpacks []buildpack.LayersMetadata `json:"buildpacks" toml:"buildpacks"` + Config LayerMetadata `json:"config" toml:"config"` + Launcher LayerMetadata `json:"launcher" toml:"launcher"` + ProcessTypes LayerMetadata `json:"process-types" toml:"process-types"` + RunImage PreviousImageRunImageMetadata `json:"runImage" toml:"run-image"` + Stack StackMetadata `json:"stack" toml:"stack"` } // NOTE: This struct MUST be kept in sync with `LayersMetadata`. // It exists for situations where the `App` field type cannot be // guaranteed, yet the original struct data must be maintained. type LayersMetadataCompat struct { - App interface{} `json:"app" toml:"app"` - BOM *LayerMetadata `json:"sbom,omitempty" toml:"sbom,omitempty"` - Buildpacks []buildpack.LayersMetadata `json:"buildpacks" toml:"buildpacks"` - Config LayerMetadata `json:"config" toml:"config"` - Launcher LayerMetadata `json:"launcher" toml:"launcher"` - ProcessTypes LayerMetadata `json:"process-types" toml:"process-types"` - RunImage RunImageMetadata `json:"runImage" toml:"run-image"` - Stack StackMetadata `json:"stack" toml:"stack"` + App interface{} `json:"app" toml:"app"` + BOM *LayerMetadata `json:"sbom,omitempty" toml:"sbom,omitempty"` + Buildpacks []buildpack.LayersMetadata `json:"buildpacks" toml:"buildpacks"` + Config LayerMetadata `json:"config" toml:"config"` + Launcher LayerMetadata `json:"launcher" toml:"launcher"` + ProcessTypes LayerMetadata `json:"process-types" toml:"process-types"` + RunImage PreviousImageRunImageMetadata `json:"runImage" toml:"run-image"` + Stack StackMetadata `json:"stack" toml:"stack"` } func (m *LayersMetadata) MetadataForBuildpack(id string) buildpack.LayersMetadata { @@ -82,7 +82,7 @@ type LayerMetadata struct { SHA string `json:"sha" toml:"sha"` } -type RunImageMetadata struct { +type PreviousImageRunImageMetadata struct { TopLayer string `json:"topLayer" toml:"top-layer"` Reference string `json:"reference" toml:"reference"` } @@ -250,23 +250,41 @@ type ImageReport struct { ManifestSize int64 `toml:"manifest-size,omitzero"` } +// run.toml + +type RunMetadata struct { + Images []RunImageMetadata `json:"-" toml:"image"` +} + +func ReadRun(runPath string, logger log.Logger) (RunMetadata, error) { + var runMD RunMetadata + if _, err := toml.DecodeFile(runPath, &runMD); err != nil { + if os.IsNotExist(err) { + logger.Infof("no run metadata found at path '%s'\n", runPath) + return RunMetadata{}, nil + } + return RunMetadata{}, err + } + return runMD, nil +} + // stack.toml type StackMetadata struct { - RunImage StackRunImageMetadata `json:"runImage" toml:"run-image"` + RunImage RunImageMetadata `json:"runImage" toml:"run-image"` } -type StackRunImageMetadata struct { +type RunImageMetadata struct { Image string `toml:"image" json:"image"` Mirrors []string `toml:"mirrors" json:"mirrors,omitempty"` } -func (sm *StackMetadata) BestRunImageMirror(registry string) (string, error) { - if sm.RunImage.Image == "" { +func (rm *RunImageMetadata) BestRunImageMirror(registry string) (string, error) { + if rm.Image == "" { return "", errors.New("missing run-image metadata") } - runImageMirrors := []string{sm.RunImage.Image} - runImageMirrors = append(runImageMirrors, sm.RunImage.Mirrors...) + runImageMirrors := []string{rm.Image} + runImageMirrors = append(runImageMirrors, rm.Mirrors...) runImageRef, err := byRegistry(registry, runImageMirrors) if err != nil { return "", errors.Wrap(err, "failed to find run image") @@ -274,6 +292,10 @@ func (sm *StackMetadata) BestRunImageMirror(registry string) (string, error) { return runImageRef, nil } +func (sm *StackMetadata) BestRunImageMirror(registry string) (string, error) { + return sm.RunImage.BestRunImageMirror(registry) +} + func byRegistry(reg string, imgs []string) (string, error) { if len(imgs) < 1 { return "", errors.New("no images provided to search") diff --git a/platform/files_test.go b/platform/files_test.go index 41de545c0..68dd0bd4f 100644 --- a/platform/files_test.go +++ b/platform/files_test.go @@ -99,7 +99,7 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { var stackMD *platform.StackMetadata it.Before(func() { - stackMD = &platform.StackMetadata{RunImage: platform.StackRunImageMetadata{ + stackMD = &platform.StackMetadata{RunImage: platform.RunImageMetadata{ Image: "first.com/org/repo", Mirrors: []string{ "myorg/myrepo", diff --git a/platform/lifecycle_inputs.go b/platform/lifecycle_inputs.go index 762c0c977..7f8200eaf 100644 --- a/platform/lifecycle_inputs.go +++ b/platform/lifecycle_inputs.go @@ -45,6 +45,7 @@ type LifecycleInputs struct { ProjectMetadataPath string ReportPath string RunImageRef string + RunPath string StackPath string UID int GID int @@ -102,6 +103,7 @@ func notIn(list []string, str string) bool { var ( ErrOutputImageRequired = "image argument is required" ErrRunImageRequiredWhenNoStackMD = "-run-image is required when there is no stack metadata available" + ErrRunImageRequiredWhenNoRunMD = "-run-image is required when there is no run metadata available" ErrSupplyOnlyOneRunImage = "supply only one of -run-image or (deprecated) -image" ErrRunImageUnsupported = "-run-image is unsupported" ErrImageUnsupported = "-image is unsupported" @@ -185,8 +187,33 @@ func CheckLaunchCache(i *LifecycleInputs, logger log.Logger) error { return nil } -// fillRunImageFromStackTOMLIfNeeded updates the provided lifecycle inputs to include the run image from stack.toml if it is missing. -// When there are multiple run images in stack.toml, the run image with registry matching the output image is selected. +// fillRunImageFromRunTOMLIfNeeded updates the provided lifecycle inputs to include the run image from run.toml if the run image input it is missing. +// When there are multiple images in run.toml, the first image is selected. +// When there are registry mirrors for the selected image, the image with registry matching the output image is selected. +func fillRunImageFromRunTOMLIfNeeded(i *LifecycleInputs, logger log.Logger) error { + if i.RunImageRef != "" { + return nil + } + targetRegistry, err := parseRegistry(i.OutputImageRef) + if err != nil { + return err + } + runMD, err := ReadRun(i.RunPath, logger) + if err != nil { + return err + } + if len(runMD.Images) == 0 { + return errors.New(ErrRunImageRequiredWhenNoRunMD) + } + i.RunImageRef, err = runMD.Images[0].BestRunImageMirror(targetRegistry) + if err != nil { + return errors.New(ErrRunImageRequiredWhenNoRunMD) + } + return nil +} + +// fillRunImageFromStackTOMLIfNeeded updates the provided lifecycle inputs to include the run image from stack.toml if the run image input it is missing. +// When there are registry mirrors in stack.toml, the image with registry matching the output image is selected. func fillRunImageFromStackTOMLIfNeeded(i *LifecycleInputs, logger log.Logger) error { if i.RunImageRef != "" { return nil diff --git a/platform/testdata/cnb/run.toml b/platform/testdata/cnb/run.toml new file mode 100644 index 000000000..f5b4d0e9e --- /dev/null +++ b/platform/testdata/cnb/run.toml @@ -0,0 +1,3 @@ +[[image]] + image = "some-run-image" + mirrors = ["some-run-image-mirror", "some-other-run-image-mirror"]