diff --git a/api/apis.go b/api/apis.go index 465854eda..02dee991a 100644 --- a/api/apis.go +++ b/api/apis.go @@ -6,7 +6,7 @@ import ( var ( Platform = newApisMustParse([]string{"0.3", "0.4", "0.5", "0.6"}, nil) - Buildpack = newApisMustParse([]string{"0.2", "0.3", "0.4", "0.5"}, nil) + Buildpack = newApisMustParse([]string{"0.2", "0.3", "0.4", "0.5", "0.6"}, nil) ) type APIs struct { diff --git a/builder.go b/builder.go index 3b48574cd..4928d5879 100644 --- a/builder.go +++ b/builder.go @@ -76,7 +76,7 @@ func (b *Builder) Build() (*BuildMetadata, error) { return nil, err } - procMap := processMap{} + processMap := newProcessMap() plan := b.Plan var bom []BOMEntry var slices []layers.Slice @@ -94,10 +94,19 @@ func (b *Builder) Build() (*BuildMetadata, error) { return nil, err } + updateDefaultProcesses(br.Processes, api.MustParse(bp.API), b.PlatformAPI) + bom = append(bom, br.BOM...) labels = append(labels, br.Labels...) plan = plan.filter(br.MetRequires) - procMap.add(br.Processes) + + warning := processMap.add(br.Processes) + + if warning != "" { + if _, err := b.Out.Write([]byte(warning)); err != nil { + return nil, err + } + } slices = append(slices, br.Slices...) } @@ -106,16 +115,31 @@ func (b *Builder) Build() (*BuildMetadata, error) { bom[i].convertMetadataToVersion() } } + procList := processMap.list() return &BuildMetadata{ - BOM: bom, - Buildpacks: b.Group.Group, - Labels: labels, - Processes: procMap.list(), - Slices: slices, + BOM: bom, + Buildpacks: b.Group.Group, + Labels: labels, + Processes: procList, + Slices: slices, + BuildpackDefaultProcessType: processMap.defaultType, }, nil } +// we set default = true for web processes when platformAPI >= 0.6 and buildpackAPI < 0.6 +func updateDefaultProcesses(processes []launch.Process, buildpackAPI *api.Version, platformAPI *api.Version) { + if platformAPI.Compare(api.MustParse("0.6")) < 0 || buildpackAPI.Compare(api.MustParse("0.6")) >= 0 { + return + } + + for i := range processes { + if processes[i].Type == "web" { + processes[i].Default = true + } + } +} + func (b *Builder) BuildConfig() (BuildConfig, error) { appDir, err := filepath.Abs(b.AppDir) if err != nil { @@ -175,25 +199,51 @@ func containsEntry(metRequires []string, entry BuildPlanEntry) bool { return false } -type processMap map[string]launch.Process +type processMap struct { + typeToProcess map[string]launch.Process + defaultType string +} + +func newProcessMap() processMap { + return processMap{ + typeToProcess: make(map[string]launch.Process), + defaultType: "", + } +} -func (m processMap) add(l []launch.Process) { - for _, proc := range l { - m[proc.Type] = proc +// This function adds the processes from listToAdd to processMap +// it sets m.defaultType to the last default process +// if a non-default process overrides a default process, it returns a warning and unset m.defaultType +func (m *processMap) add(listToAdd []launch.Process) string { + warning := "" + for _, procToAdd := range listToAdd { + if procToAdd.Default { + m.defaultType = procToAdd.Type + warning = "" + } else if procToAdd.Type == m.defaultType { + // non-default process overrides a default process + m.defaultType = "" + warning = fmt.Sprintf("Warning: redefining the following default process type with a process not marked as default: %s\n", procToAdd.Type) + } + m.typeToProcess[procToAdd.Type] = procToAdd } + return warning } +// list returns a sorted array of processes. +// The array is sorted based on the process types. +// The list is sorted for reproducibility. func (m processMap) list() []launch.Process { var keys []string - for key := range m { - keys = append(keys, key) + for proc := range m.typeToProcess { + keys = append(keys, proc) } sort.Strings(keys) - procs := []launch.Process{} + result := []launch.Process{} for _, key := range keys { - procs = append(procs, m[key]) + result = append(result, m.typeToProcess[key].NoDefault()) // we set the default to false so it won't be part of metadata.toml } - return procs + return result } func (bom *BOMEntry) convertMetadataToVersion() { diff --git a/builder_test.go b/builder_test.go index f2cf5d409..d7d4988f7 100644 --- a/builder_test.go +++ b/builder_test.go @@ -68,8 +68,8 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { Env: mockEnv, Group: lifecycle.BuildpackGroup{ Group: []lifecycle.GroupBuildpack{ - {ID: "A", Version: "v1", API: "0.5", Homepage: "Buildpack A Homepage"}, - {ID: "B", Version: "v2", API: "0.2"}, + {ID: "A", Version: "v1", API: latestBuildpackAPI.String(), Homepage: "Buildpack A Homepage"}, + {ID: "B", Version: "v2", API: latestBuildpackAPI.String()}, }, }, Out: stdout, @@ -220,8 +220,8 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { t.Fatalf("Unexpected error:\n%s\n", err) } if s := cmp.Diff(metadata.Buildpacks, []lifecycle.GroupBuildpack{ - {ID: "A", Version: "v1", API: "0.5", Homepage: "Buildpack A Homepage"}, - {ID: "B", Version: "v2", API: "0.2"}, + {ID: "A", Version: "v1", API: latestBuildpackAPI.String(), Homepage: "Buildpack A Homepage"}, + {ID: "B", Version: "v2", API: latestBuildpackAPI.String()}, }); s != "" { t.Fatalf("Unexpected:\n%s\n", s) } @@ -334,6 +334,273 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { }); s != "" { t.Fatalf("Unexpected:\n%s\n", s) } + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "") + }) + + when("multiple default process types", func() { + it.Before(func() { + builder.Group.Group = []lifecycle.GroupBuildpack{ + {ID: "A", Version: "v1", API: latestBuildpackAPI.String()}, + {ID: "B", Version: "v2", API: latestBuildpackAPI.String()}, + {ID: "C", Version: "v3", API: latestBuildpackAPI.String()}, + } + }) + + it("last default process type wins", func() { + bpA := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("A", "v1").Return(bpA, nil) + bpA.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "override-type", + Command: "bpA-command", + Args: []string{"bpA-arg"}, + Direct: true, + BuildpackID: "A", + Default: true, + }, + }, + }, nil) + bpB := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("B", "v2").Return(bpB, nil) + bpB.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "some-type", + Command: "bpB-command", + Args: []string{"bpB-arg"}, + Direct: false, + BuildpackID: "B", + Default: true, + }, + }, + }, nil) + + bpC := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("C", "v3").Return(bpC, nil) + bpC.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "override-type", + Command: "bpC-command", + Args: []string{"bpC-arg"}, + Direct: false, + BuildpackID: "C", + }, + }, + }, nil) + + metadata, err := builder.Build() + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + + if s := cmp.Diff(metadata.Processes, []launch.Process{ + { + Type: "override-type", + Command: "bpC-command", + Args: []string{"bpC-arg"}, + Direct: false, + BuildpackID: "C", + }, + { + Type: "some-type", + Command: "bpB-command", + Args: []string{"bpB-arg"}, + Direct: false, + BuildpackID: "B", + }, + }); s != "" { + t.Fatalf("Unexpected:\n%s\n", s) + } + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "some-type") + }) + }) + + when("overriding default process type, with a non-default process type", func() { + it.Before(func() { + builder.Group.Group = []lifecycle.GroupBuildpack{ + {ID: "A", Version: "v1", API: latestBuildpackAPI.String()}, + {ID: "B", Version: "v2", API: latestBuildpackAPI.String()}, + {ID: "C", Version: "v3", API: latestBuildpackAPI.String()}, + } + }) + + it("should warn and not set any default process", func() { + bpB := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("A", "v1").Return(bpB, nil) + bpB.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "some-type", + Command: "bpA-command", + Args: []string{"bpA-arg"}, + Direct: false, + BuildpackID: "A", + Default: true, + }, + }, + }, nil) + + bpA := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("B", "v2").Return(bpA, nil) + bpA.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "override-type", + Command: "bpB-command", + Args: []string{"bpB-arg"}, + Direct: true, + BuildpackID: "B", + Default: true, + }, + }, + }, nil) + + bpC := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("C", "v3").Return(bpC, nil) + bpC.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "override-type", + Command: "bpC-command", + Args: []string{"bpC-arg"}, + Direct: false, + BuildpackID: "C", + }, + }, + }, nil) + + metadata, err := builder.Build() + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + if s := cmp.Diff(metadata.Processes, []launch.Process{ + { + Type: "override-type", + Command: "bpC-command", + Args: []string{"bpC-arg"}, + Direct: false, + BuildpackID: "C", + }, + { + Type: "some-type", + Command: "bpA-command", + Args: []string{"bpA-arg"}, + Direct: false, + BuildpackID: "A", + }, + }); s != "" { + t.Fatalf("Unexpected:\n%s\n", s) + } + + expected := "Warning: redefining the following default process type with a process not marked as default: override-type" + h.AssertStringContains(t, stdout.String(), expected) + + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "") + }) + }) + + when("there is a web process", func() { + when("buildpack API >= 0.6", func() { + it.Before(func() { + builder.Group.Group = []lifecycle.GroupBuildpack{ + {ID: "A", Version: "v1", API: latestBuildpackAPI.String()}, + } + }) + it("shouldn't set it as a default process", func() { + bpA := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("A", "v1").Return(bpA, nil) + bpA.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + }, + }, nil) + + metadata, err := builder.Build() + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + + if s := cmp.Diff(metadata.Processes, []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + }); s != "" { + t.Fatalf("Unexpected:\n%s\n", s) + } + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "") + }) + }) + + when("buildpack api < 0.6", func() { + it.Before(func() { + builder.Group.Group = []lifecycle.GroupBuildpack{ + {ID: "A", Version: "v1", API: "0.5"}, + } + }) + it("should set it as a default process", func() { + bpA := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("A", "v1").Return(bpA, nil) + bpA.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + { + Type: "not-web", + Command: "not-web-cmd", + Args: []string{"not-web-arg"}, + Direct: true, + BuildpackID: "A", + Default: false, + }, + }, + }, nil) + + metadata, err := builder.Build() + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + + if s := cmp.Diff(metadata.Processes, []launch.Process{ + { + Type: "not-web", + Command: "not-web-cmd", + Args: []string{"not-web-arg"}, + Direct: true, + BuildpackID: "A", + }, + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + }, + }); s != "" { + t.Fatalf("Unexpected:\n%s\n", s) + } + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "web") + }) + }) }) }) @@ -454,5 +721,100 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("platform api < 0.6", func() { + it.Before(func() { + builder.PlatformAPI = api.MustParse("0.5") + }) + + when("there is a web process", func() { + when("buildpack API >= 0.6", func() { + it.Before(func() { + builder.Group.Group = []lifecycle.GroupBuildpack{ + {ID: "A", Version: "v1", API: latestBuildpackAPI.String()}, + } + }) + it("shouldn't set it as a default process", func() { + bpA := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("A", "v1").Return(bpA, nil) + bpA.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + }, + }, nil) + + metadata, err := builder.Build() + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + + if s := cmp.Diff(metadata.Processes, []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + }); s != "" { + t.Fatalf("Unexpected:\n%s\n", s) + } + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "") + }) + }) + + when("buildpack api < 0.6", func() { + it.Before(func() { + builder.Group.Group = []lifecycle.GroupBuildpack{ + {ID: "A", Version: "v1", API: "0.5"}, + } + }) + + it("shouldn't set it as a default process", func() { + bpA := testmock.NewMockBuildpack(mockCtrl) + buildpackStore.EXPECT().Lookup("A", "v1").Return(bpA, nil) + bpA.EXPECT().Build(gomock.Any(), config).Return(lifecycle.BuildResult{ + Processes: []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + }, + }, nil) + + metadata, err := builder.Build() + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + + if s := cmp.Diff(metadata.Processes, []launch.Process{ + { + Type: "web", + Command: "web-cmd", + Args: []string{"web-arg"}, + Direct: false, + BuildpackID: "A", + Default: false, + }, + }); s != "" { + t.Fatalf("Unexpected:\n%s\n", s) + } + h.AssertEq(t, metadata.BuildpackDefaultProcessType, "") + }) + }) + }) + }) }) } diff --git a/buildpacktoml.go b/buildpacktoml.go index 7a25654df..c9cbd2d39 100644 --- a/buildpacktoml.go +++ b/buildpacktoml.go @@ -3,10 +3,12 @@ package lifecycle import ( "errors" "fmt" + "io" "io/ioutil" "os" "os/exec" "path/filepath" + "strings" "github.com/BurntSushi/toml" @@ -84,7 +86,7 @@ func (b *BuildpackTOML) Build(bpPlan BuildpackPlan, config BuildConfig) (BuildRe return BuildResult{}, err } - return b.readOutputFiles(bpLayersDir, bpPlanPath, bpPlan) + return b.readOutputFiles(bpLayersDir, bpPlanPath, bpPlan, config.Out) } func preparePaths(bpID string, bpPlan BuildpackPlan, layersDir, planDir string) (string, string, error) { @@ -181,7 +183,7 @@ func isBuild(path string) bool { return err == nil && layerTOML.Build } -func (b *BuildpackTOML) readOutputFiles(bpLayersDir, bpPlanPath string, bpPlanIn BuildpackPlan) (BuildResult, error) { +func (b *BuildpackTOML) readOutputFiles(bpLayersDir, bpPlanPath string, bpPlanIn BuildpackPlan, out io.Writer) (BuildResult, error) { br := BuildResult{} bpFromBpInfo := GroupBuildpack{ID: b.Buildpack.ID, Version: b.Buildpack.Version} @@ -243,6 +245,14 @@ func (b *BuildpackTOML) readOutputFiles(bpLayersDir, bpPlanPath string, bpPlanIn br.BOM = withBuildpack(bpFromBpInfo, launchTOML.BOM) } + if err := overrideDefaultForOldBuildpacks(launchTOML.Processes, b.API, out); err != nil { + return BuildResult{}, err + } + + if err := validateNoMultipleDefaults(launchTOML.Processes); err != nil { + return BuildResult{}, err + } + // set data from launch.toml br.Labels = append([]Label{}, launchTOML.Labels...) for i := range launchTOML.Processes { @@ -254,6 +264,39 @@ func (b *BuildpackTOML) readOutputFiles(bpLayersDir, bpPlanPath string, bpPlanIn return br, nil } +func overrideDefaultForOldBuildpacks(processes []launch.Process, bpAPI string, out io.Writer) error { + if api.MustParse(bpAPI).Compare(api.MustParse("0.6")) >= 0 { + return nil + } + replacedDefaults := []string{} + for i := range processes { + if processes[i].Default { + replacedDefaults = append(replacedDefaults, processes[i].Type) + } + processes[i].Default = false + } + if len(replacedDefaults) > 0 { + warning := fmt.Sprintf("Warning: default processes aren't supported in this buildpack api version. Overriding the default value to false for the following processes: [%s]", strings.Join(replacedDefaults, ", ")) + if _, err := out.Write([]byte(warning)); err != nil { + return err + } + } + return nil +} + +func validateNoMultipleDefaults(processes []launch.Process) error { + defaultType := "" + for _, process := range processes { + if process.Default && defaultType != "" { + return fmt.Errorf("multiple default process types aren't allowed") + } + if process.Default { + defaultType = process.Type + } + } + return nil +} + func validateBOM(bom []BOMEntry, bpAPI string) error { if api.MustParse(bpAPI).Compare(api.MustParse("0.5")) < 0 { for _, entry := range bom { diff --git a/buildpacktoml_test.go b/buildpacktoml_test.go index 39adcb7ae..258b98427 100644 --- a/buildpacktoml_test.go +++ b/buildpacktoml_test.go @@ -286,32 +286,36 @@ func testBuildpackTOML(t *testing.T, when spec.G, it spec.S) { } }) - it("should include processes", func() { - h.Mkfile(t, - `[[processes]]`+"\n"+ - `type = "some-type"`+"\n"+ - `command = "some-cmd"`+"\n"+ + when("processes", func() { + it("should include processes and use the default value that is set", func() { + h.Mkfile(t, `[[processes]]`+"\n"+ - `type = "other-type"`+"\n"+ - `command = "other-cmd"`+"\n", - filepath.Join(appDir, "launch-A-v1.toml"), - ) - br, err := bpTOML.Build(lifecycle.BuildpackPlan{}, config) - if err != nil { - t.Fatalf("Unexpected error:\n%s\n", err) - } - if s := cmp.Diff(br, lifecycle.BuildResult{ - BOM: nil, - Labels: []lifecycle.Label{}, - MetRequires: nil, - Processes: []launch.Process{ - {Type: "some-type", Command: "some-cmd", BuildpackID: "A"}, - {Type: "other-type", Command: "other-cmd", BuildpackID: "A"}, - }, - Slices: []layers.Slice{}, - }); s != "" { - t.Fatalf("Unexpected metadata:\n%s\n", s) - } + `type = "some-type"`+"\n"+ + `command = "some-cmd"`+"\n"+ + `default = true`+"\n"+ + `[[processes]]`+"\n"+ + `type = "web"`+"\n"+ + `command = "other-cmd"`+"\n", + // default is false and therefore doesn't appear + filepath.Join(appDir, "launch-A-v1.toml"), + ) + br, err := bpTOML.Build(lifecycle.BuildpackPlan{}, config) + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + if s := cmp.Diff(br, lifecycle.BuildResult{ + BOM: nil, + Labels: []lifecycle.Label{}, + MetRequires: nil, + Processes: []launch.Process{ + {Type: "some-type", Command: "some-cmd", BuildpackID: "A", Default: true}, + {Type: "web", Command: "other-cmd", BuildpackID: "A", Default: false}, + }, + Slices: []layers.Slice{}, + }); s != "" { + t.Fatalf("Unexpected metadata:\n%s\n", s) + } + }) }) it("should include slices", func() { @@ -523,6 +527,52 @@ func testBuildpackTOML(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("there is more than one default=true process", func() { + it.Before(func() { + mockEnv.EXPECT().WithPlatform(platformDir).Return(append(os.Environ(), "TEST_ENV=Av1"), nil) + }) + + when("the processes are with the same type", func() { + it("should error", func() { + h.Mkfile(t, + `[[processes]]`+"\n"+ + `type = "some-type"`+"\n"+ + `command = "some-cmd"`+"\n"+ + `default = true`+"\n"+ + `[[processes]]`+"\n"+ + `type = "some-type"`+"\n"+ + `command = "some-other-cmd"`+"\n"+ + `default = true`+"\n", + filepath.Join(appDir, "launch-A-v1.toml"), + ) + _, err := bpTOML.Build(lifecycle.BuildpackPlan{}, config) + h.AssertNotNil(t, err) + expected := "multiple default process types aren't allowed" + h.AssertStringContains(t, err.Error(), expected) + }) + }) + + when("the processes are with different types", func() { + it("should error", func() { + h.Mkfile(t, + `[[processes]]`+"\n"+ + `type = "some-type"`+"\n"+ + `command = "some-cmd"`+"\n"+ + `default = true`+"\n"+ + `[[processes]]`+"\n"+ + `type = "other-type"`+"\n"+ + `command = "other-cmd"`+"\n"+ + `default = true`+"\n", + filepath.Join(appDir, "launch-A-v1.toml"), + ) + _, err := bpTOML.Build(lifecycle.BuildpackPlan{}, config) + h.AssertNotNil(t, err) + expected := "multiple default process types aren't allowed" + h.AssertStringContains(t, err.Error(), expected) + }) + }) + }) }) when("buildpack api = 0.2", func() { @@ -767,6 +817,44 @@ func testBuildpackTOML(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("buildpack api < 0.6", func() { + it.Before(func() { + bpTOML.API = "0.5" + mockEnv.EXPECT().WithPlatform(platformDir).Return(append(os.Environ(), "TEST_ENV=Av1"), nil) + }) + + it("should include processes and set/override their default value to false", func() { + h.Mkfile(t, + `[[processes]]`+"\n"+ + `type = "type-with-no-default"`+"\n"+ + `command = "some-cmd"`+"\n"+ + `[[processes]]`+"\n"+ + `type = "type-with-default"`+"\n"+ + `command = "other-cmd"`+"\n"+ + `default = true`+"\n", + filepath.Join(appDir, "launch-A-v1.toml"), + ) + br, err := bpTOML.Build(lifecycle.BuildpackPlan{}, config) + if err != nil { + t.Fatalf("Unexpected error:\n%s\n", err) + } + if s := cmp.Diff(br, lifecycle.BuildResult{ + BOM: nil, + Labels: []lifecycle.Label{}, + MetRequires: nil, + Processes: []launch.Process{ + {Type: "type-with-no-default", Command: "some-cmd", BuildpackID: "A", Default: false}, + {Type: "type-with-default", Command: "other-cmd", BuildpackID: "A", Default: false}, + }, + Slices: []layers.Slice{}, + }); s != "" { + t.Fatalf("Unexpected metadata:\n%s\n", s) + } + expected := "Warning: default processes aren't supported in this buildpack api version. Overriding the default value to false for the following processes: [type-with-default]" + h.AssertStringContains(t, stdout.String(), expected) + }) + }) }) } diff --git a/exporter.go b/exporter.go index 285df0b94..d6185f893 100644 --- a/exporter.go +++ b/exporter.go @@ -136,7 +136,7 @@ func (e *Exporter) Export(opts ExportOptions) (ExportReport, error) { } } - entrypoint, err := e.entrypoint(buildMD.toLaunchMD(), opts.DefaultProcessType) + entrypoint, err := e.entrypoint(buildMD.toLaunchMD(), opts.DefaultProcessType, buildMD.BuildpackDefaultProcessType) if err != nil { return ExportReport{}, errors.Wrap(err, "determining entrypoint") } @@ -379,24 +379,35 @@ func (e *Exporter) setWorkingDir(opts ExportOptions) error { return opts.WorkingImage.SetWorkingDir(opts.AppDir) } -func (e *Exporter) entrypoint(launchMD launch.Metadata, defaultProcessType string) (string, error) { +func (e *Exporter) entrypoint(launchMD launch.Metadata, userDefaultProcessType, buildpackDefaultProcessType string) (string, error) { if !e.supportsMulticallLauncher() { return launch.LauncherPath, nil } - if defaultProcessType == "" { - if len(launchMD.Processes) == 1 { - e.Logger.Infof("Setting default process type '%s'", launchMD.Processes[0].Type) - return launch.ProcessPath(launchMD.Processes[0].Type), nil + + if userDefaultProcessType == "" && e.PlatformAPI.Compare(api.MustParse("0.6")) < 0 && len(launchMD.Processes) == 1 { + // if there is only one process, we set it to the default for platform API < 0.6 + e.Logger.Infof("Setting default process type '%s'", launchMD.Processes[0].Type) + return launch.ProcessPath(launchMD.Processes[0].Type), nil + } + + if userDefaultProcessType != "" { + defaultProcess, ok := launchMD.FindProcessType(userDefaultProcessType) + if !ok { + if e.PlatformAPI.Compare(api.MustParse("0.6")) < 0 { + e.Logger.Warn(processTypeWarning(launchMD, userDefaultProcessType)) + return launch.LauncherPath, nil + } + return "", fmt.Errorf("tried to set %s to default but it doesn't exist", userDefaultProcessType) } - return launch.LauncherPath, nil + e.Logger.Infof("Setting default process type '%s'", defaultProcess.Type) + return launch.ProcessPath(defaultProcess.Type), nil } - defaultProcess, ok := launchMD.FindProcessType(defaultProcessType) - if !ok { - e.Logger.Warn(processTypeWarning(launchMD, defaultProcessType)) + if buildpackDefaultProcessType == "" { + e.Logger.Info("no default process type") return launch.LauncherPath, nil } - e.Logger.Infof("Setting default process type '%s'", defaultProcess.Type) - return launch.ProcessPath(defaultProcess.Type), nil + e.Logger.Infof("Setting default process type '%s'", buildpackDefaultProcessType) + return launch.ProcessPath(buildpackDefaultProcessType), nil } // processTypes adds diff --git a/exporter_test.go b/exporter_test.go index 205146126..bf81a14e0 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -918,65 +918,100 @@ version = "4.5.6" h.AssertEq(t, val, opts.AppDir) }) - when("default process type is set", func() { - it.Before(func() { - opts.DefaultProcessType = "some-process-type" - }) + it("sets empty CMD", func() { + _, err := exporter.Export(opts) + h.AssertNil(t, err) + + val, err := fakeAppImage.Cmd() + h.AssertNil(t, err) + h.AssertEq(t, val, []string(nil)) + }) + + it("saves the image for all provided AdditionalNames", func() { + _, err := exporter.Export(opts) + h.AssertNil(t, err) + h.AssertContains(t, fakeAppImage.SavedNames(), append(opts.AdditionalNames, fakeAppImage.Name())...) + }) + }) - when("platform API is < 0.4", func() { + when("default process", func() { + when("-process-type is set", func() { + when("it is set to an existing type", func() { it.Before(func() { - exporter.PlatformAPI = api.MustParse("0.3") + opts.DefaultProcessType = "some-process-type" + h.RecursiveCopy(t, filepath.Join("testdata", "exporter", "default-process", "metadata-with-no-default", "layers"), opts.LayersDir) }) - it("sets CNB_PROCESS_TYPE", func() { + it("sets the ENTRYPOINT to this process type", func() { _, err := exporter.Export(opts) h.AssertNil(t, err) - val, err := fakeAppImage.Env("CNB_PROCESS_TYPE") - h.AssertNil(t, err) - h.AssertEq(t, val, "some-process-type") + assertHasEntrypoint(t, fakeAppImage, filepath.Join(rootDir, "cnb", "process", "some-process-type"+execExt)) }) - it("sets ENTRYPOINT to launcher", func() { + it("doesn't set CNB_PROCESS_TYPE", func() { _, err := exporter.Export(opts) h.AssertNil(t, err) - ep, err := fakeAppImage.Entrypoint() + val, err := fakeAppImage.Env("CNB_PROCESS_TYPE") h.AssertNil(t, err) - h.AssertEq(t, len(ep), 1) - if runtime.GOOS == "windows" { - h.AssertEq(t, ep[0], `c:\cnb\lifecycle\launcher.exe`) - } else { - h.AssertEq(t, ep[0], `/cnb/lifecycle/launcher`) - } + h.AssertEq(t, val, "") }) + }) - when("default process type is not in metadata.toml", func() { - it("returns an error", func() { - opts.DefaultProcessType = "some-missing-process" - _, err := exporter.Export(opts) - h.AssertError(t, err, "default process type 'some-missing-process' not present in list [some-process-type]") - }) + when("it is set to a process type that doesn't exist", func() { + it.Before(func() { + opts.DefaultProcessType = "some-non-existing-process-type" + h.RecursiveCopy(t, filepath.Join("testdata", "exporter", "default-process", "metadata-with-no-default", "layers"), opts.LayersDir) + }) + it("fails", func() { + _, err := exporter.Export(opts) + h.AssertError(t, err, "tried to set some-non-existing-process-type to default but it doesn't exist") }) }) + }) - when("platform API is >= 0.4", func() { - it("sets the ENTRYPOINT to the default process", func() { - opts.DefaultProcessType = "some-process-type" + when("-process-type is not set", func() { + when("buildpack-default-process-type is not set in metadata.toml", func() { + it.Before(func() { + h.RecursiveCopy(t, filepath.Join("testdata", "exporter", "default-process", "metadata-with-no-default", "layers"), opts.LayersDir) + }) + + it("send an info message that there is no default process, and sets the ENTRYPOINT to the launcher", func() { _, err := exporter.Export(opts) h.AssertNil(t, err) + assertLogEntry(t, logHandler, "no default process type") + assertHasEntrypoint(t, fakeAppImage, filepath.Join(rootDir, "cnb", "lifecycle", "launcher"+execExt)) + }) + }) - ep, err := fakeAppImage.Entrypoint() + when("buildpack-default-process-type is set in metadata.toml", func() { + it.Before(func() { + h.RecursiveCopy(t, filepath.Join("testdata", "exporter", "default-process", "metadata-with-default", "layers"), opts.LayersDir) + layerFactory.EXPECT(). + ProcessTypesLayer(launch.Metadata{ + Processes: []launch.Process{ + { + Type: "some-process-type", + Command: "/some/command", + Args: []string{"some", "command", "args"}, + Direct: true, + BuildpackID: "buildpack.id", + }}, + }). + DoAndReturn(func(_ launch.Metadata) (layers.Layer, error) { + return createTestLayer("process-types", tmpDir) + }). + AnyTimes() + }) + + it("sets the ENTRYPOINT to this process type", func() { + _, err := exporter.Export(opts) h.AssertNil(t, err) - h.AssertEq(t, len(ep), 1) - if runtime.GOOS == "windows" { - h.AssertEq(t, ep[0], `c:\cnb\process\some-process-type.exe`) - } else { - h.AssertEq(t, ep[0], `/cnb/process/some-process-type`) - } + assertHasEntrypoint(t, fakeAppImage, filepath.Join(rootDir, "cnb", "process", "some-process-type"+execExt)) }) - it("does not set CNB_PROCESS_TYPE", func() { + it("doesn't set CNB_PROCESS_TYPE", func() { _, err := exporter.Export(opts) h.AssertNil(t, err) @@ -984,90 +1019,68 @@ version = "4.5.6" h.AssertNil(t, err) h.AssertEq(t, val, "") }) + }) + }) - when("default process type is not in metadata.toml", func() { - it("warns and sets the ENTRYPOINT to launcher", func() { - opts.DefaultProcessType = "some-missing-process" - _, err := exporter.Export(opts) - h.AssertNil(t, err) + when("platform API < 0.6", func() { + it.Before(func() { + exporter.PlatformAPI = api.MustParse("0.5") + h.RecursiveCopy(t, filepath.Join("testdata", "exporter", "default-process", "metadata-with-no-default", "layers"), opts.LayersDir) + }) - assertLogEntry(t, logHandler, "default process type 'some-missing-process' not present in list [some-process-type]") - ep, err := fakeAppImage.Entrypoint() - h.AssertNil(t, err) - h.AssertEq(t, len(ep), 1) - if runtime.GOOS == "windows" { - h.AssertEq(t, ep[0], `c:\cnb\lifecycle\launcher.exe`) - } else { - h.AssertEq(t, ep[0], `/cnb/lifecycle/launcher`) - } - }) + when("-process-type is set to a process type that doesn't exist", func() { + it.Before(func() { + opts.DefaultProcessType = "some-non-existing-process-type" + }) + it("warns the process type doesn't exist, and sets the ENTRYPOINT to the launcher", func() { + _, err := exporter.Export(opts) + h.AssertNil(t, err) + assertLogEntry(t, logHandler, "default process type 'some-non-existing-process-type' not present in list [some-process-type]") + assertHasEntrypoint(t, fakeAppImage, filepath.Join(rootDir, "cnb", "lifecycle", "launcher"+execExt)) }) }) - }) - - when("default process type is empty", func() { - when("platform API is >= 0.4", func() { - when("there is exactly one process", func() { - it("sets the ENTRYPOINT to the only process", func() { - _, err := exporter.Export(opts) - h.AssertNil(t, err) - ep, err := fakeAppImage.Entrypoint() - h.AssertNil(t, err) - h.AssertEq(t, len(ep), 1) - if runtime.GOOS == "windows" { - h.AssertEq(t, ep[0], `c:\cnb\process\some-process-type.exe`) - } else { - h.AssertEq(t, ep[0], `/cnb/process/some-process-type`) - } - }) + when("-process-type is not set and there is exactly one process", func() { + it("sets the ENTRYPOINT to the only process", func() { + _, err := exporter.Export(opts) + h.AssertNil(t, err) + assertHasEntrypoint(t, fakeAppImage, filepath.Join(rootDir, "cnb", "process", "some-process-type"+execExt)) }) }) + }) - when("platform API is < 0.4", func() { + when("platform API < 0.4", func() { + it.Before(func() { + exporter.PlatformAPI = api.MustParse("0.3") + h.RecursiveCopy(t, filepath.Join("testdata", "exporter", "default-process", "metadata-with-no-default", "layers"), opts.LayersDir) + }) + + when("-process-type is set to an existing process type", func() { it.Before(func() { - exporter.PlatformAPI = api.MustParse("0.3") + opts.DefaultProcessType = "some-process-type" }) - it("does not set CNB_PROCESS_TYPE", func() { + it("sets CNB_PROCESS_TYPE", func() { _, err := exporter.Export(opts) h.AssertNil(t, err) val, err := fakeAppImage.Env("CNB_PROCESS_TYPE") h.AssertNil(t, err) - h.AssertEq(t, val, "") + h.AssertEq(t, val, "some-process-type") }) + }) - it("sets ENTRYPOINT to launcher", func() { + when("-process-type is not set", func() { + it("doesn't set CNB_PROCESS_TYPE", func() { _, err := exporter.Export(opts) h.AssertNil(t, err) - ep, err := fakeAppImage.Entrypoint() + val, err := fakeAppImage.Env("CNB_PROCESS_TYPE") h.AssertNil(t, err) - h.AssertEq(t, len(ep), 1) - if runtime.GOOS == "windows" { - h.AssertEq(t, ep[0], `c:\cnb\lifecycle\launcher.exe`) - } else { - h.AssertEq(t, ep[0], `/cnb/lifecycle/launcher`) - } + h.AssertEq(t, val, "") }) }) }) - - it("sets empty CMD", func() { - _, err := exporter.Export(opts) - h.AssertNil(t, err) - - val, err := fakeAppImage.Cmd() - h.AssertNil(t, err) - h.AssertEq(t, val, []string(nil)) - }) - - it("saves the image for all provided AdditionalNames", func() { - _, err := exporter.Export(opts) - h.AssertNil(t, err) - h.AssertContains(t, fakeAppImage.SavedNames(), append(opts.AdditionalNames, fakeAppImage.Name())...) - }) }) when("report.toml", func() { @@ -1307,6 +1320,13 @@ version = "4.5.6" }) } +func assertHasEntrypoint(t *testing.T, image *fakes.Image, entrypointPath string) { + ep, err := image.Entrypoint() + h.AssertNil(t, err) + h.AssertEq(t, len(ep), 1) + h.AssertEq(t, ep[0], entrypointPath) +} + func createTestLayer(id string, tmpDir string) (layers.Layer, error) { tarPath := filepath.Join(tmpDir, "artifacts", strings.Replace(id, "/", "_", -1)) f, err := os.Create(tarPath) diff --git a/launch/launch.go b/launch/launch.go index dacff7c6e..3d0c3e408 100644 --- a/launch/launch.go +++ b/launch/launch.go @@ -11,9 +11,15 @@ type Process struct { Command string `toml:"command" json:"command"` Args []string `toml:"args" json:"args"` Direct bool `toml:"direct" json:"direct"` + Default bool `toml:"default,omitempty" json:"default,omitempty"` BuildpackID string `toml:"buildpack-id" json:"buildpackID"` } +func (p Process) NoDefault() Process { + p.Default = false + return p +} + // ProcessPath returns the absolute path to the symlink for a given process type func ProcessPath(pType string) string { return filepath.Join(ProcessDir, pType+exe) diff --git a/lifecycle.toml b/lifecycle.toml index 7e80b91ab..ca7575982 100644 --- a/lifecycle.toml +++ b/lifecycle.toml @@ -1,7 +1,7 @@ [apis] [apis.buildpack] deprecated = [] - supported = ["0.2", "0.3", "0.4", "0.5"] + supported = ["0.2", "0.3", "0.4", "0.5", "0.6"] [apis.platform] deprecated = [] supported = ["0.3", "0.4", "0.5", "0.6"] diff --git a/lifecycle_unix_test.go b/lifecycle_unix_test.go new file mode 100644 index 000000000..2adf4f6f2 --- /dev/null +++ b/lifecycle_unix_test.go @@ -0,0 +1,8 @@ +// +build linux darwin + +package lifecycle_test + +const ( + rootDir = "/" + execExt = "" +) diff --git a/lifecycle_windows_test.go b/lifecycle_windows_test.go new file mode 100644 index 000000000..cb7665f0b --- /dev/null +++ b/lifecycle_windows_test.go @@ -0,0 +1,6 @@ +package lifecycle_test + +const ( + rootDir = `c:\` + execExt = ".exe" +) diff --git a/metadata.go b/metadata.go index 0ad0f7ef2..848d3fd97 100644 --- a/metadata.go +++ b/metadata.go @@ -16,12 +16,13 @@ const ( ) type BuildMetadata struct { - BOM []BOMEntry `toml:"bom" json:"bom"` - Buildpacks []GroupBuildpack `toml:"buildpacks" json:"buildpacks"` - Labels []Label `toml:"labels" json:"-"` - Launcher LauncherMetadata `toml:"-" json:"launcher"` - Processes []launch.Process `toml:"processes" json:"processes"` - Slices []layers.Slice `toml:"slices" json:"-"` + BOM []BOMEntry `toml:"bom" json:"bom"` + Buildpacks []GroupBuildpack `toml:"buildpacks" json:"buildpacks"` + Labels []Label `toml:"labels" json:"-"` + Launcher LauncherMetadata `toml:"-" json:"launcher"` + Processes []launch.Process `toml:"processes" json:"processes"` + Slices []layers.Slice `toml:"slices" json:"-"` + BuildpackDefaultProcessType string `toml:"buildpack-default-process-type,omitempty" json:"buildpack-default-process-type,omitempty"` } type LauncherMetadata struct { diff --git a/testdata/exporter/default-process/metadata-with-default/layers/config/metadata.toml b/testdata/exporter/default-process/metadata-with-default/layers/config/metadata.toml new file mode 100644 index 000000000..b66084d31 --- /dev/null +++ b/testdata/exporter/default-process/metadata-with-default/layers/config/metadata.toml @@ -0,0 +1,8 @@ +buildpack-default-process-type = "some-process-type" + +[[processes]] + type = "some-process-type" + direct = true + command = "/some/command" + args = ["some", "command", "args"] + buildpack-id = "buildpack.id" diff --git a/testdata/exporter/default-process/metadata-with-no-default/layers/config/metadata.toml b/testdata/exporter/default-process/metadata-with-no-default/layers/config/metadata.toml new file mode 100644 index 000000000..61ec8b8fd --- /dev/null +++ b/testdata/exporter/default-process/metadata-with-no-default/layers/config/metadata.toml @@ -0,0 +1,6 @@ +[[processes]] + type = "some-process-type" + direct = true + command = "/some/command" + args = ["some", "command", "args"] + buildpack-id = "buildpack.id"