diff --git a/acceptance/detector_test.go b/acceptance/detector_test.go index e15f9c047..8ab7609cc 100644 --- a/acceptance/detector_test.go +++ b/acceptance/detector_test.go @@ -396,7 +396,7 @@ fail: fail_detect_buildpack@some_version var analyzed files.Analyzed _, err = toml.DecodeFile(foundAnalyzedTOML, &analyzed) h.AssertNil(t, err) - h.AssertEq(t, analyzed.RunImage.Reference, "some-run-image-from-extension") + h.AssertEq(t, analyzed.RunImage.Image, "some-run-image-from-extension") }) }) diff --git a/acceptance/testdata/restorer/container/layers/some-extend-false-analyzed.toml.placeholder b/acceptance/testdata/restorer/container/layers/some-extend-false-analyzed.toml.placeholder index 3713c97ae..9569caabc 100644 --- a/acceptance/testdata/restorer/container/layers/some-extend-false-analyzed.toml.placeholder +++ b/acceptance/testdata/restorer/container/layers/some-extend-false-analyzed.toml.placeholder @@ -1,3 +1,3 @@ [run-image] - reference = "REPLACE" + reference = "" image = "REPLACE" diff --git a/acceptance/testdata/restorer/container/layers/some-extend-true-analyzed.toml.placeholder b/acceptance/testdata/restorer/container/layers/some-extend-true-analyzed.toml.placeholder index 7ca5eea9a..123af333d 100644 --- a/acceptance/testdata/restorer/container/layers/some-extend-true-analyzed.toml.placeholder +++ b/acceptance/testdata/restorer/container/layers/some-extend-true-analyzed.toml.placeholder @@ -1,4 +1,4 @@ [run-image] - reference = "REPLACE" + reference = "" extend = true image = "REPLACE" diff --git a/analyzer.go b/analyzer.go index aaedd19d8..0206aa562 100644 --- a/analyzer.go +++ b/analyzer.go @@ -242,8 +242,14 @@ func (a *Analyzer) Analyze() (files.Analyzed, error) { } return files.Analyzed{ - PreviousImage: &files.ImageIdentifier{Reference: previousImageRef}, - RunImage: &files.RunImage{Reference: runImageRef, TargetMetadata: atm, Image: runImageName}, + PreviousImage: &files.ImageIdentifier{ + Reference: previousImageRef, + }, + RunImage: &files.RunImage{ + Reference: runImageRef, // the image identifier, e.g. "s0m3d1g3st" (the image identifier) when exporting to a daemon, or "some.registry/some-repo@sha256:s0m3d1g3st" when exporting to a registry + TargetMetadata: atm, + Image: runImageName, // the provided tag, e.g., "some.registry/some-repo:some-tag" if supported by the platform + }, LayersMetadata: appMeta, }, nil } diff --git a/buildpack/dockerfile.go b/buildpack/dockerfile.go index 2a967f914..4f191d72a 100644 --- a/buildpack/dockerfile.go +++ b/buildpack/dockerfile.go @@ -150,10 +150,8 @@ func ValidateRunDockerfile(dInfo *DockerfileInfo, logger log.Logger) error { if stage.BaseName != baseImageArgRef { newBase = stage.BaseName } - for idx, command := range stage.Commands { - if idx > 0 { - extend = true - } + for _, command := range stage.Commands { + extend = true found := false for _, rc := range recommendedCommands { if rc == strings.ToUpper(command.Name()) { diff --git a/buildpack/dockerfile_test.go b/buildpack/dockerfile_test.go index 1b3af915e..cc467a147 100644 --- a/buildpack/dockerfile_test.go +++ b/buildpack/dockerfile_test.go @@ -194,16 +194,19 @@ COPY --from=0 /some-source.txt ./some-dest.txt when("run", func() { when("valid", func() { - it("succeeds", func() { + it("succeeds and sets extend to true in the result", func() { for i, content := range validCases { dockerfileName := fmt.Sprintf("Dockerfile%d", i) dockerfilePath := filepath.Join(tmpDir, dockerfileName) h.AssertNil(t, os.WriteFile(dockerfilePath, []byte(content), 0600)) - err := buildpack.ValidateRunDockerfile(&buildpack.DockerfileInfo{Path: dockerfilePath}, logger) + dInfo := &buildpack.DockerfileInfo{Path: dockerfilePath} + err := buildpack.ValidateRunDockerfile(dInfo, logger) if err != nil { t.Fatalf("Error validating Dockerfile %d: %s", i, err) } h.AssertEq(t, len(logHandler.Entries), 0) + h.AssertEq(t, dInfo.Extend, true) + h.AssertEq(t, dInfo.WithBase, "") } }) @@ -218,22 +221,25 @@ FROM ${base_image} h.AssertNil(t, os.WriteFile(dockerfilePath, []byte(preamble+tc.dockerfileContent), 0600)) logHandler = memory.New() logger = &log.Logger{Handler: logHandler} - err := buildpack.ValidateRunDockerfile(&buildpack.DockerfileInfo{Path: dockerfilePath}, logger) + dInfo := &buildpack.DockerfileInfo{Path: dockerfilePath} + err := buildpack.ValidateRunDockerfile(dInfo, logger) h.AssertNil(t, err) assertLogEntry(t, logHandler, "run.Dockerfile "+tc.expectedWarning) + h.AssertEq(t, dInfo.Extend, true) + h.AssertEq(t, dInfo.WithBase, "") } }) }) when("switching the runtime base image", func() { - it("returns the new base image", func() { + it("sets the new base image in the result", func() { dockerfilePath := filepath.Join(tmpDir, "run.Dockerfile") h.AssertNil(t, os.WriteFile(dockerfilePath, []byte(`FROM some-base-image`), 0600)) dInfo := &buildpack.DockerfileInfo{Path: dockerfilePath} err := buildpack.ValidateRunDockerfile(dInfo, logger) h.AssertNil(t, err) - h.AssertEq(t, dInfo.WithBase, "some-base-image") h.AssertEq(t, dInfo.Extend, false) + h.AssertEq(t, dInfo.WithBase, "some-base-image") }) }) }) diff --git a/cmd/lifecycle/analyzer.go b/cmd/lifecycle/analyzer.go index 5fbc2e367..adba0b7ee 100644 --- a/cmd/lifecycle/analyzer.go +++ b/cmd/lifecycle/analyzer.go @@ -131,6 +131,8 @@ func (a *analyzeCmd) Exec() error { if err != nil { return cmd.FailErrCode(err, a.CodeFor(platform.AnalyzeError), "analyze") } + cmd.DefaultLogger.Debugf("Run image info in analyzed metadata is: ") + cmd.DefaultLogger.Debugf(encoding.ToJSONMaybe(analyzedMD.RunImage)) if err = encoding.WriteTOML(a.AnalyzedPath, analyzedMD); err != nil { return cmd.FailErr(err, "write analyzed") } diff --git a/cmd/lifecycle/detector.go b/cmd/lifecycle/detector.go index a375ae5be..2fdff6528 100644 --- a/cmd/lifecycle/detector.go +++ b/cmd/lifecycle/detector.go @@ -182,6 +182,8 @@ func (d *detectCmd) writeDetectData(group buildpack.Group, plan files.Plan) erro // writeGenerateData re-outputs the analyzedMD that we read previously, but now we've added the RunImage, if a custom runImage was configured func (d *detectCmd) writeGenerateData(analyzedMD files.Analyzed) error { + cmd.DefaultLogger.Debugf("Run image info in analyzed metadata is: ") + cmd.DefaultLogger.Debugf(encoding.ToJSONMaybe(analyzedMD.RunImage)) if err := encoding.WriteTOML(d.AnalyzedPath, analyzedMD); err != nil { return cmd.FailErr(err, "write analyzed metadata") } diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go index d540b0e8f..0c7125a6d 100644 --- a/cmd/lifecycle/exporter.go +++ b/cmd/lifecycle/exporter.go @@ -234,16 +234,6 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore lifecycle.Cache, an } func (e *exportCmd) initDaemonAppImage(analyzedMD files.Analyzed) (imgutil.Image, string, error) { - if isDigestRef(e.RunImageRef) { - // If extensions were used to extend the runtime base image, the run image reference will contain a digest. - // The restorer uses a name reference to pull the image from the registry (because the extender needs a manifest), - // and writes a digest reference to analyzed.toml. - // For remote images, this works perfectly well. - // However for local images, the daemon can't find the image when the reference contains a digest, - // so we use image name from analyzed.toml which is the reference written by the extension. - e.RunImageRef = analyzedMD.RunImageImage() - } - var opts = []local.ImageOption{ local.FromBaseImage(e.RunImageRef), } @@ -295,14 +285,6 @@ func (e *exportCmd) initDaemonAppImage(analyzedMD files.Analyzed) (imgutil.Image return appImage, runImageID.String(), nil } -func isDigestRef(ref string) bool { - digest, err := name.NewDigest(ref) - if err != nil { - return false - } - return digest.DigestStr() != "" -} - func toContainerConfig(v1C *v1.Config) *container.Config { return &container.Config{ ArgsEscaped: v1C.ArgsEscaped, @@ -364,6 +346,7 @@ func (e *exportCmd) initRemoteAppImage(analyzedMD files.Analyzed) (imgutil.Image return nil, "", cmd.FailErr(err, "get extended image config") } if extendedConfig != nil { + cmd.DefaultLogger.Debugf("Using config from extensions...") opts = append(opts, remote.WithConfig(extendedConfig)) } } diff --git a/cmd/lifecycle/restorer.go b/cmd/lifecycle/restorer.go index 9c916089b..b74fd4fa4 100644 --- a/cmd/lifecycle/restorer.go +++ b/cmd/lifecycle/restorer.go @@ -11,7 +11,6 @@ import ( "github.com/buildpacks/imgutil/layout/sparse" "github.com/buildpacks/imgutil/remote" "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" "github.com/buildpacks/lifecycle" "github.com/buildpacks/lifecycle/auth" @@ -85,60 +84,43 @@ func (r *restoreCmd) Exec() error { ) if analyzedMD, err = files.ReadAnalyzed(r.AnalyzedPath, cmd.DefaultLogger); err == nil { if r.supportsBuildImageExtension() && r.BuildImageRef != "" { - cmd.DefaultLogger.Debugf("Pulling builder image metadata...") - buildImage, err := r.pullSparse(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, "read builder image") + return cmd.FailErr(err, "pull builder image") } - digestRef, err := digestReference(r.BuildImageRef, buildImage) + digestRef, err := remoteBuildImage.Identifier() if err != nil { return cmd.FailErr(err, "get digest reference for builder image") } - analyzedMD.BuildImage = &files.ImageIdentifier{Reference: digestRef} + 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 ( + remoteRunImage 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...") - runImageRef := analyzedMD.RunImageImage() - if runImageRef == "" { - runImageRef = analyzedMD.RunImage.Reference // older platforms don't populate Image - } - runImage, err := r.pullSparse(runImageRef) - if err != nil { - return cmd.FailErr(err, "read run image") - } - targetData, err := platform.GetTargetMetadata(runImage) + cmd.DefaultLogger.Debugf("Pulling run image metadata for %s...", runImageName) + remoteRunImage, err = r.pullSparse(runImageName) if err != nil { - return cmd.FailErr(err, "read target data from run image") + return cmd.FailErr(err, "pull run image") } - digestRef, err := digestReference(runImageRef, runImage) - if err != nil { - return cmd.FailErr(err, "get digest reference for builder image") - } - analyzedMD.RunImage = &files.RunImage{ - Reference: digestRef, - Image: analyzedMD.RunImageImage(), - Extend: true, - TargetMetadata: targetData, + // 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 = updateAnalyzedMD(&analyzedMD, remoteRunImage); err != nil { + return cmd.FailErr(err, "update analyzed metadata") } } else if r.supportsTargetData() && needsUpdating(analyzedMD.RunImage) { - cmd.DefaultLogger.Debugf("Updating analyzed metadata...") - runImage, err := remote.NewImage(analyzedMD.RunImage.Reference, r.keychain) - if err != nil { - return cmd.FailErr(err, "read run image") - } - targetData, err := platform.GetTargetMetadata(runImage) - if err != nil { - return cmd.FailErr(err, "read target data from run image") + cmd.DefaultLogger.Debugf("Updating run image info in analyzed metadata...") + remoteRunImage, err = remote.NewImage(runImageName, r.keychain) + if err != nil || !remoteRunImage.Found() { + return cmd.FailErr(err, "pull run image") } - digestRef, err := digestReference(analyzedMD.RunImage.Reference, runImage) - if err != nil { - return cmd.FailErr(err, "get digest reference for builder image") - } - analyzedMD.RunImage = &files.RunImage{ - Reference: digestRef, - Image: analyzedMD.RunImageImage(), - Extend: analyzedMD.RunImage.Extend, - TargetMetadata: targetData, + if err = updateAnalyzedMD(&analyzedMD, remoteRunImage); err != nil { + return cmd.FailErr(err, "update analyzed metadata") } } if err = encoding.WriteTOML(r.AnalyzedPath, analyzedMD); err != nil { @@ -169,20 +151,47 @@ func (r *restoreCmd) Exec() error { return r.restore(appMeta, group, cacheStore) } +func updateAnalyzedMD(analyzedMD *files.Analyzed, remoteRunImage imgutil.Image) error { + digestRef, err := remoteRunImage.Identifier() + if err != nil { + return cmd.FailErr(err, "get digest reference for run image") + } + targetData, err := platform.GetTargetMetadata(remoteRunImage) + 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 { - return runImage != nil && runImage.Extend + if runImage == nil { + // sanity check to prevent panic, should be unreachable + return false + } + return runImage.Extend } func needsUpdating(runImage *files.RunImage) bool { if runImage == nil { + // sanity check to prevent panic, should be unreachable return false } - if runImage.TargetMetadata != nil && runImage.TargetMetadata.OS != "" { + if runImage.Reference != "" && 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") } @@ -201,9 +210,12 @@ func (r *restoreCmd) pullSparse(imageRef string) (imgutil.Image, error) { return nil, fmt.Errorf("failed to create cache directory: %w", err) } // get remote image - remoteImage, err := remote.NewV1Image(imageRef, r.keychain) + remoteImage, err := remote.NewImage(imageRef, r.keychain, remote.FromBaseImage(imageRef)) if err != nil { - return nil, fmt.Errorf("failed to get remote image: %w", err) + 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 { @@ -213,13 +225,15 @@ func (r *restoreCmd) pullSparse(imageRef string) (imgutil.Image, error) { return nil, nil } // save to disk - h, err := remoteImage.Digest() + 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( - filepath.Join(baseCacheDir, h.String()), - remoteImage, + path, + remoteImage.UnderlyingImage(), layout.WithMediaTypes(imgutil.DefaultTypes), ) if err != nil { @@ -228,32 +242,7 @@ func (r *restoreCmd) pullSparse(imageRef string) (imgutil.Image, error) { if err = sparseImage.Save(); err != nil { return nil, fmt.Errorf("failed to save sparse image: %w", err) } - return sparseImage, nil -} - -func digestReference(imageRef string, image imgutil.Image) (string, error) { - ir, err := name.ParseReference(imageRef) - if err != nil { - return "", err - } - _, err = name.NewDigest(ir.String()) - if err == nil { - // if we already have a digest reference, return it - return imageRef, nil - } - id, err := image.Identifier() - if err != nil { - return "", err - } - digest, err := name.NewDigest(id.String()) - if err != nil { - return "", err - } - digestRef, err := name.NewDigest(fmt.Sprintf("%s@%s", ir.Context().Name(), digest.DigestStr()), name.WeakValidation) - if err != nil { - return "", err - } - return digestRef.String(), nil + return remoteImage, nil } func (r *restoreCmd) restoresLayerMetadata() bool { diff --git a/generator.go b/generator.go index 9fd55dc02..52402ea10 100644 --- a/generator.go +++ b/generator.go @@ -7,7 +7,6 @@ import ( "path/filepath" "github.com/buildpacks/lifecycle/api" - "github.com/buildpacks/lifecycle/internal/name" "github.com/buildpacks/lifecycle/platform" "github.com/buildpacks/lifecycle/buildpack" @@ -157,13 +156,24 @@ func (g *Generator) Generate() (GenerateResult, error) { g.Logger.Debugf("Finished running generate for extension %s", ext) } - g.Logger.Debug("Checking for new run image") - runRef, extend := g.runImageFrom(dockerfiles) - if err != nil { - return GenerateResult{}, err + g.Logger.Debug("Checking run image") + finalAnalyzedMD := g.AnalyzedMD + generatedRunImageRef, extend := g.runImageFrom(dockerfiles) + if generatedRunImageRef != "" && g.isNew(generatedRunImageRef) { + if !g.RunMetadata.Contains(generatedRunImageRef) { + g.Logger.Warnf("new runtime base image '%s' not found in run metadata", generatedRunImageRef) + } + g.Logger.Debugf("Updating analyzed metadata with new run image '%s'", generatedRunImageRef) + finalAnalyzedMD.RunImage = &files.RunImage{ // reference and target data are cleared + Extend: extend, + Image: generatedRunImageRef, + } } - if runRef != "" && !satisfies(g.RunMetadata.Images, runRef) { - g.Logger.Warnf("new runtime base image '%s' not found in run metadata", runRef) + if extend { + if finalAnalyzedMD.RunImage != nil { // sanity check to prevent panic + g.Logger.Debug("Updating analyzed metadata to indicate run image extension") + finalAnalyzedMD.RunImage.Extend = true + } } g.Logger.Debug("Copying Dockerfiles") @@ -171,39 +181,13 @@ func (g *Generator) Generate() (GenerateResult, error) { return GenerateResult{}, err } - newAnalyzedMD := g.AnalyzedMD - if shouldReplacePrevious(runRef, g.AnalyzedMD) { - g.Logger.Debugf("Updating analyzed metadata with new run image '%s'", runRef) - newAnalyzedMD.RunImage = &files.RunImage{ // target data is cleared - Reference: runRef, - Extend: extend, - Image: runRef, - } - } else if extend && g.AnalyzedMD.RunImage != nil { - g.Logger.Debug("Updating analyzed metadata with run image extend") - newAnalyzedMD.RunImage.Extend = true - } - return GenerateResult{ - AnalyzedMD: newAnalyzedMD, + AnalyzedMD: finalAnalyzedMD, Plan: filteredPlan, UsePlan: true, }, nil } -func satisfies(images []files.RunImageForExport, imageName string) bool { - if len(images) == 0 { - // if no run image metadata was provided, consider it a match - return true - } - for _, image := range images { - if name.ParseMaybe(image.Image) == name.ParseMaybe(imageName) { - return true - } - } - return false -} - func (g *Generator) getGenerateInputs() buildpack.GenerateInputs { return buildpack.GenerateInputs{ AppDir: g.AppDir, @@ -268,12 +252,9 @@ func (g *Generator) runImageFrom(dockerfiles []buildpack.DockerfileInfo) (newBas return newBase, extend } -func shouldReplacePrevious(base string, analyzedMD files.Analyzed) bool { - if base == "" { - return false - } - if analyzedMD.RunImage == nil { - return true +func (g *Generator) isNew(ref string) bool { + if g.PlatformAPI.AtLeast("0.12") { + return ref != g.AnalyzedMD.RunImageImage() // don't use `name.ParseMaybe` as this will strip the digest, and we want to use exactly what the extension author wrote } - return base != analyzedMD.RunImage.Reference + return ref != "" } diff --git a/generator_test.go b/generator_test.go index 373149cef..d096e310b 100644 --- a/generator_test.go +++ b/generator_test.go @@ -394,21 +394,22 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { generator.AnalyzedMD = files.Analyzed{ RunImage: &files.RunImage{ - Reference: "some-existing-run-image", + Reference: "some-existing-run-image@sha256:s0m3d1g3st", + Image: "some-existing-run-image", }, } }) type testCase struct { - before func() - descCondition string - descResult string - aDockerfiles []buildpack.DockerfileInfo - bDockerfiles []buildpack.DockerfileInfo - expectedRunImageReference string - expectedRunImageExtend bool - expectedErr string - assertAfter func() + before func() + descCondition string + descResult string + aDockerfiles []buildpack.DockerfileInfo + bDockerfiles []buildpack.DockerfileInfo + expectedRunImageImage string + expectedRunImageExtend bool + expectedErr string + assertAfter func() } for _, tc := range []testCase{ { @@ -428,8 +429,8 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { WithBase: "", Extend: true, }}, - expectedRunImageReference: "some-existing-run-image", - expectedRunImageExtend: true, + expectedRunImageImage: "some-existing-run-image", + expectedRunImageExtend: true, }, { descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow", @@ -452,8 +453,8 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { Extend: true, }, }, - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: true, + expectedRunImageImage: "some-new-run-image", + expectedRunImageExtend: true, }, { descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow", @@ -476,8 +477,8 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { Extend: false, }, }, - expectedRunImageReference: "some-other-base-image", - expectedRunImageExtend: false, + expectedRunImageImage: "some-other-base-image", + expectedRunImageExtend: false, assertAfter: func() { t.Log("copies Dockerfiles to the correct locations") t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory") @@ -499,9 +500,9 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { Extend: true, }, }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: true, + bDockerfiles: []buildpack.DockerfileInfo{}, + expectedRunImageImage: "some-new-run-image", + expectedRunImageExtend: true, }, { before: func() { @@ -522,9 +523,9 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { Extend: false, }, }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: false, + bDockerfiles: []buildpack.DockerfileInfo{}, + expectedRunImageImage: "some-new-run-image", + expectedRunImageExtend: false, }, { before: func() { @@ -545,8 +546,8 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { Extend: false, }, }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageReference: "some-other-run-image", + bDockerfiles: []buildpack.DockerfileInfo{}, + expectedRunImageImage: "some-other-run-image", assertAfter: func() { h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata") }, @@ -575,7 +576,7 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { // do generate result, err := generator.Generate() if err == nil { - h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference) + h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage) h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend) } else { t.Log(err) diff --git a/handlers.go b/handlers.go index 77c94328b..64eef525c 100644 --- a/handlers.go +++ b/handlers.go @@ -39,7 +39,7 @@ type BuildpackAPIVerifier interface { //go:generate mockgen -package testmock -destination testmock/config_handler.go github.com/buildpacks/lifecycle ConfigHandler type ConfigHandler interface { - ReadAnalyzed(path string, logr log.Logger) (files.Analyzed, error) + ReadAnalyzed(path string, logger log.Logger) (files.Analyzed, error) ReadGroup(path string) (buildpackGroup []buildpack.GroupElement, extensionsGroup []buildpack.GroupElement, err error) ReadOrder(path string) (buildpack.Order, buildpack.Order, error) ReadRun(runPath string, logger log.Logger) (files.Run, error) diff --git a/internal/encoding/utils.go b/internal/encoding/utils.go index 01e1cea3e..c95a485d6 100644 --- a/internal/encoding/utils.go +++ b/internal/encoding/utils.go @@ -12,7 +12,13 @@ import ( // json +// ToJSONMaybe returns the provided interface as JSON if marshaling is successful, +// or as a string if an error is encountered. +// It is only intended to be used for logging. func ToJSONMaybe(v interface{}) string { + if v == nil { + return "" + } b, err := json.Marshal(v) if err != nil { return fmt.Sprintf("%s", v) // hopefully v is a Stringer diff --git a/internal/name/ref.go b/internal/name/ref.go index 8190d5a23..2def8255c 100644 --- a/internal/name/ref.go +++ b/internal/name/ref.go @@ -1,10 +1,32 @@ package name -import "github.com/google/go-containerregistry/pkg/name" +import ( + "strings" -func ParseMaybe(ref string) string { - if nameRef, err := name.ParseReference(ref); err == nil { - return nameRef.Name() + "github.com/google/go-containerregistry/pkg/name" +) + +// ParseMaybe attempts to parse the provided reference as a GGCR `name.Reference`, returning a modified `reference.Name()` if parsing is successful. +// Unlike GGCR's `reference.Name()`, `ParseMaybe` will strip the digest portion of the reference, +// retaining the provided tag or adding a `latest` tag if no tag is provided. +// This is to aid in comparing two references when we really care about image names and not about image digests, +// such as when populating `files.RunImageForRebase` information on an exported image. +func ParseMaybe(provided string) string { + toParse := provided + if hasDigest(provided) { + toParse = trimDigest(provided) + } + if ref, err := name.ParseReference(toParse); err == nil { + return ref.Name() } - return ref + return provided +} + +func hasDigest(ref string) bool { + return strings.Contains(ref, "@sha256:") +} + +func trimDigest(ref string) string { + parts := strings.Split(ref, "@") + return parts[0] } diff --git a/internal/name/ref_test.go b/internal/name/ref_test.go index 7df2abdbb..73c3dd997 100644 --- a/internal/name/ref_test.go +++ b/internal/name/ref_test.go @@ -16,47 +16,72 @@ func TestRef(t *testing.T) { func testRef(t *testing.T, when spec.G, it spec.S) { when(".ParseMaybe", func() { when("provided reference", func() { - when("invalid", func() { - it("returns the provided reference", func() { - got := name.ParseMaybe("!@#$") - h.AssertEq(t, got, "!@#$") - }) - }) - - when("has implicit registry", func() { - it("returns the fully qualified reference", func() { - got := name.ParseMaybe("some-library/some-repo:latest") - h.AssertEq(t, got, "index.docker.io/some-library/some-repo:latest") - }) - }) - - when("has implicit library", func() { - it("returns the provided reference", func() { - got := name.ParseMaybe("some.registry/some-repo:latest") - h.AssertEq(t, got, "some.registry/some-repo:latest") - }) - - when("registry is docker.io", func() { - it("returns the fully qualified reference", func() { - got := name.ParseMaybe("index.docker.io/some-repo:latest") - h.AssertEq(t, got, "index.docker.io/library/some-repo:latest") + type testCase struct { + condition string + provided string + does string + expected string + } + testCases := []testCase{ + { + condition: "is invalid", + provided: "!@#$", + does: "returns the provided reference", + expected: "!@#$", + }, + { + condition: "has an implicit registry", + provided: "some-library/some-repo:some-tag", + does: "adds an explicit registry", + expected: "index.docker.io/some-library/some-repo:some-tag", + }, + { + condition: "has an implicit library", + provided: "some.registry/some-repo:some-tag", + does: "returns the provided reference", + expected: "some.registry/some-repo:some-tag", + }, + { + condition: "has an implicit library and has registry index.docker.io", + provided: "index.docker.io/some-repo:some-tag", + does: "adds an explicit library", + expected: "index.docker.io/library/some-repo:some-tag", + }, + { + condition: "has an implicit tag", + provided: "some.registry/some-library/some-repo", + does: "adds an explicit tag", + expected: "some.registry/some-library/some-repo:latest", + }, + { + condition: "has an implicit tag and has a digest", + provided: "some.registry/some-library/some-repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + does: "adds an explicit tag and removes the digest", + expected: "some.registry/some-library/some-repo:latest", + }, + { + condition: "has an explicit tag", + provided: "some.registry/some-library/some-repo:some-tag", + does: "returns the provided reference", + expected: "some.registry/some-library/some-repo:some-tag", + }, + { + condition: "has an explicit tag and has a digest", + provided: "some.registry/some-library/some-repo:some-tag@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + does: "removes the digest", + expected: "some.registry/some-library/some-repo:some-tag", + }, + } + for _, tc := range testCases { + tc := tc + w := when + w(tc.condition, func() { + it(tc.does, func() { + actual := name.ParseMaybe(tc.provided) + h.AssertEq(t, actual, tc.expected) }) }) - }) - - when("has implicit tag", func() { - it("returns the fully qualified reference", func() { - got := name.ParseMaybe("some.registry/some-library/some-repo") - h.AssertEq(t, got, "some.registry/some-library/some-repo:latest") - }) - }) - - when("is fully qualified", func() { - it("returns the provided reference", func() { - got := name.ParseMaybe("some.registry/some-library/some-repo:some-tag") - h.AssertEq(t, got, "some.registry/some-library/some-repo:some-tag") - }) - }) + } }) }) } diff --git a/platform/files/analyzed.go b/platform/files/analyzed.go index 5ea7d8f5c..e36bbbea0 100644 --- a/platform/files/analyzed.go +++ b/platform/files/analyzed.go @@ -125,8 +125,10 @@ type RunImageForRebase struct { RunImageForExport } -func (r *RunImageForRebase) Contains(ref string) bool { - return r.RunImageForExport.Contains(ref) +// Contains returns true if the provided image reference is found in the existing metadata, +// removing the digest portion of the reference when determining if two image names are equivalent. +func (r *RunImageForRebase) Contains(providedImage string) bool { + return r.RunImageForExport.Contains(providedImage) } func (r *RunImageForRebase) ToStack() Stack { @@ -140,9 +142,9 @@ func (r *RunImageForRebase) ToStack() Stack { type RunImage struct { Reference string `toml:"reference"` - // Image specifies the repository name for the image. + // Image specifies the repository name for the image that was provided - either by the platform, or by extensions. // When exporting to a daemon, the restorer uses this field to pull the run image if needed for the extender; - // it can't use reference because this may be a daemon image ID if analyzed.toml was last written by the analyzer. + // it can't use `Reference` because this may be a daemon image ID if analyzed.toml was last written by the analyzer. Image string `toml:"image,omitempty"` // Extend if true indicates that the run image should be extended by the extender. Extend bool `toml:"extend,omitempty"` diff --git a/platform/files/run.go b/platform/files/run.go index 95c5fd1b4..84866745c 100644 --- a/platform/files/run.go +++ b/platform/files/run.go @@ -17,6 +17,17 @@ type Run struct { Images []RunImageForExport `json:"-" toml:"images"` } +// Contains returns true if the provided image reference is found in the existing metadata, +// removing the digest portion of the reference when determining if two image names are equivalent. +func (r *Run) Contains(providedImage string) bool { + for _, i := range r.Images { + if i.Contains(providedImage) { + return true + } + } + return false +} + func ReadRun(runPath string, logger log.Logger) (Run, error) { var runMD Run if _, err := toml.DecodeFile(runPath, &runMD); err != nil { diff --git a/platform/files/stack.go b/platform/files/stack.go index 6f9accfe0..7931d8340 100644 --- a/platform/files/stack.go +++ b/platform/files/stack.go @@ -5,7 +5,7 @@ import ( "github.com/BurntSushi/toml" - "github.com/buildpacks/lifecycle/internal/name" + iname "github.com/buildpacks/lifecycle/internal/name" "github.com/buildpacks/lifecycle/log" ) @@ -23,15 +23,15 @@ type RunImageForExport struct { Mirrors []string `toml:"mirrors,omitempty" json:"mirrors,omitempty"` } -// Contains returns true if the provided reference matches either the primary image, -// or the image mirrors. -func (r *RunImageForExport) Contains(ref string) bool { - ref = name.ParseMaybe(ref) - if name.ParseMaybe(r.Image) == ref { +// Contains returns true if the provided image reference is found in the existing metadata, +// removing the digest portion of the reference when determining if two image names are equivalent. +func (r *RunImageForExport) Contains(providedImage string) bool { + providedImage = iname.ParseMaybe(providedImage) + if iname.ParseMaybe(r.Image) == providedImage { return true } for _, m := range r.Mirrors { - if name.ParseMaybe(m) == ref { + if iname.ParseMaybe(m) == providedImage { return true } } diff --git a/platform/resolve_analyze_inputs_test.go b/platform/resolve_analyze_inputs_test.go index 392dcdc6d..a9dc2e729 100644 --- a/platform/resolve_analyze_inputs_test.go +++ b/platform/resolve_analyze_inputs_test.go @@ -95,7 +95,7 @@ func testResolveAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G inputs.StackPath = filepath.Join("testdata", "layers", "stack.toml") err := platform.ResolveInputs(platform.Analyze, inputs, logger) h.AssertNil(t, err) - h.AssertEq(t, inputs.RunImageRef, "some-run-image-from-stack-toml") + h.AssertEq(t, inputs.RunImageRef, "some-other-user-provided-run-image") }) when("stack.toml", func() { diff --git a/platform/resolve_create_inputs_test.go b/platform/resolve_create_inputs_test.go index 783d231da..2466ebeea 100644 --- a/platform/resolve_create_inputs_test.go +++ b/platform/resolve_create_inputs_test.go @@ -94,7 +94,7 @@ func testResolveCreateInputs(platformAPI string) func(t *testing.T, when spec.G, 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-from-stack-toml") + h.AssertEq(t, inputs.RunImageRef, "some-other-user-provided-run-image") }) when("stack.toml", func() { diff --git a/platform/run_image.go b/platform/run_image.go index da147e283..2c7f722d9 100644 --- a/platform/run_image.go +++ b/platform/run_image.go @@ -9,7 +9,6 @@ import ( "github.com/buildpacks/lifecycle/auth" "github.com/buildpacks/lifecycle/cmd" - iname "github.com/buildpacks/lifecycle/internal/name" "github.com/buildpacks/lifecycle/launch" "github.com/buildpacks/lifecycle/platform/files" ) @@ -20,43 +19,6 @@ const ( OSDistributionVersionLabel = "io.buildpacks.distribution.version" ) -func GetRunImageForExport(inputs LifecycleInputs) (files.RunImageForExport, error) { - if inputs.PlatformAPI.LessThan("0.12") { - stackMD, err := files.ReadStack(inputs.StackPath, cmd.DefaultLogger) - if err != nil { - return files.RunImageForExport{}, err - } - return stackMD.RunImage, nil - } - runMD, err := files.ReadRun(inputs.RunPath, cmd.DefaultLogger) - if err != nil { - return files.RunImageForExport{}, err - } - if len(runMD.Images) == 0 { - return files.RunImageForExport{}, nil - } - inputRef := iname.ParseMaybe(inputs.RunImageRef) - for _, runImage := range runMD.Images { - if iname.ParseMaybe(runImage.Image) == inputRef { - return runImage, nil - } - for _, mirror := range runImage.Mirrors { - if iname.ParseMaybe(mirror) == inputRef { - return runImage, nil - } - } - } - buildMD := &files.BuildMetadata{} - if err = files.DecodeBuildMetadata(launch.GetMetadataFilePath(inputs.LayersDir), inputs.PlatformAPI, buildMD); err != nil { - return files.RunImageForExport{}, err - } - if len(buildMD.Extensions) > 0 { - // Extensions could have switched the run image, so we can't assume the first run image in run.toml was intended - return files.RunImageForExport{Image: inputs.RunImageRef}, nil - } - return runMD.Images[0], nil -} - func BestRunImageMirrorFor(targetRegistry string, runImageMD files.RunImageForExport, checkReadAccess CheckReadAccess) (string, error) { var runImageMirrors []string if runImageMD.Image == "" { @@ -100,3 +62,50 @@ func byRegistry(reg string, images []string, checkReadAccess CheckReadAccess, ke } return "" } + +// GetRunImageForExport takes platform inputs and returns run image information +// for populating the io.buildpacks.lifecycle.metadata on the exported app image. +// The run image information is read from: +// - stack.toml for older platforms +// - run.toml for newer platforms, where the run image information returned is +// - the first set of image & mirrors that contains the platform-provided run image, or +// - the platform-provided run image if extensions were used and the image was not found, or +// - the first set of image & mirrors in run.toml +// +// The "platform-provided run image" is the run image "image" in analyzed.toml, +// NOT the run image "reference", +// as the run image "reference" could be a daemon image ID (which we'd not expect to find in run.toml). +func GetRunImageForExport(inputs LifecycleInputs) (files.RunImageForExport, error) { + if inputs.PlatformAPI.LessThan("0.12") { + stackMD, err := files.ReadStack(inputs.StackPath, cmd.DefaultLogger) + if err != nil { + return files.RunImageForExport{}, err + } + return stackMD.RunImage, nil + } + runMD, err := files.ReadRun(inputs.RunPath, cmd.DefaultLogger) + if err != nil { + return files.RunImageForExport{}, err + } + if len(runMD.Images) == 0 { + return files.RunImageForExport{}, nil + } + analyzedMD, err := files.ReadAnalyzed(inputs.AnalyzedPath, cmd.DefaultLogger) + if err != nil { + return files.RunImageForExport{}, err + } + for _, runImage := range runMD.Images { + if runImage.Contains(analyzedMD.RunImageImage()) { + return runImage, nil + } + } + buildMD := &files.BuildMetadata{} + if err = files.DecodeBuildMetadata(launch.GetMetadataFilePath(inputs.LayersDir), inputs.PlatformAPI, buildMD); err != nil { + return files.RunImageForExport{}, err + } + if len(buildMD.Extensions) > 0 { // FIXME: try to know for sure if extensions were used to switch the run image + // Extensions could have switched the run image, so we can't assume the first run image in run.toml was intended + return files.RunImageForExport{Image: analyzedMD.RunImageImage()}, nil + } + return runMD.Images[0], nil +} diff --git a/platform/run_image_test.go b/platform/run_image_test.go index 4c30c9eb5..3e3211ab5 100644 --- a/platform/run_image_test.go +++ b/platform/run_image_test.go @@ -23,11 +23,11 @@ func TestRunImage(t *testing.T) { func testRunImage(t *testing.T, when spec.G, it spec.S) { when(".GetRunImageForExport", func() { var inputs = platform.LifecycleInputs{ - LayersDir: filepath.Join("testdata", "layers"), - PlatformAPI: api.Platform.Latest(), - RunImageRef: "some-run-image-ref", - RunPath: filepath.Join("testdata", "layers", "run.toml"), - StackPath: filepath.Join("testdata", "layers", "stack.toml"), + AnalyzedPath: filepath.Join("testdata", "layers", "analyzed.toml"), + LayersDir: filepath.Join("testdata", "layers"), + PlatformAPI: api.Platform.Latest(), + RunPath: filepath.Join("testdata", "layers", "run.toml"), + StackPath: filepath.Join("testdata", "layers", "stack.toml"), } when("run.toml", func() { @@ -52,74 +52,60 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) { }) when("contains an image matching run image ref", func() { - inputs.RunImageRef = "some-run-image-from-run-toml-1" - it("returns the image", func() { result, err := platform.GetRunImageForExport(inputs) h.AssertNil(t, err) h.AssertEq(t, result, files.RunImageForExport{ - Image: "some-run-image-from-run-toml-1", - Mirrors: []string{"some-run-image-mirror-from-run-toml-1", "some-other-run-image-mirror-from-run-toml-1"}, + Image: "some-user-provided-run-image", + Mirrors: []string{"some-user-provided-run-image-mirror-1", "some-user-provided-run-image-mirror-2"}, }) }) when("reference includes docker registry", func() { - inputs.RunImageRef = "index.docker.io/some-run-image-from-run-toml-1" + inputs.AnalyzedPath = filepath.Join("testdata", "layers", "analyzed-docker.toml") it("still matches", func() { result, err := platform.GetRunImageForExport(inputs) h.AssertNil(t, err) h.AssertEq(t, result, files.RunImageForExport{ - Image: "some-run-image-from-run-toml-1", - Mirrors: []string{"some-run-image-mirror-from-run-toml-1", "some-other-run-image-mirror-from-run-toml-1"}, + Image: "some-user-provided-run-image", + Mirrors: []string{"some-user-provided-run-image-mirror-1", "some-user-provided-run-image-mirror-2"}, }) }) }) }) when("contains an image mirror matching run image ref", func() { - inputs.RunImageRef = "some-other-run-image-mirror-from-run-toml-1" - it("returns the image", func() { result, err := platform.GetRunImageForExport(inputs) h.AssertNil(t, err) h.AssertEq(t, result, files.RunImageForExport{ - Image: "some-run-image-from-run-toml-1", - Mirrors: []string{"some-run-image-mirror-from-run-toml-1", "some-other-run-image-mirror-from-run-toml-1"}, - }) - }) - - when("reference includes docker registry", func() { - inputs.RunImageRef = "index.docker.io/some-other-run-image-mirror-from-run-toml-1" - - it("still matches", func() { - result, err := platform.GetRunImageForExport(inputs) - h.AssertNil(t, err) - h.AssertEq(t, result, files.RunImageForExport{ - Image: "some-run-image-from-run-toml-1", - Mirrors: []string{"some-run-image-mirror-from-run-toml-1", "some-other-run-image-mirror-from-run-toml-1"}, - }) + Image: "some-user-provided-run-image", + Mirrors: []string{"some-user-provided-run-image-mirror-1", "some-user-provided-run-image-mirror-2"}, }) }) }) when("contains no image or image mirror matching run image ref", func() { + inputs.AnalyzedPath = filepath.Join("testdata", "layers", "analyzed-other.toml") + it("returns the first image in run.toml", func() { result, err := platform.GetRunImageForExport(inputs) h.AssertNil(t, err) h.AssertEq(t, result, files.RunImageForExport{ - Image: "some-run-image-from-run-toml", - Mirrors: []string{"some-run-image-mirror-from-run-toml", "some-other-run-image-mirror-from-run-toml"}, + Image: "some-other-user-provided-run-image", + Mirrors: []string{"some-other-user-provided-run-image-mirror-1", "some-other-user-provided-run-image-mirror-2"}, }) }) when("there are extensions", func() { - inputs.LayersDir = filepath.Join("testdata", "other-layers") + inputs.AnalyzedPath = filepath.Join("testdata", "layers", "analyzed-other.toml") + inputs.LayersDir = filepath.Join("testdata", "other-layers") // force /config/metadata.toml - it("returns the run image ref", func() { + it("returns the run image ref from analyzed.toml", func() { result, err := platform.GetRunImageForExport(inputs) h.AssertNil(t, err) - h.AssertEq(t, result, files.RunImageForExport{Image: "some-run-image-ref"}) + h.AssertEq(t, result, files.RunImageForExport{Image: "some-new-user-provided-run-image"}) }) }) }) @@ -133,8 +119,8 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) { result, err := platform.GetRunImageForExport(inputs) h.AssertNil(t, err) h.AssertEq(t, result, files.RunImageForExport{ - Image: "some-run-image-from-stack-toml", - Mirrors: []string{"some-run-image-mirror-from-stack-toml", "some-other-run-image-mirror-from-stack-toml"}, + Image: "some-other-user-provided-run-image", + Mirrors: []string{"some-other-user-provided-run-image-mirror-1", "some-other-user-provided-run-image-mirror-2"}, }) }) diff --git a/platform/target_data.go b/platform/target_data.go index bb9ba9fbe..5a8298138 100644 --- a/platform/target_data.go +++ b/platform/target_data.go @@ -29,9 +29,6 @@ func EnvVarsFor(tm files.TargetMetadata) []string { func GetTargetMetadata(fromImage imgutil.Image) (*files.TargetMetadata, error) { tm := files.TargetMetadata{} - if !fromImage.Found() { - return &tm, nil - } var err error tm.OS, err = fromImage.OS() if err != nil { diff --git a/platform/testdata/layers/analyzed-docker.toml b/platform/testdata/layers/analyzed-docker.toml new file mode 100644 index 000000000..3536c8b7d --- /dev/null +++ b/platform/testdata/layers/analyzed-docker.toml @@ -0,0 +1,2 @@ +[run-image] + image = "index.docker.io/some-user-provided-run-image" diff --git a/platform/testdata/layers/analyzed-mirror.toml b/platform/testdata/layers/analyzed-mirror.toml new file mode 100644 index 000000000..b36b679c7 --- /dev/null +++ b/platform/testdata/layers/analyzed-mirror.toml @@ -0,0 +1,2 @@ +[run-image] + image = "some-user-provided-run-image-mirror-1" diff --git a/platform/testdata/layers/analyzed-other.toml b/platform/testdata/layers/analyzed-other.toml new file mode 100644 index 000000000..ee96b2ce3 --- /dev/null +++ b/platform/testdata/layers/analyzed-other.toml @@ -0,0 +1,2 @@ +[run-image] + image = "some-new-user-provided-run-image" diff --git a/platform/testdata/layers/analyzed.toml b/platform/testdata/layers/analyzed.toml new file mode 100644 index 000000000..ce8b6b7a9 --- /dev/null +++ b/platform/testdata/layers/analyzed.toml @@ -0,0 +1,2 @@ +[run-image] + image = "some-user-provided-run-image" diff --git a/platform/testdata/layers/run.toml b/platform/testdata/layers/run.toml index 60d90685a..52bba8424 100644 --- a/platform/testdata/layers/run.toml +++ b/platform/testdata/layers/run.toml @@ -1,7 +1,7 @@ [[images]] - image = "some-run-image-from-run-toml" - mirrors = ["some-run-image-mirror-from-run-toml", "some-other-run-image-mirror-from-run-toml"] + image = "some-other-user-provided-run-image" + mirrors = ["some-other-user-provided-run-image-mirror-1", "some-other-user-provided-run-image-mirror-2"] [[images]] - image = "some-run-image-from-run-toml-1" - mirrors = ["some-run-image-mirror-from-run-toml-1", "some-other-run-image-mirror-from-run-toml-1"] + image = "some-user-provided-run-image" + mirrors = ["some-user-provided-run-image-mirror-1", "some-user-provided-run-image-mirror-2"] diff --git a/platform/testdata/layers/stack.toml b/platform/testdata/layers/stack.toml index d29f134f3..873027385 100644 --- a/platform/testdata/layers/stack.toml +++ b/platform/testdata/layers/stack.toml @@ -1,3 +1,3 @@ [run-image] - image = "some-run-image-from-stack-toml" - mirrors = ["some-run-image-mirror-from-stack-toml", "some-other-run-image-mirror-from-stack-toml"] + image = "some-other-user-provided-run-image" + mirrors = ["some-other-user-provided-run-image-mirror-1", "some-other-user-provided-run-image-mirror-2"]