From 68b4ce9a55dfe985c6bab71b4d6843f8b14bc573 Mon Sep 17 00:00:00 2001 From: Natalie Arellano Date: Fri, 28 Apr 2023 15:43:52 -0400 Subject: [PATCH] Fixes for pack acceptance in the current-current-current configuration (#1075) * Fixes for pack acceptance in the current-current-current configuration Signed-off-by: Joe Kimmel Signed-off-by: Natalie Arellano * Add unit tests for fixes Signed-off-by: Natalie Arellano --------- Signed-off-by: Joe Kimmel Signed-off-by: Natalie Arellano --- buildpack/bp_descriptor.go | 13 ++ buildpack/bp_descriptor_test.go | 35 ++++ cmd/lifecycle/exporter.go | 29 +--- cmd/lifecycle/rebaser.go | 2 +- detector.go | 15 +- detector_test.go | 163 ++++++++++++------ exporter.go | 2 +- exporter_test.go | 3 + platform/files.go | 23 ++- platform/files_test.go | 4 +- platform/resolve_analyze_inputs_test.go | 2 +- platform/resolve_create_inputs_test.go | 2 +- platform/run_image.go | 39 +++++ platform/run_image_test.go | 133 ++++++++++++++ platform/testdata/layers/config/metadata.toml | 0 platform/testdata/layers/empty-run.toml | 0 platform/testdata/layers/run.toml | 7 + platform/testdata/layers/stack.toml | 4 +- .../other-layers/config/metadata.toml | 2 + rebaser_test.go | 6 +- 20 files changed, 375 insertions(+), 109 deletions(-) create mode 100644 platform/run_image_test.go create mode 100644 platform/testdata/layers/config/metadata.toml create mode 100644 platform/testdata/layers/empty-run.toml create mode 100644 platform/testdata/layers/run.toml create mode 100644 platform/testdata/other-layers/config/metadata.toml diff --git a/buildpack/bp_descriptor.go b/buildpack/bp_descriptor.go index 3614921ad..bf72a7a72 100644 --- a/buildpack/bp_descriptor.go +++ b/buildpack/bp_descriptor.go @@ -3,6 +3,7 @@ package buildpack import ( + "fmt" "os" "path/filepath" @@ -30,11 +31,23 @@ type TargetMetadata struct { Distributions []OSDistribution `json:"distributions" toml:"distributions"` } +func (t *TargetMetadata) String() string { + s := fmt.Sprintf("OS: %s, Arch: %s, ArchVariant: %s", t.OS, t.Arch, t.ArchVariant) + if len(t.Distributions) > 0 { + s += fmt.Sprintf(", Distributions: %s", t.Distributions) + } + return s +} + type OSDistribution struct { Name string `json:"name" toml:"name"` Version string `json:"version" toml:"version"` } +func (d OSDistribution) String() string { + return fmt.Sprintf("Distribution: (Name: %s, Version: %s)", d.Name, d.Version) +} + type BpInfo struct { BaseInfo SBOM []string `toml:"sbom-formats,omitempty" json:"sbom-formats,omitempty"` diff --git a/buildpack/bp_descriptor_test.go b/buildpack/bp_descriptor_test.go index 7c578510c..24692e1ad 100644 --- a/buildpack/bp_descriptor_test.go +++ b/buildpack/bp_descriptor_test.go @@ -16,6 +16,38 @@ func TestBpDescriptor(t *testing.T) { } func testBpDescriptor(t *testing.T, when spec.G, it spec.S) { + when("TargetMetadata", func() { + when("#String()", func() { + when("there is a distribution", func() { + it("prints the target", func() { + tm := &buildpack.TargetMetadata{ + OS: "some-os", + Arch: "some-arch", + ArchVariant: "some-arch-variant", + Distributions: []buildpack.OSDistribution{ + { + Name: "some-os-dist", + Version: "some-os-dist-version", + }, + }, + } + h.AssertEq(t, tm.String(), "OS: some-os, Arch: some-arch, ArchVariant: some-arch-variant, Distributions: [Distribution: (Name: some-os-dist, Version: some-os-dist-version)]") + }) + }) + + when("there is no distribution", func() { + it("prints the target", func() { + tm := &buildpack.TargetMetadata{ + OS: "some-os", + Arch: "some-arch", + ArchVariant: "some-arch-variant", + } + h.AssertEq(t, tm.String(), "OS: some-os, Arch: some-arch, ArchVariant: some-arch-variant") + }) + }) + }) + }) + when("#ReadBpDescriptor", func() { it("returns a buildpack descriptor", func() { path := filepath.Join("testdata", "buildpack", "by-id", "A", "v1", "buildpack.toml") @@ -88,6 +120,7 @@ func testBpDescriptor(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, descriptor.Targets[0].Distributions[0].Name, "ubuntu") h.AssertEq(t, descriptor.Targets[0].Distributions[0].Version, "18.04") }) + it("does not translate non-special stack values", func() { path := filepath.Join("testdata", "buildpack", "by-id", "C", "v1", "buildpack.toml") descriptor, err := buildpack.ReadBpDescriptor(path) @@ -103,6 +136,7 @@ func testBpDescriptor(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, descriptor.Stacks[0].ID, "some.non-magic.value") h.AssertEq(t, len(descriptor.Targets), 0) }) + it("does autodetect linux buildpacks from the bin dir contents", func() { path := filepath.Join("testdata", "buildpack", "by-id", "C", "v2", "buildpack.toml") descriptor, err := buildpack.ReadBpDescriptor(path) @@ -122,6 +156,7 @@ func testBpDescriptor(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, descriptor.Targets[0].OS, "linux") h.AssertEq(t, len(descriptor.Targets[0].Distributions), 0) }) + it("detects windows/* if batch files are present and ignores linux", func() { path := filepath.Join("testdata", "buildpack", "by-id", "A", "v1", "buildpack.toml") descriptor, err := buildpack.ReadBpDescriptor(path) diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go index 7c1e40a93..2da3b6f4b 100644 --- a/cmd/lifecycle/exporter.go +++ b/cmd/lifecycle/exporter.go @@ -199,7 +199,7 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore lifecycle.Cache, an return err } - runImageForExport, err := e.getRunImageForExport() + runImageForExport, err := platform.GetRunImageForExport(*e.LifecycleInputs) if err != nil { return err } @@ -508,33 +508,6 @@ func (e *exportCmd) getExtendedConfig(runImage *platform.RunImage) (*v1.Config, return &extendedConfig.Config, nil } -func (e *exportCmd) getRunImageForExport() (platform.RunImageForExport, error) { - if e.PlatformAPI.LessThan("0.12") { - stackMD, err := platform.ReadStack(e.StackPath, cmd.DefaultLogger) - if err != nil { - return platform.RunImageForExport{}, err - } - return stackMD.RunImage, nil - } - runMD, err := platform.ReadRun(e.RunPath, cmd.DefaultLogger) - if err != nil { - return platform.RunImageForExport{}, err - } - if len(runMD.Images) == 0 { - return platform.RunImageForExport{Image: e.RunImageRef}, nil - } - runRef, err := name.ParseReference(e.RunImageRef) - if err != nil { - return platform.RunImageForExport{}, err - } - for _, runImage := range runMD.Images { - if runImage.Image == runRef.Context().Name() { - return runImage, nil - } - } - return platform.RunImageForExport{Image: e.RunImageRef}, nil -} - func (e *exportCmd) hasExtendedLayers() bool { if e.ExtendedDir == "" { return false diff --git a/cmd/lifecycle/rebaser.go b/cmd/lifecycle/rebaser.go index 861da216f..23a377fa0 100644 --- a/cmd/lifecycle/rebaser.go +++ b/cmd/lifecycle/rebaser.go @@ -171,7 +171,7 @@ func (r *rebaseCmd) setAppImage() error { // for backwards compatibility, we need to fallback to the stack metadata // fail if there is no run image metadata available from either location - if md.Stack.RunImage.Image == "" { + if md.Stack == nil || md.Stack.RunImage.Image == "" { return cmd.FailErrCode(errors.New("-run-image is required when there is no run image metadata available"), cmd.CodeForInvalidArgs, "parse arguments") } diff --git a/detector.go b/detector.go index 11d66a971..0aaed8b2d 100644 --- a/detector.go +++ b/detector.go @@ -230,6 +230,13 @@ func (d *Detector) detectGroup(group buildpack.Group, done []buildpack.GroupElem return nil, nil, err } + // Resolve order if element is a composite buildpack. + if order := bpDescriptor.Order; len(order) > 0 { + // FIXME: double-check slice safety here + // FIXME: cyclical references lead to infinite recursion + return d.detectOrder(order, done, group.Group[i+1:], groupEl.Optional, wg) + } + if d.PlatformAPI.AtLeast("0.12") { targetMatch := false if isWildcard(d.AnalyzeMD.RunImageTarget()) || hasWildcard(bpDescriptor.Targets) { @@ -249,18 +256,12 @@ func (d *Detector) detectGroup(group buildpack.Group, done []buildpack.GroupElem keyFor(groupEl), buildpack.DetectOutputs{ Code: -1, - Err: fmt.Errorf("unable to satisfy Target OS/Arch constraints: %v", d.AnalyzeMD.RunImage.TargetMetadata), + Err: fmt.Errorf("unable to satisfy Target OS/Arch constraints; run image: %v, buildpack: %v", d.AnalyzeMD.RunImage.TargetMetadata, bpDescriptor.Targets), }) continue } } - // Resolve order if element is a composite buildpack. - if order := bpDescriptor.Order; len(order) > 0 { - // FIXME: double-check slice safety here - // FIXME: cyclical references lead to infinite recursion - return d.detectOrder(order, done, group.Group[i+1:], groupEl.Optional, wg) - } descriptor = bpDescriptor // standardize the type so below we don't have to care whether it was an extension } else { descriptor, err = d.DirStore.LookupExt(groupEl.ID, groupEl.Version) diff --git a/detector_test.go b/detector_test.go index 10b4ac58c..bbc88fff5 100644 --- a/detector_test.go +++ b/detector_test.go @@ -794,48 +794,79 @@ func testDetector(t *testing.T, when spec.G, it spec.S) { } }) }) + }) - when("A Target is provided from AnalyzeMD", func() { - it("errors if the buildpacks don't share that target arch/os", func() { - detector.AnalyzeMD.RunImage = &platform.RunImage{ - TargetMetadata: &platform.TargetMetadata{ - OS: "MacOS", - Arch: "ARM64", - Distribution: &platform.OSDistribution{Name: "MacOS", Version: "some kind of big cat"}, - }, - } + when("target resolution", func() { + it("totally works if the constraints are met", func() { + detector.AnalyzeMD.RunImage = &platform.RunImage{ + TargetMetadata: &platform.TargetMetadata{ + OS: "MacOS", + Arch: "ARM64", + Distribution: &platform.OSDistribution{Name: "MacOS", Version: "snow cheetah"}, + }, + } - bpA1 := &buildpack.BpDescriptor{ - WithAPI: "0.12", - Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}, - Targets: []buildpack.TargetMetadata{ - {Arch: "P6", ArchVariant: "Pentium Pro", OS: "Win95", - Distributions: []buildpack.OSDistribution{ - {Name: "Windows 95", Version: "OSR1"}, {Name: "Windows 95", Version: "OSR2.5"}}}, - {Arch: "Pentium M", OS: "Win98", - Distributions: []buildpack.OSDistribution{{Name: "Windows 2000", Version: "Server"}}}, - }, - } - dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes() + bpA1 := &buildpack.BpDescriptor{ + WithAPI: "0.12", + Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}, + Targets: []buildpack.TargetMetadata{ + {Arch: "P6", ArchVariant: "Pentium Pro", OS: "Win95", + Distributions: []buildpack.OSDistribution{ + {Name: "Windows 95", Version: "OSR1"}, {Name: "Windows 95", Version: "OSR2.5"}}}, + {Arch: "ARM64", OS: "MacOS", Distributions: []buildpack.OSDistribution{{Name: "MacOS", Version: "snow cheetah"}}}}, + } + dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes() + executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()) - resolver.EXPECT().Resolve(gomock.Any(), gomock.Any()).DoAndReturn( - func(done []buildpack.GroupElement, detectRuns *sync.Map) ([]buildpack.GroupElement, []platform.BuildPlanEntry, error) { - h.AssertEq(t, len(done), 1) - val, ok := detectRuns.Load("Buildpack A@v1") - h.AssertEq(t, ok, true) - outs := val.(buildpack.DetectOutputs) - h.AssertEq(t, outs.Code, -1) - h.AssertStringContains(t, outs.Err.Error(), "unable to satisfy Target OS/Arch constraints") - return []buildpack.GroupElement{}, []platform.BuildPlanEntry{}, nil - }) + group := []buildpack.GroupElement{ + {ID: "A", Version: "v1", API: "0.12"}, + } + // the most meaningful assertion in this test is that `group` is the first argument to Resolve, meaning that the buildpack matched. + resolver.EXPECT().Resolve(group, detector.Runs).Return( + []buildpack.GroupElement{}, + []platform.BuildPlanEntry{}, + nil, + ) - group := []buildpack.GroupElement{ - {ID: "A", Version: "v1", API: "0.3"}, - } - detector.Order = buildpack.Order{{Group: group}} - _, _, err := detector.Detect() // even though the returns from this are directly from the mock above, if we don't check the returns the linter declares we've done it wrong and fails on the lack of assertions. - h.AssertNil(t, err) - }) + detector.Order = buildpack.Order{{Group: group}} + _, _, err := detector.Detect() + h.AssertNil(t, err) + }) + + it("was born to be wildcard compliant", func() { + detector.AnalyzeMD.RunImage = &platform.RunImage{ + TargetMetadata: &platform.TargetMetadata{ + OS: "MacOS", + Arch: "ARM64", + Distribution: &platform.OSDistribution{Name: "MacOS", Version: "snow cheetah"}, + }, + } + + bpA1 := &buildpack.BpDescriptor{ + WithAPI: "0.12", + Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}, + Targets: []buildpack.TargetMetadata{ + {Arch: "*", OS: "*"}}, + } + dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes() + executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()) + + group := []buildpack.GroupElement{ + {ID: "A", Version: "v1", API: "0.12"}, + } + // the most meaningful assertion in this test is that `group` is the first argument to Resolve, meaning that the buildpack matched. + resolver.EXPECT().Resolve(group, detector.Runs).Return( + []buildpack.GroupElement{}, + []platform.BuildPlanEntry{}, + nil, + ) + + detector.Order = buildpack.Order{{Group: group}} + _, _, err := detector.Detect() + h.AssertNil(t, err) + }) + + when("there is a composite buildpack", func() { it("totally works if the constraints are met", func() { detector.AnalyzeMD.RunImage = &platform.RunImage{ TargetMetadata: &platform.TargetMetadata{ @@ -845,6 +876,16 @@ func testDetector(t *testing.T, when spec.G, it spec.S) { }, } + bpF1 := &buildpack.BpDescriptor{ + WithAPI: "0.2", + Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "F", Version: "v1"}}, + Order: []buildpack.Group{ + {Group: []buildpack.GroupElement{ + {ID: "A", Version: "v1"}, + }}, + }, + } + dirStore.EXPECT().LookupBp("F", "v1").Return(bpF1, nil).AnyTimes() bpA1 := &buildpack.BpDescriptor{ WithAPI: "0.12", Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}, @@ -855,29 +896,33 @@ func testDetector(t *testing.T, when spec.G, it spec.S) { {Arch: "ARM64", OS: "MacOS", Distributions: []buildpack.OSDistribution{{Name: "MacOS", Version: "snow cheetah"}}}}, } dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes() + executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()) - group := []buildpack.GroupElement{ + expectedGroup := []buildpack.GroupElement{ {ID: "A", Version: "v1", API: "0.12"}, } - // the most meaningful assertion in this test is that `group` is the first argument to Resolve, meaning that the buildpack matched. - resolver.EXPECT().Resolve(group, detector.Runs).Return( + // the most meaningful assertion in this test is that `expectedGroup` is the first argument to Resolve, meaning that the buildpack matched. + resolver.EXPECT().Resolve(expectedGroup, detector.Runs).Return( []buildpack.GroupElement{}, []platform.BuildPlanEntry{}, nil, ) - detector.Order = buildpack.Order{{Group: group}} + detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{ + {ID: "F", Version: "v1", API: "0.12"}, + }}} _, _, err := detector.Detect() h.AssertNil(t, err) }) }) - it("was born to be wildcard compliant", func() { + + it("errors if the buildpacks don't share that target arch/os", func() { detector.AnalyzeMD.RunImage = &platform.RunImage{ TargetMetadata: &platform.TargetMetadata{ OS: "MacOS", Arch: "ARM64", - Distribution: &platform.OSDistribution{Name: "MacOS", Version: "snow cheetah"}, + Distribution: &platform.OSDistribution{Name: "MacOS", Version: "some kind of big cat"}, }, } @@ -885,23 +930,31 @@ func testDetector(t *testing.T, when spec.G, it spec.S) { WithAPI: "0.12", Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}, Targets: []buildpack.TargetMetadata{ - {Arch: "*", OS: "*"}}, + {Arch: "P6", ArchVariant: "Pentium Pro", OS: "Win95", + Distributions: []buildpack.OSDistribution{ + {Name: "Windows 95", Version: "OSR1"}, {Name: "Windows 95", Version: "OSR2.5"}}}, + {Arch: "Pentium M", OS: "Win98", + Distributions: []buildpack.OSDistribution{{Name: "Windows 2000", Version: "Server"}}}, + }, } dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes() - executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()) + + resolver.EXPECT().Resolve(gomock.Any(), gomock.Any()).DoAndReturn( + func(done []buildpack.GroupElement, detectRuns *sync.Map) ([]buildpack.GroupElement, []platform.BuildPlanEntry, error) { + h.AssertEq(t, len(done), 1) + val, ok := detectRuns.Load("Buildpack A@v1") + h.AssertEq(t, ok, true) + outs := val.(buildpack.DetectOutputs) + h.AssertEq(t, outs.Code, -1) + h.AssertStringContains(t, outs.Err.Error(), "unable to satisfy Target OS/Arch constraints") + return []buildpack.GroupElement{}, []platform.BuildPlanEntry{}, nil + }) group := []buildpack.GroupElement{ - {ID: "A", Version: "v1", API: "0.12"}, + {ID: "A", Version: "v1", API: "0.3"}, } - // the most meaningful assertion in this test is that `group` is the first argument to Resolve, meaning that the buildpack matched. - resolver.EXPECT().Resolve(group, detector.Runs).Return( - []buildpack.GroupElement{}, - []platform.BuildPlanEntry{}, - nil, - ) - detector.Order = buildpack.Order{{Group: group}} - _, _, err := detector.Detect() + _, _, err := detector.Detect() // even though the returns from this are directly from the mock above, if we don't check the returns the linter declares we've done it wrong and fails on the lack of assertions. h.AssertNil(t, err) }) }) diff --git a/exporter.go b/exporter.go index f82073c9e..5f001dfa7 100644 --- a/exporter.go +++ b/exporter.go @@ -117,7 +117,7 @@ func (e *Exporter) Export(opts ExportOptions) (platform.ExportReport, error) { meta.RunImage.Image = opts.RunImageForExport.Image meta.RunImage.Mirrors = opts.RunImageForExport.Mirrors } else { - meta.Stack = opts.Stack + meta.Stack = &opts.Stack } buildMD := &platform.BuildMetadata{} diff --git a/exporter_test.go b/exporter_test.go index 1738a3ccd..c122c9094 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -412,6 +412,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) h.AssertEq(t, meta.RunImage.Image, "some/run") h.AssertEq(t, meta.RunImage.Mirrors, []string{"registry.example.com/some/run", "other.example.com/some/run"}) + h.AssertEq(t, meta.Stack, (*platform.StackMetadata)(nil)) }) when("platform api < 0.12", func() { @@ -437,6 +438,8 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) h.AssertEq(t, meta.Stack.RunImage.Image, "some/run") h.AssertEq(t, meta.Stack.RunImage.Mirrors, []string{"registry.example.com/some/run", "other.example.com/some/run"}) + h.AssertEq(t, meta.RunImage.Image, "") + h.AssertEq(t, meta.RunImage.Mirrors, []string(nil)) }) }) }) diff --git a/platform/files.go b/platform/files.go index c017d757e..341420f2b 100644 --- a/platform/files.go +++ b/platform/files.go @@ -137,12 +137,10 @@ func (t *TargetMetadata) IsValidRebaseTargetFor(appTargetMetadata *TargetMetadat } func (t *TargetMetadata) String() string { - var distName, distVersion string if t.Distribution != nil { - distName = t.Distribution.Name - distVersion = t.Distribution.Version + return fmt.Sprintf("OS: %s, Arch: %s, ArchVariant: %s, Distribution: (Name: %s, Version: %s)", t.OS, t.Arch, t.ArchVariant, t.Distribution.Name, t.Distribution.Version) } - return fmt.Sprintf("OS: %s, Arch: %s, ArchVariant: %s, Distribution: (Name: %s, Version: %s)", t.OS, t.Arch, t.ArchVariant, distName, distVersion) + return fmt.Sprintf("OS: %s, Arch: %s, ArchVariant: %s", t.OS, t.Arch, t.ArchVariant) } // PopulateTargetOSFromFileSystem populates the target metadata you pass in if the information is available @@ -188,7 +186,7 @@ type LayersMetadata struct { Launcher LayerMetadata `json:"launcher" toml:"launcher"` ProcessTypes LayerMetadata `json:"process-types" toml:"process-types"` RunImage RunImageForRebase `json:"runImage" toml:"run-image"` - Stack StackMetadata `json:"stack,omitempty" toml:"stack,omitempty"` + Stack *StackMetadata `json:"stack,omitempty" toml:"stack,omitempty"` } // NOTE: This struct MUST be kept in sync with `LayersMetadata`. @@ -202,7 +200,7 @@ type LayersMetadataCompat struct { Launcher LayerMetadata `json:"launcher" toml:"launcher"` ProcessTypes LayerMetadata `json:"process-types" toml:"process-types"` RunImage RunImageForRebase `json:"runImage" toml:"run-image"` - Stack StackMetadata `json:"stack" toml:"stack"` + Stack *StackMetadata `json:"stack,omitempty" toml:"stack,omitempty"` } func (m *LayersMetadata) MetadataForBuildpack(id string) buildpack.LayersMetadata { @@ -227,6 +225,15 @@ type RunImageForRebase struct { Mirrors []string `toml:"mirrors,omitempty" json:"mirrors,omitempty"` } +func (r *RunImageForRebase) ToStackMetadata() StackMetadata { + return StackMetadata{ + RunImage: RunImageForExport{ + Image: r.Image, + Mirrors: r.Mirrors, + }, + } +} + // metadata.toml type BuildMetadata struct { @@ -411,11 +418,11 @@ func ReadRun(runPath string, logger log.Logger) (RunMetadata, error) { // stack.toml type StackMetadata struct { - RunImage RunImageForExport `json:"runImage" toml:"run-image"` + RunImage RunImageForExport `json:"runImage,omitempty" toml:"run-image"` } type RunImageForExport struct { - Image string `toml:"image" json:"image"` + Image string `toml:"image" json:"image,omitempty"` Mirrors []string `toml:"mirrors" json:"mirrors,omitempty"` } diff --git a/platform/files_test.go b/platform/files_test.go index 4051680d1..608e632c4 100644 --- a/platform/files_test.go +++ b/platform/files_test.go @@ -168,7 +168,7 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { amd := platform.AnalyzedMetadata{ PreviousImage: &platform.ImageIdentifier{Reference: "previous-img"}, Metadata: platform.LayersMetadata{ - Stack: platform.StackMetadata{ + Stack: &platform.StackMetadata{ RunImage: platform.RunImageForExport{Image: "imagine that"}, }, }, @@ -186,7 +186,7 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { amd := platform.AnalyzedMetadata{ PreviousImage: &platform.ImageIdentifier{Reference: "previous-img"}, Metadata: platform.LayersMetadata{ - Stack: platform.StackMetadata{ + Stack: &platform.StackMetadata{ RunImage: platform.RunImageForExport{Image: "imagine that"}, }, }, diff --git a/platform/resolve_analyze_inputs_test.go b/platform/resolve_analyze_inputs_test.go index 68487bb3a..2cbde7eaa 100644 --- a/platform/resolve_analyze_inputs_test.go +++ b/platform/resolve_analyze_inputs_test.go @@ -94,7 +94,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") + h.AssertEq(t, inputs.RunImageRef, "some-run-image-from-stack-toml") }) when("stack.toml", func() { diff --git a/platform/resolve_create_inputs_test.go b/platform/resolve_create_inputs_test.go index 8d53d7ea3..a6ee08afe 100644 --- a/platform/resolve_create_inputs_test.go +++ b/platform/resolve_create_inputs_test.go @@ -93,7 +93,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") + h.AssertEq(t, inputs.RunImageRef, "some-run-image-from-stack-toml") }) when("stack.toml", func() { diff --git a/platform/run_image.go b/platform/run_image.go index 081cc44dc..999dee7ad 100644 --- a/platform/run_image.go +++ b/platform/run_image.go @@ -2,6 +2,9 @@ package platform import ( "github.com/buildpacks/imgutil" + + "github.com/buildpacks/lifecycle/cmd" + "github.com/buildpacks/lifecycle/launch" ) const ( @@ -10,6 +13,42 @@ const ( OSDistributionVersionLabel = "io.buildpacks.distribution.version" ) +func GetRunImageForExport(inputs LifecycleInputs) (RunImageForExport, error) { + if inputs.PlatformAPI.LessThan("0.12") { + stackMD, err := ReadStack(inputs.StackPath, cmd.DefaultLogger) + if err != nil { + return RunImageForExport{}, err + } + return stackMD.RunImage, nil + } + runMD, err := ReadRun(inputs.RunPath, cmd.DefaultLogger) + if err != nil { + return RunImageForExport{}, err + } + if len(runMD.Images) == 0 { + return RunImageForExport{}, err + } + for _, runImage := range runMD.Images { + if runImage.Image == inputs.RunImageRef { + return runImage, nil + } + for _, mirror := range runImage.Mirrors { + if mirror == inputs.RunImageRef { + return runImage, nil + } + } + } + buildMD := &BuildMetadata{} + if err = DecodeBuildMetadataTOML(launch.GetMetadataFilePath(inputs.LayersDir), inputs.PlatformAPI, buildMD); err != nil { + return 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 RunImageForExport{Image: inputs.RunImageRef}, nil + } + return runMD.Images[0], nil +} + func GetTargetFromImage(image imgutil.Image) (*TargetMetadata, error) { tm := TargetMetadata{} if !image.Found() { diff --git a/platform/run_image_test.go b/platform/run_image_test.go new file mode 100644 index 000000000..4d96c8eb4 --- /dev/null +++ b/platform/run_image_test.go @@ -0,0 +1,133 @@ +package platform_test + +import ( + "path/filepath" + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/lifecycle/api" + "github.com/buildpacks/lifecycle/platform" + h "github.com/buildpacks/lifecycle/testhelpers" +) + +func TestRunImage(t *testing.T) { + spec.Run(t, "RunImage", testRunImage, spec.Report(report.Terminal{})) +} + +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"), + } + + when("run.toml", func() { + when("not exists", func() { + inputs.RunPath = "foo" + + it("returns empty info", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.RunImageForExport{}) + }) + }) + + when("contains no images", func() { + inputs.RunPath = filepath.Join("testdata", "layers", "empty-run.toml") + + it("returns empty info", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.RunImageForExport{}) + }) + }) + + when("contains an image matching run image ref", func() { + inputs.RunImageRef = "some-run-image-from-run-toml" + + it("returns the image", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.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"}, + }) + }) + }) + + when("contains an image mirror matching run image ref", func() { + inputs.RunImageRef = "some-other-run-image-mirror-from-run-toml" + + it("returns the image", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.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"}, + }) + }) + }) + + when("contains no image or image mirror matching run image ref", func() { + it("returns the first image in run.toml", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.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"}, + }) + }) + + when("there are extensions", func() { + inputs.LayersDir = filepath.Join("testdata", "other-layers") + + it("returns the run image ref", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.RunImageForExport{Image: "some-run-image-ref"}) + }) + }) + }) + }) + + when("platform api < 0.12", func() { + inputs.PlatformAPI = api.MustParse("0.11") + + when("stack.toml", func() { + it("returns the data in stack.toml", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.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"}, + }) + }) + + when("not exists", func() { + inputs.StackPath = "foo" + + it("returns empty info", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.RunImageForExport{}) + }) + }) + + when("contains no images", func() { + inputs.StackPath = filepath.Join("testdata", "layers", "empty-run.toml") + + it("returns empty info", func() { + result, err := platform.GetRunImageForExport(inputs) + h.AssertNil(t, err) + h.AssertEq(t, result, platform.RunImageForExport{}) + }) + }) + }) + }) + }) +} diff --git a/platform/testdata/layers/config/metadata.toml b/platform/testdata/layers/config/metadata.toml new file mode 100644 index 000000000..e69de29bb diff --git a/platform/testdata/layers/empty-run.toml b/platform/testdata/layers/empty-run.toml new file mode 100644 index 000000000..e69de29bb diff --git a/platform/testdata/layers/run.toml b/platform/testdata/layers/run.toml new file mode 100644 index 000000000..60d90685a --- /dev/null +++ b/platform/testdata/layers/run.toml @@ -0,0 +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"] + +[[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"] diff --git a/platform/testdata/layers/stack.toml b/platform/testdata/layers/stack.toml index be40b8df8..d29f134f3 100644 --- a/platform/testdata/layers/stack.toml +++ b/platform/testdata/layers/stack.toml @@ -1,3 +1,3 @@ [run-image] - image = "some-run-image" - mirrors = ["some-run-image-mirror", "some-other-run-image-mirror"] + image = "some-run-image-from-stack-toml" + mirrors = ["some-run-image-mirror-from-stack-toml", "some-other-run-image-mirror-from-stack-toml"] diff --git a/platform/testdata/other-layers/config/metadata.toml b/platform/testdata/other-layers/config/metadata.toml new file mode 100644 index 000000000..195958b2c --- /dev/null +++ b/platform/testdata/other-layers/config/metadata.toml @@ -0,0 +1,2 @@ +[[extensions]] +id = "some-extension-id" diff --git a/rebaser_test.go b/rebaser_test.go index 0e5981b68..d9378ae6d 100644 --- a/rebaser_test.go +++ b/rebaser_test.go @@ -482,7 +482,7 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, fakeNewBaseImage.SetOS("notlinux")) _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) - h.AssertError(t, err, "invalid base image target: 'OS: notlinux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: )'") + h.AssertError(t, err, "invalid base image target: 'OS: notlinux, Arch: amd64, ArchVariant: ' is not equal to 'OS: linux, Arch: amd64, ArchVariant: '") }) it("returns an error and prevents the rebase from taking place when the architecture are different", func() { @@ -490,7 +490,7 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, fakeNewBaseImage.SetArchitecture("arm64")) _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) - h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: arm64, ArchVariant: , Distribution: (Name: , Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: , Distribution: (Name: , Version: )'") + h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: arm64, ArchVariant: ' is not equal to 'OS: linux, Arch: amd64, ArchVariant: '") }) it("returns an error and prevents the rebase from taking place when the architecture variant are different", func() { @@ -498,7 +498,7 @@ func testRebaser(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, fakeNewBaseImage.SetVariant("variant2")) _, err := rebaser.Rebase(fakeAppImage, fakeNewBaseImage, fakeAppImage.Name(), additionalNames) - h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: amd64, ArchVariant: variant2, Distribution: (Name: , Version: )' is not equal to 'OS: linux, Arch: amd64, ArchVariant: variant1, Distribution: (Name: , Version: )'") + h.AssertError(t, err, "invalid base image target: 'OS: linux, Arch: amd64, ArchVariant: variant2' is not equal to 'OS: linux, Arch: amd64, ArchVariant: variant1'") }) it("returns an error and prevents the rebase from taking place when the io.buildpacks.distribution.name are different", func() {