diff --git a/README.md b/README.md index 00d94e63..ba8cb128 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ according to the specification: ## Buildpack Interface -According to the specification, the buildpack interface is composed of both +According to the CNB specification, the buildpack interface is composed of both a detect and build phase. Each of these phases has a corresponding set of packit primitives enable developers to easily implement a buildpack. diff --git a/bom.go b/bom.go new file mode 100644 index 00000000..ed827c87 --- /dev/null +++ b/bom.go @@ -0,0 +1,16 @@ +package packit + +// BOMEntry contains a bill of materials entry. +type BOMEntry struct { + // Name represents the name of the entry. + Name string `toml:"name"` + + // Metadata is the metadata of the entry. Optional. + Metadata map[string]interface{} `toml:"metadata,omitempty"` +} + +// UnmetEntry contains the name of an unmet dependency from the build process +type UnmetEntry struct { + // Name represents the name of the entry. + Name string `toml:"name"` +} diff --git a/build.go b/build.go index b4273327..3d80984a 100644 --- a/build.go +++ b/build.go @@ -12,6 +12,11 @@ import ( "github.com/paketo-buildpacks/packit/internal" ) +// BuildFunc is the definition of a callback that can be invoked when the Build +// function is executed. Buildpack authors should implement a BuildFunc that +// performs the specific build phase operations for a buildpack. +type BuildFunc func(BuildContext) (BuildResult, error) + // BuildContext provides the contextual details that are made available by the // buildpack lifecycle during the build phase. This context is populated by the // Build function and passed to BuildFunc during execution. @@ -25,6 +30,10 @@ type BuildContext struct { // files included in the buildpack. CNBPath string + // Platform includes the platform context according to the specification: + // https://github.com/buildpacks/spec/blob/main/buildpack.md#build + Platform Platform + // Layers provides access to layers managed by the buildpack. It can be used // to create new layers or retrieve cached layers from previous builds. Layers Layers @@ -43,11 +52,6 @@ type BuildContext struct { WorkingDir string } -// BuildFunc is the definition of a callback that can be invoked when the Build -// function is executed. Buildpack authors should implement a BuildFunc that -// performs the specific build phase operations for a buildpack. -type BuildFunc func(BuildContext) (BuildResult, error) - // BuildResult allows buildpack authors to indicate the result of the build // phase for a given buildpack. This result, returned in a BuildFunc callback, // will be parsed and persisted by the Build function and returned to the @@ -76,137 +80,6 @@ type BuildResult struct { Build BuildMetadata } -// BOMEntry contains a bill of materials entry. -type BOMEntry struct { - // Name represents the name of the entry. - Name string `toml:"name"` - - // Metadata is the metadata of the entry. Optional. - Metadata map[string]interface{} `toml:"metadata,omitempty"` -} - -// UnmetEntry contains the name of an unmet dependency from the build process -type UnmetEntry struct { - // Name represents the name of the entry. - Name string `toml:"name"` -} - -// LaunchMetadata represents the launch metadata details persisted in the -// launch.toml file according to the buildpack lifecycle specification: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml. -type LaunchMetadata struct { - // Processes is a list of processes that will be returned to the lifecycle to - // be executed during the launch phase. - Processes []Process - - // 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 - - // Labels is a map of key-value pairs that will be returned to the lifecycle to be - // added as config label on the image metadata. Keys must be unique. - Labels map[string]string - - // BOM is the Bill-of-Material entries containing information about the - // dependencies provided to the launch environment. - BOM []BOMEntry -} - -func (l LaunchMetadata) isEmpty() bool { - return (len(l.Processes) == 0 && - len(l.Slices) == 0 && - len(l.Labels) == 0 && - len(l.BOM) == 0) -} - -func (b BuildMetadata) isEmpty() bool { - return (len(b.BOM) == 0 && - len(b.Unmet) == 0) -} - -// BuildMetadata represents the build metadata details persisted in the -// build.toml file according to the buildpack lifecycle specification: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildtoml-toml. -type BuildMetadata struct { - // BOM is the Bill-of-Material entries containing information about the - // dependencies provided to the build environment. - BOM []BOMEntry `toml:"bom"` - - // Unmet is a list of unmet entries from the build process that it was unable - // to provide. - Unmet []UnmetEntry `toml:"unmet"` -} - -// Process represents a process to be run during the launch phase as described -// in the specification: -// 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 Process 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"` - - // Direct indicates whether the process should bypass the shell when invoked. - Direct bool `toml:"direct"` -} - -// Slice represents a layer of the working directory to be exported during the -// export phase. These slices help to optimize data transfer for files that are -// commonly shared across applications. Slices are described in the layers -// section of the buildpack spec: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#layers. The slice -// fields are described in the specification of the launch.toml file: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml. -type Slice struct { - Paths []string `toml:"paths"` -} - -// BuildpackInfo is a representation of the basic information for a buildpack -// provided in its buildpack.toml file as described in the specification: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml. -type BuildpackInfo struct { - // ID is the identifier specified in the `buildpack.id` field of the buildpack.toml. - ID string `toml:"id"` - - // Name is the identifier specified in the `buildpack.name` field of the buildpack.toml. - Name string `toml:"name"` - - // Version is the identifier specified in the `buildpack.version` field of the buildpack.toml. - Version string `toml:"version"` -} - -// BuildpackPlan is a representation of the buildpack plan provided by the -// lifecycle and defined in the specification: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpack-plan-toml. -// It is also used to return a set of refinements to the plan at the end of the -// build phase. -type BuildpackPlan struct { - // Entries is a list of BuildpackPlanEntry fields that are declared in the - // buildpack plan TOML file. - Entries []BuildpackPlanEntry `toml:"entries"` -} - -// BuildpackPlanEntry is a representation of a single buildpack plan entry -// specified by the lifecycle. -type BuildpackPlanEntry struct { - // Name is the name of the dependency the the buildpack should provide. - Name string `toml:"name"` - - // Metadata is an unspecified field allowing buildpacks to communicate extra - // details about their requirement. Examples of this type of metadata might - // include details about what source was used to decide the version - // constraint for a requirement. - Metadata map[string]interface{} `toml:"metadata"` -} - // Build is an implementation of the build phase according to the Cloud Native // Buildpacks specification. Calling this function with a BuildFunc will // perform the build phase process. @@ -223,8 +96,9 @@ func Build(f BuildFunc, options ...Option) { } var ( - layersPath = config.args[1] - planPath = config.args[3] + layersPath = config.args[1] + platformPath = config.args[2] + planPath = config.args[3] ) pwd, err := os.Getwd() @@ -264,7 +138,10 @@ func Build(f BuildFunc, options ...Option) { } result, err := f(BuildContext{ - CNBPath: cnbPath, + CNBPath: cnbPath, + Platform: Platform{ + Path: platformPath, + }, Stack: os.Getenv("CNB_STACK_ID"), WorkingDir: pwd, Plan: plan, diff --git a/build_metadata.go b/build_metadata.go new file mode 100644 index 00000000..120dc1fc --- /dev/null +++ b/build_metadata.go @@ -0,0 +1,14 @@ +package packit + +// BuildMetadata represents the build metadata details persisted in the +// build.toml file according to the buildpack lifecycle specification: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildtoml-toml. +type BuildMetadata struct { + // BOM is the Bill-of-Material entries containing information about the + // dependencies provided to the build environment. + BOM []BOMEntry `toml:"bom"` + + // Unmet is a list of unmet entries from the build process that it was unable + // to provide. + Unmet []UnmetEntry `toml:"unmet"` +} diff --git a/build_plan.go b/build_plan.go new file mode 100644 index 00000000..b64d948a --- /dev/null +++ b/build_plan.go @@ -0,0 +1,40 @@ +package packit + +// BuildPlan is a representation of the Build Plan as specified in the +// specification: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#build-plan-toml. +// The BuildPlan allows buildpacks to indicate what dependencies they provide +// or require. +type BuildPlan struct { + // Provides is a list of BuildPlanProvisions that are provided by this + // buildpack. + Provides []BuildPlanProvision `toml:"provides"` + + // Requires is a list of BuildPlanRequirements that are required by this + // buildpack. + Requires []BuildPlanRequirement `toml:"requires"` + + // Or is a list of additional BuildPlans that may be selected by the + // lifecycle + Or []BuildPlan `toml:"or,omitempty"` +} + +// BuildPlanProvision is a representation of a dependency that can be provided +// by a buildpack. +type BuildPlanProvision struct { + // Name is the identifier whereby buildpacks can coordinate that a dependency + // is provided or required. + Name string `toml:"name"` +} + +type BuildPlanRequirement struct { + // Name is the identifier whereby buildpacks can coordinate that a dependency + // is provided or required. + Name string `toml:"name"` + + // Metadata is an unspecified field allowing buildpacks to communicate extra + // details about their requirement. Examples of this type of metadata might + // include details about what source was used to decide the version + // constraint for a requirement. + Metadata interface{} `toml:"metadata"` +} diff --git a/build_test.go b/build_test.go index 81002c6c..ca1f924a 100644 --- a/build_test.go +++ b/build_test.go @@ -20,6 +20,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect = NewWithT(t).Expect workingDir string + platformDir string tmpDir string layersDir string planPath string @@ -45,6 +46,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { layersDir, err = os.MkdirTemp("", "layers") Expect(err).NotTo(HaveOccurred()) + platformDir, err = os.MkdirTemp("", "platform") + Expect(err).NotTo(HaveOccurred()) + file, err := os.CreateTemp("", "plan.toml") Expect(err).NotTo(HaveOccurred()) defer file.Close() @@ -91,6 +95,7 @@ api = "0.5" Expect(os.Chdir(workingDir)).To(Succeed()) Expect(os.RemoveAll(tmpDir)).To(Succeed()) Expect(os.RemoveAll(layersDir)).To(Succeed()) + Expect(os.RemoveAll(platformDir)).To(Succeed()) }) it("provides the build context to the given BuildFunc", func() { @@ -100,11 +105,14 @@ api = "0.5" context = ctx return packit.BuildResult{}, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) Expect(context).To(Equal(packit.BuildContext{ - CNBPath: cnbDir, - Stack: "some-stack", + CNBPath: cnbDir, + Stack: "some-stack", + Platform: packit.Platform{ + Path: platformDir, + }, WorkingDir: tmpDir, Plan: packit.BuildpackPlan{ Entries: []packit.BuildpackPlanEntry{ @@ -151,7 +159,7 @@ api = "0.4" return packit.BuildResult{ Plan: ctx.Plan, }, nil - }, packit.WithArgs([]string{binaryPath, "", "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(planPath) Expect(err).NotTo(HaveOccurred()) @@ -173,7 +181,7 @@ api = "0.4" return packit.BuildResult{ Plan: ctx.Plan, }, nil - }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("buildpack plan is read only"))) }) @@ -199,7 +207,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "some-layer.toml")) Expect(err).NotTo(HaveOccurred()) @@ -232,7 +240,7 @@ cache = true return packit.BuildResult{ Layers: []packit.Layer{}, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) Expect(obsoleteLayerPath).NotTo(BeARegularFile()) Expect(obsoleteLayerPath + ".toml").NotTo(BeARegularFile()) @@ -258,10 +266,13 @@ cache = true context = ctx return packit.BuildResult{}, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) Expect(context).To(Equal(packit.BuildContext{ - CNBPath: envCnbDir, + CNBPath: envCnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, Stack: "some-stack", WorkingDir: tmpDir, Plan: packit.BuildpackPlan{ @@ -307,7 +318,7 @@ cache = true return packit.BuildResult{ Layers: []packit.Layer{}, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("failed to remove layer toml:"))) }) }) @@ -332,7 +343,7 @@ cache = true }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "build.toml")) Expect(err).NotTo(HaveOccurred()) @@ -378,7 +389,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("build.toml is only supported with Buildpack API v0.5 or higher"))) }) @@ -400,7 +411,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "build.toml")) Expect(err).NotTo(HaveOccurred()) @@ -441,7 +452,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("build.toml is only supported with Buildpack API v0.5 or higher"))) }) @@ -467,7 +478,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -498,7 +509,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -527,7 +538,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -555,7 +566,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -578,7 +589,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -602,7 +613,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -629,7 +640,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) Expect(err).NotTo(HaveOccurred()) @@ -651,7 +662,7 @@ api = "0.4" it("does not persist a launch.toml", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{}, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) Expect(filepath.Join(layersDir, "launch.toml")).NotTo(BeARegularFile()) }) @@ -675,7 +686,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) for _, modifier := range []string{"append", "default", "delim", "prepend", "override"} { contents, err := os.ReadFile(filepath.Join(layersDir, "some-layer", "env", fmt.Sprintf("SOME_VAR.%s", modifier))) @@ -702,7 +713,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) for _, modifier := range []string{"append", "default", "delim", "prepend", "override"} { contents, err := os.ReadFile(filepath.Join(layersDir, "some-layer", "env.launch", fmt.Sprintf("SOME_VAR.%s", modifier))) @@ -735,7 +746,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) for _, process := range []string{"process-name", "another-process-name"} { for _, modifier := range []string{"append", "default", "delim", "prepend", "override"} { @@ -764,7 +775,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) for _, modifier := range []string{"append", "default", "delim", "prepend", "override"} { contents, err := os.ReadFile(filepath.Join(layersDir, "some-layer", "env.build", fmt.Sprintf("SOME_VAR.%s", modifier))) @@ -785,7 +796,7 @@ api = "0.4" it("calls the exit handler", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{}, nil - }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("bare keys cannot contain '%'"))) }) @@ -795,7 +806,7 @@ api = "0.4" it("calls the exit handler", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{}, errors.New("build failed") - }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("build failed")) }) @@ -810,7 +821,7 @@ api = "0.4" it("calls the exit handler", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{}, nil - }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("bare keys cannot contain '%'"))) }) @@ -834,7 +845,7 @@ api = "0.4" it("calls the exit handler", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{Plan: ctx.Plan}, nil - }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) @@ -859,7 +870,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) @@ -882,7 +893,7 @@ api = "0.4" Processes: []packit.Process{{}}, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) @@ -915,7 +926,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) }) @@ -933,7 +944,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) }) @@ -951,7 +962,7 @@ api = "0.4" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) }) @@ -980,7 +991,7 @@ api = "0.4" }, }}, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) }) @@ -1007,7 +1018,7 @@ api = "0.4" }, }}, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) }) @@ -1034,7 +1045,7 @@ api = "0.4" }, }}, }, nil - }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) }) diff --git a/buildpack_info.go b/buildpack_info.go new file mode 100644 index 00000000..fc7f1b27 --- /dev/null +++ b/buildpack_info.go @@ -0,0 +1,15 @@ +package packit + +// BuildpackInfo is a representation of the basic information for a buildpack +// provided in its buildpack.toml file as described in the specification: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml. +type BuildpackInfo struct { + // ID is the identifier specified in the `buildpack.id` field of the buildpack.toml. + ID string `toml:"id"` + + // Name is the identifier specified in the `buildpack.name` field of the buildpack.toml. + Name string `toml:"name"` + + // Version is the identifier specified in the `buildpack.version` field of the buildpack.toml. + Version string `toml:"version"` +} diff --git a/buildpack_plan.go b/buildpack_plan.go new file mode 100644 index 00000000..dac274b6 --- /dev/null +++ b/buildpack_plan.go @@ -0,0 +1,25 @@ +package packit + +// BuildpackPlan is a representation of the buildpack plan provided by the +// lifecycle and defined in the specification: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpack-plan-toml. +// It is also used to return a set of refinements to the plan at the end of the +// build phase. +type BuildpackPlan struct { + // Entries is a list of BuildpackPlanEntry fields that are declared in the + // buildpack plan TOML file. + Entries []BuildpackPlanEntry `toml:"entries"` +} + +// BuildpackPlanEntry is a representation of a single buildpack plan entry +// specified by the lifecycle. +type BuildpackPlanEntry struct { + // Name is the name of the dependency the the buildpack should provide. + Name string `toml:"name"` + + // Metadata is an unspecified field allowing buildpacks to communicate extra + // details about their requirement. Examples of this type of metadata might + // include details about what source was used to decide the version + // constraint for a requirement. + Metadata map[string]interface{} `toml:"metadata"` +} diff --git a/detect.go b/detect.go index 21bb360e..db20dbe2 100644 --- a/detect.go +++ b/detect.go @@ -9,6 +9,11 @@ import ( "github.com/paketo-buildpacks/packit/internal" ) +// DetectFunc is the definition of a callback that can be invoked when the +// Detect function is executed. Buildpack authors should implement a DetectFunc +// that performs the specific detect phase operations for a buildpack. +type DetectFunc func(DetectContext) (DetectResult, error) + // DetectContext provides the contextual details that are made available by the // buildpack lifecycle during the detect phase. This context is populated by // the Detect function and passed to the DetectFunc during execution. @@ -22,6 +27,10 @@ type DetectContext struct { // files included in the buildpack. CNBPath string + // Platform includes the platform context according to the specification: + // https://github.com/buildpacks/spec/blob/main/buildpack.md#detection + Platform Platform + // BuildpackInfo includes the details of the buildpack parsed from the // buildpack.toml included in the buildpack contents. BuildpackInfo BuildpackInfo @@ -31,11 +40,6 @@ type DetectContext struct { Stack string } -// DetectFunc is the definition of a callback that can be invoked when the -// Detect function is executed. Buildpack authors should implement a DetectFunc -// that performs the specific detect phase operations for a buildpack. -type DetectFunc func(DetectContext) (DetectResult, error) - // DetectResult allows buildpack authors to indicate the result of the detect // phase for a given buildpack. This result, returned in a DetectFunc callback, // will be parsed and persisted by the Detect function and returned to the @@ -46,45 +50,6 @@ type DetectResult struct { Plan BuildPlan } -// BuildPlan is a representation of the Build Plan as specified in the -// specification: -// https://github.com/buildpacks/spec/blob/main/buildpack.md#build-plan-toml. -// The BuildPlan allows buildpacks to indicate what dependencies they provide -// or require. -type BuildPlan struct { - // Provides is a list of BuildPlanProvisions that are provided by this - // buildpack. - Provides []BuildPlanProvision `toml:"provides"` - - // Requires is a list of BuildPlanRequirements that are required by this - // buildpack. - Requires []BuildPlanRequirement `toml:"requires"` - - // Or is a list of additional BuildPlans that may be selected by the - // lifecycle - Or []BuildPlan `toml:"or,omitempty"` -} - -// BuildPlanProvision is a representation of a dependency that can be provided -// by a buildpack. -type BuildPlanProvision struct { - // Name is the identifier whereby buildpacks can coordinate that a dependency - // is provided or required. - Name string `toml:"name"` -} - -type BuildPlanRequirement struct { - // Name is the identifier whereby buildpacks can coordinate that a dependency - // is provided or required. - Name string `toml:"name"` - - // Metadata is an unspecified field allowing buildpacks to communicate extra - // details about their requirement. Examples of this type of metadata might - // include details about what source was used to decide the version - // constraint for a requirement. - Metadata interface{} `toml:"metadata"` -} - // Detect is an implementation of the detect phase according to the Cloud // Native Buildpacks specification. Calling this function with a DetectFunc // will perform the detect phase process. @@ -119,7 +84,10 @@ func Detect(f DetectFunc, options ...Option) { } result, err := f(DetectContext{ - WorkingDir: dir, + WorkingDir: dir, + Platform: Platform{ + Path: config.args[1], + }, CNBPath: cnbPath, BuildpackInfo: buildpackInfo.Buildpack, Stack: os.Getenv("CNB_STACK_ID"), diff --git a/detect_test.go b/detect_test.go index d5f59ed6..9d58cd33 100644 --- a/detect_test.go +++ b/detect_test.go @@ -22,10 +22,14 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { workingDir string tmpDir string + platformDir string cnbDir string cnbEnvDir string binaryPath string stackID string + planDir string + planPath string + exitHandler *fakes.ExitHandler ) @@ -42,6 +46,9 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { Expect(os.Chdir(tmpDir)).To(Succeed()) + platformDir, err = os.MkdirTemp("", "platform") + Expect(err).NotTo(HaveOccurred()) + cnbDir, err = os.MkdirTemp("", "cnb") Expect(err).NotTo(HaveOccurred()) @@ -65,6 +72,11 @@ api = "0.5" Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOMLContent, 0600)).To(Succeed()) Expect(os.WriteFile(filepath.Join(cnbEnvDir, "buildpack.toml"), bpTOMLContent, 0600)).To(Succeed()) + planDir, err = os.MkdirTemp("", "buildplan.toml") + Expect(err).NotTo(HaveOccurred()) + + planPath = filepath.Join(planDir, "buildplan.toml") + exitHandler = &fakes.ExitHandler{} }) @@ -72,16 +84,12 @@ api = "0.5" Expect(os.Chdir(workingDir)).To(Succeed()) Expect(os.RemoveAll(tmpDir)).To(Succeed()) Expect(os.RemoveAll(cnbDir)).To(Succeed()) + Expect(os.RemoveAll(planDir)).To(Succeed()) + Expect(os.RemoveAll(platformDir)).To(Succeed()) Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) }) context("when providing the detect context to the given DetectFunc", func() { - var filePath string - - it.Before(func() { - filePath = filepath.Join(os.TempDir(), "buildplan.toml") - }) - it("succeeds", func() { var context packit.DetectContext @@ -89,11 +97,14 @@ api = "0.5" context = ctx return packit.DetectResult{}, nil - }, packit.WithArgs([]string{binaryPath, "", filePath})) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) Expect(context).To(Equal(packit.DetectContext{ WorkingDir: tmpDir, CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, BuildpackInfo: packit.BuildpackInfo{ ID: "some-id", Name: "some-name", @@ -105,8 +116,6 @@ api = "0.5" }) it("writes out the buildplan.toml", func() { - path := filepath.Join(tmpDir, "buildplan.toml") - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{ Plan: packit.BuildPlan{ @@ -124,9 +133,9 @@ api = "0.5" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, "", path})) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) - contents, err := os.ReadFile(path) + contents, err := os.ReadFile(planPath) Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(MatchTOML(` @@ -143,8 +152,6 @@ api = "0.5" }) it("writes out the buildplan.toml with multiple plans", func() { - path := filepath.Join(tmpDir, "buildplan.toml") - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{ Plan: packit.BuildPlan{ @@ -192,9 +199,9 @@ api = "0.5" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, "", path})) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) - contents, err := os.ReadFile(path) + contents, err := os.ReadFile(planPath) Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(MatchTOML(` @@ -234,10 +241,7 @@ api = "0.5" }) context("when CNB_BUILDPACK_DIR is set", func() { - var filePath string - it.Before(func() { - filePath = filepath.Join(os.TempDir(), "buildplan.toml") Expect(os.Setenv("CNB_BUILDPACK_DIR", cnbEnvDir)).To(Succeed()) }) @@ -252,11 +256,14 @@ api = "0.5" context = ctx return packit.DetectResult{}, nil - }, packit.WithArgs([]string{binaryPath, "", filePath})) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) Expect(context).To(Equal(packit.DetectContext{ WorkingDir: tmpDir, CNBPath: cnbEnvDir, + Platform: packit.Platform{ + Path: platformDir, + }, BuildpackInfo: packit.BuildpackInfo{ ID: "some-id", Name: "some-name", @@ -271,7 +278,7 @@ api = "0.5" it("calls the ExitHandler with that error", func() { packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{}, errors.New("failed to detect") - }, packit.WithArgs([]string{binaryPath, "", ""}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("failed to detect")) }) @@ -285,7 +292,7 @@ api = "0.5" packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{}, packit.Fail.WithMessage("failure message") }, - packit.WithArgs([]string{binaryPath, "", ""}), + packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler( internal.NewExitHandler( internal.WithExitHandlerExitFunc(func(code int) { @@ -304,22 +311,21 @@ api = "0.5" context("failure cases", func() { context("when the buildpack.toml cannot be read", func() { it("returns an error", func() { - path := filepath.Join(tmpDir, "buildplan.toml") - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{}, nil - }, packit.WithArgs([]string{"", "", path}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, platformDir, "/no/such/plan/path"}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("no such file or directory"))) }) }) context("when the buildplan.toml cannot be opened", func() { - it("returns an error", func() { - path := filepath.Join(tmpDir, "buildplan.toml") - _, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0000) + it.Before(func() { + _, err := os.OpenFile(planPath, os.O_CREATE|os.O_RDWR, 0000) Expect(err).NotTo(HaveOccurred()) + }) + it("returns an error", func() { packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{ Plan: packit.BuildPlan{ @@ -337,7 +343,7 @@ api = "0.5" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, "", path}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) }) @@ -345,8 +351,6 @@ api = "0.5" context("when the buildplan.toml cannot be encoded", func() { it("returns an error", func() { - path := filepath.Join(tmpDir, "buildplan.toml") - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{ Plan: packit.BuildPlan{ @@ -361,7 +365,7 @@ api = "0.5" }, }, }, nil - }, packit.WithArgs([]string{binaryPath, "", path}), packit.WithExitHandler(exitHandler)) + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("cannot encode a map with non-string key type"))) }) diff --git a/launch_metadata.go b/launch_metadata.go new file mode 100644 index 00000000..87e847ca --- /dev/null +++ b/launch_metadata.go @@ -0,0 +1,34 @@ +package packit + +// LaunchMetadata represents the launch metadata details persisted in the +// launch.toml file according to the buildpack lifecycle specification: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml. +type LaunchMetadata struct { + // Processes is a list of processes that will be returned to the lifecycle to + // be executed during the launch phase. + Processes []Process + + // 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 + + // Labels is a map of key-value pairs that will be returned to the lifecycle to be + // added as config label on the image metadata. Keys must be unique. + Labels map[string]string + + // BOM is the Bill-of-Material entries containing information about the + // dependencies provided to the launch environment. + BOM []BOMEntry +} + +func (l LaunchMetadata) isEmpty() bool { + return (len(l.Processes) == 0 && + len(l.Slices) == 0 && + len(l.Labels) == 0 && + len(l.BOM) == 0) +} + +func (b BuildMetadata) isEmpty() bool { + return (len(b.BOM) == 0 && + len(b.Unmet) == 0) +} diff --git a/platform.go b/platform.go new file mode 100644 index 00000000..0e7fa603 --- /dev/null +++ b/platform.go @@ -0,0 +1,10 @@ +package packit + +// Platform contains the context of the buildpack platform including its +// location on the filesystem. +type Platform struct { + // Path provides the location of the platform directory on the filesystem. + // This location can be used to find platform extensions like service + // bindings. + Path string +} diff --git a/postal/service.go b/postal/service.go index 6b6dbca6..233c0391 100644 --- a/postal/service.go +++ b/postal/service.go @@ -3,6 +3,7 @@ package postal import ( "fmt" "io" + "path/filepath" "regexp" "sort" "strings" @@ -129,14 +130,15 @@ func (s Service) Resolve(path, id, version, stack string) (Dependency, error) { return compatibleVersions[0], nil } -// Install will fetch and expand a dependency into a layer path location. The +// Deliver will fetch and expand a dependency into a layer path location. The // location of the CNBPath is given so that dependencies that may be included -// in a buildpack when packaged for offline consumption can be retrieved. The -// dependency is validated against the checksum value provided on the -// Dependency and will error if there are inconsistencies in the fetched -// result. -func (s Service) Install(dependency Dependency, cnbPath, layerPath string) error { - dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependency.SHA256, "/platform/bindings") +// in a buildpack when packaged for offline consumption can be retrieved. If +// there is a dependency mapping for the specified dependency, Deliver will use +// the given dependency mapping URI to fetch the dependency. The dependency is +// validated against the checksum value provided on the Dependency and will +// error if there are inconsistencies in the fetched result. +func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath string) error { + dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependency.SHA256, filepath.Join(platformPath, "bindings")) if err != nil { return fmt.Errorf("failure checking out the bindings") } @@ -168,3 +170,10 @@ func (s Service) Install(dependency Dependency, cnbPath, layerPath string) error return nil } + +// Install will invoke Deliver with a hardcoded value of /platform for the platform path. +// +// Deprecated: Use Deliver instead. +func (s Service) Install(dependency Dependency, cnbPath, layerPath string) error { + return s.Deliver(dependency, cnbPath, layerPath, "/platform") +} diff --git a/postal/service_test.go b/postal/service_test.go index 1460937b..9daea968 100644 --- a/postal/service_test.go +++ b/postal/service_test.go @@ -231,7 +231,7 @@ sha256 = "some-sha" stacks = ["some-stack"] uri = "some-uri" version = "4.5.6" -`), 0644) +`), 0600) Expect(err).NotTo(HaveOccurred()) }) @@ -267,7 +267,7 @@ version = "4.5.6" context("failure cases", func() { context("when the buildpack.toml is malformed", func() { it.Before(func() { - err := os.WriteFile(path, []byte("this is not toml"), 0644) + err := os.WriteFile(path, []byte("this is not toml"), 0600) Expect(err).NotTo(HaveOccurred()) }) @@ -293,7 +293,7 @@ sha256 = "some-sha" stacks = ["some-stack"] uri = "some-uri" version = "this is super not semver" -`), 0644) +`), 0600) Expect(err).NotTo(HaveOccurred()) }) @@ -312,6 +312,264 @@ version = "this is super not semver" }) }) + context("Deliver", func() { + var ( + dependencySHA string + layerPath string + platformPath string + deliver func() error + ) + + it.Before(func() { + var err error + layerPath, err = os.MkdirTemp("", "layer") + Expect(err).NotTo(HaveOccurred()) + + platformPath, err = os.MkdirTemp("", "platform") + Expect(err).NotTo(HaveOccurred()) + + buffer := bytes.NewBuffer(nil) + zw := gzip.NewWriter(buffer) + tw := tar.NewWriter(zw) + + Expect(tw.WriteHeader(&tar.Header{Name: "./some-dir", Mode: 0755, Typeflag: tar.TypeDir})).To(Succeed()) + _, err = tw.Write(nil) + Expect(err).NotTo(HaveOccurred()) + + nestedFile := "./some-dir/some-file" + Expect(tw.WriteHeader(&tar.Header{Name: nestedFile, Mode: 0755, Size: int64(len(nestedFile))})).To(Succeed()) + _, err = tw.Write([]byte(nestedFile)) + Expect(err).NotTo(HaveOccurred()) + + for _, file := range []string{"./first", "./second", "./third"} { + Expect(tw.WriteHeader(&tar.Header{Name: file, Mode: 0755, Size: int64(len(file))})).To(Succeed()) + _, err = tw.Write([]byte(file)) + Expect(err).NotTo(HaveOccurred()) + } + + linkName := "./symlink" + linkDest := "./first" + Expect(tw.WriteHeader(&tar.Header{Name: linkName, Mode: 0777, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: linkDest})).To(Succeed()) + // what does a sylink actually look like?? + _, err = tw.Write([]byte{}) + Expect(err).NotTo(HaveOccurred()) + // add a symlink header + + Expect(tw.Close()).To(Succeed()) + Expect(zw.Close()).To(Succeed()) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + deliver = func() error { + return service.Deliver(postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + SHA256: dependencySHA, + Version: "1.2.3", + }, "some-cnb-path", + layerPath, + platformPath, + ) + } + }) + + it.After(func() { + Expect(os.RemoveAll(layerPath)).To(Succeed()) + }) + + it("downloads the dependency and unpackages it into the path", func() { + err := deliver() + + Expect(err).NotTo(HaveOccurred()) + + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("some-entry.tgz")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + + context("when there is a dependency mapping via binding", func() { + it.Before(func() { + mappingResolver.FindDependencyMappingCall.Returns.String = "dependency-mapping-entry.tgz" + }) + + it("looks up the dependency from the platform binding and downloads that instead", func() { + err := deliver() + + Expect(err).NotTo(HaveOccurred()) + + Expect(mappingResolver.FindDependencyMappingCall.Receives.SHA256).To(Equal(dependencySHA)) + Expect(mappingResolver.FindDependencyMappingCall.Receives.BindingPath).To(Equal(filepath.Join(platformPath, "bindings"))) + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mapping-entry.tgz")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + }) + + context("failure cases", func() { + context("when the transport cannot fetch a dependency", func() { + it.Before(func() { + transport.DropCall.Returns.Error = errors.New("there was an error") + }) + + it("returns an error", func() { + err := deliver() + + Expect(err).To(MatchError("failed to fetch dependency: there was an error")) + }) + }) + + context("when the file contents are empty", func() { + it.Before(func() { + buffer := bytes.NewBuffer(nil) + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + }) + + it("fails to create a gzip reader", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("unsupported archive type"))) + }) + }) + + context("when the file contents are malformed", func() { + it.Before(func() { + buffer := bytes.NewBuffer(nil) + gzipWriter := gzip.NewWriter(buffer) + + _, err := gzipWriter.Write([]byte("something")) + Expect(err).NotTo(HaveOccurred()) + + Expect(gzipWriter.Close()).To(Succeed()) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + }) + + it("fails to create a tar reader", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("failed to read tar response"))) + }) + }) + + context("when the file checksum does not match", func() { + it("fails to create a tar reader", func() { + err := service.Deliver(postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + SHA256: "this is not a valid checksum", + Version: "1.2.3", + }, "some-cnb-path", + layerPath, + platformPath, + ) + + Expect(err).To(MatchError(ContainSubstring("checksum does not match"))) + }) + }) + + context("when it does not have permission to write into directory on container", func() { + it.Before(func() { + Expect(os.Chmod(layerPath, 0000)).To(Succeed()) + }) + + it.After(func() { + Expect(os.Chmod(layerPath, 0755)).To(Succeed()) + }) + + it("fails to make a dir", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("failed to create archived directory"))) + }) + }) + + context("when it does not have permission to write into directory that it decompressed", func() { + var testDir string + it.Before(func() { + testDir = filepath.Join(layerPath, "some-dir") + Expect(os.MkdirAll(testDir, os.ModePerm)).To(Succeed()) + Expect(os.Chmod(testDir, 0000)).To(Succeed()) + }) + + it.After(func() { + Expect(os.Chmod(testDir, 0755)).To(Succeed()) + }) + + it("fails to make a file", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("failed to create archived file"))) + }) + }) + + context("when it is given a broken symlink", func() { + it.Before(func() { + buffer := bytes.NewBuffer(nil) + zw := gzip.NewWriter(buffer) + tw := tar.NewWriter(zw) + + linkName := "./symlink" + Expect(tw.WriteHeader(&tar.Header{Name: linkName, Mode: 0777, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: ""})).To(Succeed()) + // what does a sylink actually look like?? + _, err := tw.Write([]byte{}) + Expect(err).NotTo(HaveOccurred()) + // add a symlink header + + Expect(tw.Close()).To(Succeed()) + Expect(zw.Close()).To(Succeed()) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + }) + + it("fails to extract the symlink", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("failed to extract symlink"))) + }) + }) + }) + }) + context("Install", func() { var ( dependencySHA string @@ -321,7 +579,7 @@ version = "this is super not semver" it.Before(func() { var err error - layerPath, err = os.MkdirTemp("", "path") + layerPath, err = os.MkdirTemp("", "layer") Expect(err).NotTo(HaveOccurred()) buffer := bytes.NewBuffer(nil) @@ -428,7 +686,6 @@ version = "this is super not semver" Expect(err).NotTo(HaveOccurred()) Expect(info.Mode()).To(Equal(os.FileMode(0755))) }) - }) context("failure cases", func() { diff --git a/process.go b/process.go new file mode 100644 index 00000000..a29a170a --- /dev/null +++ b/process.go @@ -0,0 +1,22 @@ +package packit + +// Process represents a process to be run during the launch phase as described +// in the specification: +// 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 Process 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"` + + // Direct indicates whether the process should bypass the shell when invoked. + Direct bool `toml:"direct"` +} diff --git a/slice.go b/slice.go new file mode 100644 index 00000000..9e928bd0 --- /dev/null +++ b/slice.go @@ -0,0 +1,12 @@ +package packit + +// Slice represents a layer of the working directory to be exported during the +// export phase. These slices help to optimize data transfer for files that are +// commonly shared across applications. Slices are described in the layers +// section of the buildpack spec: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#layers. The slice +// fields are described in the specification of the launch.toml file: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml. +type Slice struct { + Paths []string `toml:"paths"` +}