diff --git a/acceptance/analyzer_test.go b/acceptance/analyzer_test.go index be6880286..4c5286f4d 100644 --- a/acceptance/analyzer_test.go +++ b/acceptance/analyzer_test.go @@ -73,6 +73,36 @@ func TestAnalyzer(t *testing.T) { h.AssertNil(t, os.RemoveAll(filepath.Join(targetDockerConfig, "config.json"))) h.RecursiveCopy(t, authRegistry.DockerDirectory, targetDockerConfig) + // build run-images into test registry + runImageContext := filepath.Join("testdata", "analyzer", "run-image") + buildAuthRegistryImage( + t, + "company/stack:bionic", + runImageContext, + "-f", filepath.Join(runImageContext, dockerfileName), + "--build-arg", "stackid=io.buildpacks.stacks.bionic", + ) + buildAuthRegistryImage( + t, + "company/stack:centos", + runImageContext, + "-f", filepath.Join(runImageContext, dockerfileName), + "--build-arg", "stackid=io.company.centos", + ) + + // build run-image into daemon + h.DockerBuild( + t, + "localcompany/stack:bionic", + runImageContext, + h.WithArgs( + "-f", filepath.Join(runImageContext, dockerfileName), + "--build-arg", "stackid=io.buildpacks.stacks.bionic", + ), + ) + + defer h.DockerImageRemove(t, "localcompany/stack:bionic") + // Setup test container h.MakeAndCopyLifecycle(t, daemonOS, analyzerBinaryDir) @@ -81,6 +111,7 @@ func TestAnalyzer(t *testing.T) { analyzeDockerContext, h.WithFlags( "-f", filepath.Join(analyzeDockerContext, dockerfileName), + "--build-arg", "registry="+noAuthRegistry.Host+":"+noAuthRegistry.Port, ), ) defer h.DockerImageRemove(t, analyzeImage) @@ -253,7 +284,10 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe copyDir, ctrPath("/some-dir/some-analyzed.toml"), analyzeImage, - h.WithFlags("--env", "CNB_PLATFORM_API="+platformAPI), + h.WithFlags( + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + ), h.WithArgs(execArgs...), ) @@ -277,6 +311,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe h.WithFlags(append( dockerSocketMount, "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/local-bionic-stack.toml", // /cnb/local-bionic-stack.toml has `io.buildpacks.stacks.bionic` and points to run image `localcompany/stack:bionic` with same stack id )...), h.WithArgs(execArgs...), ) @@ -316,6 +351,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe h.WithFlags(append( dockerSocketMount, "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/local-bionic-stack.toml", // /cnb/local-bionic-stack.toml has `io.buildpacks.stacks.bionic` and points to run image `localcompany/stack:bionic` with same stack id )...), h.WithArgs( ctrPath(analyzerPath), @@ -624,6 +660,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe ctrPath("/layers/analyzed.toml"), analyzeImage, h.WithFlags( + "--network", registryNetwork, "--env", "CNB_PLATFORM_API="+platformAPI, ), h.WithArgs(ctrPath(analyzerPath), "some-image"), @@ -1065,6 +1102,163 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe assertAnalyzedMetadata(t, filepath.Join(copyDir, "some-other-layers", "analyzed.toml")) // analyzed.toml is written at the provided -layers directory: /some-other-layers }) }) + + when("validating stack", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).Compare(api.MustParse("0.7")) < 0, "Platform API < 0.7 does not validate stack") + }) + + when("stack metadata is present", func() { + when("stacks match", func() { + it("passes validation", func() { + execArgs := []string{ctrPath(analyzerPath)} + h.DockerRun(t, + analyzeImage, // /cnb/stack.toml has `io.buildpacks.stacks.bionic` and points to run image `company/stack:bionic` with same stack id + h.WithFlags( + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + ), + h.WithArgs(execArgs...), + ) + }) + }) + + when("CNB_RUN_IMAGE is present", func() { + it("uses CNB_RUN_IMAGE for validation", func() { + execArgs := []string{ctrPath(analyzerPath)} + + h.DockerRun(t, + analyzeImage, + h.WithFlags( + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/mismatch-stack.toml", // /cnb/mismatch-stack.toml points to run image `company/stack:centos` + "--env", "CNB_RUN_IMAGE="+noAuthRegistry.RepoName("company/stack:bionic"), + ), + h.WithArgs(execArgs...), + ) + }) + }) + + when("stack metadata file is invalid", func() { + it("fails validation", func() { + cmd := exec.Command( + "docker", "run", "--rm", + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/bad-stack.toml", + analyzeImage, + ctrPath(analyzerPath), + ) // #nosec G204 + output, err := cmd.CombinedOutput() + + h.AssertNotNil(t, err) + expected := "get stack metadata" + h.AssertStringContains(t, string(output), expected) + }) + }) + + when("run image inaccessible", func() { + it("fails validation", func() { + cmd := exec.Command( + "docker", "run", "--rm", + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_RUN_IMAGE=fake.example.com/company/example:20", + analyzeImage, + ctrPath(analyzerPath), + ) // #nosec G204 + output, err := cmd.CombinedOutput() + + h.AssertNotNil(t, err) + expected := "failed to resolve run image" + h.AssertStringContains(t, string(output), expected) + }) + }) + + when("run image has mirrors", func() { + it("uses expected mirror for run-image", func() { + execArgs := []string{ctrPath(analyzerPath), "--previous-image=" + noAuthRegistry.RepoName("apprepo/myapp")} // previous image located on same registry as mirror + + h.DockerRunAndCopy(t, + containerName, + copyDir, + ctrPath("/layers/analyzed.toml"), + analyzeImage, + h.WithFlags( + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/run-mirror-stack.toml", // /cnb/run-mirror-stack.toml points to run image on gcr.io and mirror on test registry + ), + h.WithArgs(execArgs...), + ) + }) + }) + + when("daemon case", func() { + when("stacks match", func() { + it("passes validation", func() { + execArgs := []string{ctrPath(analyzerPath), "-daemon"} + + h.DockerRunAndCopy(t, + containerName, + copyDir, + ctrPath("/layers/analyzed.toml"), + analyzeImage, + h.WithFlags(append( + dockerSocketMount, + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/local-bionic-stack.toml", // /cnb/local-bionic-stack.toml has `io.buildpacks.stacks.bionic` and points to run image `localcompany/stack:bionic` with same stack id + )...), + h.WithArgs(execArgs...), + ) + }) + }) + }) + }) + + when("stack metadata is not present", func() { + when("CNB_RUN_IMAGE and CNB_STACK_ID are set", func() { + it("passes validation", func() { + execArgs := []string{ctrPath(analyzerPath)} + + h.DockerRunAndCopy(t, + containerName, + copyDir, + ctrPath("/layers/analyzed.toml"), + analyzeImage, + h.WithFlags( + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/file-does-not-exist.toml", + "--env", "CNB_RUN_IMAGE="+noAuthRegistry.RepoName("company/stack:bionic"), + "--env", "CNB_STACK_ID=io.buildpacks.stacks.bionic", + ), + h.WithArgs(execArgs...), + ) + }) + }) + + when("run image and stack id are not provided as arguments or in the environment", func() { + it("fails validation", func() { + cmd := exec.Command( + "docker", "run", "--rm", + "--network", registryNetwork, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_STACK_PATH=/cnb/file-does-not-exist.toml", + analyzeImage, + ctrPath(analyzerPath), + ) // #nosec G204 + output, err := cmd.CombinedOutput() + + h.AssertNotNil(t, err) + expected := "a run image must be specified when there is no stack metadata available" + h.AssertStringContains(t, string(output), expected) + }) + }) + }) + }) } } diff --git a/acceptance/testdata/analyzer/analyze-image/Dockerfile b/acceptance/testdata/analyzer/analyze-image/Dockerfile index 0ebe49555..2f5d3b7ed 100644 --- a/acceptance/testdata/analyzer/analyze-image/Dockerfile +++ b/acceptance/testdata/analyzer/analyze-image/Dockerfile @@ -19,3 +19,44 @@ RUN chown -R $CNB_USER_ID:$CNB_GROUP_ID /layers # ensure docker config directory is root owned and NOT world readable RUN chown -R root /docker-config; chmod -R 700 /docker-config + +ARG registry + +# write some stack.toml files to use in tests +RUN echo "\ +[run-image]\n\ + image = \"${registry}/company/stack:bionic\"\n\ + mirrors = []\n\ +[build-image]\n\ + stack-id = \"io.buildpacks.stacks.bionic\"\n\ + mixins = []\n\ +" > /cnb/stack.toml + +RUN echo "\ +[run-image]\n\ + image = \"${registry}/company/stack:centos\"\n\ + mirrors = []\n\ +[build-image]\n\ + stack-id = \"io.buildpacks.stacks.bionic\"\n\ + mixins = []\n\ +" > /cnb/mismatch-stack.toml + +RUN echo "\ +[run-image]\n\ + image = \"gcr.io/paketobuildpacks/invalidimg:20\"\n\ + mirrors = [\"${registry}/company/stack:bionic\"]\n\ +[build-image]\n\ + stack-id = \"io.buildpacks.stacks.bionic\"\n\ + mixins = []\n\ +" > /cnb/run-mirror-stack.toml + +RUN echo "\ +[run-image]\n\ + image = \"localcompany/stack:bionic\"\n\ + mirrors = []\n\ +[build-image]\n\ + stack-id = \"io.buildpacks.stacks.bionic\"\n\ + mixins = []\n\ +" > /cnb/local-bionic-stack.toml + +RUN echo "[run-images" > /cnb/bad-stack.toml diff --git a/acceptance/testdata/analyzer/analyze-image/Dockerfile.windows b/acceptance/testdata/analyzer/analyze-image/Dockerfile.windows index 44f9d8339..9b749b518 100644 --- a/acceptance/testdata/analyzer/analyze-image/Dockerfile.windows +++ b/acceptance/testdata/analyzer/analyze-image/Dockerfile.windows @@ -10,3 +10,36 @@ ENV CNB_USER_ID=1 ENV CNB_GROUP_ID=1 ENV CNB_PLATFORM_API=${cnb_platform_api} + +ARG registry + +# write some stack.toml files to use in tests +RUN echo [run-image] > /cnb/stack.toml &\ + echo image = "%registry%/company/stack:bionic" >> /cnb/stack.toml &\ + echo mirrors = [] >> /cnb/stack.toml &\ + echo [build-image] >> /cnb/stack.toml &\ + echo stack-id = "io.buildpacks.stacks.bionic" >> /cnb/stack.toml &\ + echo mixins = [] >> /cnb/stack.toml + +RUN echo [run-image] > /cnb/mismatch-stack.toml &\ + echo image = "%registry%/company/stack:centos" >> /cnb/mismatch-stack.toml &\ + echo mirrors = [] >> /cnb/mismatch-stack.toml &\ + echo [build-image] >> /cnb/mismatch-stack.toml &\ + echo stack-id = "io.buildpacks.stacks.bionic" >> /cnb/mismatch-stack.toml &\ + echo mixins = [] >> /cnb/mismatch-stack.toml + +RUN echo [run-image] > /cnb/run-mirror-stack.toml &\ + echo image = "gcr.io/paketobuildpacks/invalidimg:20" >> /cnb/run-mirror-stack.toml &\ + echo mirrors = ["%registry%/company/stack:bionic"] >> /cnb/run-mirror-stack.toml &\ + echo [build-image] >> /cnb/run-mirror-stack.toml &\ + echo stack-id = "io.buildpacks.stacks.bionic" >> /cnb/run-mirror-stack.toml &\ + echo mixins = [] >> /cnb/run-mirror-stack.toml + +RUN echo [run-image] > /cnb/local-bionic-stack.toml &\ + echo image = "localcompany/stack:bionic" >> /cnb/local-bionic-stack.toml &\ + echo mirrors = [] >> /cnb/local-bionic-stack.toml &\ + echo [build-image] >> /cnb/local-bionic-stack.toml &\ + echo stack-id = "io.buildpacks.stacks.bionic" >> /cnb/local-bionic-stack.toml &\ + echo mixins = [] >> /cnb/local-bionic-stack.toml + +RUN echo [run-images > /cnb/bad-stack.toml diff --git a/acceptance/testdata/analyzer/run-image/Dockerfile b/acceptance/testdata/analyzer/run-image/Dockerfile new file mode 100644 index 000000000..074bd54ed --- /dev/null +++ b/acceptance/testdata/analyzer/run-image/Dockerfile @@ -0,0 +1,4 @@ +FROM scratch + +ARG stackid +LABEL io.buildpacks.stack.id=${stackid} diff --git a/acceptance/testdata/analyzer/run-image/Dockerfile.windows b/acceptance/testdata/analyzer/run-image/Dockerfile.windows new file mode 100644 index 000000000..a12552e59 --- /dev/null +++ b/acceptance/testdata/analyzer/run-image/Dockerfile.windows @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/windows/nanoserver:1809 +USER ContainerAdministrator + +ARG stackid +LABEL io.buildpacks.stack.id=${stackid} diff --git a/cmd/flags.go b/cmd/flags.go index 3b55b5e94..7af0925b2 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -62,6 +62,7 @@ const ( EnvSkipLayers = "CNB_ANALYZE_SKIP_LAYERS" // defaults to false EnvSkipRestore = "CNB_SKIP_RESTORE" // defaults to false EnvStackPath = "CNB_STACK_PATH" + EnvStackID = "CNB_STACK_ID" EnvUID = "CNB_USER_ID" EnvUseDaemon = "CNB_USE_DAEMON" // defaults to false ) diff --git a/cmd/lifecycle/analyzer.go b/cmd/lifecycle/analyzer.go index 52934dda4..e42cf950f 100644 --- a/cmd/lifecycle/analyzer.go +++ b/cmd/lifecycle/analyzer.go @@ -2,7 +2,9 @@ package main import ( "fmt" + "os" + "github.com/BurntSushi/toml" "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" @@ -159,6 +161,10 @@ func (a *analyzeCmd) Exec() error { } } + if err := a.validateStack(); err != nil { + return cmd.FailErr(err, "validate stack") + } + analyzedMD, err := a.analyze() if err != nil { return err @@ -207,6 +213,51 @@ func (aa analyzeArgs) analyze() (platform.AnalyzedMetadata, error) { return analyzedMD, nil } +func (a *analyzeCmd) validateStack() error { + if !a.supportsStackValidation() { + return nil + } + + var stackMD platform.StackMetadata + if _, err := toml.DecodeFile(a.stackPath, &stackMD); err != nil && !os.IsNotExist(err) { + return cmd.FailErr(err, "get stack metadata") + } + + runImage, err := a.getRunImage(stackMD) + if err != nil { + return cmd.FailErr(err, "resolve run image") + } + + return lifecycle.ValidateStack(stackMD, runImage) +} + +func (a *analyzeCmd) getRunImage(stackMD platform.StackMetadata) (imgutil.Image, error) { + if a.runImageRef == "" { + runImageRef, err := lifecycle.ResolveRunImage(stackMD, a.imageName) + if err != nil { + return nil, err + } + a.runImageRef = runImageRef + } + + var runImage imgutil.Image + var err error + if a.useDaemon { + runImage, err = local.NewImage( + a.runImageRef, + a.docker, + local.FromBaseImage(a.runImageRef), + ) + } else { + runImage, err = remote.NewImage( + a.runImageRef, + a.keychain, + remote.FromBaseImage(a.runImageRef), + ) + } + return runImage, err +} + func (a *analyzeCmd) registryImages() []string { var registryImages []string if a.platform06.cacheImageTag != "" { @@ -222,6 +273,10 @@ func (a *analyzeCmd) restoresLayerMetadata() bool { return !a.platformAPIVersionGreaterThan06() } +func (a *analyzeCmd) supportsStackValidation() bool { + return a.platformAPIVersionGreaterThan06() +} + func (a *analyzeCmd) platformAPIVersionGreaterThan06() bool { return api.MustParse(a.platform.API()).Compare(api.MustParse("0.7")) >= 0 } diff --git a/platform/files.go b/platform/files.go index 75b3bcdf7..f628933e9 100644 --- a/platform/files.go +++ b/platform/files.go @@ -205,7 +205,8 @@ type ImageReport struct { // stack.toml type StackMetadata struct { - RunImage StackRunImageMetadata `json:"runImage" toml:"run-image"` + RunImage StackRunImageMetadata `json:"runImage" toml:"run-image"` + BuildImage StackBuildImageMetadata `json:"buildImage" toml:"build-image"` } type StackRunImageMetadata struct { @@ -213,6 +214,11 @@ type StackRunImageMetadata struct { Mirrors []string `toml:"mirrors" json:"mirrors,omitempty"` } +type StackBuildImageMetadata struct { + StackID string `toml:"stack-id" json:"stack-id"` + Mixins []string `toml:"mixins" json:"mixins,omitempty"` +} + func (sm *StackMetadata) BestRunImageMirror(registry string) (string, error) { if sm.RunImage.Image == "" { return "", errors.New("missing run-image metadata") diff --git a/stack_validation.go b/stack_validation.go new file mode 100644 index 000000000..58fb89fc3 --- /dev/null +++ b/stack_validation.go @@ -0,0 +1,51 @@ +package lifecycle + +import ( + "fmt" + "os" + + "github.com/buildpacks/imgutil" + "github.com/pkg/errors" + + "github.com/buildpacks/lifecycle/cmd" + "github.com/buildpacks/lifecycle/platform" +) + +func ValidateStack(stackMD platform.StackMetadata, runImage imgutil.Image) error { + buildStackID, err := getBuildStack(stackMD) + if err != nil { + return err + } + + runStackID, err := getRunStack(runImage) + if err != nil { + return err + } + + if buildStackID != runStackID { + return errors.New(fmt.Sprintf("incompatible stack: '%s' is not compatible with '%s'", runStackID, buildStackID)) + } + return nil +} + +func getRunStack(runImage imgutil.Image) (string, error) { + runStackID, err := runImage.Label(platform.StackIDLabel) + if err != nil { + return "", errors.Wrap(err, "get run image label") + } + if runStackID == "" { + return "", errors.New("get run image label: io.buildpacks.stack.id") + } + return runStackID, nil +} + +func getBuildStack(stackMD platform.StackMetadata) (string, error) { + var buildStackID string + if buildStackID = os.Getenv(cmd.EnvStackID); buildStackID != "" { + return buildStackID, nil + } + if buildStackID = stackMD.BuildImage.StackID; buildStackID != "" { + return buildStackID, nil + } + return "", errors.New("CNB_STACK_ID is required when there is no stack metadata available") +} diff --git a/stack_validation_test.go b/stack_validation_test.go new file mode 100644 index 000000000..77bbdca35 --- /dev/null +++ b/stack_validation_test.go @@ -0,0 +1,82 @@ +package lifecycle_test + +import ( + "os" + "testing" + + "github.com/buildpacks/imgutil/fakes" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/lifecycle" + "github.com/buildpacks/lifecycle/platform" + h "github.com/buildpacks/lifecycle/testhelpers" +) + +func TestStackValidation(t *testing.T) { + spec.Run(t, "StackValidation", testStackValidation, spec.Report(report.Terminal{})) +} + +func testStackValidation(t *testing.T, when spec.G, it spec.S) { + when("ValidateStack", func() { + when("build and run stack ids match", func() { + it("should not err", func() { + md := platform.StackMetadata{BuildImage: platform.StackBuildImageMetadata{StackID: "my-stack"}} + runImage := fakes.NewImage("runimg", "", nil) + h.AssertNil(t, runImage.SetLabel(platform.StackIDLabel, "my-stack")) + err := lifecycle.ValidateStack(md, runImage) + h.AssertNil(t, err) + }) + }) + + when("build and run stack ids do not match", func() { + it("should fail", func() { + md := platform.StackMetadata{BuildImage: platform.StackBuildImageMetadata{StackID: "my-stack"}} + runImage := fakes.NewImage("runimg", "", nil) + h.AssertNil(t, runImage.SetLabel(platform.StackIDLabel, "my-other-stack")) + err := lifecycle.ValidateStack(md, runImage) + h.AssertNotNil(t, err) + h.AssertError(t, err, "incompatible stack: 'my-other-stack' is not compatible with 'my-stack'") + }) + }) + + when("run image is missing io.buildpacks.stack.id label", func() { + it("should fail", func() { + md := platform.StackMetadata{BuildImage: platform.StackBuildImageMetadata{StackID: "my-stack"}} + runImage := fakes.NewImage("runimg", "", nil) + err := lifecycle.ValidateStack(md, runImage) + h.AssertNotNil(t, err) + h.AssertError(t, err, "get run image label: io.buildpacks.stack.id") + }) + }) + + when("CNB_STACK_ID is present", func() { + it.Before(func() { + os.Setenv("CNB_STACK_ID", "my-stack") + }) + + it.After(func() { + h.AssertNil(t, os.Unsetenv("CNB_STACK_ID")) + }) + + it("prefers that value", func() { + md := platform.StackMetadata{BuildImage: platform.StackBuildImageMetadata{StackID: "my-other-stack"}} + runImage := fakes.NewImage("runimg", "", nil) + h.AssertNil(t, runImage.SetLabel(platform.StackIDLabel, "my-stack")) + err := lifecycle.ValidateStack(md, runImage) + h.AssertNil(t, err) + }) + }) + + when("no build stack is present", func() { + it("should fail", func() { + md := platform.StackMetadata{BuildImage: platform.StackBuildImageMetadata{StackID: ""}} + runImage := fakes.NewImage("runimg", "", nil) + h.AssertNil(t, runImage.SetLabel(platform.StackIDLabel, "my-stack")) + err := lifecycle.ValidateStack(md, runImage) + h.AssertNotNil(t, err) + h.AssertError(t, err, "CNB_STACK_ID is required when there is no stack metadata available") + }) + }) + }) +} diff --git a/utils.go b/utils.go index 0f18ffaf2..4e8954340 100644 --- a/utils.go +++ b/utils.go @@ -8,9 +8,11 @@ import ( "github.com/BurntSushi/toml" "github.com/buildpacks/imgutil" + "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "github.com/buildpacks/lifecycle/buildpack" + "github.com/buildpacks/lifecycle/platform" ) func WriteTOML(path string, data interface{}) error { @@ -64,6 +66,30 @@ func DecodeLabel(image imgutil.Image, label string, v interface{}) error { return nil } +func ResolveRunImage(stackMD platform.StackMetadata, destinationImageRef string) (string, error) { + runImageRef := stackMD.RunImage.Image + + if runImageRef == "" { + return "", errors.New("a run image must be specified when there is no stack metadata available") + } + + if destinationImageRef != "" && len(stackMD.RunImage.Mirrors) > 0 { + ref, err := name.ParseReference(destinationImageRef, name.WeakValidation) + if err != nil { + return "", err + } + + registry := ref.Context().RegistryStr() + + runImageRef, err = stackMD.BestRunImageMirror(registry) + if err != nil { + return "", err + } + } + + return runImageRef, nil +} + func removeStagePrefixes(mixins []string) []string { var result []string for _, m := range mixins { diff --git a/utils_test.go b/utils_test.go index 55ac108b6..cd1668700 100644 --- a/utils_test.go +++ b/utils_test.go @@ -12,6 +12,7 @@ import ( "github.com/buildpacks/lifecycle" "github.com/buildpacks/lifecycle/buildpack" + "github.com/buildpacks/lifecycle/platform" h "github.com/buildpacks/lifecycle/testhelpers" ) @@ -145,4 +146,38 @@ func testUtils(t *testing.T, when spec.G, it spec.S) { } }) }) + + when("ResolveRunImage", func() { + when("there are no mirrors", func() { + it("should return run-image", func() { + md := platform.StackMetadata{RunImage: platform.StackRunImageMetadata{Image: "company/run:focal"}} + dstImage := "someregistry/whatever" + res, err := lifecycle.ResolveRunImage(md, dstImage) + h.AssertNil(t, err) + h.AssertEq(t, res, md.RunImage.Image) + }) + }) + + when("there are mirrors", func() { + it("should return a run-image from the mirror matching the destination image", func() { + md := platform.StackMetadata{RunImage: platform.StackRunImageMetadata{ + Image: "company/run:focal", + Mirrors: []string{"some.registry/_/run:focal"}, + }} + dstImage := "some.registry/app/web" + res, err := lifecycle.ResolveRunImage(md, dstImage) + h.AssertNil(t, err) + h.AssertEq(t, res, "some.registry/_/run:focal") + }) + }) + + when("there is no run image defined", func() { + it("should fail", func() { + md := platform.StackMetadata{} + dstImage := "someregistry/whatever" + _, err := lifecycle.ResolveRunImage(md, dstImage) + h.AssertError(t, err, "a run image must be specified when there is no stack metadata available") + }) + }) + }) }