diff --git a/build.go b/build.go index 17ff652..4682808 100644 --- a/build.go +++ b/build.go @@ -105,7 +105,7 @@ func (b BuildResult) String() string { ) } -//go:generate mockery -name Builder -case=underscore +//go:generate mockery --name=Builder --case=underscore // Builder describes an interface for types that can be used by the Build function. type Builder interface { diff --git a/config.go b/config.go index 3eaf6ee..d3ac37c 100644 --- a/config.go +++ b/config.go @@ -16,7 +16,7 @@ package libcnb -//go:generate mockery -name EnvironmentWriter -case=underscore +//go:generate mockery --name EnvironmentWriter --case=underscore // EnvironmentWriter is the interface implemented by a type that wants to serialize a map of environment variables to // the file system. @@ -27,7 +27,7 @@ type EnvironmentWriter interface { Write(dir string, environment map[string]string) error } -//go:generate mockery -name ExitHandler -case=underscore +//go:generate mockery --name ExitHandler --case=underscore // ExitHandler is the interface implemented by a type that wants to handle exit behavior when a buildpack encounters an // error. @@ -43,7 +43,7 @@ type ExitHandler interface { Pass() } -//go:generate mockery -name TOMLWriter -case=underscore +//go:generate mockery --name TOMLWriter --case=underscore // TOMLWriter is the interface implemented by a type that wants to serialize an object to a TOML file. type TOMLWriter interface { @@ -52,12 +52,23 @@ type TOMLWriter interface { Write(path string, value interface{}) error } +//go:generate mockery --name ExecDWriter --case=underscore + +// ExecDWriter is the interface implemented by a type that wants to write exec.d output to file descriptor 3. +type ExecDWriter interface { + + // Write is called with the map of environment value key value + // pairs that will be written out + Write(value map[string]string) error +} + // Config is an object that contains configurable properties for execution. type Config struct { arguments []string environmentWriter EnvironmentWriter exitHandler ExitHandler tomlWriter TOMLWriter + execdWriter ExecDWriter } // Option is a function for configuring a Config instance. @@ -94,3 +105,11 @@ func WithTOMLWriter(tomlWriter TOMLWriter) Option { return config } } + +// WithExecDWriter creates an Option that sets a ExecDWriter implementation. +func WithExecDWriter(execdWriter ExecDWriter) Option { + return func(config Config) Config { + config.execdWriter = execdWriter + return config + } +} diff --git a/detect.go b/detect.go index 71a9da7..aa6696b 100644 --- a/detect.go +++ b/detect.go @@ -55,7 +55,7 @@ type DetectResult struct { Plans []BuildPlan } -//go:generate mockery -name Detector -case=underscore +//go:generate mockery --name Detector --case=underscore // Detector describes an interface for types that can be used by the Detect function. type Detector interface { diff --git a/exec_d.go b/exec_d.go new file mode 100644 index 0000000..d290632 --- /dev/null +++ b/exec_d.go @@ -0,0 +1,71 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcnb + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/buildpacks/libcnb/internal" +) + +//go:generate mockery --name ExecD --case=underscore + +// ExecD describes an interface for types that follow the Exec.d specification. +// It should return a map of environment variables and their values as output. +type ExecD interface { + Execute() (map[string]string, error) +} + +// RunExecD is called by the main function of a buildpack's execd binary, encompassing multiple execd +// executors in one binary. +func RunExecD(execDMap map[string]ExecD, options ...Option) { + config := Config{ + arguments: os.Args, + execdWriter: internal.NewExecDWriter(), + exitHandler: internal.NewExitHandler(), + } + + for _, option := range options { + config = option(config) + } + + if len(config.arguments) == 0 { + config.exitHandler.Error(fmt.Errorf("expected command name")) + + return + } + + c := filepath.Base(config.arguments[0]) + e, ok := execDMap[c] + if !ok { + config.exitHandler.Error(fmt.Errorf("unsupported command %s", c)) + return + } + + r, err := e.Execute() + if err != nil { + config.exitHandler.Error(err) + return + } + + if err := config.execdWriter.Write(r); err != nil { + config.exitHandler.Error(err) + return + } +} diff --git a/exec_d_test.go b/exec_d_test.go new file mode 100644 index 0000000..01f3c39 --- /dev/null +++ b/exec_d_test.go @@ -0,0 +1,130 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcnb_test + +import ( + "fmt" + "testing" + + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + "github.com/stretchr/testify/mock" + + "github.com/buildpacks/libcnb" + "github.com/buildpacks/libcnb/mocks" +) + +func testExecD(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + exitHandler *mocks.ExitHandler + execdWriter *mocks.ExecDWriter + ) + + it.Before(func() { + execdWriter = &mocks.ExecDWriter{} + execdWriter.On("Write", mock.Anything).Return(nil) + exitHandler = &mocks.ExitHandler{} + exitHandler.On("Error", mock.Anything) + exitHandler.On("Pass", mock.Anything) + exitHandler.On("Fail", mock.Anything) + }) + + it("encounters the wrong number of arguments", func() { + libcnb.RunExecD(map[string]libcnb.ExecD{}, + libcnb.WithArguments([]string{}), + libcnb.WithExitHandler(exitHandler), + ) + + Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected command name")) + }) + + it("encounters an unsupported execd binary name", func() { + libcnb.RunExecD(map[string]libcnb.ExecD{}, + libcnb.WithArguments([]string{"/dne"}), + libcnb.WithExitHandler(exitHandler), + ) + + Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unsupported command dne")) + }) + + it("calls the appropriate execd for a given execd invoker binary", func() { + execd1 := &mocks.ExecD{} + execd2 := &mocks.ExecD{} + execd1.On("Execute", mock.Anything).Return(map[string]string{}, nil) + + libcnb.RunExecD(map[string]libcnb.ExecD{"execd1": execd1, "execd2": execd2}, + libcnb.WithArguments([]string{"execd1"}), + libcnb.WithExitHandler(exitHandler), + libcnb.WithExecDWriter(execdWriter), + ) + + Expect(execd1.Calls).To(HaveLen(1)) + Expect(execd2.Calls).To(BeEmpty()) + }) + + it("calls exitHandler with the error from the execd", func() { + e := &mocks.ExecD{} + err := fmt.Errorf("example error") + e.On("Execute", mock.Anything).Return(nil, err) + + libcnb.RunExecD(map[string]libcnb.ExecD{"e": e}, + libcnb.WithArguments([]string{"/bin/e"}), + libcnb.WithExitHandler(exitHandler), + libcnb.WithExecDWriter(execdWriter), + ) + + Expect(e.Calls).To(HaveLen(1)) + Expect(execdWriter.Calls).To(HaveLen(0)) + Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(err)) + }) + + it("calls execdWriter.write with the appropriate input", func() { + e := &mocks.ExecD{} + o := map[string]string{"test": "test"} + e.On("Execute", mock.Anything).Return(o, nil) + + libcnb.RunExecD(map[string]libcnb.ExecD{"e": e}, + libcnb.WithArguments([]string{"/bin/e"}), + libcnb.WithExitHandler(exitHandler), + libcnb.WithExecDWriter(execdWriter), + ) + + Expect(e.Calls).To(HaveLen(1)) + Expect(execdWriter.Calls).To(HaveLen(1)) + Expect(execdWriter.Calls[0].Method).To(BeIdenticalTo("Write")) + Expect(execdWriter.Calls[0].Arguments).To(HaveLen(1)) + Expect(execdWriter.Calls[0].Arguments[0]).To(Equal(o)) + }) + + it("calls exitHandler with the error from the execd", func() { + e := &mocks.ExecD{} + err := fmt.Errorf("example error") + e.On("Execute", mock.Anything).Return(nil, err) + + libcnb.RunExecD(map[string]libcnb.ExecD{"e": e}, + libcnb.WithArguments([]string{"/bin/e"}), + libcnb.WithExitHandler(exitHandler), + libcnb.WithExecDWriter(execdWriter), + ) + + Expect(e.Calls).To(HaveLen(1)) + Expect(execdWriter.Calls).To(HaveLen(0)) + Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(err)) + }) +} diff --git a/init_test.go b/init_test.go index 73d7935..313577b 100644 --- a/init_test.go +++ b/init_test.go @@ -32,5 +32,6 @@ func TestUnit(t *testing.T) { suite("Layer", testLayer) suite("Main", testMain) suite("Platform", testPlatform) + suite("ExecD", testExecD) suite.Run(t) } diff --git a/internal/execd_writer.go b/internal/execd_writer.go new file mode 100644 index 0000000..541efb7 --- /dev/null +++ b/internal/execd_writer.go @@ -0,0 +1,62 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "io" + "os" + + "github.com/BurntSushi/toml" +) + +// ExecDWriter is a type used to write TOML files to fd3. +type ExecDWriter struct { + outputWriter io.Writer +} + +// Option is a function for configuring an ExitHandler instance. +type ExecDOption func(handler ExecDWriter) ExecDWriter + +// WithExecDOutputWriter creates an Option that configures the writer. +func WithExecDOutputWriter(writer io.Writer) ExecDOption { + return func(execdWriter ExecDWriter) ExecDWriter { + execdWriter.outputWriter = writer + return execdWriter + } +} + +// NewExitHandler creates a new instance that calls os.Exit and writes to os.stderr. +func NewExecDWriter(options ...ExecDOption) ExecDWriter { + h := ExecDWriter{ + outputWriter: os.NewFile(3, "/dev/fd/3"), + } + + for _, option := range options { + h = option(h) + } + + return h +} + +// Write outputs the value serialized in TOML format to the appropriate writer. +func (e ExecDWriter) Write(value map[string]string) error { + if value == nil { + return nil + } + + return toml.NewEncoder(e.outputWriter).Encode(value) +} diff --git a/internal/execd_writer_test.go b/internal/execd_writer_test.go new file mode 100644 index 0000000..b19c2b3 --- /dev/null +++ b/internal/execd_writer_test.go @@ -0,0 +1,55 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal_test + +import ( + "bytes" + "testing" + + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + + "github.com/buildpacks/libcnb/internal" +) + +func testExecDWriter(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + b *bytes.Buffer + writer internal.ExecDWriter + ) + + it.Before(func() { + b = bytes.NewBuffer([]byte{}) + + writer = internal.NewExecDWriter( + internal.WithExecDOutputWriter(b), + ) + }) + + it("writes the correct set of values", func() { + env := map[string]string{ + "test": "test", + "test2": "te∆t", + } + Expect(writer.Write(env)).To(BeNil()) + Expect(b.String()).To(internal.MatchTOML(` + test = "test" + test2 = "te∆t"`)) + }) +} diff --git a/internal/init_test.go b/internal/init_test.go index 9f0f848..6e66d22 100644 --- a/internal/init_test.go +++ b/internal/init_test.go @@ -30,5 +30,6 @@ func TestUnit(t *testing.T) { suite("EnvironmentWriter", testEnvironmentWriter) suite("ExitHandler", testExitHandler) suite("TOMLWriter", testTOMLWriter) + suite("ExecDWriter", testExecDWriter) suite.Run(t) } diff --git a/layer.go b/layer.go index 5577bb0..1795849 100644 --- a/layer.go +++ b/layer.go @@ -112,7 +112,7 @@ type LayerTypes struct { Launch bool `toml:"launch"` } -//go:generate mockery -name LayerContributor -case=underscore +//go:generate mockery --name LayerContributor --case=underscore // LayerContributor is an interface for types that create layers. type LayerContributor interface { diff --git a/mocks/builder.go b/mocks/builder.go index 46e67e0..44ea7a7 100644 --- a/mocks/builder.go +++ b/mocks/builder.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery 2.9.4. DO NOT EDIT. package mocks diff --git a/mocks/detector.go b/mocks/detector.go index ee17320..8d3174f 100644 --- a/mocks/detector.go +++ b/mocks/detector.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery 2.9.4. DO NOT EDIT. package mocks diff --git a/mocks/environment_writer.go b/mocks/environment_writer.go index c9885eb..3e24d95 100644 --- a/mocks/environment_writer.go +++ b/mocks/environment_writer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery 2.9.4. DO NOT EDIT. package mocks diff --git a/mocks/exec_d.go b/mocks/exec_d.go new file mode 100644 index 0000000..7d8075e --- /dev/null +++ b/mocks/exec_d.go @@ -0,0 +1,33 @@ +// Code generated by mockery 2.9.4. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// ExecD is an autogenerated mock type for the ExecD type +type ExecD struct { + mock.Mock +} + +// Execute provides a mock function with given fields: +func (_m *ExecD) Execute() (map[string]string, error) { + ret := _m.Called() + + var r0 map[string]string + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mocks/exec_d_writer.go b/mocks/exec_d_writer.go new file mode 100644 index 0000000..81323a9 --- /dev/null +++ b/mocks/exec_d_writer.go @@ -0,0 +1,24 @@ +// Code generated by mockery 2.9.4. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// ExecDWriter is an autogenerated mock type for the ExecDWriter type +type ExecDWriter struct { + mock.Mock +} + +// Write provides a mock function with given fields: value +func (_m *ExecDWriter) Write(value map[string]string) error { + ret := _m.Called(value) + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]string) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/exit_handler.go b/mocks/exit_handler.go index 9f4595e..3ac5ed5 100644 --- a/mocks/exit_handler.go +++ b/mocks/exit_handler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery 2.9.4. DO NOT EDIT. package mocks diff --git a/mocks/layer_contributor.go b/mocks/layer_contributor.go index cdac62c..23ef13d 100644 --- a/mocks/layer_contributor.go +++ b/mocks/layer_contributor.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery 2.9.4. DO NOT EDIT. package mocks @@ -13,20 +13,20 @@ type LayerContributor struct { mock.Mock } -// Contribute provides a mock function with given fields: _a0 -func (_m *LayerContributor) Contribute(_a0 libcnb.Layer) (libcnb.Layer, error) { - ret := _m.Called(_a0) +// Contribute provides a mock function with given fields: layer +func (_m *LayerContributor) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { + ret := _m.Called(layer) var r0 libcnb.Layer if rf, ok := ret.Get(0).(func(libcnb.Layer) libcnb.Layer); ok { - r0 = rf(_a0) + r0 = rf(layer) } else { r0 = ret.Get(0).(libcnb.Layer) } var r1 error if rf, ok := ret.Get(1).(func(libcnb.Layer) error); ok { - r1 = rf(_a0) + r1 = rf(layer) } else { r1 = ret.Error(1) } diff --git a/mocks/toml_writer.go b/mocks/toml_writer.go index 46a9a5e..aad4357 100644 --- a/mocks/toml_writer.go +++ b/mocks/toml_writer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery 2.9.4. DO NOT EDIT. package mocks