Skip to content

Commit

Permalink
Merge pull request #421 from buildpacks/project.toml
Browse files Browse the repository at this point in the history
Add support for project.toml
  • Loading branch information
jromero authored Feb 11, 2020
2 parents 1a6e03a + 52c93e4 commit d0bca6b
Show file tree
Hide file tree
Showing 5 changed files with 542 additions and 13 deletions.
81 changes: 68 additions & 13 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
package commands

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/buildpacks/pack"
"github.com/buildpacks/pack/internal/config"
"github.com/buildpacks/pack/internal/paths"
"github.com/buildpacks/pack/internal/project"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/logging"
)

type BuildFlags struct {
AppPath string
Builder string
RunImage string
Env []string
EnvFiles []string
Publish bool
NoPull bool
ClearCache bool
Buildpacks []string
Network string
AppPath string
Builder string
RunImage string
Env []string
EnvFiles []string
Publish bool
NoPull bool
ClearCache bool
Buildpacks []string
Network string
DescriptorPath string
}

func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cobra.Command {
Expand All @@ -41,10 +46,39 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
suggestSettingBuilder(logger, packClient)
return MakeSoftError()
}
env, err := parseEnv(flags.EnvFiles, flags.Env)

descriptor, actualDescriptorPath, err := parseProjectToml(flags.AppPath, flags.DescriptorPath)
if err != nil {
return err
}
if actualDescriptorPath != "" {
logger.Debugf("Using project descriptor located at '%s'", actualDescriptorPath)
}

env, err := parseEnv(descriptor, flags.EnvFiles, flags.Env)
if err != nil {
return err
}

buildpacks := flags.Buildpacks
if len(buildpacks) == 0 {
buildpacks = []string{}
projectDescriptorDir := filepath.Dir(actualDescriptorPath)
for _, bp := range descriptor.Build.Buildpacks {
if len(bp.URI) == 0 {
// there are several places through out the pack code where the "id@version" format is used.
// we should probably central this, but it's not clear where it belongs
buildpacks = append(buildpacks, fmt.Sprintf("%s@%s", bp.ID, bp.Version))
} else {
uri, err := paths.ToAbsolute(bp.URI, projectDescriptorDir)
if err != nil {
return err
}
buildpacks = append(buildpacks, uri)
}
}
}

if err := packClient.Build(ctx, pack.BuildOptions{
AppPath: flags.AppPath,
Builder: flags.Builder,
Expand All @@ -55,7 +89,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
Publish: flags.Publish,
NoPull: flags.NoPull,
ClearCache: flags.ClearCache,
Buildpacks: flags.Buildpacks,
Buildpacks: buildpacks,
ContainerConfig: pack.ContainerConfig{
Network: flags.Network,
},
Expand All @@ -82,11 +116,15 @@ func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Co
cmd.Flags().BoolVar(&buildFlags.ClearCache, "clear-cache", false, "Clear image's associated cache before building")
cmd.Flags().StringSliceVarP(&buildFlags.Buildpacks, "buildpack", "b", nil, "Buildpack reference in the form of '<buildpack>@<version>',\n path to a buildpack directory (not supported on Windows), or\n path/URL to a buildpack .tar or .tgz file"+multiValueHelp("buildpack"))
cmd.Flags().StringVar(&buildFlags.Network, "network", "", "Connect detect and build containers to network")
cmd.Flags().StringVarP(&buildFlags.DescriptorPath, "descriptor", "d", "", "Path to the project descriptor file")
}

func parseEnv(envFiles []string, envVars []string) (map[string]string, error) {
func parseEnv(project project.Descriptor, envFiles []string, envVars []string) (map[string]string, error) {
env := map[string]string{}

for _, envVar := range project.Build.Env {
env[envVar.Name] = envVar.Value
}
for _, envFile := range envFiles {
envFileVars, err := parseEnvFile(envFile)
if err != nil {
Expand Down Expand Up @@ -128,3 +166,20 @@ func addEnvVar(env map[string]string, item string) map[string]string {
}
return env
}

func parseProjectToml(appPath, descriptorPath string) (project.Descriptor, string, error) {
actualDescriptorPath := descriptorPath
if descriptorPath == "" {
actualDescriptorPath = filepath.Join(appPath, "project.toml")
}

if _, err := os.Stat(actualDescriptorPath); descriptorPath == "" && os.IsNotExist(err) {
return project.Descriptor{}, "", nil
}
if _, err := os.Stat(actualDescriptorPath); descriptorPath != "" && os.IsNotExist(err) {
return project.Descriptor{}, "", errors.New(fmt.Sprintf("project descriptor '%s' does not exist", actualDescriptorPath))
}

descriptor, err := project.ReadProjectDescriptor(actualDescriptorPath)
return descriptor, actualDescriptorPath, err
}
144 changes: 144 additions & 0 deletions internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,122 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) {
h.AssertNil(t, command.Execute())
})
})

when("user specifies an invalid project descriptor file", func() {
it("should show an error", func() {
projectTomlPath := "/incorrect/path/to/project.toml"
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")).
Return(nil)

command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"})
h.AssertNotNil(t, command.Execute())
})
})

