diff --git a/build.go b/build.go index 408eaaac97..e6197a43bf 100644 --- a/build.go +++ b/build.go @@ -37,7 +37,7 @@ import ( "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/logging" "github.com/buildpacks/pack/pkg/archive" - "github.com/buildpacks/pack/project" + projectCommon "github.com/buildpacks/pack/project/common" ) const ( @@ -151,7 +151,7 @@ type BuildOptions struct { ProjectDescriptorBaseDir string // ProjectDescriptor describes the project and any configuration specific to the project - ProjectDescriptor project.Descriptor + ProjectDescriptor projectCommon.Descriptor // The lifecycle image that will be used for the analysis, restore and export phases // when using an untrusted builder. @@ -368,7 +368,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return c.logImageNameAndSha(ctx, opts.Publish, imageRef) } -func getFileFilter(descriptor project.Descriptor) (func(string) bool, error) { +func getFileFilter(descriptor projectCommon.Descriptor) (func(string) bool, error) { if len(descriptor.Build.Exclude) > 0 { excludes := ignore.CompileIgnoreLines(descriptor.Build.Exclude...) return func(fileName string) bool { @@ -920,7 +920,7 @@ func parseDigestFromImageID(id imgutil.Identifier) string { return fmt.Sprintf("sha256:%s", digest) } -func createInlineBuildpack(bp project.Buildpack, stackID string) (string, error) { +func createInlineBuildpack(bp projectCommon.Buildpack, stackID string) (string, error) { pathToInlineBuilpack, err := ioutil.TempDir("", "inline-cnb") if err != nil { return pathToInlineBuilpack, err diff --git a/build_test.go b/build_test.go index 8d10403aaf..ba576a9aac 100644 --- a/build_test.go +++ b/build_test.go @@ -42,7 +42,7 @@ import ( ilogging "github.com/buildpacks/pack/internal/logging" rg "github.com/buildpacks/pack/internal/registry" "github.com/buildpacks/pack/internal/style" - "github.com/buildpacks/pack/project" + projectCommon "github.com/buildpacks/pack/project/common" h "github.com/buildpacks/pack/testhelpers" ) @@ -1237,9 +1237,9 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { Image: "some/app", Builder: defaultBuilderName, ClearCache: true, - ProjectDescriptor: project.Descriptor{ - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + ProjectDescriptor: projectCommon.Descriptor{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ URI: server.URL(), }}, }, @@ -1303,11 +1303,11 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { Image: "some/app", Builder: defaultBuilderName, ClearCache: true, - ProjectDescriptor: project.Descriptor{ - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + ProjectDescriptor: projectCommon.Descriptor{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ ID: "my/inline", - Script: project.Script{ + Script: projectCommon.Script{ API: "0.4", Inline: "touch foo.txt", }, @@ -1338,11 +1338,11 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { Image: "some/app", Builder: defaultBuilderName, ClearCache: true, - ProjectDescriptor: project.Descriptor{ - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + ProjectDescriptor: projectCommon.Descriptor{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ ID: "my/inline", - Script: project.Script{ + Script: projectCommon.Script{ Inline: "touch foo.txt", }, }}, @@ -1359,10 +1359,10 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { Image: "some/app", Builder: defaultBuilderName, ClearCache: true, - ProjectDescriptor: project.Descriptor{ - Build: project.Build{ - Buildpacks: []project.Buildpack{{ - Script: project.Script{ + ProjectDescriptor: projectCommon.Descriptor{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ + Script: projectCommon.Script{ API: "0.4", Inline: "touch foo.txt", }, @@ -1380,12 +1380,12 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { Image: "some/app", Builder: defaultBuilderName, ClearCache: true, - ProjectDescriptor: project.Descriptor{ - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + ProjectDescriptor: projectCommon.Descriptor{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ ID: "buildpack.1.id", Version: "buildpack.1.version", - Script: project.Script{ + Script: projectCommon.Script{ Inline: "touch foo.txt", }, }}, diff --git a/internal/commands/build.go b/internal/commands/build.go index c877c74d56..c1373946bc 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -18,6 +18,7 @@ import ( "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/logging" "github.com/buildpacks/pack/project" + projectCommon "github.com/buildpacks/pack/project/common" ) type BuildFlags struct { @@ -253,7 +254,7 @@ func addEnvVar(env map[string]string, item string) map[string]string { return env } -func parseProjectToml(appPath, descriptorPath string) (project.Descriptor, string, error) { +func parseProjectToml(appPath, descriptorPath string) (projectCommon.Descriptor, string, error) { actualPath := descriptorPath computePath := descriptorPath == "" @@ -263,9 +264,9 @@ func parseProjectToml(appPath, descriptorPath string) (project.Descriptor, strin if _, err := os.Stat(actualPath); err != nil { if computePath { - return project.Descriptor{}, "", nil + return projectCommon.Descriptor{}, "", nil } - return project.Descriptor{}, "", errors.Wrap(err, "stat project descriptor") + return projectCommon.Descriptor{}, "", errors.Wrap(err, "stat project descriptor") } descriptor, err := project.ReadProjectDescriptor(actualPath) diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index fdaa913f55..6595892060 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -9,8 +9,10 @@ import ( "reflect" "testing" + "github.com/buildpacks/lifecycle/api" + pubcfg "github.com/buildpacks/pack/config" - "github.com/buildpacks/pack/project" + projectCommon "github.com/buildpacks/pack/project/common" "github.com/golang/mock/gomock" "github.com/heroku/color" @@ -501,16 +503,17 @@ version = "1.0" it("should build an image with configuration in descriptor", func() { mockClient.EXPECT(). - Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(project.Descriptor{ - Project: project.Project{ + Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(projectCommon.Descriptor{ + Project: projectCommon.Project{ Name: "Sample", }, - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ ID: "example/lua", Version: "1.0", }}, }, + SchemaVersion: api.MustParse("0.1"), })). Return(nil) @@ -594,20 +597,21 @@ builder = "my-builder" it("should use project.toml in source repo", func() { mockClient.EXPECT(). - Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(project.Descriptor{ - Project: project.Project{ + Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(projectCommon.Descriptor{ + Project: projectCommon.Project{ Name: "Sample", }, - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ ID: "example/lua", Version: "1.0", }}, - Env: []project.EnvVar{{ + Env: []projectCommon.EnvVar{{ Name: "KEY1", Value: "VALUE1", }}, }, + SchemaVersion: api.MustParse("0.1"), })). Return(nil) @@ -637,20 +641,21 @@ builder = "my-builder" it("should use specified descriptor", func() { mockClient.EXPECT(). - Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(project.Descriptor{ - Project: project.Project{ + Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(projectCommon.Descriptor{ + Project: projectCommon.Project{ Name: "Sample", }, - Build: project.Build{ - Buildpacks: []project.Buildpack{{ + Build: projectCommon.Build{ + Buildpacks: []projectCommon.Buildpack{{ ID: "example/lua", Version: "1.0", }}, - Env: []project.EnvVar{{ + Env: []projectCommon.EnvVar{{ Name: "KEY1", Value: "VALUE1", }}, }, + SchemaVersion: api.MustParse("0.1"), })). Return(nil) @@ -803,7 +808,7 @@ func EqBuildOptionsWithAdditionalTags(additionalTags []string) gomock.Matcher { } } -func EqBuildOptionsWithProjectDescriptor(descriptor project.Descriptor) gomock.Matcher { +func EqBuildOptionsWithProjectDescriptor(descriptor projectCommon.Descriptor) gomock.Matcher { return buildOptionsMatcher{ description: fmt.Sprintf("Descriptor=%s", descriptor), equals: func(o pack.BuildOptions) bool { diff --git a/project/common/project.go b/project/common/project.go new file mode 100644 index 0000000000..9b537d7ac9 --- /dev/null +++ b/project/common/project.go @@ -0,0 +1,48 @@ +package common + +import ( + "github.com/buildpacks/lifecycle/api" + + "github.com/buildpacks/pack/internal/dist" +) + +type Script struct { + API string `toml:"api"` + Inline string `toml:"inline"` + Shell string `toml:"shell"` +} +type Buildpack struct { + ID string `toml:"id"` + Version string `toml:"version"` + URI string `toml:"uri"` + Script Script `toml:"script"` +} + +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"` + Builder string `toml:"builder"` +} + +type Project struct { + Name string `toml:"name"` + Licenses []dist.License `toml:"licenses"` +} + +type Descriptor struct { + Project Project `toml:"project"` + Build Build `toml:"build"` + Metadata map[string]interface{} `toml:"metadata"` + SchemaVersion *api.Version +} + +type ProjectDescriptorSchema interface { + DescriptorFromToml(projectTomlContents string) (Descriptor, error) +} diff --git a/project/project.go b/project/project.go index 20c0a88cd1..6428aed6f7 100644 --- a/project/project.go +++ b/project/project.go @@ -1,62 +1,62 @@ package project import ( + "fmt" "io/ioutil" "path/filepath" "github.com/BurntSushi/toml" "github.com/pkg/errors" - "github.com/buildpacks/pack/internal/dist" -) - -type Script struct { - API string `toml:"api"` - Inline string `toml:"inline"` - Shell string `toml:"shell"` -} - -type Buildpack struct { - ID string `toml:"id"` - Version string `toml:"version"` - URI string `toml:"uri"` - Script Script `toml:"script"` -} + "github.com/buildpacks/lifecycle/api" -type EnvVar struct { - Name string `toml:"name"` - Value string `toml:"value"` -} + "github.com/buildpacks/pack/project/common" + v01 "github.com/buildpacks/pack/project/v01" + v02 "github.com/buildpacks/pack/project/v02" +) -type Build struct { - Include []string `toml:"include"` - Exclude []string `toml:"exclude"` - Buildpacks []Buildpack `toml:"buildpacks"` - Env []EnvVar `toml:"env"` - Builder string `toml:"builder"` +var supportedSchemas = map[string]common.ProjectDescriptorSchema{ + "0.1": v01.Descriptor{}, + "0.2": v02.Descriptor{}, } type Project struct { - Name string `toml:"name"` - Licenses []dist.License `toml:"licenses"` + Version string `toml:"schema-version"` } - -type Descriptor struct { - Project Project `toml:"project"` - Build Build `toml:"build"` - Metadata map[string]interface{} `toml:"metadata"` +type VersionDescriptor struct { + Project Project `toml:"_"` } -func ReadProjectDescriptor(pathToFile string) (Descriptor, error) { +func ReadProjectDescriptor(pathToFile string) (common.Descriptor, error) { projectTomlContents, err := ioutil.ReadFile(filepath.Clean(pathToFile)) if err != nil { - return Descriptor{}, err + return common.Descriptor{}, err + } + + var versionDescriptor VersionDescriptor + _, err = toml.Decode(string(projectTomlContents), &versionDescriptor) + if err != nil { + return common.Descriptor{}, errors.Wrapf(err, "parsing schema version") + } + + var schema common.ProjectDescriptorSchema + var ok bool + if versionDescriptor.Project.Version == "" { + // _.schema-version does not exist in 0.1 + schema = supportedSchemas["0.1"] + } else { + if _, err := api.NewVersion(versionDescriptor.Project.Version); err != nil { + return common.Descriptor{}, err + } + schema, ok = supportedSchemas[versionDescriptor.Project.Version] + if !ok { + return common.Descriptor{}, fmt.Errorf("unknown project descriptor schema version %s", versionDescriptor.Project.Version) + } } - var descriptor Descriptor - _, err = toml.Decode(string(projectTomlContents), &descriptor) + descriptor, err := schema.DescriptorFromToml(string(projectTomlContents)) if err != nil { - return Descriptor{}, err + return descriptor, err } err = validate(descriptor) @@ -64,7 +64,7 @@ func ReadProjectDescriptor(pathToFile string) (Descriptor, error) { return descriptor, err } -func validate(p Descriptor) error { +func validate(p common.Descriptor) error { if p.Build.Exclude != nil && p.Build.Include != nil { return errors.New("project.toml: cannot have both include and exclude defined") } diff --git a/project/project_test.go b/project/project_test.go index b1ab42c9ac..c9b53e641c 100644 --- a/project/project_test.go +++ b/project/project_test.go @@ -5,6 +5,7 @@ import ( "log" "math/rand" "os" + "reflect" "testing" "time" @@ -12,6 +13,8 @@ import ( "github.com/sclevine/spec" "github.com/sclevine/spec/report" + "github.com/buildpacks/lifecycle/api" + h "github.com/buildpacks/pack/testhelpers" ) @@ -26,7 +29,93 @@ func TestProject(t *testing.T) { func testProject(t *testing.T, when spec.G, it spec.S) { when("#ReadProjectDescriptor", func() { - it("should parse a valid project.toml file", func() { + it("should parse a valid v0.2 project.toml file", func() { + projectToml := ` +[_] +name = "gallant 0.2" +schema-version="0.2" +[[_.licenses]] +type = "MIT" +[_.metadata] +pipeline = "Lucerne" +[io.buildpacks] +exclude = [ "*.jar" ] +[[io.buildpacks.group]] +id = "example/lua" +version = "1.0" +[[io.buildpacks.group]] +uri = "https://example.com/buildpack" +[[io.buildpacks.env.build]] +name = "JAVA_OPTS" +value = "-Xmx300m" +` + tmpProjectToml, err := createTmpProjectTomlFile(projectToml) + if err != nil { + t.Fatal(err) + } + + projectDescriptor, err := ReadProjectDescriptor(tmpProjectToml.Name()) + if err != nil { + t.Fatal(err) + } + + var expected string + + expected = "gallant 0.2" + if projectDescriptor.Project.Name != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Project.Name) + } + + expectedVersion := api.MustParse("0.2") + if !reflect.DeepEqual(expectedVersion, projectDescriptor.SchemaVersion) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expectedVersion, projectDescriptor.SchemaVersion) + } + + expected = "example/lua" + if projectDescriptor.Build.Buildpacks[0].ID != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Buildpacks[0].ID) + } + + expected = "1.0" + if projectDescriptor.Build.Buildpacks[0].Version != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Buildpacks[0].Version) + } + + expected = "https://example.com/buildpack" + if projectDescriptor.Build.Buildpacks[1].URI != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Buildpacks[1].URI) + } + + expected = "JAVA_OPTS" + if projectDescriptor.Build.Env[0].Name != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Env[0].Name) + } + + expected = "-Xmx300m" + if projectDescriptor.Build.Env[0].Value != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Env[0].Value) + } + + expected = "MIT" + if projectDescriptor.Project.Licenses[0].Type != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Project.Licenses[0].Type) + } + + expected = "Lucerne" + if projectDescriptor.Metadata["pipeline"] != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Metadata["pipeline"]) + } + }) + it("should parse a valid v0.1 project.toml file", func() { projectToml := ` [project] name = "gallant" @@ -63,6 +152,12 @@ pipeline = "Lucerne" expected, projectDescriptor.Project.Name) } + expectedVersion := api.MustParse("0.1") + if !reflect.DeepEqual(expectedVersion, projectDescriptor.SchemaVersion) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expectedVersion, projectDescriptor.SchemaVersion) + } + expected = "example/lua" if projectDescriptor.Build.Buildpacks[0].ID != expected { t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", diff --git a/project/v01/project.go b/project/v01/project.go new file mode 100644 index 0000000000..978648d15c --- /dev/null +++ b/project/v01/project.go @@ -0,0 +1,33 @@ +package v01 + +import ( + "github.com/BurntSushi/toml" + + "github.com/buildpacks/lifecycle/api" + + "github.com/buildpacks/pack/project/common" +) + +type Descriptor struct { + Project common.Project `toml:"project"` + Build common.Build `toml:"build"` + Metadata map[string]interface{} `toml:"metadata"` +} + +func NewDescriptor() Descriptor { + return Descriptor{} +} +func (descriptor Descriptor) DescriptorFromToml(projectTomlContents string) (common.Descriptor, error) { + _, err := toml.Decode(projectTomlContents, &descriptor) + if err != nil { + return common.Descriptor{}, err + } + + var commonDescriptor = common.Descriptor{ + Project: descriptor.Project, + Build: descriptor.Build, + Metadata: descriptor.Metadata, + SchemaVersion: api.MustParse("0.1"), + } + return commonDescriptor, nil +} diff --git a/project/v02/project.go b/project/v02/project.go new file mode 100644 index 0000000000..6c46176060 --- /dev/null +++ b/project/v02/project.go @@ -0,0 +1,62 @@ +package v02 + +import ( + "github.com/BurntSushi/toml" + + "github.com/buildpacks/lifecycle/api" + + "github.com/buildpacks/pack/internal/dist" + "github.com/buildpacks/pack/project/common" +) + +type Buildpacks struct { + Include []string `toml:"include"` + Exclude []string `toml:"exclude"` + Group []common.Buildpack `toml:"group"` + Env Env `toml:"env"` + Builder string `toml:"builder"` +} + +type Env struct { + Build []common.EnvVar `toml:"build"` +} +type Project struct { + Name string `toml:"name"` + Licenses []dist.License `toml:"licenses"` + Metadata map[string]interface{} `toml:"metadata"` + SchemaVersion string `toml:"schema-version"` +} +type IO struct { + Buildpacks Buildpacks `toml:"buildpacks"` +} +type Descriptor struct { + Project Project `toml:"_"` + IO IO `toml:"io"` +} + +func NewDescriptor() Descriptor { + return Descriptor{} +} +func (descriptor Descriptor) DescriptorFromToml(projectTomlContents string) (common.Descriptor, error) { + _, err := toml.Decode(projectTomlContents, &descriptor) + if err != nil { + return common.Descriptor{}, err + } + + var commonDescriptor = common.Descriptor{ + Project: common.Project{ + Name: descriptor.Project.Name, + Licenses: descriptor.Project.Licenses, + }, + Build: common.Build{ + Include: descriptor.IO.Buildpacks.Include, + Exclude: descriptor.IO.Buildpacks.Exclude, + Buildpacks: descriptor.IO.Buildpacks.Group, + Env: descriptor.IO.Buildpacks.Env.Build, + Builder: descriptor.IO.Buildpacks.Builder, + }, + Metadata: descriptor.Project.Metadata, + SchemaVersion: api.MustParse("0.2"), + } + return commonDescriptor, nil +}