Skip to content

Commit

Permalink
Allow new buildpack API to use new launch.toml format (#889)
Browse files Browse the repository at this point in the history
* Ignore .tool-versions for asdf and other tooling

* Allow new buildpack API to use new launch.toml format

- Buildpacks using the newer buildpack API must use the new launch.toml format
- The older format is still allowed on older buildpack API versions

Issue: #870

* Update buildpack/build_test.go

Co-authored-by: Natalie Arellano <narellano@vmware.com>
Signed-off-by: Jesse Brown <jabrown85@gmail.com>

* Update buildpack/build_test.go

Co-authored-by: Natalie Arellano <narellano@vmware.com>
Signed-off-by: Jesse Brown <jabrown85@gmail.com>

* Update buildpack/build_test.go

Co-authored-by: Natalie Arellano <narellano@vmware.com>
Signed-off-by: Jesse Brown <jabrown85@gmail.com>

* Added Buildpack 0.10 as valid buildpack API

Fixed tests that were now invalid due to the default buildpack being 0.10

Signed-off-by: Jesse Brown <jabrown85@gmail.com>

* Apply suggestions from code review

Co-authored-by: Natalie Arellano <narellano@vmware.com>
Signed-off-by: Jesse Brown <jabrown85@gmail.com>

Signed-off-by: Jesse Brown <jabrown85@gmail.com>
Co-authored-by: Natalie Arellano <narellano@vmware.com>
  • Loading branch information
jabrown85 and natalieparellano committed Aug 23, 2022
1 parent 512528c commit 328f07b
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.coverprofile
*.test
*~
.tool-versions
/out

acceptance/testdata/*/**/container/cnb/lifecycle/*
Expand Down
11 changes: 5 additions & 6 deletions buildpack/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func (d *Descriptor) readOutputFilesBp(bpLayersDir, bpPlanPath string, bpPlanIn
}

// read launch.toml, return if not exists
if _, err := toml.DecodeFile(launchPath, &launchTOML); os.IsNotExist(err) {
if err := DecodeLaunchTOML(launchPath, d.API, &launchTOML); os.IsNotExist(err) {
return br, nil
} else if err != nil {
return BuildResult{}, err
Expand Down Expand Up @@ -328,7 +328,7 @@ func (d *Descriptor) readOutputFilesBp(bpLayersDir, bpPlanPath string, bpPlanIn
}

// read launch.toml, return if not exists
if _, err := toml.DecodeFile(launchPath, &launchTOML); os.IsNotExist(err) {
if err := DecodeLaunchTOML(launchPath, d.API, &launchTOML); os.IsNotExist(err) {
return br, nil
} else if err != nil {
return BuildResult{}, err
Expand All @@ -352,15 +352,14 @@ func (d *Descriptor) readOutputFilesBp(bpLayersDir, bpPlanPath string, bpPlanIn
// set data from launch.toml
br.Labels = append([]Label{}, launchTOML.Labels...)
for i := range launchTOML.Processes {
launchTOML.Processes[i].BuildpackID = d.Info().ID
if api.MustParse(d.API).LessThan("0.8") {
if launchTOML.Processes[i].WorkingDirectory != "" {
logger.Warn(fmt.Sprintf("Warning: process working directory isn't supported in this buildpack api version. Ignoring working directory for process '%s'", launchTOML.Processes[i].Type))
launchTOML.Processes[i].WorkingDirectory = ""
}
}
}
br.Processes = append([]launch.Process{}, launchTOML.Processes...)
br.Processes = append([]launch.Process{}, launchTOML.ToLaunchProcessesForBuildpack(d.Info().ID)...)
br.Slices = append([]layers.Slice{}, launchTOML.Slices...)

return br, nil
Expand All @@ -385,7 +384,7 @@ func (d *Descriptor) readOutputFilesExt(extOutputDir string, extPlanIn Plan) (Bu
return br, nil
}

func overrideDefaultForOldBuildpacks(processes []launch.Process, bpAPI string, logger log.Logger) error {
func overrideDefaultForOldBuildpacks(processes []ProcessEntry, bpAPI string, logger log.Logger) error {
if api.MustParse(bpAPI).AtLeast("0.6") {
return nil
}
Expand All @@ -402,7 +401,7 @@ func overrideDefaultForOldBuildpacks(processes []launch.Process, bpAPI string, l
return nil
}

func validateNoMultipleDefaults(processes []launch.Process) error {
func validateNoMultipleDefaults(processes []ProcessEntry) error {
defaultType := ""
for _, process := range processes {
if process.Default && defaultType != "" {
Expand Down
117 changes: 106 additions & 11 deletions buildpack/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,20 +757,20 @@ func testBuild(kind string) func(t *testing.T, when spec.G, it spec.S) {
h.Mkfile(t,
`[[processes]]`+"\n"+
`type = "some-type"`+"\n"+
`command = "some-cmd"`+"\n"+
`command = ["some-cmd"]`+"\n"+
`default = true`+"\n"+
`[[processes]]`+"\n"+
`type = "web"`+"\n"+
`command = "other-cmd"`+"\n",
`command = ["other-cmd"]`+"\n",
// default is false and therefore doesn't appear
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertNil(t, err)

h.AssertEq(t, br.Processes, []launch.Process{
{Type: "some-type", Command: "some-cmd", BuildpackID: "A", Default: true},
{Type: "web", Command: "other-cmd", BuildpackID: "A", Default: false},
{Type: "some-type", Command: "some-cmd", BuildpackID: "A", Default: true, Direct: true},
{Type: "web", Command: "other-cmd", BuildpackID: "A", Default: false, Direct: true},
})
})

Expand All @@ -779,11 +779,11 @@ func testBuild(kind string) func(t *testing.T, when spec.G, it spec.S) {
h.Mkfile(t,
`[[processes]]`+"\n"+
`type = "some-type"`+"\n"+
`command = "some-cmd"`+"\n"+
`command = ["some-cmd"]`+"\n"+
`default = true`+"\n"+
`[[processes]]`+"\n"+
`type = "some-type"`+"\n"+
`command = "some-other-cmd"`+"\n"+
`command = ["some-other-cmd"]`+"\n"+
`default = true`+"\n",
filepath.Join(appDir, "launch-A-v1.toml"),
)
Expand All @@ -797,11 +797,11 @@ func testBuild(kind string) func(t *testing.T, when spec.G, it spec.S) {
h.Mkfile(t,
`[[processes]]`+"\n"+
`type = "some-type"`+"\n"+
`command = "some-cmd"`+"\n"+
`command = ["some-cmd"]`+"\n"+
`default = true`+"\n"+
`[[processes]]`+"\n"+
`type = "other-type"`+"\n"+
`command = "other-cmd"`+"\n"+
`command = ["other-cmd"]`+"\n"+
`default = true`+"\n",
filepath.Join(appDir, "launch-A-v1.toml"),
)
Expand All @@ -812,16 +812,110 @@ func testBuild(kind string) func(t *testing.T, when spec.G, it spec.S) {
})
})

it("sets the working directory", func() {
it("does not allow string command", func() {
h.Mkfile(t,
"[[processes]]\n"+
`working-dir = "/working-directory"`,
`command = "some-cmd"`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
_, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertError(t, err, "toml: incompatible types: TOML key \"processes.command\" has type string; destination has type slice")
})

it("returns extra commands as args before defined args", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = ["some-cmd", "cmd-arg"]`+"\n"+
`args = ["first-arg"]`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].WorkingDirectory, "/working-directory")
h.AssertEq(t, br.Processes[0].Command, "some-cmd")
h.AssertEq(t, br.Processes[0].Args[0], "cmd-arg")
h.AssertEq(t, br.Processes[0].Args[1], "first-arg")
})

it("returns direct=true for processes", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = ["some-cmd", "cmd-arg"]`+"\n"+
`args = ["first-arg"]`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].Command, "some-cmd")
h.AssertEq(t, br.Processes[0].Direct, true)
})

it("does not allow direct flag", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = ["some-cmd"]`+"\n"+
`direct = false`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
_, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertError(t, err, "process.direct is not supported on this buildpack version")
})

when("buildpack api < 0.9", func() {
it.Before(func() {
h.SkipIf(t, kind == buildpack.KindExtension, "")
descriptor.API = "0.8"
})

it("allows setting direct", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = "some-cmd"`+"\n"+
`direct = false`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].Direct, false)
})

it("allows setting a single command string", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = "some-command"`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].Command, "some-command")
})

it("sets the working directory", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = "some-cmd"`+"\n"+
`working-dir = "/working-directory"`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].WorkingDirectory, "/working-directory")
})

it("does not allow commands as list of string", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = ["some-cmd"]`+"\n"+
`direct = false`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
_, err := descriptor.Build(buildpack.Plan{}, config, mockEnv)
h.AssertError(t, err, "toml: incompatible types: TOML key \"processes.command\" has type []interface {}; destination has type string")
})
})
})

Expand Down Expand Up @@ -1351,6 +1445,7 @@ func testBuild(kind string) func(t *testing.T, when spec.G, it spec.S) {
it("ignores process working directory and warns", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = "echo"`+"\n"+
`working-dir = "/working-directory"`+"\n"+
`type = "some-type"`+"\n",
filepath.Join(appDir, "launch-A-v1.toml"),
Expand Down
102 changes: 100 additions & 2 deletions buildpack/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package buildpack
import (
"fmt"

"github.com/BurntSushi/toml"

"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/launch"
"github.com/buildpacks/lifecycle/layers"
)
Expand All @@ -14,8 +17,103 @@ import (
type LaunchTOML struct {
BOM []BOMEntry
Labels []Label
Processes []launch.Process `toml:"processes"`
Slices []layers.Slice `toml:"slices"`
Processes []ProcessEntry `toml:"processes"`
Slices []layers.Slice `toml:"slices"`
}

type ProcessEntry struct {
Type string `toml:"type" json:"type"`
Command []string `toml:"-"` // ignored
RawCommandValue toml.Primitive `toml:"command" json:"command"`
Args []string `toml:"args" json:"args"`
Direct *bool `toml:"direct" json:"direct"`
Default bool `toml:"default,omitempty" json:"default,omitempty"`
WorkingDirectory string `toml:"working-dir,omitempty" json:"working-dir,omitempty"`
}

// DecodeLaunchTOML reads a launch.toml file
func DecodeLaunchTOML(launchPath string, bpAPI string, launchTOML *LaunchTOML) error {
// decode the common bits
md, err := toml.DecodeFile(launchPath, &launchTOML)
if err != nil {
return err
}

// decode the process.commands, which differ based on buildpack API
commandsAreStrings := api.MustParse(bpAPI).LessThan("0.9")

// processes are defined differently depending on API version
// and will be decoded into different values
for i, process := range launchTOML.Processes {
if commandsAreStrings {
var commandString string
if err = md.PrimitiveDecode(process.RawCommandValue, &commandString); err != nil {
return err
}
// legacy Direct defaults to false
if process.Direct == nil {
direct := false
launchTOML.Processes[i].Direct = &direct
}
launchTOML.Processes[i].Command = []string{commandString}
} else {
// direct is no longer allowed as a key
if process.Direct != nil {
return fmt.Errorf("process.direct is not supported on this buildpack version")
}
var command []string
if err = md.PrimitiveDecode(process.RawCommandValue, &command); err != nil {
return err
}
launchTOML.Processes[i].Command = command
}
}

return nil
}

// ToLaunchProcess converts a buildpack.ProcessEntry to a launch.Process
func (p *ProcessEntry) ToLaunchProcess(bpID string) launch.Process {
// turn the command collection into a single command + args
// for the current platform API
// note: this will change once the platform API takes a collection of commands
var command string
if len(p.Command) > 0 {
command = p.Command[0]
}

var args []string
if len(p.Command) > 1 {
args = p.Command[1:]
}

// legacy processes will always have a value
// new processes will have a nil value but are always direct processes
var direct bool
if p.Direct == nil {
direct = true
} else {
direct = *p.Direct
}

return launch.Process{
Type: p.Type,
Command: command,
Args: append(args, p.Args...),
Direct: direct, // launch.Process requires a value
Default: p.Default,
BuildpackID: bpID,
WorkingDirectory: p.WorkingDirectory,
}
}

// converts launch.toml processes to launch.Processes
func (lt LaunchTOML) ToLaunchProcessesForBuildpack(bpID string) []launch.Process {
var processes []launch.Process
for _, process := range lt.Processes {
processes = append(processes, process.ToLaunchProcess(bpID))
}
return processes
}

type BOMEntry struct {
Expand Down

0 comments on commit 328f07b

Please sign in to comment.