when("repo has a project.toml", func() {
when("that is invalid", func() {
var projectTomlPath string

it.Before(func() {
projectToml, err := ioutil.TempFile("", "project.toml")
h.AssertNil(t, err)
defer projectToml.Close()

projectToml.WriteString("project]")
projectTomlPath = projectToml.Name()
})

it.After(func() {
h.AssertNil(t, os.RemoveAll(projectTomlPath))
})

it("fails to build", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")).
Return(nil)

command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"})
h.AssertNotNil(t, command.Execute())
})
})

when("that is not in the root dir", func() {
var projectTomlPath string

it.Before(func() {
projectToml, err := ioutil.TempFile("", "project.toml")
h.AssertNil(t, err)
defer projectToml.Close()

projectToml.WriteString(`
[project]
name = "Sample"
[[build.buildpacks]]
id = "example/lua"
version = "1.0"
[[build.env]]
name = "KEY1"
value = "VALUE1"
[[build.env]]
name = "KEY2"
value = "VALUE2"
`)
projectTomlPath = projectToml.Name()
})

it.After(func() {
h.AssertNil(t, os.RemoveAll(projectTomlPath))
})

it("builds an image with the env variables", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{
"KEY1": "VALUE1",
"KEY2": "VALUE2",
})).
Return(nil)

command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"})
h.AssertNil(t, command.Execute())
})

it("builds an image with the buildpacks", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithBuildpacks([]string{
"example/lua@1.0",
})).
Return(nil)

command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"})
h.AssertNil(t, command.Execute())
})
})

when("that is in the root dir", func() {
it.Before(func() {
h.AssertNil(t, os.Chdir("testdata"))
})

it.After(func() {
h.AssertNil(t, os.Chdir(".."))
})

it("builds an image with the env variables", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{
"KEY1": "VALUE1",
})).
Return(nil)

command.SetArgs([]string{"--builder", "my-builder", "image"})
h.AssertNil(t, command.Execute())
})
})
})
})
}

Expand Down Expand Up @@ -180,6 +296,25 @@ func EqBuildOptionsWithEnv(env map[string]string) gomock.Matcher {
}
}

func EqBuildOptionsWithBuildpacks(buildpacks []string) gomock.Matcher {
return buildOptionsMatcher{
description: fmt.Sprintf("Buildpacks=%+v", buildpacks),
equals: func(o pack.BuildOptions) bool {
for _, bp := range o.Buildpacks {
if !contains(buildpacks, bp) {
return false
}
}
for _, bp := range buildpacks {
if !contains(o.Buildpacks, bp) {
return false
}
}
return true
},
}
}

type buildOptionsMatcher struct {
equals func(pack.BuildOptions) bool
description string
Expand All @@ -195,3 +330,12 @@ func (m buildOptionsMatcher) Matches(x interface{}) bool {
func (m buildOptionsMatcher) String() string {
return "is a BuildOptions with " + m.description
}

func contains(arr []string, str string) bool {
for _, a := range arr {
if a == str {
return true
}
}
return false
}
10 changes: 10 additions & 0 deletions internal/commands/testdata/project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "Sample"

[[build.buildpacks]]
id = "example/lua"
version = "1.0"

[[build.env]]
name = "KEY1"
value = "VALUE1"
86 changes: 86 additions & 0 deletions internal/project/project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package project

import (
"fmt"
"io/ioutil"
"os"

"github.com/BurntSushi/toml"
"github.com/pkg/errors"
)

type Buildpack struct {
ID string `toml:"id"`
Version string `toml:"version"`
URI string `toml:"uri"`
}

type EnvVar struct {
Name string `toml:"name"`
Value string `toml:"value"`
}

type Build struct {
Include []string `toml:"include"`
Exclude []string `toml:"exclude"`
Buildpacks []Buildpack `toml:"buildpacks"`
Env []EnvVar `toml:"env"`
}

type License struct {
Type string `toml:"type"`
URI string `toml:"uri"`
}

type Project struct {
Name string `toml:"name"`
Licenses []License `toml:"licenses"`
}

type Descriptor struct {
Project Project `toml:"project"`
Build Build `toml:"build"`
Metadata map[string]interface{} `toml:"metadata"`
}

func ReadProjectDescriptor(pathToFile string) (Descriptor, error) {
if _, err := os.Stat(pathToFile); os.IsNotExist(err) {
return Descriptor{}, err
}
projectTomlContents, err := ioutil.ReadFile(pathToFile)
if err != nil {
fmt.Print(err)
}

var descriptor Descriptor
_, err = toml.Decode(string(projectTomlContents), &descriptor)
if err != nil {
return Descriptor{}, err
}

return descriptor, descriptor.validate()
}

func (p Descriptor) validate() error {
if p.Build.Exclude != nil && p.Build.Include != nil {
return errors.New("project.toml: cannot have both include and exclude defined")
}
if len(p.Project.Licenses) > 0 {
for _, license := range p.Project.Licenses {
if license.Type == "" && license.URI == "" {
return errors.New("project.toml: must have a type or uri defined for each license")
}
}
}

for _, bp := range p.Build.Buildpacks {
if bp.ID == "" && bp.URI == "" {
return errors.New("project.toml: buildpacks must have an id or url defined")
}
if bp.URI != "" && bp.Version != "" {
return errors.New("project.toml: buildpacks cannot have both uri and version defined")
}
}

return nil
}
Loading

0 comments on commit d0bca6b

Please sign in to comment.