diff --git a/build.go b/build.go index 17f656ca..aafb9079 100644 --- a/build.go +++ b/build.go @@ -144,6 +144,7 @@ func Build(f BuildFunc, options ...Option) { apiV05, _ := semver.NewVersion("0.5") apiV06, _ := semver.NewVersion("0.6") apiV08, _ := semver.NewVersion("0.8") + apiV09, _ := semver.NewVersion("0.9") apiVersion, err := semver.NewVersion(buildpackInfo.APIVersion) if err != nil { config.exitHandler.Error(err) @@ -284,13 +285,27 @@ func Build(f BuildFunc, options ...Option) { } var launch struct { - Processes []Process `toml:"processes"` - Slices []Slice `toml:"slices"` - Labels []label `toml:"labels"` - BOM []BOMEntry `toml:"bom"` + Processes []Process `toml:"processes,omitempty"` + DirectProcesses []DirectProcess `toml:"processes,omitempty"` + Slices []Slice `toml:"slices"` + Labels []label `toml:"labels"` + BOM []BOMEntry `toml:"bom"` + } + + if apiVersion.LessThan(apiV09) { + if result.Launch.DirectProcesses != nil { + config.exitHandler.Error(errors.New("direct processes can only be used with Buildpack API v0.9 or higher")) + return + } + launch.Processes = result.Launch.Processes + } else { + if result.Launch.Processes != nil { + config.exitHandler.Error(errors.New("non direct processes can only be used with Buildpack API v0.8 or lower")) + return + } + launch.DirectProcesses = result.Launch.DirectProcesses } - launch.Processes = result.Launch.Processes if apiVersion.LessThan(apiV06) { for _, process := range launch.Processes { if process.Default { diff --git a/build_test.go b/build_test.go index b975a7f2..a6326697 100644 --- a/build_test.go +++ b/build_test.go @@ -209,7 +209,7 @@ api = "0.4" return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", Build: true, @@ -256,7 +256,7 @@ api = "0.5" return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", Build: true, @@ -292,7 +292,7 @@ cache = true return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", SBOM: packit.SBOMFormats{ @@ -339,7 +339,7 @@ api = "0.6" return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", SBOM: packit.SBOMFormats{ @@ -1136,6 +1136,140 @@ api = "0.7" Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("processes can only have a specific working directory with Buildpack API v0.8 or higher"))) }) }) + + context("when the api version is less than 0.9", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +api = "0.8" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`), 0600)).To(Succeed()) + }) + + it("persists a launch.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + Processes: []packit.Process{ + { + Type: "some-type", + Command: "some-command", + Args: []string{"some-arg"}, + Direct: false, + Default: true, + WorkingDirectory: "some-working-dir", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[processes]] + args = ["some-arg"] + command = "some-command" + direct = false + default = true + type = "some-type" + working-directory = "some-working-dir" + `)) + }) + + context("failure cases", func() { + it("throws a specific error when new style proccesses are used", func() { + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + DirectProcesses: []packit.DirectProcess{ + { + Type: "some-type", + Command: []string{"some-command"}, + Args: []string{"some-arg"}, + Default: false, + WorkingDirectory: workingDir, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("direct processes can only be used with Buildpack API v0.9 or higher")) + }) + }) + }) + + context("when the api version is 0.9", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +api = "0.9" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`), 0600)).To(Succeed()) + }) + + it("persists a launch.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + DirectProcesses: []packit.DirectProcess{ + { + Type: "some-type", + Command: []string{"some-command"}, + Args: []string{"some-arg"}, + Default: true, + WorkingDirectory: "some-working-dir", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[processes]] + args = ["some-arg"] + command = ["some-command"] + default = true + type = "some-type" + working-directory = "some-working-dir" + `)) + }) + context("failure cases", func() { + it("throws a specific error when old style proccesses are used", func() { + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + Processes: []packit.Process{ + { + Type: "some-type", + Command: "some-command", + Args: []string{"some-arg"}, + Direct: false, + Default: false, + WorkingDirectory: workingDir, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("non direct processes can only be used with Buildpack API v0.8 or lower")) + }) + }) + }) }) context("when there are slices in the result", func() { @@ -1527,7 +1661,7 @@ api = "0.4" packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: filepath.Join(layersDir, "some-layer"), Name: "some-layer", }, @@ -1544,7 +1678,7 @@ api = "0.4" packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: filepath.Join(layersDir, "some-layer"), Name: "some-layer", SBOM: packit.SBOMFormats{ diff --git a/launch_metadata.go b/launch_metadata.go index 2642f2c4..dc6fe08e 100644 --- a/launch_metadata.go +++ b/launch_metadata.go @@ -8,6 +8,10 @@ type LaunchMetadata struct { // be executed during the launch phase. Processes []Process + // DirectProcesses is a list of processes that will be returned to the lifecycle to + // be executed directly during the launch phase. + DirectProcesses []DirectProcess + // Slices is a list of slices that will be returned to the lifecycle to be // exported as separate layers during the export phase. Slices []Slice @@ -31,5 +35,5 @@ func (l LaunchMetadata) isEmpty() bool { sbom = l.SBOM.Formats() } - return len(sbom)+len(l.Processes)+len(l.Slices)+len(l.Labels)+len(l.BOM) == 0 + return len(sbom)+len(l.Processes)+len(l.DirectProcesses)+len(l.Slices)+len(l.Labels)+len(l.BOM) == 0 } diff --git a/process.go b/process.go index 90aa4e64..fd84ab8e 100644 --- a/process.go +++ b/process.go @@ -1,7 +1,7 @@ package packit // Process represents a process to be run during the launch phase as described -// in the specification: +// in the specification lower than v0.9: // https://github.com/buildpacks/spec/blob/main/buildpack.md#launch. The // fields of the process are describe in the specification of the launch.toml // file: @@ -28,3 +28,29 @@ type Process struct { // absolute path or one relative to the default application directory. WorkingDirectory string `toml:"working-directory,omitempty"` } + +// DirectProcess represents a process to be run during the launch phase as described +// in the specification higher or equal than v0.9: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#launch. The +// fields of the process are describe in the specification of the launch.toml +// file: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml. +type DirectProcess struct { + // Type is an identifier to describe the type of process to be executed, eg. + // "web". + Type string `toml:"type"` + + // Command is the start command to be executed at launch. + Command []string `toml:"command"` + + // Args is a list of arguments to be passed to the command at launch. + Args []string `toml:"args"` + + // Default indicates if this process should be the default when launched. + Default bool `toml:"default,omitempty"` + + // WorkingDirectory indicates if this process should be run in a working + // directory other than the application directory. This can either be an + // absolute path or one relative to the default application directory. + WorkingDirectory string `toml:"working-directory,omitempty"` +} diff --git a/scribe/emitter.go b/scribe/emitter.go index 2dbd4614..c90bfb24 100644 --- a/scribe/emitter.go +++ b/scribe/emitter.go @@ -11,6 +11,47 @@ import ( "github.com/paketo-buildpacks/packit/v2/postal" ) +type launchProcess interface { + GetType() string + GetCommand() []string + GetArgs() []string + GetDefault() bool +} + +type indirectProcess struct { + packit.Process +} + +func (p indirectProcess) GetType() string { + return p.Type +} +func (p indirectProcess) GetCommand() []string { + return strings.Split(p.Command, " ") +} +func (p indirectProcess) GetArgs() []string { + return p.Args +} +func (p indirectProcess) GetDefault() bool { + return p.Default +} + +type directProcess struct { + packit.DirectProcess +} + +func (p directProcess) GetType() string { + return p.Type +} +func (p directProcess) GetCommand() []string { + return p.Command +} +func (p directProcess) GetArgs() []string { + return p.Args +} +func (p directProcess) GetDefault() bool { + return p.Default +} + // An Emitter embeds the scribe.Logger type to provide an interface for // complicated shared logging tasks. type Emitter struct { @@ -106,11 +147,31 @@ Entries: e.Break() } -// LaunchProcesses take a list of processes and a map of process specific +// LaunchProcesses take a list of (indirect) processes and a map of process specific // enivronment varables and prints out a formatted table including the type // name, whether or not it is a default process, the command, arguments, and // any process specific environment variables. func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[string]packit.Environment) { + launchProcesses := []launchProcess{} + for _, process := range processes { + launchProcesses = append(launchProcesses, indirectProcess{process}) + } + e.launch(launchProcesses, processEnvs...) +} + +// LaunchDirectProcesses take a list of direct processes and a map of process specific +// enivronment varables and prints out a formatted table including the type +// name, whether or not it is a default process, the command, arguments, and +// any process specific environment variables. +func (e Emitter) LaunchDirectProcesses(processes []packit.DirectProcess, processEnvs ...map[string]packit.Environment) { + launchProcesses := []launchProcess{} + for _, process := range processes { + launchProcesses = append(launchProcesses, directProcess{process}) + } + e.launch(launchProcesses, processEnvs...) +} + +func (e Emitter) launch(processes []launchProcess, processEnvs ...map[string]packit.Environment) { e.Process("Assigning launch processes:") var ( @@ -118,8 +179,8 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ ) for _, process := range processes { - pType := process.Type - if process.Default { + pType := process.GetType() + if process.GetDefault() { pType += " " + "(default)" } @@ -129,16 +190,17 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ } for _, process := range processes { - pType := process.Type - if process.Default { + pType := process.GetType() + if process.GetDefault() { pType += " " + "(default)" } - pad := typePadding + len(process.Command) - len(pType) - p := fmt.Sprintf("%s: %*s", pType, pad, process.Command) + command := strings.Join(process.GetCommand(), " ") + pad := typePadding + len(command) - len(pType) + p := fmt.Sprintf("%s: %*s", pType, pad, command) - if process.Args != nil { - p += " " + strings.Join(process.Args, " ") + if process.GetArgs() != nil { + p += " " + strings.Join(process.GetArgs(), " ") } e.Subprocess(p) @@ -147,7 +209,7 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ // matter the order of the process envs map list processEnv := packit.Environment{} for _, pEnvs := range processEnvs { - if env, ok := pEnvs[process.Type]; ok { + if env, ok := pEnvs[process.GetType()]; ok { for key, value := range env { processEnv[key] = value } diff --git a/scribe/emitter_test.go b/scribe/emitter_test.go index cfaebca5..b02b45af 100644 --- a/scribe/emitter_test.go +++ b/scribe/emitter_test.go @@ -323,6 +323,74 @@ func testEmitter(t *testing.T, context spec.G, it spec.S) { }) }) + context("LaunchDirectProcesses", func() { + var processes []packit.DirectProcess + + it.Before(func() { + processes = []packit.DirectProcess{ + { + Type: "some-type", + Command: []string{"some-command"}, + }, + { + Type: "web", + Command: []string{"web-command"}, + Default: true, + }, + { + Type: "some-other-type", + Command: []string{"some-other-command"}, + Args: []string{"some", "args"}, + }, + } + }) + + it("prints a list of launch processes", func() { + emitter.LaunchDirectProcesses(processes) + + Expect(buffer.String()).To(ContainLines( + " Assigning launch processes:", + " some-type: some-command", + " web (default): web-command", + " some-other-type: some-other-command some args", + "", + )) + }) + + context("when passed process specific environment variables", func() { + var processEnvs []map[string]packit.Environment + + it.Before(func() { + processEnvs = []map[string]packit.Environment{ + { + "web": packit.Environment{ + "WEB_VAR.default": "some-env", + }, + }, + { + "web": packit.Environment{ + "ANOTHER_WEB_VAR.default": "another-env", + }, + }, + } + }) + + it("prints a list of the launch processes and their processes specific env vars", func() { + emitter.LaunchDirectProcesses(processes, processEnvs...) + + Expect(buffer.String()).To(ContainLines( + " Assigning launch processes:", + " some-type: some-command", + " web (default): web-command", + ` ANOTHER_WEB_VAR -> "another-env"`, + ` WEB_VAR -> "some-env"`, + " some-other-type: some-other-command some args", + "", + )) + }) + }) + }) + context("EnvironmentVariables", func() { it("prints a list of environment variables available during launch and build", func() { emitter.EnvironmentVariables(packit.Layer{