Skip to content

Commit 176903e

Browse files
ndon55555dfreilich
andcommitted
Resolve conflicting node versions in plans during build [#167707118]
Co-authored-by: David Freilich <dfreilich@pivotal.io>
1 parent ee4a830 commit 176903e

4 files changed

Lines changed: 256 additions & 3 deletions

File tree

cmd/detect/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ func main() {
2929
}
3030

3131
func runDetect(context detect.Detect) (int, error) {
32-
version := ""
32+
var version, versionSource string
3333
nvmrcPath := filepath.Join(context.Application.Root, ".nvmrc")
3434
nvmrcExists, err := helper.FileExists(nvmrcPath)
3535
if err != nil {
3636
return context.Fail(), err
3737
}
3838

3939
if nvmrcExists {
40+
versionSource = ".nvmrc"
4041
version, err = nvmrc.GetVersion(nvmrcPath, context.Logger)
4142
if err != nil {
4243
return context.Fail(), err
@@ -51,6 +52,7 @@ func runDetect(context detect.Detect) (int, error) {
5152
}
5253

5354
if buildpackYamlExists {
55+
versionSource = "buildpack.yml"
5456
bpYml := &node.BuildpackYAML{}
5557
err := helper.ReadBuildpackYaml(buildpackYamlPath, bpYml)
5658
if err != nil {
@@ -68,7 +70,7 @@ func runDetect(context detect.Detect) (int, error) {
6870
Requires: []buildplan.Required{{
6971
Name: node.Dependency,
7072
Version: version,
71-
Metadata: buildplan.Metadata{"launch": true},
73+
Metadata: buildplan.Metadata{"launch": true, "version-source": versionSource},
7274
}},
7375
})
7476
}

integration/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
var (
16-
bp, npmBP string
16+
bp string
1717
)
1818

1919
func TestIntegration(t *testing.T) {

node/node.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
package node
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
67

8+
"github.com/pkg/errors"
9+
10+
"github.com/Masterminds/semver"
11+
12+
"github.com/cloudfoundry/libcfbuildpack/buildpackplan"
13+
714
"github.com/cloudfoundry/libcfbuildpack/build"
815
"github.com/cloudfoundry/libcfbuildpack/helper"
916
"github.com/cloudfoundry/libcfbuildpack/layers"
1017
)
1118

1219
const Dependency = "node"
20+
const VersionSource = "version-source"
1321

1422
type Config struct {
1523
OptimizeMemory bool `yaml:"optimize-memory"`
@@ -138,6 +146,143 @@ func LoadBuildpackYAML(appRoot string) (BuildpackYAML, error) {
138146
return buildpackYAML, err
139147
}
140148

149+
func PriorityPlanMerge(a, b buildpackplan.Plan) (buildpackplan.Plan, error) {
150+
aVersion := a.Version
151+
bVersion := b.Version
152+
aSource := a.Metadata[VersionSource]
153+
bSource := b.Metadata[VersionSource]
154+
155+
if aVersion == "" && bVersion == "" {
156+
return mergePlans(a, b, "", nil)
157+
} else if aVersion == "" {
158+
return mergePlans(a, b, bVersion, bSource)
159+
} else if bVersion == "" {
160+
return mergePlans(a, b, aVersion, aSource)
161+
}
162+
163+
aPriority := getPriority(aSource)
164+
bPriority := getPriority(bSource)
165+
if aPriority > bPriority {
166+
return mergePlans(a, b, aVersion, aSource)
167+
} else if aPriority == bPriority {
168+
version, err := getHighestVersion(aVersion, bVersion)
169+
if err != nil {
170+
return buildpackplan.Plan{}, fmt.Errorf("failed to get the highest version between %s and %s: %v", aVersion, bVersion, err)
171+
}
172+
return mergePlans(a, b, version, aSource)
173+
} else {
174+
return mergePlans(a, b, bVersion, bSource)
175+
}
176+
}
177+
178+
func getHighestVersion(aVersion, bVersion string) (string, error) {
179+
aSemver, err := semver.NewVersion(aVersion)
180+
if err != nil {
181+
return "", fmt.Errorf("failed to convert version %s to semver", aVersion)
182+
}
183+
bSemver, err := semver.NewVersion(bVersion)
184+
if err != nil {
185+
return "", fmt.Errorf("failed to convert version %s to semver", bVersion)
186+
}
187+
version := aVersion
188+
if aSemver.LessThan(bSemver) {
189+
version = bVersion
190+
}
191+
192+
return version, nil
193+
}
194+
195+
func getPriority(versionSource interface{}) int {
196+
priorities := map[interface{}]int{
197+
"buildpack.yml": 3,
198+
"package.json": 2,
199+
".nvmrc": 1,
200+
"": -1,
201+
}
202+
val, ok := priorities[versionSource]
203+
204+
// Any source is higher than empty string
205+
if !ok {
206+
val = 0
207+
}
208+
return val
209+
}
210+
211+
func mergePlans(a, b buildpackplan.Plan, version string, versionSource interface{}) (buildpackplan.Plan, error) {
212+
aBuildVal, err := getBooleanVal(a.Metadata["build"])
213+
if err != nil {
214+
return buildpackplan.Plan{}, errors.Wrapf(err, "could not determine 'build' metadata of %s", a.Name)
215+
}
216+
217+
bBuildVal, err := getBooleanVal(b.Metadata["build"])
218+
if err != nil {
219+
return buildpackplan.Plan{}, errors.Wrapf(err, "could not determine 'build' metadata of %s", b.Name)
220+
}
221+
222+
aLaunchVal, err := getBooleanVal(a.Metadata["launch"])
223+
if err != nil {
224+
return buildpackplan.Plan{}, errors.Wrapf(err, "could not determine 'launch' metadata of %s", a.Name)
225+
}
226+
227+
bLaunchVal, err := getBooleanVal(b.Metadata["launch"])
228+
if err != nil {
229+
return buildpackplan.Plan{}, errors.Wrapf(err, "could not determine 'launch' metadata of %s", b.Name)
230+
}
231+
232+
metadata := a.Metadata // NOTE: Mutating metadata also mutates a.Metadata
233+
for key, val := range b.Metadata {
234+
ignoreKeys := []string{VersionSource, "build", "launch"}
235+
if !contains(ignoreKeys, key) && val != "" {
236+
if aVal, ok := metadata[key]; ok && aVal != "" && aVal != val {
237+
val = aVal.(string) + "," + val.(string)
238+
}
239+
metadata[key] = val
240+
}
241+
}
242+
243+
if versionSource != nil && versionSource != "" {
244+
metadata[VersionSource] = versionSource
245+
}
246+
247+
if aBuildVal || bBuildVal {
248+
metadata["build"] = true
249+
}
250+
251+
if aLaunchVal || bLaunchVal {
252+
metadata["launch"] = true
253+
}
254+
255+
return buildpackplan.Plan{
256+
Name: a.Name,
257+
Version: version,
258+
Metadata: metadata,
259+
}, nil
260+
}
261+
262+
func contains(slice []string, val string) bool {
263+
for _, x := range slice {
264+
if x == val {
265+
return true
266+
}
267+
}
268+
269+
return false
270+
}
271+
272+
func getBooleanVal(val interface{}) (bool, error) {
273+
if val == nil || val == "" {
274+
return false, nil
275+
}
276+
277+
if b, isString := val.(string); isString {
278+
return b == "true", nil
279+
} else if b, isBool := val.(bool); isBool {
280+
return b, nil
281+
}
282+
283+
return false, fmt.Errorf("could not get boolean value of %v", val)
284+
}
285+
141286
func memoryAvailable() string {
142287
return `which jq
143288
if [[ $? -eq 0 ]]; then

node/node_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ func testNode(t *testing.T, when spec.G, it spec.S) {
2626
f *test.BuildFactory
2727
stubNodeFixture = filepath.Join("testdata", "stub-node.tar.gz")
2828
)
29+
var testPriorityMerge = func(planA, planB, expected buildpackplan.Plan) {
30+
output, err := node.PriorityPlanMerge(planA, planB)
31+
Expect(err).NotTo(HaveOccurred())
32+
Expect(output).To(Equal(expected))
33+
}
2934

3035
it.Before(func() {
3136
RegisterTestingT(t)
@@ -186,4 +191,105 @@ export MEMORY_AVAILABLE
186191
})
187192
})
188193
})
194+
195+
when("PriorityPlanMerge", func() {
196+
var (
197+
planA, planB, expected buildpackplan.Plan
198+
)
199+
200+
when("version key is empty for both", func() {
201+
it("leaves the version empty and merges metadata", func() {
202+
planA := createNodeBuildPlan("", buildpackplan.Metadata{"key": "1"})
203+
planB := createNodeBuildPlan("", buildpackplan.Metadata{"key": "2"})
204+
expected := createNodeBuildPlan("", buildpackplan.Metadata{"key": "1,2"})
205+
206+
testPriorityMerge(planA, planB, expected)
207+
})
208+
})
209+
210+
when("version key is empty for one", func() {
211+
it("picks the other version and merges metadata", func() {
212+
planA := createNodeBuildPlan("", buildpackplan.Metadata{"key": "1"})
213+
planB := createNodeBuildPlan("1", buildpackplan.Metadata{"key": "2"})
214+
expected := createNodeBuildPlan("1", buildpackplan.Metadata{"key": "1,2"})
215+
216+
testPriorityMerge(planA, planB, expected)
217+
})
218+
})
219+
220+
when("both have versions and VersionSource key is unset for both", func() {
221+
it("should pick latest version of the two", func() {
222+
planA := createNodeBuildPlan("1.0", buildpackplan.Metadata{})
223+
planB := createNodeBuildPlan("2.0", buildpackplan.Metadata{})
224+
expected := createNodeBuildPlan("2.0", buildpackplan.Metadata{})
225+
testPriorityMerge(planA, planB, expected)
226+
testPriorityMerge(planB, planA, expected)
227+
228+
planA = createNodeBuildPlan("1.0", buildpackplan.Metadata{node.VersionSource: ""})
229+
planB = createNodeBuildPlan("2.0", buildpackplan.Metadata{node.VersionSource: ""})
230+
expected = createNodeBuildPlan("2.0", buildpackplan.Metadata{node.VersionSource: ""})
231+
testPriorityMerge(planA, planB, expected)
232+
testPriorityMerge(planB, planA, expected)
233+
})
234+
235+
})
236+
237+
when("both have versions and VersionSource key is empty for one", func() {
238+
it.Before(func() {
239+
planA = createNodeBuildPlan("2.0", buildpackplan.Metadata{node.VersionSource: ""})
240+
planB = createNodeBuildPlan("1.0", buildpackplan.Metadata{node.VersionSource: "package.json"})
241+
expected = createNodeBuildPlan("1.0", buildpackplan.Metadata{node.VersionSource: "package.json"})
242+
})
243+
it("picks the version and source from the second input", func() {
244+
testPriorityMerge(planA, planB, expected)
245+
})
246+
247+
it("picks the version and source from the first input", func() {
248+
testPriorityMerge(planB, planA, expected)
249+
})
250+
})
251+
252+
when("different priority versions", func() {
253+
it.Before(func() {
254+
planA = createNodeBuildPlan("2.0", buildpackplan.Metadata{node.VersionSource: "package.json"})
255+
planB = createNodeBuildPlan("1.0", buildpackplan.Metadata{node.VersionSource: "buildpack.yml"})
256+
expected = createNodeBuildPlan("1.0", buildpackplan.Metadata{node.VersionSource: "buildpack.yml"})
257+
})
258+
259+
it("picks the version and source from the second input", func() {
260+
testPriorityMerge(planA, planB, expected)
261+
})
262+
263+
it("picks the version and source from the first input", func() {
264+
testPriorityMerge(planB, planA, expected)
265+
})
266+
})
267+
268+
when("they are the same priority", func() {
269+
it("picks the higher version", func() {
270+
planA := createNodeBuildPlan("2.0", buildpackplan.Metadata{node.VersionSource: "buildpack.yml"})
271+
planB := createNodeBuildPlan("1.0", buildpackplan.Metadata{node.VersionSource: "buildpack.yml"})
272+
expected := createNodeBuildPlan("2.0", buildpackplan.Metadata{node.VersionSource: "buildpack.yml"})
273+
testPriorityMerge(planA, planB, expected)
274+
testPriorityMerge(planB, planA, expected)
275+
})
276+
})
277+
278+
when("different build and launch metadata are set", func() {
279+
it("does an or-map of each key", func() {
280+
planA := createNodeBuildPlan("", buildpackplan.Metadata{"build": true})
281+
planB := createNodeBuildPlan("", buildpackplan.Metadata{"launch": "true", "build": false})
282+
expected := createNodeBuildPlan("", buildpackplan.Metadata{"build": true, "launch": true})
283+
testPriorityMerge(planA, planB, expected)
284+
})
285+
})
286+
})
287+
}
288+
289+
func createNodeBuildPlan(version string, metadata buildpackplan.Metadata) buildpackplan.Plan {
290+
return buildpackplan.Plan{
291+
Name: node.Dependency,
292+
Version: version,
293+
Metadata: metadata,
294+
}
189295
}

0 commit comments

Comments
 (0)