diff --git a/acceptance/testdata/pack_fixtures/report_output.txt b/acceptance/testdata/pack_fixtures/report_output.txt index 80decb0f90..85bb370fa9 100644 --- a/acceptance/testdata/pack_fixtures/report_output.txt +++ b/acceptance/testdata/pack_fixtures/report_output.txt @@ -4,7 +4,7 @@ Pack: Default Lifecycle Version: 0.20.11 -Supported Platform APIs: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13 +Supported Platform APIs: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15 Config: default-builder-image = "{{ .DefaultBuilder }}" diff --git a/builder/config_reader.go b/builder/config_reader.go index 870ffdd698..116d56bb33 100644 --- a/builder/config_reader.go +++ b/builder/config_reader.go @@ -88,10 +88,11 @@ const ( ) type BuildConfigEnv struct { - Name string `toml:"name"` - Value string `toml:"value"` - Suffix Suffix `toml:"suffix,omitempty"` - Delim string `toml:"delim,omitempty"` + Name string `toml:"name"` + Value string `toml:"value"` + Suffix Suffix `toml:"suffix,omitempty"` + Delim string `toml:"delim,omitempty"` + ExecEnv []string `toml:"exec-env,omitempty"` } // ReadConfig reads a builder configuration from the file path provided and returns the diff --git a/builder/config_reader_test.go b/builder/config_reader_test.go index 9bdbefcdab..b5f6a7fc20 100644 --- a/builder/config_reader_test.go +++ b/builder/config_reader_test.go @@ -54,6 +54,15 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { [[order]] [[order.group]] id = "buildpack/1" + exec-env = ["production"] + +[[build.env]] + name = "key1" + value = "value1" + suffix = "append" + delim = "%" + exec-env = ["test"] + `), 0666)) }) @@ -76,6 +85,15 @@ func testConfig(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, builderConfig.Buildpacks[2].ImageName, "") h.AssertEq(t, builderConfig.Order[0].Group[0].ID, "buildpack/1") + h.AssertTrue(t, len(builderConfig.Order[0].Group[0].ExecEnv) == 1) + h.AssertEq(t, builderConfig.Order[0].Group[0].ExecEnv[0], "production") + + h.AssertTrue(t, len(builderConfig.Build.Env) == 1) + h.AssertEq(t, builderConfig.Build.Env[0].Name, "key1") + h.AssertEq(t, builderConfig.Build.Env[0].Value, "value1") + h.AssertEq(t, string(builderConfig.Build.Env[0].Suffix), "append") + h.AssertTrue(t, len(builderConfig.Build.Env[0].ExecEnv) == 1) + h.AssertEq(t, builderConfig.Build.Env[0].ExecEnv[0], "test") }) }) diff --git a/buildpackage/config_reader_test.go b/buildpackage/config_reader_test.go index fb079cda83..eee0bdaae0 100644 --- a/buildpackage/config_reader_test.go +++ b/buildpackage/config_reader_test.go @@ -222,6 +222,39 @@ func testBuildpackageConfigReader(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, err, "missing 'buildpack.uri' configuration") }) }) + + when("#ReadBuildpackDescriptor", func() { + var tmpDir string + + it.Before(func() { + var err error + tmpDir, err = os.MkdirTemp("", "buildpack-descriptor-test") + h.AssertNil(t, err) + }) + + it.After(func() { + _ = os.RemoveAll(tmpDir) + }) + + it("returns exec-env when a composite buildpack toml file is provided", func() { + buildPackTomlFilePath := filepath.Join(tmpDir, "buildpack-1.toml") + + err := os.WriteFile(buildPackTomlFilePath, []byte(validCompositeBuildPackTomlWithExecEnv), os.ModePerm) + h.AssertNil(t, err) + + packageConfigReader := buildpackage.NewConfigReader() + + buildpackDescriptor, err := packageConfigReader.ReadBuildpackDescriptor(buildPackTomlFilePath) + h.AssertNil(t, err) + + h.AssertTrue(t, len(buildpackDescriptor.Order()) == 1) + h.AssertTrue(t, len(buildpackDescriptor.Order()[0].Group) == 2) + h.AssertTrue(t, len(buildpackDescriptor.Order()[0].Group[0].ExecEnv) == 1) + h.AssertTrue(t, len(buildpackDescriptor.Order()[0].Group[1].ExecEnv) == 1) + h.AssertEq(t, buildpackDescriptor.Order()[0].Group[0].ExecEnv[0], "production.1") + h.AssertEq(t, buildpackDescriptor.Order()[0].Group[1].ExecEnv[0], "production.2") + }) + }) } const validPackageToml = ` @@ -306,3 +339,24 @@ const missingBuildpackPackageToml = ` [[dependencies]] uri = "bp/b" ` + +const validCompositeBuildPackTomlWithExecEnv = ` +api = "0.15" + +[buildpack] +id = "samples/hello-universe" +version = "0.0.1" +name = "Hello Universe Buildpack" + +# Order used for detection +[[order]] +[[order.group]] +id = "samples/hello-world" +version = "0.0.1" +exec-env = ["production.1"] + +[[order.group]] +id = "samples/hello-moon" +version = "0.0.1" +exec-env = ["production.2"] +` diff --git a/internal/build/fakes/fake_builder.go b/internal/build/fakes/fake_builder.go index 39048f84ef..2472dd8a73 100644 --- a/internal/build/fakes/fake_builder.go +++ b/internal/build/fakes/fake_builder.go @@ -131,3 +131,10 @@ func WithEnableUsernsHost() func(*build.LifecycleOptions) { opts.EnableUsernsHost = true } } + +// WithExecutionEnvironment creates a LifecycleOptions option that sets the execution environment +func WithExecutionEnvironment(execEnv string) func(*build.LifecycleOptions) { + return func(opts *build.LifecycleOptions) { + opts.ExecutionEnvironment = execEnv + } +} diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 39595d662a..c15dcc5a78 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -33,6 +33,8 @@ var ( api.MustParse("0.11"), api.MustParse("0.12"), api.MustParse("0.13"), + api.MustParse("0.14"), + api.MustParse("0.15"), } ) @@ -87,6 +89,7 @@ type LifecycleOptions struct { Termui Termui DockerHost string Cache cache.CacheOpts + ExecutionEnvironment string CacheImage string HTTPProxy string HTTPSProxy string diff --git a/internal/build/phase_config_provider.go b/internal/build/phase_config_provider.go index e74c030d73..a247119916 100644 --- a/internal/build/phase_config_provider.go +++ b/internal/build/phase_config_provider.go @@ -17,6 +17,7 @@ const ( linuxContainerAdmin = "root" windowsContainerAdmin = "ContainerAdministrator" platformAPIEnvVar = "CNB_PLATFORM_API" + executionEnvVar = "CNB_EXEC_ENV" ) type PhaseConfigProviderOperation func(*PhaseConfigProvider) @@ -59,6 +60,7 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops ops = append(ops, WithEnv(fmt.Sprintf("%s=%s", platformAPIEnvVar, lifecycleExec.platformAPI.String())), + If(lifecycleExec.platformAPI.AtLeast("0.15"), WithEnv(fmt.Sprintf("%s=%s", executionEnvVar, lifecycleExec.opts.ExecutionEnvironment))), WithLifecycleProxy(lifecycleExec), WithBinds([]string{ fmt.Sprintf("%s:%s", lifecycleExec.layersVolume, lifecycleExec.mountPaths.layersDir()), diff --git a/internal/build/phase_config_provider_test.go b/internal/build/phase_config_provider_test.go index 942c88b7a2..43663c890f 100644 --- a/internal/build/phase_config_provider_test.go +++ b/internal/build/phase_config_provider_test.go @@ -6,6 +6,7 @@ import ( "testing" ifakes "github.com/buildpacks/imgutil/fakes" + "github.com/buildpacks/lifecycle/api" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" @@ -204,6 +205,46 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { }) }) + when("execution environment is set", func() { + when("platform API >= 0.15", func() { + it("sets CNB_EXEC_ENV environment variable", func() { + expectedBuilderImage := ifakes.NewImage("some-builder-name", "", nil) + fakeBuilder, err := fakes.NewFakeBuilder( + fakes.WithImage(expectedBuilderImage), + fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.15")}), + ) + h.AssertNil(t, err) + lifecycle := newTestLifecycleExec(t, false, "some-temp-dir", + fakes.WithBuilder(fakeBuilder), + fakes.WithExecutionEnvironment("test"), + ) + + phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle) + + h.AssertSliceContains(t, phaseConfigProvider.ContainerConfig().Env, "CNB_EXEC_ENV=test") + }) + }) + + when("platform API < 0.15", func() { + it("does not set CNB_EXEC_ENV environment variable", func() { + expectedBuilderImage := ifakes.NewImage("some-builder-name", "", nil) + fakeBuilder, err := fakes.NewFakeBuilder( + fakes.WithImage(expectedBuilderImage), + fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.14")}), + ) + h.AssertNil(t, err) + lifecycle := newTestLifecycleExec(t, false, "some-temp-dir", + fakes.WithBuilder(fakeBuilder), + fakes.WithExecutionEnvironment("test"), + ) + + phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle) + + h.AssertSliceNotContains(t, phaseConfigProvider.ContainerConfig().Env, "CNB_EXEC_ENV=test") + }) + }) + }) + when("called with WithImage", func() { it("sets the image on the config", func() { lifecycle := newTestLifecycleExec(t, false, "some-temp-dir") diff --git a/internal/commands/build.go b/internal/commands/build.go index 958b80610f..d3db7a69b9 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -38,6 +39,7 @@ type BuildFlags struct { Cache cache.CacheOpts AppPath string Builder string + ExecutionEnv string Registry string RunImage string Platform string @@ -220,6 +222,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob PreviousInputImage: inputPreviousImage, LayoutRepoDir: cfg.LayoutRepositoryDir, }, + CNBExecutionEnv: flags.ExecutionEnv, InsecureRegistries: flags.InsecureRegistries, }); err != nil { return errors.Wrap(err, "failed to build") @@ -284,6 +287,7 @@ This option may set DOCKER_HOST environment variable for the build container if cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`) cmd.Flags().StringVar(&buildFlags.Platform, "platform", "", `Platform to build on (e.g., "linux/amd64").`) cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`) + cmd.Flags().StringVar(&buildFlags.ExecutionEnv, "exec-env", "production", `Execution environment to use. (default "production"`) cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name") cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)") cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to.\nTags should be in the format 'image:tag' or 'repository/image:tag'."+stringSliceHelp("tag")) @@ -347,6 +351,14 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef clie inputImageRef.Name(), inputImageRef.Name()) } + if flags.ExecutionEnv != "" && flags.ExecutionEnv != "production" && flags.ExecutionEnv != "test" { + // RFC: the / character is reserved in case we need to introduce namespacing in the future. + var executionEnvRegex = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`) + if ok := executionEnvRegex.MatchString(flags.ExecutionEnv); !ok { + return errors.New("exec-env MUST only contain numbers, letters, and the characters: . or -") + } + } + return nil } diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index 7305d0891f..9a9d0ce248 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -983,6 +983,86 @@ builder = "my-builder" }) }) + when("--exec-env", func() { + when("is not provided", func() { + it("set 'production' as default value", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("production")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder"}) + h.AssertNil(t, command.Execute()) + }) + }) + + when("is provided", func() { + when("contains valid characters", func() { + it("forwards the exec-value (only letters) into the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("something")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "something"}) + h.AssertNil(t, command.Execute()) + }) + + it("forwards the exec-value (only numbers) into the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("1234")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "1234"}) + h.AssertNil(t, command.Execute()) + }) + + it("forwards the exec-value (mix letters and numbers) into the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("env1")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env1"}) + h.AssertNil(t, command.Execute()) + }) + + it("forwards the exec-value (mix letters, numbers and .) into the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("env1.1")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env1.1"}) + h.AssertNil(t, command.Execute()) + }) + + it("forwards the exec-value (mix letters, numbers and -) into the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("env-1")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env-1"}) + h.AssertNil(t, command.Execute()) + }) + + it("forwards the exec-value (mix letters, numbers, . and -) into the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithExecEnv("env-1.1")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env-1.1"}) + h.AssertNil(t, command.Execute()) + }) + }) + + when("contains invalid characters", func() { + it("errors with a descriptive message", func() { + command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "$production"}) + err := command.Execute() + h.AssertNotNil(t, err) + h.AssertError(t, err, "exec-env MUST only contain numbers, letters, and the characters: . or -") + }) + }) + }) + }) + when("--insecure-registry is provided", func() { it("sets one insecure registry", func() { mockClient.EXPECT(). @@ -1268,6 +1348,15 @@ func EqBuildOptionsWithLayoutConfig(image, previousImage string, sparse bool, la } } +func EqBuildOptionsWithExecEnv(s string) interface{} { + return buildOptionsMatcher{ + description: fmt.Sprintf("exec-env=%s", s), + equals: func(o client.BuildOptions) bool { + return o.CNBExecutionEnv == s + }, + } +} + func EqBuildOptionsWithInsecureRegistries(insecureRegistries []string) gomock.Matcher { return buildOptionsMatcher{ description: fmt.Sprintf("Insercure Registries=%s", insecureRegistries), diff --git a/pkg/client/build.go b/pkg/client/build.go index 574640c669..736cf18b32 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -108,6 +108,9 @@ type BuildOptions struct { // e.g. tcp://example.com:1234, unix:///run/user/1000/podman/podman.sock DockerHost string + // the target environment the OCI image is expected to be run in, i.e. production, test, development. + CNBExecutionEnv string + // Used to determine a run-image mirror if Run Image is empty. // Used in combination with Builder metadata to determine to the 'best' mirror. // 'best' is defined as: @@ -667,6 +670,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { Layout: opts.Layout(), Keychain: c.keychain, EnableUsernsHost: opts.EnableUsernsHost, + ExecutionEnvironment: opts.CNBExecutionEnv, InsecureRegistries: opts.InsecureRegistries, } diff --git a/pkg/dist/buildmodule.go b/pkg/dist/buildmodule.go index b0f0d5c838..b5f665e7e9 100644 --- a/pkg/dist/buildmodule.go +++ b/pkg/dist/buildmodule.go @@ -20,6 +20,7 @@ type ModuleInfo struct { Description string `toml:"description,omitempty" json:"description,omitempty" yaml:"description,omitempty"` Homepage string `toml:"homepage,omitempty" json:"homepage,omitempty" yaml:"homepage,omitempty"` Keywords []string `toml:"keywords,omitempty" json:"keywords,omitempty" yaml:"keywords,omitempty"` + ExecEnv []string `toml:"exec-env,omitempty" json:"exec-env,omitempty" yaml:"exec-env,omitempty"` Licenses []License `toml:"licenses,omitempty" json:"licenses,omitempty" yaml:"licenses,omitempty"` ClearEnv bool `toml:"clear-env,omitempty" json:"clear-env,omitempty" yaml:"clear-env,omitempty"` } diff --git a/pkg/project/project.go b/pkg/project/project.go index adab93a19d..a531e06b5a 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -6,6 +6,8 @@ import ( "path/filepath" "strings" + v03 "github.com/buildpacks/pack/pkg/project/v03" + "github.com/BurntSushi/toml" "github.com/pkg/errors" @@ -26,6 +28,7 @@ type VersionDescriptor struct { var parsers = map[string]func(string) (types.Descriptor, toml.MetaData, error){ "0.1": v01.NewDescriptor, "0.2": v02.NewDescriptor, + "0.3": v03.NewDescriptor, } func ReadProjectDescriptor(pathToFile string, logger logging.Logger) (types.Descriptor, error) { diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go index 3f4163f179..1f9541b2c9 100644 --- a/pkg/project/project_test.go +++ b/pkg/project/project_test.go @@ -6,6 +6,8 @@ import ( "reflect" "testing" + "github.com/buildpacks/pack/pkg/project/types" + "github.com/buildpacks/lifecycle/api" "github.com/heroku/color" "github.com/sclevine/spec" @@ -37,6 +39,128 @@ func testProject(t *testing.T, when spec.G, it spec.S) { }) when("#ReadProjectDescriptor", func() { + when("valid 0.3 project.toml file is provided", func() { + it("should exec-env on [[io.buildpacks.group]]", func() { + projectToml := ` +[_] +name = "gallant 0.3" +schema-version = "0.3" + +[[io.buildpacks.group]] +id = "buildpacks/metrics-agent" +version = "latest" +exec-env = ["production"] +` + tmpProjectToml, err := createTmpProjectTomlFile(projectToml) + if err != nil { + t.Fatal(err) + } + + projectDescriptor, err := ReadProjectDescriptor(tmpProjectToml.Name(), logger) + if err != nil { + t.Fatal(err) + } + + assertProjectName(t, "gallant 0.3", projectDescriptor) + assertSchemaVersion(t, api.MustParse("0.3"), projectDescriptor) + + expectedNumberOfBuildPacks := 1 + expectedNumberOfExecEnvs := 1 + atIndex := 0 + assertBuildPackGroupExecEnv(t, "production", expectedNumberOfBuildPacks, expectedNumberOfExecEnvs, atIndex, projectDescriptor) + }) + + it("should exec-env on [[io.buildpacks.pre.group]]", func() { + projectToml := ` +[_] +name = "gallant 0.3" +schema-version = "0.3" + +[[io.buildpacks.pre.group]] +id = "buildpacks/procfile" +version = "latest" +exec-env = ["test"] +` + tmpProjectToml, err := createTmpProjectTomlFile(projectToml) + if err != nil { + t.Fatal(err) + } + + projectDescriptor, err := ReadProjectDescriptor(tmpProjectToml.Name(), logger) + if err != nil { + t.Fatal(err) + } + + assertProjectName(t, "gallant 0.3", projectDescriptor) + assertSchemaVersion(t, api.MustParse("0.3"), projectDescriptor) + + expectedNumberOfBuildPacks := 1 + expectedNumberOfExecEnvs := 1 + atIndex := 0 + assertBuildPackPreGroupExecEnv(t, "test", expectedNumberOfBuildPacks, expectedNumberOfExecEnvs, atIndex, projectDescriptor) + }) + }) + + it("should exec-env on [[io.buildpacks.post.group]]", func() { + projectToml := ` +[_] +name = "gallant 0.3" +schema-version = "0.3" + +[[io.buildpacks.post.group]] +id = "buildpacks/headless-chrome" +version = "latest" +exec-env = ["test-1"] +` + tmpProjectToml, err := createTmpProjectTomlFile(projectToml) + if err != nil { + t.Fatal(err) + } + + projectDescriptor, err := ReadProjectDescriptor(tmpProjectToml.Name(), logger) + if err != nil { + t.Fatal(err) + } + + assertProjectName(t, "gallant 0.3", projectDescriptor) + assertSchemaVersion(t, api.MustParse("0.3"), projectDescriptor) + + expectedNumberOfBuildPacks := 1 + expectedNumberOfExecEnvs := 1 + atIndex := 0 + assertBuildPackPostGroupExecEnv(t, "test-1", expectedNumberOfBuildPacks, expectedNumberOfExecEnvs, atIndex, projectDescriptor) + }) + + it("should exec-env on [[io.buildpacks.build.env]]", func() { + projectToml := ` +[_] +name = "gallant 0.3" +schema-version = "0.3" + +[[io.buildpacks.build.env]] +name = "RAILS_ENV" +value = "test" +exec-env = ["test-1.1"] +` + tmpProjectToml, err := createTmpProjectTomlFile(projectToml) + if err != nil { + t.Fatal(err) + } + + projectDescriptor, err := ReadProjectDescriptor(tmpProjectToml.Name(), logger) + if err != nil { + t.Fatal(err) + } + + assertProjectName(t, "gallant 0.3", projectDescriptor) + assertSchemaVersion(t, api.MustParse("0.3"), projectDescriptor) + + expectedNumberOfBuildPacks := 1 + expectedNumberOfExecEnvs := 1 + atIndex := 0 + assertBuildPackBuildExecEnv(t, "test-1.1", expectedNumberOfBuildPacks, expectedNumberOfExecEnvs, atIndex, projectDescriptor) + }) + it("should parse a valid v0.2 project.toml file", func() { projectToml := ` [_] @@ -492,6 +616,56 @@ buzz = ["a", "b", "c"] }) } +func assertProjectName(t *testing.T, expected string, projectDescriptor types.Descriptor) { + if projectDescriptor.Project.Name != expected { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Project.Name) + } +} + +func assertSchemaVersion(t *testing.T, expected *api.Version, projectDescriptor types.Descriptor) { + if !reflect.DeepEqual(expected, projectDescriptor.SchemaVersion) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.SchemaVersion) + } +} + +func assertBuildPackGroupExecEnv(t *testing.T, expected string, bpLength int, execEnvLength int, atIndex int, projectDescriptor types.Descriptor) { + h.AssertTrue(t, len(projectDescriptor.Build.Buildpacks) == bpLength) + h.AssertTrue(t, len(projectDescriptor.Build.Buildpacks[atIndex].ExecEnv) == execEnvLength) + if !reflect.DeepEqual(expected, projectDescriptor.Build.Buildpacks[atIndex].ExecEnv[atIndex]) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Buildpacks[atIndex].ExecEnv[atIndex]) + } +} + +func assertBuildPackPreGroupExecEnv(t *testing.T, expected string, bpLength int, execEnvLength int, atIndex int, projectDescriptor types.Descriptor) { + h.AssertTrue(t, len(projectDescriptor.Build.Pre.Buildpacks) == bpLength) + h.AssertTrue(t, len(projectDescriptor.Build.Pre.Buildpacks[atIndex].ExecEnv) == execEnvLength) + if !reflect.DeepEqual(expected, projectDescriptor.Build.Pre.Buildpacks[atIndex].ExecEnv[atIndex]) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Pre.Buildpacks[atIndex].ExecEnv[atIndex]) + } +} + +func assertBuildPackPostGroupExecEnv(t *testing.T, expected string, bpLength int, execEnvLength int, atIndex int, projectDescriptor types.Descriptor) { + h.AssertTrue(t, len(projectDescriptor.Build.Post.Buildpacks) == bpLength) + h.AssertTrue(t, len(projectDescriptor.Build.Post.Buildpacks[atIndex].ExecEnv) == execEnvLength) + if !reflect.DeepEqual(expected, projectDescriptor.Build.Post.Buildpacks[atIndex].ExecEnv[atIndex]) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Post.Buildpacks[atIndex].ExecEnv[atIndex]) + } +} + +func assertBuildPackBuildExecEnv(t *testing.T, expected string, bpLength int, execEnvLength int, atIndex int, projectDescriptor types.Descriptor) { + h.AssertTrue(t, len(projectDescriptor.Build.Env) == bpLength) + h.AssertTrue(t, len(projectDescriptor.Build.Env[atIndex].ExecEnv) == execEnvLength) + if !reflect.DeepEqual(expected, projectDescriptor.Build.Env[atIndex].ExecEnv[atIndex]) { + t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", + expected, projectDescriptor.Build.Env[atIndex].ExecEnv[atIndex]) + } +} + func createTmpProjectTomlFile(projectToml string) (*os.File, error) { tmpProjectToml, err := os.CreateTemp(os.TempDir(), "project-") if err != nil { diff --git a/pkg/project/types/types.go b/pkg/project/types/types.go index a9c73b7f78..a238e58c9c 100644 --- a/pkg/project/types/types.go +++ b/pkg/project/types/types.go @@ -11,15 +11,17 @@ type Script struct { } type Buildpack struct { - ID string `toml:"id"` - Version string `toml:"version"` - URI string `toml:"uri"` - Script Script `toml:"script"` + ID string `toml:"id"` + Version string `toml:"version"` + URI string `toml:"uri"` + Script Script `toml:"script"` + ExecEnv []string `toml:"exec-env"` } type EnvVar struct { - Name string `toml:"name"` - Value string `toml:"value"` + Name string `toml:"name"` + Value string `toml:"value"` + ExecEnv []string `toml:"exec-env"` } type Build struct { diff --git a/pkg/project/v02/project.go b/pkg/project/v02/project.go index 67fc1270bb..c965087e84 100644 --- a/pkg/project/v02/project.go +++ b/pkg/project/v02/project.go @@ -8,23 +8,23 @@ import ( ) type Buildpacks struct { - Include []string `toml:"include"` - Exclude []string `toml:"exclude"` - Group []types.Buildpack `toml:"group"` - Env Env `toml:"env"` - Build Build `toml:"build"` - Builder string `toml:"builder"` - Pre types.GroupAddition `toml:"pre"` - Post types.GroupAddition `toml:"post"` + Include []string `toml:"include"` + Exclude []string `toml:"exclude"` + Group []Buildpack `toml:"group"` + Env Env `toml:"env"` + Build Build `toml:"build"` + Builder string `toml:"builder"` + Pre GroupAddition `toml:"pre"` + Post GroupAddition `toml:"post"` } type Build struct { - Env []types.EnvVar `toml:"env"` + Env []EnvVar `toml:"env"` } // Deprecated: use `[[io.buildpacks.build.env]]` instead. see https://github.com/buildpacks/pack/pull/1479 type Env struct { - Build []types.EnvVar `toml:"build"` + Build []EnvVar `toml:"build"` } type Project struct { @@ -48,6 +48,22 @@ type Descriptor struct { IO IO `toml:"io"` } +type Buildpack struct { + ID string `toml:"id"` + Version string `toml:"version"` + URI string `toml:"uri"` + Script types.Script `toml:"script"` +} + +type EnvVar struct { + Name string `toml:"name"` + Value string `toml:"value"` +} + +type GroupAddition struct { + Buildpacks []Buildpack `toml:"group"` +} + func NewDescriptor(projectTomlContents string) (types.Descriptor, toml.MetaData, error) { versionedDescriptor := &Descriptor{} tomlMetaData, err := toml.Decode(projectTomlContents, &versionedDescriptor) @@ -69,13 +85,51 @@ func NewDescriptor(projectTomlContents string) (types.Descriptor, toml.MetaData, Build: types.Build{ Include: versionedDescriptor.IO.Buildpacks.Include, Exclude: versionedDescriptor.IO.Buildpacks.Exclude, - Buildpacks: versionedDescriptor.IO.Buildpacks.Group, - Env: env, + Buildpacks: mapToBuildPacksDescriptor(versionedDescriptor.IO.Buildpacks.Group), + Env: mapToEnvVarsDescriptor(env), Builder: versionedDescriptor.IO.Buildpacks.Builder, - Pre: versionedDescriptor.IO.Buildpacks.Pre, - Post: versionedDescriptor.IO.Buildpacks.Post, + Pre: types.GroupAddition{ + Buildpacks: mapToBuildPacksDescriptor(versionedDescriptor.IO.Buildpacks.Pre.Buildpacks), + }, + Post: types.GroupAddition{ + Buildpacks: mapToBuildPacksDescriptor(versionedDescriptor.IO.Buildpacks.Post.Buildpacks), + }, }, Metadata: versionedDescriptor.Project.Metadata, SchemaVersion: api.MustParse("0.2"), }, tomlMetaData, nil } + +func mapToBuildPacksDescriptor(v2BuildPacks []Buildpack) []types.Buildpack { + var buildPacks []types.Buildpack + for _, v2BuildPack := range v2BuildPacks { + buildPacks = append(buildPacks, mapToBuildPackDescriptor(v2BuildPack)) + } + return buildPacks +} + +func mapToBuildPackDescriptor(v2BuildPack Buildpack) types.Buildpack { + return types.Buildpack{ + ID: v2BuildPack.ID, + Version: v2BuildPack.Version, + URI: v2BuildPack.URI, + Script: v2BuildPack.Script, + ExecEnv: []string{}, // schema v2 doesn't handle execution environments variables + } +} + +func mapToEnvVarsDescriptor(v2EnvVars []EnvVar) []types.EnvVar { + var envVars []types.EnvVar + for _, v2EnvVar := range v2EnvVars { + envVars = append(envVars, mapToEnVarDescriptor(v2EnvVar)) + } + return envVars +} + +func mapToEnVarDescriptor(v2EnVar EnvVar) types.EnvVar { + return types.EnvVar{ + Name: v2EnVar.Name, + Value: v2EnVar.Value, + ExecEnv: []string{}, // schema v2 doesn't handle execution environments variables + } +} diff --git a/pkg/project/v03/project.go b/pkg/project/v03/project.go new file mode 100644 index 0000000000..d01e6c354a --- /dev/null +++ b/pkg/project/v03/project.go @@ -0,0 +1,65 @@ +package v03 + +import ( + "github.com/BurntSushi/toml" + "github.com/buildpacks/lifecycle/api" + + "github.com/buildpacks/pack/pkg/project/types" +) + +type Buildpacks struct { + Include []string `toml:"include"` + Exclude []string `toml:"exclude"` + Group []types.Buildpack `toml:"group"` + Build types.Build `toml:"build"` + Builder string `toml:"builder"` + Pre types.GroupAddition `toml:"pre"` + Post types.GroupAddition `toml:"post"` +} + +type Project struct { + SchemaVersion string `toml:"schema-version"` + ID string `toml:"id"` + Name string `toml:"name"` + Version string `toml:"version"` + Authors []string `toml:"authors"` + Licenses []types.License `toml:"licenses"` + DocumentationURL string `toml:"documentation-url"` + SourceURL string `toml:"source-url"` + Metadata map[string]interface{} `toml:"metadata"` +} + +type IO struct { + Buildpacks Buildpacks `toml:"buildpacks"` +} + +type Descriptor struct { + Project Project `toml:"_"` + IO IO `toml:"io"` +} + +func NewDescriptor(projectTomlContents string) (types.Descriptor, toml.MetaData, error) { + versionedDescriptor := &Descriptor{} + tomlMetaData, err := toml.Decode(projectTomlContents, &versionedDescriptor) + if err != nil { + return types.Descriptor{}, tomlMetaData, err + } + + return types.Descriptor{ + Project: types.Project{ + Name: versionedDescriptor.Project.Name, + Licenses: versionedDescriptor.Project.Licenses, + }, + Build: types.Build{ + Include: versionedDescriptor.IO.Buildpacks.Include, + Exclude: versionedDescriptor.IO.Buildpacks.Exclude, + Buildpacks: versionedDescriptor.IO.Buildpacks.Group, + Env: versionedDescriptor.IO.Buildpacks.Build.Env, + Builder: versionedDescriptor.IO.Buildpacks.Builder, + Pre: versionedDescriptor.IO.Buildpacks.Pre, + Post: versionedDescriptor.IO.Buildpacks.Post, + }, + Metadata: versionedDescriptor.Project.Metadata, + SchemaVersion: api.MustParse("0.3"), + }, tomlMetaData, nil +}