Compare commits

..

No commits in common. "main" and "v1.22.0" have entirely different histories.

85 changed files with 2631 additions and 4974 deletions

View File

@ -4,8 +4,6 @@ updates:
directory: /
schedule:
interval: daily
ignore:
- dependency-name: github.com/onsi/gomega
labels:
- semver:patch
- type:dependency-upgrade

15
.github/labels.yml vendored
View File

@ -25,18 +25,3 @@
- name: type:task
description: A general task
color: e3d9fc
- name: type:informational
description: Provides information or notice to the community
color: e3d9fc
- name: type:poll
description: Request for feedback from the community
color: e3d9fc
- name: note:ideal-for-contribution
description: An issue that a contributor can help us with
color: 54f7a8
- name: note:on-hold
description: We can't start working on this issue yet
color: 54f7a8
- name: note:good-first-issue
description: A good first issue to get started with
color: 54f7a8

View File

@ -14,19 +14,7 @@ test:
set -euo pipefail
echo "Installing richgo ${RICHGO_VERSION}"
mkdir -p "${HOME}"/bin
echo "${HOME}/bin" >> "${GITHUB_PATH}"
curl \
--location \
--show-error \
--silent \
"https://github.com/kyoh86/richgo/releases/download/v${RICHGO_VERSION}/richgo_${RICHGO_VERSION}_linux_amd64.tar.gz" \
| tar -C "${HOME}"/bin -xz richgo
env:
RICHGO_VERSION: 0.3.10
GO111MODULE=on go get -u -ldflags="-s -w" github.com/kyoh86/richgo
- name: Run Tests
run: |
#!/usr/bin/env bash

View File

@ -1 +1 @@
1.42.0
1.10.0

1
.github/pipelines-version vendored Normal file
View File

@ -0,0 +1 @@
1.4.0

View File

@ -12,7 +12,7 @@ jobs:
runs-on:
- ubuntu-latest
steps:
- uses: mheap/github-action-required-labels@v5
- uses: mheap/github-action-required-labels@v1
with:
count: 1
labels: semver:major, semver:minor, semver:patch
@ -22,7 +22,7 @@ jobs:
runs-on:
- ubuntu-latest
steps:
- uses: mheap/github-action-required-labels@v5
- uses: mheap/github-action-required-labels@v1
with:
count: 1
labels: type:bug, type:dependency-upgrade, type:documentation, type:enhancement, type:question, type:task

View File

@ -1,54 +0,0 @@
name: Tests
"on":
merge_group:
types:
- checks_requested
branches:
- main
pull_request: {}
push:
branches:
- main
jobs:
unit:
name: Unit Test
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
path: ${{ env.HOME }}/go/pkg/mod
restore-keys: ${{ runner.os }}-go-
- uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install richgo
run: |
#!/usr/bin/env bash
set -euo pipefail
echo "Installing richgo ${RICHGO_VERSION}"
mkdir -p "${HOME}"/bin
echo "${HOME}/bin" >> "${GITHUB_PATH}"
curl \
--location \
--show-error \
--silent \
"https://github.com/kyoh86/richgo/releases/download/v${RICHGO_VERSION}/richgo_${RICHGO_VERSION}_linux_amd64.tar.gz" \
| tar -C "${HOME}"/bin -xz richgo
env:
RICHGO_VERSION: 0.3.10
- name: Run Tests
run: |
#!/usr/bin/env bash
set -euo pipefail
GOCMD=richgo make
env:
RICHGO_FORCE_COLOR: "1"

View File

@ -1,72 +0,0 @@
name: Update Go
"on":
schedule:
- cron: 17 2 * * 1
workflow_dispatch: {}
jobs:
update:
name: Update Go
runs-on:
- ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: "1.24"
- uses: actions/checkout@v4
- name: Update Go Version & Modules
id: update-go
run: |
#!/usr/bin/env bash
set -euo pipefail
if [ -z "${GO_VERSION:-}" ]; then
echo "No go version set"
exit 1
fi
OLD_GO_VERSION=$(grep -P '^go \d\.\d+' go.mod | cut -d ' ' -f 2 | cut -d '.' -f 1-2)
go mod edit -go="$GO_VERSION"
go mod tidy
go get -u -t ./...
go mod tidy
git add go.mod go.sum
git checkout -- .
if [ "$OLD_GO_VERSION" == "$GO_VERSION" ]; then
COMMIT_TITLE="Bump Go Modules"
COMMIT_BODY="Bumps Go modules used by the project. See the commit for details on what modules were updated."
COMMIT_SEMVER="semver:patch"
else
COMMIT_TITLE="Bump Go from ${OLD_GO_VERSION} to ${GO_VERSION}"
COMMIT_BODY="Bumps Go from ${OLD_GO_VERSION} to ${GO_VERSION} and update Go modules used by the project. See the commit for details on what modules were updated."
COMMIT_SEMVER="semver:minor"
fi
echo "commit-title=${COMMIT_TITLE}" >> "$GITHUB_OUTPUT"
echo "commit-body=${COMMIT_BODY}" >> "$GITHUB_OUTPUT"
echo "commit-semver=${COMMIT_SEMVER}" >> "$GITHUB_OUTPUT"
env:
GO_VERSION: "1.24"
- uses: peter-evans/create-pull-request@v6
with:
author: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }} <${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }}@users.noreply.github.com>
body: |-
${{ steps.update-go.outputs.commit-body }}
<details>
<summary>Release Notes</summary>
${{ steps.pipeline.outputs.release-notes }}
</details>
branch: update/go
commit-message: |-
${{ steps.update-go.outputs.commit-title }}
${{ steps.update-go.outputs.commit-body }}
delete-branch: true
labels: ${{ steps.update-go.outputs.commit-semver }}, type:task
signoff: true
title: ${{ steps.update-go.outputs.commit-title }}
token: ${{ secrets.IMPLEMENTATION_GITHUB_TOKEN }}

View File

@ -11,7 +11,7 @@ jobs:
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- uses: micnncim/action-label-syncer@v1
env:
GITHUB_TOKEN: ${{ secrets.IMPLEMENTATION_GITHUB_TOKEN }}

37
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Tests
"on":
pull_request: {}
push:
branches:
- main
jobs:
unit:
name: Unit Test
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
path: ${{ env.HOME }}/go/pkg/mod
restore-keys: ${{ runner.os }}-go-
- uses: actions/setup-go@v2
with:
go-version: "1.16"
- name: Install richgo
run: |
#!/usr/bin/env bash
set -euo pipefail
GO111MODULE=on go get -u -ldflags="-s -w" github.com/kyoh86/richgo
- name: Run Tests
run: |
#!/usr/bin/env bash
set -euo pipefail
GOCMD=richgo make
env:
RICHGO_FORCE_COLOR: "1"

View File

@ -14,19 +14,19 @@ jobs:
runs-on:
- ubuntu-latest
steps:
- uses: actions/setup-go@v5
- uses: actions/setup-go@v2
with:
go-version: "1.24"
go-version: "1.16"
- name: Install octo
run: |
#!/usr/bin/env bash
set -euo pipefail
go install -ldflags="-s -w" github.com/paketo-buildpacks/pipeline-builder/cmd/octo@latest
- uses: actions/checkout@v4
- name: Update Pipeline
id: pipeline
GO111MODULE=on go get -u -ldflags="-s -w" github.com/paketo-buildpacks/pipeline-builder/cmd/octo
- uses: actions/checkout@v2
- id: pipeline
name: Update Pipeline
run: |
#!/usr/bin/env bash
@ -38,7 +38,6 @@ jobs:
OLD_VERSION="0.0.0"
fi
rm .github/workflows/pb-*.yml || true
octo --descriptor "${DESCRIPTOR}"
PAYLOAD=$(gh api /repos/paketo-buildpacks/pipeline-builder/releases/latest)
@ -55,23 +54,15 @@ jobs:
)
git add .github/
git add .gitignore
if [ -f scripts/build.sh ]; then
git add scripts/build.sh
fi
git checkout -- .
echo "old-version=${OLD_VERSION}" >> "$GITHUB_OUTPUT"
echo "new-version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
DELIMITER=$(openssl rand -hex 16) # roughly the same entropy as uuid v4 used in https://github.com/actions/toolkit/blob/b36e70495fbee083eb20f600eafa9091d832577d/packages/core/src/file-command.ts#L28
printf "release-notes<<%s\n%s\n%s\n" "${DELIMITER}" "${RELEASE_NOTES}" "${DELIMITER}" >> "${GITHUB_OUTPUT}" # see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
echo "::set-output name=old-version::${OLD_VERSION}"
echo "::set-output name=new-version::${NEW_VERSION}"
echo "::set-output name=release-notes::${RELEASE_NOTES//$'\n'/%0A}"
env:
DESCRIPTOR: .github/pipeline-descriptor.yml
GITHUB_TOKEN: ${{ secrets.IMPLEMENTATION_GITHUB_TOKEN }}
- uses: peter-evans/create-pull-request@v6
- uses: peter-evans/create-pull-request@v3
with:
author: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }} <${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }}@users.noreply.github.com>
body: |-

7
.gitignore vendored
View File

@ -11,10 +11,3 @@
# 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.
bin/
linux/
dependencies/
package/
scratch/

View File

@ -1,45 +0,0 @@
# Migration Guide
This guide highlights the major differences between libcnb v1 and v2 with a focus on what you as an author need to change to upgrade your buildpacks from v1 to v2.
## Buildpack API Support
With libcnb v1, you get support for buildpack API 0.5 to 0.8. With v2, you get support for 0.8 to 0.10. This should provide a seamless transition as you can continue to use buildpack API 0.8 with
## Removal of LayerContributor
The LayerContributor has been removed. Previously, libcnb would take a list of LayerContributors and execute them to retrieve the list of Layers to process. Now it just directly takes the list of Layers to process.
The path forward is to either update your buildpacks to return layers or to implement your own LayerContributor interface as a means to ease the transition to libcnb v2.
## Replace Builder and Detector with Functions
The Builder and Detector interfaces have been removed and replaced with functions, specifically BuildFunc and DetectFunc. They serve the same purpose, but simplify your implementation because you do not need to implement the single method interface, you can only need pass in a function that will be called back.
The path forward is to remove your Builder and Detector structs and pass the method directly into libcnb.
## Rename `poet` to `log`
We have renamed the `poet` module to be called `log`. This is a simple find & replace change in your code. We have also simplified the method names, so instead of needing to say `poet.NewLogger`, you can say `log.New` or `log.NewWithOptions`.
We have also introduced a new `Logger` interface which can be overridden to control how libcnb logs. A basic implementation has been provided, called `PlainLogger`. If you want to supply a custom implementation, it can be passed in through main/build/detect functions.
Lastly, libcnb has been modified such that it will only log at the `Debug` level. Anything problems that arise will instead return an error, which you as the buildpack author should handle.
## Remove Deprecated Pre-Buildpack API 0.7 BOM
The pre-buildpack API 0.7 BOM API was marked for deprecation in libcnb v1. It has been removed in v2. If you are still using it, you will need to migrate to use the new SBOM functionality provided by the [Buildpacks API](https://github.com/buildpacks/rfcs/blob/main/text/0095-sbom.md).
## Path to Source Code Changed
In libcnb v1, `BuildContext.Application.Path` points to the application source code. This was shortened to `BuildContext.ApplicationPath`. The same change was made for `DetectContext.ApplicationPath`.
## Remove Deprecated CNB Binding Support
The CNB Binding specification has long been replaced by the Service Binding Specification for Kubernetes. Support for the CNB Binding Support had remained, but it is removed in v2. This is unlikely to impact anyone.
## Remove Shell-Specific Logic & Overridable Process Arguments
To comply with [RFC #168](https://github.com/buildpacks/rfcs/pull/168), we remove the `Direct` field from the `Process` struct. We also change `Command` from a `string` to `string[]` on the `Process` struct, to support overridable process arguments. Both of these require at leats API 0.9.
In conjunction with this, we have also removed Profile & `profile.d` support. These should be replaced with the [Profile Buildpack](https://github.com/buildpacks/profile) and `exec.d`.

View File

@ -1,7 +1,7 @@
# Go parameters
GOCMD?=go
GO_VERSION=$(shell go list -m -f "{{.GoVersion}}")
PACKAGE_BASE=github.com/buildpacks/libcnb/v2
PACKAGE_BASE=github.com/buildpacks/libcnb
all: test

View File

@ -14,12 +14,6 @@
go get github.com/buildpacks/libcnb
```
or for the v2 alpha
```
go get github.com/buildpacks/libcnb@v2.0.0-alpha.1
```
#### Docs
https://pkg.go.dev/github.com/buildpacks/libcnb?tab=doc

119
application.go Normal file
View File

@ -0,0 +1,119 @@
/*
* 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 libcnb
// Application is the user contributed application to build.
type Application struct {
// Path is the path to the application.
Path string
}
// Label represents an image label.
type Label struct {
// Key is the key of the label.
Key string `toml:"key"`
// Value is the value of the label.
Value string `toml:"value"`
}
// Process represents metadata about a type of command that can be run.
type Process struct {
// Type is the type of the process.
Type string `toml:"type"`
// Command is the command of the process.
Command string `toml:"command"`
// Arguments are arguments to the command.
Arguments []string `toml:"args"`
// Command is exec'd directly by the os (no profile.d scripts run)
Direct bool `toml:"direct,omitempty"`
// Default can be set to true to indicate that the process
// type being defined should be the default process type for the app image.
Default bool `toml:"default,omitempty"`
}
// Slice represents metadata about a slice.
type Slice struct {
// Paths are the contents of the slice.
Paths []string `toml:"paths"`
}
// LaunchTOML represents the contents of launch.toml.
type LaunchTOML struct {
// Labels is the collection of image labels contributed by the buildpack.
Labels []Label `toml:"labels"`
// Processes is the collection of process types contributed by the buildpack.
Processes []Process `toml:"processes"`
// Slices is the collection of slices contributed by the buildpack.
Slices []Slice `toml:"slices"`
// BOM is a collection of entries for the bill of materials.
BOM []BOMEntry `toml:"bom"`
}
func (l LaunchTOML) isEmpty() bool {
return len(l.Labels) == 0 && len(l.Processes) == 0 && len(l.Slices) == 0 && len(l.BOM) == 0
}
// BuildTOML represents the contents of build.toml.
type BuildTOML struct {
// BOM contains the build-time bill of materials.
BOM []BOMEntry `toml:"bom"`
// Unmet is a collection of buildpack plan entries that should be passed through to subsequent providers.
Unmet []UnmetPlanEntry
}
func (b BuildTOML) isEmpty() bool {
return len(b.Unmet) == 0
}
// 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"`
// Launch indicates whether the given entry is included in app image. If launch is true the entry
// will be added to the app image Bill of Materials. Launch should be true if the entry describes
// the contents of a launch layer or app layer.
Launch bool `toml:"-"`
// Build indicates whether the given entry is available at build time. If build is true the entry
// will be added to the build Bill of Materials.
Build bool `toml:"-"`
}
// Store represents the contents of store.toml
type Store struct {
// Metadata represents the persistent metadata.
Metadata map[string]interface{} `toml:"metadata"`
}

289
build.go
View File

@ -25,17 +25,16 @@ import (
"strings"
"github.com/BurntSushi/toml"
"github.com/Masterminds/semver"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/internal"
"github.com/buildpacks/libcnb/poet"
)
// BuildContext contains the inputs to build.
type BuildContext struct {
// ApplicationPath is the location of the application source code as provided by
// the lifecycle.
ApplicationPath string
// Application is application to build.
Application Application
// Buildpack is metadata about the buildpack, from buildpack.toml.
Buildpack Buildpack
@ -43,9 +42,6 @@ type BuildContext struct {
// Layers is the layers available to the buildpack.
Layers Layers
// Logger is the way to write messages to the end user
Logger log.Logger
// PersistentMetadata is metadata that is persisted even across cache cleaning.
PersistentMetadata map[string]interface{}
@ -55,23 +51,20 @@ type BuildContext struct {
// Platform is the contents of the platform.
Platform Platform
// Deprecated: StackID is the ID of the stack.
// StackID is the ID of the stack.
StackID string
// TargetInfo contains info of the target (os, arch, ...).
TargetInfo TargetInfo
// TargetDistro is the target distribution (name, version).
TargetDistro TargetDistro
}
// BuildResult contains the results of detection.
type BuildResult struct {
// BOM contains entries to be appended to the app image Bill of Materials and/or build Bill of Materials.
BOM *BOM
// Labels are the image labels contributed by the buildpack.
Labels []Label
// Layers is the collection of LayerCreators contributed by the buildpack.
Layers []Layer
Layers []LayerContributor
// PersistentMetadata is metadata that is persisted even across cache cleaning.
PersistentMetadata map[string]interface{}
@ -87,19 +80,16 @@ type BuildResult struct {
Unmet []UnmetPlanEntry
}
// Constants to track minimum and maximum supported Buildpack API versions
const (
// MinSupportedBPVersion indicates the minium supported version of the Buildpacks API
MinSupportedBPVersion = "0.8"
// MaxSupportedBPVersion indicates the maximum supported version of the Buildpacks API
MaxSupportedBPVersion = "0.10"
)
// BOM contains all Bill of Materials entries
type BOM struct {
Entries []BOMEntry
}
// NewBuildResult creates a new BuildResult instance, initializing empty fields.
func NewBuildResult() BuildResult {
return BuildResult{
PersistentMetadata: make(map[string]interface{}),
BOM: &BOM{},
}
}
@ -110,46 +100,62 @@ func (b BuildResult) String() string {
}
return fmt.Sprintf(
"{Labels:%+v Layers:%s PersistentMetadata:%+v Processes:%+v Slices:%+v, Unmet:%+v}",
b.Labels, l, b.PersistentMetadata, b.PersistentMetadata, b.Slices, b.Unmet,
"{BOM: %+v, Labels:%+v Layers:%s PersistentMetadata:%+v Processes:%+v Slices:%+v, Unmet:%+v}",
b.BOM, b.Labels, l, b.PersistentMetadata, b.PersistentMetadata, b.Slices, b.Unmet,
)
}
// BuildFunc takes a context and returns a result, performing buildpack build behaviors.
type BuildFunc func(context BuildContext) (BuildResult, error)
//go:generate mockery -name Builder -case=underscore
// Builder describes an interface for types that can be used by the Build function.
type Builder interface {
// Build takes a context and returns a result, performing buildpack build behaviors.
Build(context BuildContext) (BuildResult, error)
}
// Build is called by the main function of a buildpack, for build.
func Build(build BuildFunc, config Config) {
func Build(builder Builder, options ...Option) {
config := Config{
arguments: os.Args,
environmentWriter: internal.EnvironmentWriter{},
exitHandler: internal.NewExitHandler(),
tomlWriter: internal.TOMLWriter{},
}
for _, option := range options {
config = option(config)
}
if len(config.arguments) != 4 {
config.exitHandler.Error(fmt.Errorf("expected 3 arguments and received %d", len(config.arguments)-1))
return
}
var (
err error
file string
ok bool
)
ctx := BuildContext{Logger: config.logger}
ctx := BuildContext{}
logger := poet.NewLogger(os.Stdout)
ctx.ApplicationPath, err = os.Getwd()
ctx.Application.Path, err = os.Getwd()
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil {
config.logger.Debugf("unable to write application contents\n%w", err)
}
if logger.IsDebugEnabled() {
logger.Debug(ApplicationPathFormatter(ctx.Application.Path))
}
if s, ok := os.LookupEnv(EnvBuildpackDirectory); ok {
if s, ok := os.LookupEnv("CNB_BUILDPACK_DIR"); ok {
ctx.Buildpack.Path = filepath.Clean(s)
} else {
config.exitHandler.Error(fmt.Errorf("unable to get CNB_BUILDPACK_DIR, not found"))
return
} else { // TODO: Remove branch once lifecycle has been updated to support this
ctx.Buildpack.Path = filepath.Clean(strings.TrimSuffix(config.arguments[0], filepath.Join("bin", "build")))
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Buildpack contents", ctx.Buildpack.Path); err != nil {
config.logger.Debugf("unable to write buildpack contents\n%w", err)
}
if logger.IsDebugEnabled() {
logger.Debug(BuildpackPathFormatter(ctx.Buildpack.Path))
}
file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml")
@ -157,64 +163,34 @@ func Build(build BuildFunc, config Config) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack %s\n%w", file, err))
return
}
config.logger.Debugf("Buildpack: %+v", ctx.Buildpack)
logger.Debugf("Buildpack: %+v", ctx.Buildpack)
API, err := semver.NewVersion(ctx.Buildpack.API)
if err != nil {
config.exitHandler.Error(errors.New("version cannot be parsed"))
API := strings.TrimSpace(ctx.Buildpack.API)
if API != "0.5" && API != "0.6" {
config.exitHandler.Error(errors.New("this version of libcnb is only compatible with buildpack APIs 0.5 and 0.6"))
return
}
compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
if !compatVersionCheck.Check(API) {
if MinSupportedBPVersion == MaxSupportedBPVersion {
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion))
return
}
ctx.Layers = Layers{config.arguments[1]}
logger.Debugf("Layers: %+v", ctx.Layers)
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
return
ctx.Platform.Path = config.arguments[2]
if logger.IsDebugEnabled() {
logger.Debug(PlatformFormatter(ctx.Platform))
}
layersDir, ok := os.LookupEnv(EnvLayersDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_LAYERS_DIR to be set"))
return
}
ctx.Layers = Layers{layersDir}
ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set"))
return
}
buildpackPlanPath, ok := os.LookupEnv(EnvBuildPlanPath)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_BP_PLAN_PATH to be set"))
return
}
config.logger.Debugf("Layers: %+v", ctx.Layers)
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil {
config.logger.Debugf("unable to write platform contents\n%w", err)
}
}
if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil {
if ctx.Platform.Bindings, err = NewBindingsForBuild(ctx.Platform.Path); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err))
return
}
config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
file = filepath.Join(ctx.Platform.Path, "env")
if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err))
return
}
config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
var store Store
file = filepath.Join(ctx.Layers.Path, "store.toml")
@ -223,39 +199,27 @@ func Build(build BuildFunc, config Config) {
return
}
ctx.PersistentMetadata = store.Metadata
config.logger.Debugf("Persistent Metadata: %+v", ctx.PersistentMetadata)
logger.Debugf("Persistent Metadata: %+v", ctx.PersistentMetadata)
if _, err = toml.DecodeFile(buildpackPlanPath, &ctx.Plan); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", buildpackPlanPath, err))
file = config.arguments[3]
if _, err = toml.DecodeFile(file, &ctx.Plan); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", file, err))
return
}
config.logger.Debugf("Buildpack Plan: %+v", ctx.Plan)
logger.Debugf("Buildpack Plan: %+v", ctx.Plan)
if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok {
config.logger.Debug("CNB_STACK_ID not set")
} else {
config.logger.Debugf("Stack: %s", ctx.StackID)
if ctx.StackID, ok = os.LookupEnv("CNB_STACK_ID"); !ok {
config.exitHandler.Error(fmt.Errorf("CNB_STACK_ID not set"))
return
}
logger.Debugf("Stack: %s", ctx.StackID)
if API.GreaterThan(semver.MustParse("0.9")) {
ctx.TargetInfo = TargetInfo{}
ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS)
ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch)
ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant)
config.logger.Debugf("System: %+v", ctx.TargetInfo)
ctx.TargetDistro = TargetDistro{}
ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName)
ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion)
config.logger.Debugf("Distro: %+v", ctx.TargetDistro)
}
result, err := build(ctx)
result, err := builder.Build(ctx)
if err != nil {
config.exitHandler.Error(err)
return
}
config.logger.Debugf("Result: %+v", result)
logger.Debugf("Result: %+v", result)
file = filepath.Join(ctx.Layers.Path, "*.toml")
existing, err := filepath.Glob(file)
@ -265,31 +229,60 @@ func Build(build BuildFunc, config Config) {
}
var contributed []string
for _, layer := range result.Layers {
for _, creator := range result.Layers {
name := creator.Name()
layer, err := ctx.Layers.Layer(name)
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to create layer %s\n%w", name, err))
return
}
layer, err = creator.Contribute(layer)
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to invoke layer creator\n%w", err))
return
}
file = filepath.Join(layer.Path, "env.build")
config.logger.Debugf("Writing layer env.build: %s <= %+v", file, layer.BuildEnvironment)
logger.Debugf("Writing layer env.build: %s <= %+v", file, layer.BuildEnvironment)
if err = config.environmentWriter.Write(file, layer.BuildEnvironment); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer env.build %s\n%w", file, err))
return
}
file = filepath.Join(layer.Path, "env.launch")
config.logger.Debugf("Writing layer env.launch: %s <= %+v", file, layer.LaunchEnvironment)
logger.Debugf("Writing layer env.launch: %s <= %+v", file, layer.LaunchEnvironment)
if err = config.environmentWriter.Write(file, layer.LaunchEnvironment); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer env.launch %s\n%w", file, err))
return
}
file = filepath.Join(layer.Path, "env")
config.logger.Debugf("Writing layer env: %s <= %+v", file, layer.SharedEnvironment)
logger.Debugf("Writing layer env: %s <= %+v", file, layer.SharedEnvironment)
if err = config.environmentWriter.Write(file, layer.SharedEnvironment); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer env %s\n%w", file, err))
return
}
file = filepath.Join(layer.Path, "profile.d")
logger.Debugf("Writing layer profile.d: %s <= %+v", file, layer.Profile)
if err = config.environmentWriter.Write(file, layer.Profile); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer profile.d %s\n%w", file, err))
return
}
file = filepath.Join(ctx.Layers.Path, fmt.Sprintf("%s.toml", layer.Name))
config.logger.Debugf("Writing layer metadata: %s <= %+v", file, layer)
if err = config.tomlWriter.Write(file, layer); err != nil {
logger.Debugf("Writing layer metadata: %s <= %+v", file, layer)
var toWrite interface{} = layer
if API == "0.5" {
toWrite = internal.LayerAPI5{
Build: layer.LayerTypes.Build,
Cache: layer.LayerTypes.Cache,
Launch: layer.LayerTypes.Launch,
Metadata: layer.Metadata,
}
}
if err = config.tomlWriter.Write(file, toWrite); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer metadata %s\n%w", file, err))
return
}
@ -301,7 +294,7 @@ func Build(build BuildFunc, config Config) {
continue
}
config.logger.Debugf("Removing %s", e)
logger.Debugf("Removing %s", e)
if err := os.RemoveAll(e); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to remove %s\n%w", e, err))
@ -309,20 +302,36 @@ func Build(build BuildFunc, config Config) {
}
}
if err := validateSBOMFormats(ctx.Layers.Path, ctx.Buildpack.Info.SBOMFormats); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to validate SBOM\n%w", err))
return
var launchBOM, buildBOM []BOMEntry
if result.BOM != nil {
for _, entry := range result.BOM.Entries {
if entry.Launch {
launchBOM = append(launchBOM, entry)
}
if entry.Build {
buildBOM = append(buildBOM, entry)
}
}
}
launch := LaunchTOML{
Labels: result.Labels,
Processes: result.Processes,
Slices: result.Slices,
BOM: launchBOM,
}
if !launch.isEmpty() {
file = filepath.Join(ctx.Layers.Path, "launch.toml")
config.logger.Debugf("Writing application metadata: %s <= %+v", file, launch)
logger.Debugf("Writing application metadata: %s <= %+v", file, launch)
if API == "0.5" {
for _, process := range launch.Processes {
if process.Default {
logger.Info("WARNING: Launch layer is setting default=true, but that is not supported until API version 0.6. This setting will be ignored.")
}
}
}
if err = config.tomlWriter.Write(file, launch); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write application metadata %s\n%w", file, err))
@ -330,15 +339,15 @@ func Build(build BuildFunc, config Config) {
}
}
buildTOML := BuildTOML{
build := BuildTOML{
Unmet: result.Unmet,
BOM: buildBOM,
}
if !buildTOML.isEmpty() {
if !build.isEmpty() {
file = filepath.Join(ctx.Layers.Path, "build.toml")
config.logger.Debugf("Writing build metadata: %s <= %+v", file, build)
if err = config.tomlWriter.Write(file, buildTOML); err != nil {
logger.Debugf("Writing build metadata: %s <= %+v", file, build)
if err = config.tomlWriter.Write(file, build); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write build metadata %s\n%w", file, err))
return
}
@ -349,7 +358,7 @@ func Build(build BuildFunc, config Config) {
Metadata: result.PersistentMetadata,
}
file = filepath.Join(ctx.Layers.Path, "store.toml")
config.logger.Debugf("Writing persistent metadata: %s <= %+v", file, store)
logger.Debugf("Writing persistent metadata: %s <= %+v", file, store)
if err = config.tomlWriter.Write(file, store); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write persistent metadata %s\n%w", file, err))
return
@ -366,27 +375,3 @@ func contains(candidates []string, s string) bool {
return false
}
func validateSBOMFormats(layersPath string, acceptedSBOMFormats []string) error {
sbomFiles, err := filepath.Glob(filepath.Join(layersPath, "*.sbom.*"))
if err != nil {
return fmt.Errorf("unable find SBOM files\n%w", err)
}
for _, sbomFile := range sbomFiles {
parts := strings.Split(filepath.Base(sbomFile), ".")
if len(parts) <= 2 {
return fmt.Errorf("invalid format %s", filepath.Base(sbomFile))
}
sbomFormat, err := SBOMFormatFromString(strings.Join(parts[len(parts)-2:], "."))
if err != nil {
return fmt.Errorf("unable to parse SBOM %s\n%w", sbomFormat, err)
}
if !contains(acceptedSBOMFormats, sbomFormat.MediaType()) {
return fmt.Errorf("unable to find actual SBOM Type %s in list of supported SBOM types %s", sbomFormat.MediaType(), acceptedSBOMFormats)
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
/*
* 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 libcnb
// BuildTOML represents the contents of build.toml.
type BuildTOML struct {
// Unmet is a collection of buildpack plan entries that should be passed through to subsequent providers.
Unmet []UnmetPlanEntry
}
func (b BuildTOML) isEmpty() bool {
return len(b.Unmet) == 0
}

View File

@ -41,9 +41,6 @@ type BuildpackInfo struct {
// Licenses a list of buildpack licenses.
Licenses []License `toml:"licenses"`
// SBOM is the list of supported SBOM media types
SBOMFormats []string `toml:"sbom-formats"`
}
// License contains information about a Software License
@ -76,39 +73,13 @@ type BuildpackOrder struct {
Groups []BuildpackOrderBuildpack `toml:"group"`
}
// Deprecated: BuildpackStack is a stack supported by the buildpack.
// BuildpackStack is a stack supported by the buildpack.
type BuildpackStack struct {
// ID is the id of the stack.
ID string `toml:"id"`
}
// TargetDistro is the supported target distro
type TargetDistro struct {
// Name is the name of the supported distro.
Name string `toml:"name"`
// Version is the version of the supported distro.
Version string `toml:"version"`
}
// TargetInfo is the supported target
type TargetInfo struct {
// OS is the supported os.
OS string `toml:"os"`
// Arch is the supported architecture.
Arch string `toml:"arch"`
// Variant is the supported variant of the architecture.
Variant string `toml:"variant"`
}
// Target is a target supported by the buildpack.
type Target struct {
TargetInfo
// Distros is the collection of distros associated with the target.
Distros []TargetDistro `toml:"distros"`
// Mixins is the collection of mixins associated with the stack.
Mixins []string `toml:"mixins"`
}
// Buildpack is the contents of the buildpack.toml file.
@ -120,14 +91,11 @@ type Buildpack struct {
Info BuildpackInfo `toml:"buildpack"`
// Path is the path to the buildpack.
Path string `toml:"-"`
Path string
// Deprecated: Stacks is the collection of stacks supported by the buildpack.
// Stacks is the collection of stacks supported by the buildpack.
Stacks []BuildpackStack `toml:"stacks"`
// Targets is the collection of targets supported by the buildpack.
Targets []Target `toml:"targets"`
// Metadata is arbitrary metadata attached to the buildpack.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -1,51 +0,0 @@
/*
* 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 libcnb_test
import (
"bytes"
"testing"
"github.com/BurntSushi/toml"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2"
. "github.com/onsi/gomega"
)
func testBuildpackTOML(t *testing.T, _ spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
it("does not serialize the Path field", func() {
bp := libcnb.Buildpack{
API: "0.8",
Info: libcnb.BuildpackInfo{
ID: "test-buildpack/sample",
Name: "sample",
},
Path: "../buildpack",
}
output := &bytes.Buffer{}
Expect(toml.NewEncoder(output).Encode(bp)).To(Succeed())
Expect(output.String()).NotTo(Or(ContainSubstring("Path = "), ContainSubstring("path = ")))
})
}

View File

@ -16,14 +16,7 @@
package libcnb
import (
"os"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/v2/log"
)
//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.
@ -34,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.
@ -50,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 {
@ -59,55 +52,17 @@ 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
dirContentFormatter log.DirectoryContentFormatter
environmentWriter EnvironmentWriter
execdWriter ExecDWriter
exitHandler ExitHandler
logger log.Logger
tomlWriter TOMLWriter
contentWriter internal.DirectoryContentsWriter
extension bool
arguments []string
environmentWriter EnvironmentWriter
exitHandler ExitHandler
tomlWriter TOMLWriter
}
// Option is a function for configuring a Config instance.
type Option func(config Config) Config
// NewConfig will generate a config from the given set of options
func NewConfig(options ...Option) Config {
config := Config{}
// apply defaults
options = append([]Option{
WithArguments(os.Args),
WithEnvironmentWriter(internal.EnvironmentWriter{}),
WithExitHandler(internal.NewExitHandler()),
WithLogger(log.New(os.Stdout)),
WithTOMLWriter(internal.TOMLWriter{}),
WithDirectoryContentFormatter(internal.NewPlainDirectoryContentFormatter()),
}, options...)
for _, opt := range options {
config = opt(config)
}
config.contentWriter = internal.NewDirectoryContentsWriter(config.dirContentFormatter, config.logger.DebugWriter())
return config
}
// WithArguments creates an Option that sets a collection of arguments.
func WithArguments(arguments []string) Option {
return func(config Config) Config {
@ -139,27 +94,3 @@ 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
}
}
// WithLogger creates an Option that sets a ExecDWriter implementation.
func WithLogger(logger log.Logger) Option {
return func(config Config) Config {
config.logger = logger
return config
}
}
// WithDirectoryContentFormatter creates an Option that sets a ExecDWriter implementation.
func WithDirectoryContentFormatter(formatter log.DirectoryContentFormatter) Option {
return func(config Config) Config {
config.dirContentFormatter = formatter
return config
}
}

183
detect.go
View File

@ -21,30 +21,23 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/Masterminds/semver"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/internal"
"github.com/buildpacks/libcnb/poet"
)
// DetectContext contains the inputs to detection.
type DetectContext struct {
// ApplicationPath is the location of the application source code as provided by
// the lifecycle.
ApplicationPath string
// Application is the application to build.
Application Application
// Buildpack is metadata about the buildpack from buildpack.toml (empty when processing an extension)
// Buildpack is metadata about the buildpack, from buildpack.toml.
Buildpack Buildpack
// Extension is metadata about the extension from extension.toml (empty when processing a buildpack)
Extension Extension
// Logger is the way to write messages to the end user
Logger log.Logger
// Platform is the contents of the platform.
Platform Platform
@ -62,140 +55,103 @@ type DetectResult struct {
Plans []BuildPlan
}
// DetectFunc takes a context and returns a result, performing buildpack detect behaviors.
type DetectFunc func(context DetectContext) (DetectResult, error)
//go:generate mockery -name Detector -case=underscore
// Detector describes an interface for types that can be used by the Detect function.
type Detector interface {
// Detect takes a context and returns a result, performing buildpack detect behaviors.
Detect(context DetectContext) (DetectResult, error)
}
// Detect is called by the main function of a buildpack, for detection.
func Detect(detect DetectFunc, config Config) {
var (
err error
file string
ok bool
api string
path string
destination interface{}
)
ctx := DetectContext{Logger: config.logger}
var moduletype = "buildpack"
if config.extension {
moduletype = "extension"
func Detect(detector Detector, options ...Option) {
config := Config{
arguments: os.Args,
environmentWriter: internal.EnvironmentWriter{},
exitHandler: internal.NewExitHandler(),
tomlWriter: internal.TOMLWriter{},
}
ctx.ApplicationPath, err = os.Getwd()
for _, option := range options {
config = option(config)
}
if len(config.arguments) != 3 {
config.exitHandler.Error(fmt.Errorf("expected 2 arguments and received %d", len(config.arguments)-1))
return
}
var (
err error
file string
ok bool
)
ctx := DetectContext{}
logger := poet.NewLogger(os.Stdout)
ctx.Application.Path, err = os.Getwd()
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil {
config.logger.Debugf("unable to write application contents\n%w", err)
}
if logger.IsDebugEnabled() {
logger.Debug(ApplicationPathFormatter(ctx.Application.Path))
}
if !config.extension {
if s, ok := os.LookupEnv(EnvBuildpackDirectory); ok {
path = filepath.Clean(s)
} else {
config.exitHandler.Error(fmt.Errorf("unable to get CNB_BUILDPACK_DIR, not found"))
return
}
ctx.Buildpack.Path = path
destination = &ctx.Buildpack
file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml")
} else {
if s, ok := os.LookupEnv(EnvExtensionDirectory); ok {
path = filepath.Clean(s)
} else {
config.exitHandler.Error(fmt.Errorf("unable to get CNB_EXTENSION_DIR, not found"))
return
}
ctx.Extension.Path = path
destination = &ctx.Extension
file = filepath.Join(ctx.Extension.Path, "extension.toml")
if s, ok := os.LookupEnv("CNB_BUILDPACK_DIR"); ok {
ctx.Buildpack.Path = filepath.Clean(s)
} else { // TODO: Remove branch once lifecycle has been updated to support this
ctx.Buildpack.Path = filepath.Clean(strings.TrimSuffix(config.arguments[0], filepath.Join("bin", "detect")))
}
if logger.IsDebugEnabled() {
logger.Debug(BuildpackPathFormatter(ctx.Buildpack.Path))
}
if _, err = toml.DecodeFile(file, destination); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode %s %s\n%w", moduletype, file, err))
file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml")
if _, err = toml.DecodeFile(file, &ctx.Buildpack); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack %s\n%w", file, err))
return
}
config.logger.Debugf("%s: %+v", moduletype, ctx.Buildpack)
logger.Debugf("Buildpack: %+v", ctx.Buildpack)
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write(moduletype+" contents", path); err != nil {
config.logger.Debugf("unable to write %s contents\n%w", moduletype, err)
}
}
if config.extension {
api = ctx.Extension.API
} else {
api = ctx.Buildpack.API
}
API, err := semver.NewVersion(api)
if err != nil {
config.exitHandler.Error(errors.New("version cannot be parsed"))
API := strings.TrimSpace(ctx.Buildpack.API)
if API != "0.5" && API != "0.6" {
config.exitHandler.Error(errors.New("this version of libcnb is only compatible with buildpack API 0.5 and 0.6"))
return
}
compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
if !compatVersionCheck.Check(API) {
if MinSupportedBPVersion == MaxSupportedBPVersion {
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion))
return
}
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
return
}
var buildPlanPath string
ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set"))
return
}
buildPlanPath, ok = os.LookupEnv(EnvDetectPlanPath)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_BUILD_PLAN_PATH to be set"))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil {
config.logger.Debugf("unable to write platform contents\n%w", err)
}
ctx.Platform.Path = config.arguments[1]
if logger.IsDebugEnabled() {
logger.Debug(PlatformFormatter(ctx.Platform))
}
file = filepath.Join(ctx.Platform.Path, "bindings")
if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil {
if ctx.Platform.Bindings, err = NewBindingsFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", file, err))
return
}
config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
file = filepath.Join(ctx.Platform.Path, "env")
if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err))
return
}
config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok {
config.logger.Debug("CNB_STACK_ID not set")
} else {
config.logger.Debugf("Stack: %s", ctx.StackID)
if ctx.StackID, ok = os.LookupEnv("CNB_STACK_ID"); !ok {
config.exitHandler.Error(fmt.Errorf("CNB_STACK_ID not set"))
return
}
logger.Debugf("Stack: %s", ctx.StackID)
result, err := detect(ctx)
result, err := detector.Detect(ctx)
if err != nil {
config.exitHandler.Error(err)
return
}
config.logger.Debugf("Result: %+v", result)
logger.Debugf("Result: %+v", result)
if !result.Pass {
config.exitHandler.Fail()
@ -211,9 +167,10 @@ func Detect(detect DetectFunc, config Config) {
plans.Or = result.Plans[1:]
}
config.logger.Debugf("Writing build plans: %s <= %+v", buildPlanPath, plans)
if err := config.tomlWriter.Write(buildPlanPath, plans); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write buildplan %s\n%w", buildPlanPath, err))
file = config.arguments[2]
logger.Debugf("Writing build plans: %s <= %+v", file, plans)
if err := config.tomlWriter.Write(file, plans); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write buildplan %s\n%w", file, err))
return
}
}

View File

@ -18,6 +18,7 @@ package libcnb_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -26,9 +27,8 @@ import (
"github.com/sclevine/spec"
"github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/v2/mocks"
"github.com/buildpacks/libcnb"
"github.com/buildpacks/libcnb/mocks"
)
func testDetect(t *testing.T, context spec.G, it spec.S) {
@ -39,7 +39,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
buildpackPath string
buildPlanPath string
commandPath string
detectFunc libcnb.DetectFunc
detector *mocks.Detector
exitHandler *mocks.ExitHandler
platformPath string
tomlWriter *mocks.TOMLWriter
@ -50,18 +50,18 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
it.Before(func() {
var err error
applicationPath, err = os.MkdirTemp("", "detect-application-path")
applicationPath, err = ioutil.TempDir("", "detect-application-path")
Expect(err).NotTo(HaveOccurred())
applicationPath, err = filepath.EvalSymlinks(applicationPath)
Expect(err).NotTo(HaveOccurred())
buildpackPath, err = os.MkdirTemp("", "detect-buildpack-path")
buildpackPath, err = ioutil.TempDir("", "detect-buildpack-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed())
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
Expect(ioutil.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.8"
api = "0.6"
[buildpack]
id = "test-id"
@ -81,6 +81,7 @@ uri = "https://spdx.org/licenses/Apache-1.1.html"
[[stacks]]
id = "test-id"
mixins = ["test-name"]
[metadata]
test-key = "test-value"
@ -88,39 +89,35 @@ test-key = "test-value"
0600),
).To(Succeed())
f, err := os.CreateTemp("", "detect-buildplan-path")
f, err := ioutil.TempFile("", "detect-buildplan-path")
Expect(err).NotTo(HaveOccurred())
Expect(f.Close()).NotTo(HaveOccurred())
buildPlanPath = f.Name()
commandPath = filepath.Join("bin", "detect")
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{}, nil
}
detector = &mocks.Detector{}
exitHandler = &mocks.ExitHandler{}
exitHandler.On("Error", mock.Anything)
exitHandler.On("Fail")
exitHandler.On("Pass")
platformPath, err = os.MkdirTemp("", "detect-platform-path")
platformPath, err = ioutil.TempDir("", "detect-platform-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"),
Expect(ioutil.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"),
[]byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
Expect(ioutil.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
To(Succeed())
tomlWriter = &mocks.TOMLWriter{}
tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed())
Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed())
Expect(os.Setenv("CNB_BUILD_PLAN_PATH", buildPlanPath)).To(Succeed())
workingDir, err = os.Getwd()
Expect(err).NotTo(HaveOccurred())
@ -131,8 +128,6 @@ test-key = "test-value"
Expect(os.Chdir(workingDir)).To(Succeed())
Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed())
Expect(os.RemoveAll(applicationPath)).To(Succeed())
Expect(os.RemoveAll(buildpackPath)).To(Succeed())
@ -140,11 +135,11 @@ test-key = "test-value"
Expect(os.RemoveAll(platformPath)).To(Succeed())
})
context("buildpack API is not within the supported range", func() {
context("buildpack API is not 0.5 or 0.6", func() {
it.Before(func() {
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
Expect(ioutil.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.7"
api = "0.4"
[buildpack]
id = "test-id"
@ -156,184 +151,149 @@ version = "1.1.1"
})
it("fails", func() {
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
if libcnb.MinSupportedBPVersion == libcnb.MaxSupportedBPVersion {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack API == %s", libcnb.MinSupportedBPVersion)))
} else {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion),
))
}
})
})
context("errors if required env vars are not set", func() {
for _, e := range []string{"CNB_PLATFORM_DIR", "CNB_BUILD_PLAN_PATH"} {
// We need to do this assignment because of the way that spec binds variables
envVar := e
context(fmt.Sprintf("when %s is unset", envVar), func() {
it.Before(func() {
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.8"
[buildpack]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
os.Unsetenv(envVar)
})
it("fails", func() {
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler)),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("expected %s to be set", envVar),
))
})
})
}
})
context("has a detect environment", func() {
var ctx libcnb.DetectContext
it.Before(func() {
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.8"
[buildpack]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
detectFunc = func(context libcnb.DetectContext) (libcnb.DetectResult, error) {
ctx = context
return libcnb.DetectResult{}, nil
}
})
it("creates context", func() {
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler)),
)
Expect(ctx.ApplicationPath).To(Equal(applicationPath))
Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{
API: "0.8",
Info: libcnb.BuildpackInfo{
ID: "test-id",
Name: "test-name",
Version: "1.1.1",
},
Path: buildpackPath,
}))
Expect(ctx.Platform).To(Equal(libcnb.Platform{
Bindings: libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(platformPath, "bindings", "alpha"),
Secret: map[string]string{
"test-secret-key": "test-secret-value",
},
},
},
Environment: map[string]string{"TEST_ENV": "test-value"},
Path: platformPath,
}))
Expect(ctx.StackID).To(Equal("test-stack-id"))
})
})
it("fails if CNB_BUILDPACK_DIR is not set", func() {
Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), platformPath, buildPlanPath}),
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
"this version of libcnb is only compatible with buildpack API 0.5 and 0.6",
))
})
})
it("encounters the wrong number of Arguments", func() {
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{}, nil)
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to get CNB_BUILDPACK_DIR, not found"))
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected 2 arguments and received 0"))
})
it("doesn't receive CNB_STACK_ID", func() {
Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{}, nil)
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("CNB_STACK_ID not set"))
})
it("creates context", func() {
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
)
ctx := detector.Calls[0].Arguments[0].(libcnb.DetectContext)
Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath}))
Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{
API: "0.6",
Info: libcnb.BuildpackInfo{
ID: "test-id",
Name: "test-name",
Version: "1.1.1",
ClearEnvironment: true,
Description: "A test buildpack",
Keywords: []string{"test", "buildpack"},
Licenses: []libcnb.License{
{Type: "Apache-2.0", URI: "https://spdx.org/licenses/Apache-2.0.html"},
{Type: "Apache-1.1", URI: "https://spdx.org/licenses/Apache-1.1.html"},
},
},
Path: buildpackPath,
Stacks: []libcnb.BuildpackStack{
{
ID: "test-id",
Mixins: []string{"test-name"},
},
},
Metadata: map[string]interface{}{"test-key": "test-value"},
}))
Expect(ctx.Platform).To(Equal(libcnb.Platform{
Bindings: libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(platformPath, "bindings", "alpha"),
Secret: map[string]string{
"test-secret-key": "test-secret-value",
},
},
},
Environment: map[string]string{"TEST_ENV": "test-value"},
Path: platformPath,
}))
Expect(ctx.StackID).To(Equal("test-stack-id"))
})
it("extracts buildpack path from command path if CNB_BUILDPACK_PATH is not set", func() {
Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
libcnb.Detect(detector,
libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
)
ctx := detector.Calls[0].Arguments[0].(libcnb.DetectContext)
Expect(ctx.Buildpack.Path).To(Equal(buildpackPath))
})
it("handles error from DetectFunc", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{}, fmt.Errorf("test-error")
}
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{}, fmt.Errorf("test-error"))
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error"))
})
it("does not write empty files", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{Pass: true}, nil
}
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithTOMLWriter(tomlWriter),
)
Expect(tomlWriter.Calls).To(HaveLen(0))
})
it("writes one build plan", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
Provides: []libcnb.BuildPlanProvide{
{Name: "test-name"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name",
Metadata: map[string]interface{}{"test-key": "test-value"},
},
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
Provides: []libcnb.BuildPlanProvide{
{Name: "test-name"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name",
Metadata: map[string]interface{}{"test-key": "test-value"},
},
},
},
}, nil
}
},
}, nil)
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithTOMLWriter(tomlWriter),
)
Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath))
@ -353,42 +313,38 @@ version = "1.1.1"
})
it("writes two build plans", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
Provides: []libcnb.BuildPlanProvide{
{Name: "test-name-1"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-1",
Metadata: map[string]interface{}{"test-key-1": "test-value-1"},
},
},
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
Provides: []libcnb.BuildPlanProvide{
{Name: "test-name-1"},
},
{
Provides: []libcnb.BuildPlanProvide{
{Name: "test-name-2"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-2",
Metadata: map[string]interface{}{"test-key-2": "test-value-2"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-1",
Metadata: map[string]interface{}{"test-key-1": "test-value-1"},
},
},
},
}, nil
}
{
Provides: []libcnb.BuildPlanProvide{
{Name: "test-name-2"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-2",
Metadata: map[string]interface{}{"test-key-2": "test-value-2"},
},
},
},
},
}, nil)
libcnb.Detect(detectFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithTOMLWriter(tomlWriter),
)
Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath))

View File

@ -23,10 +23,10 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb"
)
func testEnvironment(t *testing.T, _ spec.G, it spec.S) {
func testEnvironment(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect

View File

@ -1,103 +0,0 @@
package examples
import (
"fmt"
"os"
"path/filepath"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
)
const (
DefaultVersion = "0.1"
)
type Builder struct {
Logger log.Logger
}
// BuildpackPlan may contain multiple entries for a single buildpack, resolve
// into a single entry.
func resolve(plan libcnb.BuildpackPlan, name string) libcnb.BuildpackPlanEntry {
entry := libcnb.BuildpackPlanEntry{
Name: name,
Metadata: map[string]interface{}{},
}
for _, e := range plan.Entries {
for k, v := range e.Metadata {
entry.Metadata[k] = v
}
}
return entry
}
func populateLayer(layer libcnb.Layer, version string) (libcnb.Layer, error) {
exampleFile := filepath.Join(layer.Path, "example.txt")
if err := os.WriteFile(exampleFile, []byte(version), 0600); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to write example file: %w", err)
}
layer.SharedEnvironment.Default("EXAMPLE_FILE", exampleFile)
// Provide an SBOM
bom := cdx.NewBOM()
bom.Metadata = &cdx.Metadata{
Component: &cdx.Component{
Type: cdx.ComponentTypeFile,
Name: "example",
Version: version,
},
}
sbomPath := layer.SBOMPath(libcnb.CycloneDXJSON)
sbomFile, err := os.OpenFile(sbomPath, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return layer, err
}
defer sbomFile.Close()
encoder := cdx.NewBOMEncoder(sbomFile, cdx.BOMFileFormatJSON)
if err := encoder.Encode(bom); err != nil {
return layer, err
}
return layer, nil
}
func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
// Reduce possible multiple buildpack plan entries to a single entry
entry := resolve(context.Plan, Provides)
result := libcnb.NewBuildResult()
// Read metadata from the buildpack plan, often contributed by libcnb.Requires
// of the Detect phase
version := DefaultVersion
if v, ok := entry.Metadata["version"].(string); ok {
version = v
}
// Create a layer
layer, err := context.Layers.Layer("example")
if err != nil {
return result, err
}
layer.LayerTypes = libcnb.LayerTypes{
Launch: true,
Build: true,
Cache: true,
}
layer, err = populateLayer(layer, version)
if err != nil {
return result, nil
}
result.Layers = append(result.Layers, layer)
return result, nil
}
func ExampleBuild() {
detector := Detector{log.New(os.Stdout)}
builder := Builder{log.New(os.Stdout)}
libcnb.BuildpackMain(detector.Detect, builder.Build)
}

View File

@ -1,67 +0,0 @@
package examples
import (
"errors"
"os"
"path/filepath"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
)
const (
Provides = "example"
BpExampleVersion = "BP_EXAMPLE_VERSION"
)
type Detector struct {
Logger log.Logger
}
func (Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) {
version := "1.0"
// Scan the application source folder to see if the example buildpack is
// required. If `version.toml` does not exist we return a failed DetectResult
// but no runtime error has occurred, so we return an empty error.
versionPath := filepath.Join(context.ApplicationPath, "version.toml")
if _, err := os.Open(versionPath); errors.Is(err, os.ErrNotExist) {
return libcnb.DetectResult{}, nil
}
// Read the version number from the buildpack definition
if exampleVersion, exists := context.Buildpack.Metadata["version"]; exists {
version = exampleVersion.(string)
}
// Accept version number from the environment if the user provides it
if exampleVersion, exists := context.Platform.Environment[BpExampleVersion]; exists {
version = exampleVersion
}
metadata := map[string]interface{}{
"version": version,
}
return libcnb.DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
// Let the system know that if other buildpacks Require "example"
// then this buildpack Provides the implementation logic.
Provides: []libcnb.BuildPlanProvide{
{Name: Provides},
},
// It is common for a buildpack to Require itself if the build phase
// needs information from the detect phase. Here we pass the version number
// as metadata to the build phase.
Requires: []libcnb.BuildPlanRequire{
{
Name: Provides,
Metadata: metadata,
},
},
},
},
}, nil
}
func ExampleDetect() {
detector := Detector{log.New(os.Stdout)}
libcnb.BuildpackMain(detector.Detect, nil)
}

View File

@ -1,30 +0,0 @@
package examples
import (
"fmt"
"os"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
)
type Generator struct {
Logger log.Logger
}
func (Generator) Generate(context libcnb.GenerateContext) (libcnb.GenerateResult, error) {
// here you can read the context.ApplicationPath folder
// and create run.Dockerfile and build.Dockerfile in the context.OutputPath folder
// and read metadata from the context.Extension struct
// Just to use context to keep compiler happy =)
fmt.Println(context.Extension.Info.ID)
result := libcnb.NewGenerateResult()
return result, nil
}
func ExampleGenerate() {
generator := Generator{log.New(os.Stdout)}
libcnb.ExtensionMain(nil, generator.Generate)
}

View File

@ -1,71 +0,0 @@
/*
* 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/v2/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
}
}

View File

@ -1,130 +0,0 @@
/*
* 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/v2"
"github.com/buildpacks/libcnb/v2/mocks"
)
func testExecD(t *testing.T, _ 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))
})
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2023 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
// ExtensionInfo is information about the extension.
type ExtensionInfo struct {
// ID is the ID of the extension.
ID string `toml:"id"`
// Name is the name of the extension.
Name string `toml:"name"`
// Version is the version of the extension.
Version string `toml:"version"`
// Homepage is the homepage of the extension.
Homepage string `toml:"homepage"`
// Description is a string describing the extension.
Description string `toml:"description"`
// Keywords is a list of words that are associated with the extension.
Keywords []string `toml:"keywords"`
// Licenses a list of extension licenses.
Licenses []License `toml:"licenses"`
}
// Extension is the contents of the extension.toml file.
type Extension struct {
// API is the api version expected by the extension.
API string `toml:"api"`
// Info is information about the extension.
Info ExtensionInfo `toml:"extension"`
// Path is the path to the extension.
Path string `toml:"-"`
// Targets is the collection of targets supported by the buildpack.
Targets []Target `toml:"targets"`
// Metadata is arbitrary metadata attached to the extension.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -1,51 +0,0 @@
/*
* Copyright 2023 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 (
"bytes"
"testing"
"github.com/BurntSushi/toml"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2"
. "github.com/onsi/gomega"
)
func testExtensionTOML(t *testing.T, _ spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
it("does not serialize the Path field", func() {
extn := libcnb.Extension{
API: "0.8",
Info: libcnb.ExtensionInfo{
ID: "test-buildpack/sample",
Name: "sample",
},
Path: "../buildpack",
}
output := &bytes.Buffer{}
Expect(toml.NewEncoder(output).Encode(extn)).To(Succeed())
Expect(output.String()).NotTo(Or(ContainSubstring("Path = "), ContainSubstring("path = ")))
})
}

59
formatter.go Normal file
View File

@ -0,0 +1,59 @@
/*
* 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 libcnb
import (
"fmt"
"github.com/buildpacks/libcnb/internal"
)
// ApplicationPathFormatter is the formatter for an ApplicationPath.
type ApplicationPathFormatter string
func (a ApplicationPathFormatter) String() string {
contents, err := internal.DirectoryContents{Path: string(a)}.Get()
if err != nil {
return fmt.Sprintf("Application contents: %s", err)
}
return fmt.Sprintf("Application contents: %s", contents)
}
// BuildpackPathFormatter is the formatter for a BuildpackPath.
type BuildpackPathFormatter string
func (b BuildpackPathFormatter) String() string {
contents, err := internal.DirectoryContents{Path: string(b)}.Get()
if err != nil {
return fmt.Sprintf("Buildpack contents: %s", err)
}
return fmt.Sprintf("Buildpack contents: %s", contents)
}
// PlatformFormatter is the formatter for a Platform.
type PlatformFormatter Platform
func (p PlatformFormatter) String() string {
contents, err := internal.DirectoryContents{Path: p.Path}.Get()
if err != nil {
return fmt.Sprintf("Platform contents: %s", err)
}
return fmt.Sprintf("Platform contents: %s", contents)
}

119
formatter_test.go Normal file
View File

@ -0,0 +1,119 @@
/*
* 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 libcnb_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb"
)
func testFormatter(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
context("ApplicationPathFormatter", func() {
var (
app string
)
it.Before(func() {
var err error
app, err = ioutil.TempDir("", "application-path-formatter")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(app)).To(Succeed())
})
it("lists empty directory contents", func() {
Expect(libcnb.ApplicationPathFormatter(app).String()).To(Equal("Application contents: [.]"))
})
it("lists directory contents", func() {
f, err := os.Create(filepath.Join(app, "test-file"))
Expect(err).NotTo(HaveOccurred())
defer f.Close()
Expect(libcnb.ApplicationPathFormatter(app).String()).To(Equal("Application contents: [. test-file]"))
})
})
context("BuildpackPathFormatter", func() {
var (
bp string
)
it.Before(func() {
var err error
bp, err = ioutil.TempDir("", "buildpack-path-formatter")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(bp)).To(Succeed())
})
it("lists empty directory contents", func() {
Expect(libcnb.BuildpackPathFormatter(bp).String()).To(Equal("Buildpack contents: [.]"))
})
it("lists directory contents", func() {
f, err := os.Create(filepath.Join(bp, "test-file"))
Expect(err).NotTo(HaveOccurred())
defer f.Close()
Expect(libcnb.BuildpackPathFormatter(bp).String()).To(Equal("Buildpack contents: [. test-file]"))
})
})
context("PlatformFormatter", func() {
var (
plat libcnb.Platform
)
it.Before(func() {
var err error
plat.Path, err = ioutil.TempDir("", "platform-formatter")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(plat.Path)).To(Succeed())
})
it("lists empty directory contents", func() {
Expect(libcnb.PlatformFormatter(plat).String()).To(Equal("Platform contents: [.]"))
})
it("lists directory contents", func() {
f, err := os.Create(filepath.Join(plat.Path, "test-file"))
Expect(err).NotTo(HaveOccurred())
defer f.Close()
Expect(libcnb.PlatformFormatter(plat).String()).To(Equal("Platform contents: [. test-file]"))
})
})
}

View File

@ -1,261 +0,0 @@
/*
* Copyright 2023 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 (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/Masterminds/semver"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/v2/log"
)
// GenerateContext contains the inputs to generate.
type GenerateContext struct {
// ApplicationPath is the location of the application source code as provided by
// the lifecycle.
ApplicationPath string
// Extension is metadata about the extension, from extension.toml.
Extension Extension
// OutputDirectory is the location Dockerfiles should be written to.
OutputDirectory string
// Logger is the way to write messages to the end user
Logger log.Logger
// Plan is the buildpack plan provided to the buildpack.
Plan BuildpackPlan
// Platform is the contents of the platform.
Platform Platform
// TargetInfo contains info of the target (os, arch, ...).
TargetInfo TargetInfo
// TargetDistro is the target distribution (name, version).
TargetDistro TargetDistro
// Deprecated: StackID is the ID of the stack.
StackID string
}
// GenerateResult contains the results of detection.
type GenerateResult struct {
// Unmet contains buildpack plan entries that were not satisfied by the buildpack and therefore should be
// passed to subsequent providers.
Unmet []UnmetPlanEntry
RunDockerfile []byte
BuildDockerfile []byte
Config *ExtendConfig
}
// DockerfileArg is a Dockerfile argument
type DockerfileArg struct {
Name string `toml:"name"`
Value string `toml:"value"`
}
// BuildConfig contains additional arguments passed to the generated Dockerfiles
type BuildConfig struct {
Args []DockerfileArg `toml:"args"`
}
// ExtendConfig contains additional configuration for the Dockerfiles
type ExtendConfig struct {
Build BuildConfig `toml:"build"`
Run BuildConfig `toml:"run"`
}
// NewGenerateResult creates a new BuildResult instance, initializing empty fields.
func NewGenerateResult() GenerateResult {
return GenerateResult{}
}
func (b GenerateResult) String() string {
return fmt.Sprintf(
"{Unmet:%+v}",
b.Unmet,
)
}
// GenerateFunc takes a context and returns a result, performing extension generate behaviors.
type GenerateFunc func(context GenerateContext) (GenerateResult, error)
// Generate is called by the main function of a extension, for generate phase
func Generate(generate GenerateFunc, config Config) {
var (
err error
file string
ok bool
)
ctx := GenerateContext{Logger: config.logger}
ctx.ApplicationPath, err = os.Getwd()
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil {
config.logger.Debugf("unable to write application contents\n%w", err)
}
}
if s, ok := os.LookupEnv(EnvExtensionDirectory); ok {
ctx.Extension.Path = filepath.Clean(s)
} else {
config.exitHandler.Error(fmt.Errorf("unable to get CNB_EXTENSION_DIR, not found"))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Extension contents", ctx.Extension.Path); err != nil {
config.logger.Debugf("unable to write extension contents\n%w", err)
}
}
file = filepath.Join(ctx.Extension.Path, "extension.toml")
if _, err = toml.DecodeFile(file, &ctx.Extension); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode extension %s\n%w", file, err))
return
}
config.logger.Debugf("Extension: %+v", ctx.Extension)
API, err := semver.NewVersion(ctx.Extension.API)
if err != nil {
config.exitHandler.Error(errors.New("version cannot be parsed"))
return
}
compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
if !compatVersionCheck.Check(API) {
if MinSupportedBPVersion == MaxSupportedBPVersion {
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion))
return
}
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
return
}
outputDir, ok := os.LookupEnv(EnvOutputDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_OUTPUT_DIR to be set"))
return
}
ctx.OutputDirectory = outputDir
ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set"))
return
}
buildpackPlanPath, ok := os.LookupEnv(EnvBuildPlanPath)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_BP_PLAN_PATH to be set"))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil {
config.logger.Debugf("unable to write platform contents\n%w", err)
}
}
if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err))
return
}
config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
file = filepath.Join(ctx.Platform.Path, "env")
if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err))
return
}
config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
if _, err = toml.DecodeFile(buildpackPlanPath, &ctx.Plan); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", buildpackPlanPath, err))
return
}
config.logger.Debugf("Buildpack Plan: %+v", ctx.Plan)
if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok {
config.logger.Debug("CNB_STACK_ID not set")
} else {
config.logger.Debugf("Stack: %s", ctx.StackID)
}
if API.GreaterThan(semver.MustParse("0.9")) {
ctx.TargetInfo = TargetInfo{}
ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS)
ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch)
ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant)
config.logger.Debugf("System: %+v", ctx.TargetInfo)
ctx.TargetDistro = TargetDistro{}
ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName)
ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion)
config.logger.Debugf("Distro: %+v", ctx.TargetDistro)
}
result, err := generate(ctx)
if err != nil {
config.exitHandler.Error(err)
return
}
config.logger.Debugf("Result: %+v", result)
if len(result.RunDockerfile) > 0 {
//nolint:gosec
if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "run.Dockerfile"), result.RunDockerfile, 0644); err != nil {
config.exitHandler.Error(err)
return
}
}
if len(result.BuildDockerfile) > 0 {
//nolint:gosec
if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "build.Dockerfile"), result.BuildDockerfile, 0644); err != nil {
config.exitHandler.Error(err)
return
}
}
if result.Config != nil {
configFile, err := os.Create(filepath.Join(ctx.OutputDirectory, "extend-config.toml"))
if err != nil {
config.exitHandler.Error(err)
return
}
if err := toml.NewEncoder(configFile).Encode(result.Config); err != nil {
config.exitHandler.Error(err)
return
}
}
}

View File

@ -1,458 +0,0 @@
/*
* 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 libcnb_test
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"text/template"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/v2/mocks"
)
func testGenerate(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
generateFunc libcnb.GenerateFunc
applicationPath string
extensionPath string
outputPath string
buildpackPlanPath string
extnTOMLContents string
commandPath string
environmentWriter *mocks.EnvironmentWriter
exitHandler *mocks.ExitHandler
platformPath string
tomlWriter *mocks.TOMLWriter
extensionTOML *template.Template
workingDir string
)
it.Before(func() {
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.NewGenerateResult(), nil
}
var err error
applicationPath, err = os.MkdirTemp("", "generate-application-path")
Expect(err).NotTo(HaveOccurred())
applicationPath, err = filepath.EvalSymlinks(applicationPath)
Expect(err).NotTo(HaveOccurred())
extensionPath, err = os.MkdirTemp("", "generate-extension-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_EXTENSION_DIR", extensionPath)).To(Succeed())
extnTOMLContents = `
api = "{{.APIVersion}}"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
description = "A test extension"
keywords = ["test", "extension"]
[[extension.licenses]]
type = "Apache-2.0"
uri = "https://spdx.org/licenses/Apache-2.0.html"
[[extension.licenses]]
type = "Apache-1.1"
uri = "https://spdx.org/licenses/Apache-1.1.html"
[metadata]
test-key = "test-value"
`
extensionTOML, err = template.New("extension.toml").Parse(extnTOMLContents)
Expect(err).ToNot(HaveOccurred())
var b bytes.Buffer
err = extensionTOML.Execute(&b, map[string]string{"APIVersion": "0.8"})
Expect(err).ToNot(HaveOccurred())
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"), b.Bytes(), 0600)).To(Succeed())
f, err := os.CreateTemp("", "generate-buildpackplan-path")
Expect(err).NotTo(HaveOccurred())
Expect(f.Close()).NotTo(HaveOccurred())
buildpackPlanPath = f.Name()
Expect(os.WriteFile(buildpackPlanPath,
[]byte(`
[[entries]]
name = "test-name"
version = "test-version"
[entries.metadata]
test-key = "test-value"
`),
0600),
).To(Succeed())
commandPath = filepath.Join("bin", "generate")
environmentWriter = &mocks.EnvironmentWriter{}
environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
exitHandler = &mocks.ExitHandler{}
exitHandler.On("Error", mock.Anything)
platformPath, err = os.MkdirTemp("", "generate-platform-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"),
[]byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
To(Succeed())
tomlWriter = &mocks.TOMLWriter{}
tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
outputPath, err = os.MkdirTemp("", "generate-output-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_OUTPUT_DIR", outputPath)).To(Succeed())
Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed())
Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed())
Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed())
Expect(os.Setenv("CNB_TARGET_OS", "linux")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_ARCH", "arm")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_ARCH_VARIANT", "v6")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_DISTRO_NAME", "ubuntu")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_DISTRO_VERSION", "24.04")).To(Succeed())
workingDir, err = os.Getwd()
Expect(err).NotTo(HaveOccurred())
Expect(os.Chdir(applicationPath)).To(Succeed())
})
it.After(func() {
Expect(os.Chdir(workingDir)).To(Succeed())
Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed())
Expect(os.Unsetenv("CNB_OUTPUT_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_TARGET_OS"))
Expect(os.Unsetenv("CNB_TARGET_ARCH"))
Expect(os.Unsetenv("CNB_TARGET_ARCH_VARIANT"))
Expect(os.Unsetenv("CNB_TARGET_DISTRO_NAME"))
Expect(os.Unsetenv("CNB_TARGET_DISTRO_VERSION"))
Expect(os.RemoveAll(applicationPath)).To(Succeed())
Expect(os.RemoveAll(extensionPath)).To(Succeed())
Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed())
Expect(os.RemoveAll(outputPath)).To(Succeed())
Expect(os.RemoveAll(platformPath)).To(Succeed())
})
context("buildpack API is not within the supported range", func() {
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.7"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
})
it("fails", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
if libcnb.MinSupportedBPVersion == libcnb.MaxSupportedBPVersion {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack API == %s", libcnb.MinSupportedBPVersion)))
} else {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion),
))
}
})
})
context("errors if required env vars are not set", func() {
for _, e := range []string{"CNB_OUTPUT_DIR", "CNB_PLATFORM_DIR", "CNB_BP_PLAN_PATH"} {
// We need to do this assignment because of the way that spec binds variables
envVar := e
context(fmt.Sprintf("when %s is unset", envVar), func() {
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.8"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
os.Unsetenv(envVar)
})
it("fails", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler)),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("expected %s to be set", envVar),
))
})
})
}
})
context("has a build environment", func() {
var ctx libcnb.GenerateContext
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.8"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) {
ctx = context
return libcnb.NewGenerateResult(), nil
}
})
it("creates context", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath})),
)
Expect(ctx.ApplicationPath).To(Equal(applicationPath))
Expect(ctx.Extension).To(Equal(libcnb.Extension{
API: "0.8",
Info: libcnb.ExtensionInfo{
ID: "test-id",
Name: "test-name",
Version: "1.1.1",
},
Path: extensionPath,
}))
Expect(ctx.OutputDirectory).To(Equal(outputPath))
Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{
Entries: []libcnb.BuildpackPlanEntry{
{
Name: "test-name",
Metadata: map[string]interface{}{
"test-key": "test-value",
},
},
},
}))
Expect(ctx.Platform).To(Equal(libcnb.Platform{
Bindings: libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(platformPath, "bindings", "alpha"),
Secret: map[string]string{
"test-secret-key": "test-secret-value",
},
},
},
Environment: map[string]string{"TEST_ENV": "test-value"},
Path: platformPath,
}))
Expect(ctx.StackID).To(Equal("test-stack-id"))
})
})
context("has a build environment specifying target metadata", func() {
var ctx libcnb.GenerateContext
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.10"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
[[targets]]
os = "linux"
arch = "amd64"
[[targets.distros]]
name = "ubuntu"
version = "18.04"
[[targets.distros]]
name = "debian"
[[targets]]
os = "linux"
arch = "arm"
variant = "v6"
`), 0600),
).To(Succeed())
generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) {
ctx = context
return libcnb.NewGenerateResult(), nil
}
})
it("provides target information", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}),
libcnb.WithLogger(log.New(os.Stdout)),
),
)
Expect(ctx.Extension.Targets).To(HaveLen(2))
Expect(ctx.Extension.Targets[0].OS).To(Equal("linux"))
Expect(ctx.Extension.Targets[0].Arch).To(Equal("amd64"))
Expect(ctx.Extension.Targets[0].Distros).To(HaveLen(2))
Expect(ctx.Extension.Targets[0].Distros[0].Name).To(Equal("ubuntu"))
Expect(ctx.Extension.Targets[0].Distros[0].Version).To(Equal("18.04"))
Expect(ctx.Extension.Targets[0].Distros[1].Name).To(Equal("debian"))
Expect(ctx.Extension.Targets[1].Variant).To(Equal("v6"))
Expect(ctx.TargetInfo.OS).To(Equal("linux"))
Expect(ctx.TargetInfo.Arch).To(Equal("arm"))
Expect(ctx.TargetInfo.Variant).To(Equal("v6"))
Expect(ctx.TargetDistro.Name).To(Equal("ubuntu"))
Expect(ctx.TargetDistro.Version).To(Equal("24.04"))
})
})
it("fails if CNB_EXTENSION_DIR is not set", func() {
Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed())
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{filepath.Join(extensionPath, commandPath), outputPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to get CNB_EXTENSION_DIR, not found"))
})
it("handles error from GenerateFunc", func() {
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.NewGenerateResult(), errors.New("test-error")
}
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error"))
})
it("writes Dockerfiles", func() {
generateFunc = func(_ libcnb.GenerateContext) (libcnb.GenerateResult, error) {
result := libcnb.NewGenerateResult()
result.BuildDockerfile = []byte(`FROM foo:latest`)
result.RunDockerfile = []byte(`FROM bar:latest`)
return result, nil
}
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(filepath.Join(outputPath, "build.Dockerfile")).To(BeARegularFile())
Expect(filepath.Join(outputPath, "run.Dockerfile")).To(BeARegularFile())
})
it("writes extend-config.toml", func() {
generateFunc = func(_ libcnb.GenerateContext) (libcnb.GenerateResult, error) {
result := libcnb.NewGenerateResult()
result.Config = &libcnb.ExtendConfig{
Build: libcnb.BuildConfig{
Args: []libcnb.DockerfileArg{
{
Name: "foo",
Value: "bar",
},
},
},
Run: libcnb.BuildConfig{
Args: []libcnb.DockerfileArg{
{
Name: "bar",
Value: "bazz",
},
},
},
}
return result, nil
}
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(filepath.Join(outputPath, "extend-config.toml")).To(BeARegularFile())
})
}

24
go.mod
View File

@ -1,24 +1,10 @@
module github.com/buildpacks/libcnb/v2
module github.com/buildpacks/libcnb
go 1.24
go 1.15
require (
github.com/BurntSushi/toml v1.5.0
github.com/CycloneDX/cyclonedx-go v0.9.2
github.com/Masterminds/semver v1.5.0
github.com/onsi/gomega v1.37.0
github.com/BurntSushi/toml v0.3.1
github.com/onsi/gomega v1.13.0
github.com/sclevine/spec v1.4.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/stretchr/testify v1.7.0
)

150
go.sum
View File

@ -1,58 +1,106 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo=
github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,9 +5,9 @@ linters:
disable-all: true
enable:
- bodyclose
- deadcode
- dogsled
- errcheck
- copyloopvar
- exportloopref
- gocritic
- goimports
- gosec
@ -18,16 +18,14 @@ linters:
- nakedret
- revive
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
linters-settings:
revive:
rules:
- name: dot-imports
disabled: true
goimports:
local-prefixes: github.com/buildpacks/libcnb/v2
local-prefixes: github.com/buildpacks/libcnb

View File

@ -27,13 +27,10 @@ func TestUnit(t *testing.T) {
suite := spec.New("libcnb", spec.Report(report.Terminal{}))
suite("Build", testBuild)
suite("Detect", testDetect)
suite("Generate", testGenerate)
suite("Environment", testEnvironment)
suite("Formatter", testFormatter)
suite("Layer", testLayer)
suite("Main", testMain)
suite("Platform", testPlatform)
suite("ExecD", testExecD)
suite("BuildpackTOML", testBuildpackTOML)
suite("ExtensionTOML", testExtensionTOML)
suite.Run(t)
}

View File

@ -18,6 +18,7 @@ package internal
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -44,7 +45,7 @@ func NewConfigMapFromPath(path string) (ConfigMap, error) {
} else if stat.IsDir() {
continue
}
contents, err := os.ReadFile(file)
contents, err := ioutil.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("unable to read file %s\n%w", file, err)
}

View File

@ -17,6 +17,7 @@
package internal_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -24,10 +25,10 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/internal"
)
func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
func testConfigMap(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
@ -36,7 +37,7 @@ func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
it.Before(func() {
var err error
path, err = os.MkdirTemp("", "config-map")
path, err = ioutil.TempDir("", "config-map")
Expect(err).NotTo(HaveOccurred())
})
@ -54,7 +55,7 @@ func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
})
it("loads the ConfigMap from a directory", func() {
Expect(os.WriteFile(filepath.Join(path, "test-key"), []byte("test-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "test-key"), []byte("test-value"), 0600)).To(Succeed())
cm, err := internal.NewConfigMapFromPath(path)
Expect(err).NotTo(HaveOccurred())
@ -65,7 +66,7 @@ func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
it("ignores dirs and follows symlinks", func() {
// this is necessary to support bindings mounted as k8s config maps & secrets
Expect(os.MkdirAll(filepath.Join(path, ".hidden"), 0755)).To(Succeed())
Expect(os.WriteFile(
Expect(ioutil.WriteFile(
filepath.Join(path, ".hidden", "test-key"),
[]byte("test-value"),
0600,
@ -81,7 +82,7 @@ func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
})
it("ignores hidden files", func() {
Expect(os.WriteFile(filepath.Join(path, ".hidden-key"), []byte("hidden-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, ".hidden-key"), []byte("hidden-value"), 0600)).To(Succeed())
cm, err := internal.NewConfigMapFromPath(path)
Expect(err).NotTo(HaveOccurred())

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2022 the original author or authors.
* 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.
@ -18,53 +18,36 @@ package internal
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/buildpacks/libcnb/v2/log"
"sort"
)
// DirectoryContentsWriter is used write the contents of a directory to the given io.Writer
type DirectoryContentsWriter struct {
format log.DirectoryContentFormatter
writer io.Writer
// DirectoryContents is used to generate a collection of the names of all files within a directory.
type DirectoryContents struct {
Path string
}
// NewDirectoryContentsWriter returns a new DirectoryContentsWriter initialized and ready to be used
func NewDirectoryContentsWriter(format log.DirectoryContentFormatter, writer io.Writer) DirectoryContentsWriter {
return DirectoryContentsWriter{
format: format,
writer: writer,
}
}
// Get returns the names of all files within a directory
func (d DirectoryContents) Get() ([]string, error) {
var contents []string
// Write all the file contents to the writer
func (d DirectoryContentsWriter) Write(title, path string) error {
d.format.RootPath(path)
if _, err := d.writer.Write([]byte(d.format.Title(title))); err != nil {
return fmt.Errorf("unable to write title\n%w", err)
}
if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err := filepath.Walk(d.Path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
msg, err := d.format.File(path, info)
rel, err := filepath.Rel(d.Path, path)
if err != nil {
return fmt.Errorf("unable to format\n%w", err)
}
if _, err := d.writer.Write([]byte(msg)); err != nil {
return fmt.Errorf("unable to write\n%w", err)
return fmt.Errorf("unable to calculate relative path %s -> %s\n%w", d.Path, path, err)
}
contents = append(contents, rel)
return nil
}); err != nil {
return fmt.Errorf("error walking path %s\n%w", path, err)
return nil, fmt.Errorf("error walking path %s\n%w", d.Path, err)
}
return nil
sort.Strings(contents)
return contents, nil
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2022 the original author or authors.
* 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.
@ -17,8 +17,7 @@
package internal_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -26,20 +25,19 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/internal"
)
func testDirectoryContentsWriter(t *testing.T, context spec.G, it spec.S) {
func testDirectoryContents(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
path string
buf bytes.Buffer
)
it.Before(func() {
var err error
path, err = os.MkdirTemp("", "directory-contents")
path, err = ioutil.TempDir("", "directory-contents")
Expect(err).NotTo(HaveOccurred())
})
@ -47,32 +45,8 @@ func testDirectoryContentsWriter(t *testing.T, context spec.G, it spec.S) {
Expect(os.RemoveAll(path)).To(Succeed())
})
context("directory content formats", func() {
fm := internal.NewPlainDirectoryContentFormatter()
it("formats title", func() {
Expect(fm.Title("foo")).To(Equal("foo:\n"))
})
it("formats a file", func() {
cwd, err := os.Getwd()
Expect(err).ToNot(HaveOccurred())
info, err := os.Stat(cwd)
Expect(err).ToNot(HaveOccurred())
fm.RootPath(filepath.Dir(cwd))
Expect(fm.File(cwd, info)).To(Equal(fmt.Sprintf("%s\n", filepath.Base(cwd))))
})
})
it("lists empty directory contents", func() {
fm := internal.NewPlainDirectoryContentFormatter()
dc := internal.NewDirectoryContentsWriter(fm, &buf)
Expect(dc.Write("title", path)).To(Succeed())
Expect(buf.String()).To(Equal("title:\n.\n"))
Expect(internal.DirectoryContents{path}.Get()).To(Equal([]string{"."}))
})
it("lists directory contents", func() {
@ -80,10 +54,6 @@ func testDirectoryContentsWriter(t *testing.T, context spec.G, it spec.S) {
Expect(err).NotTo(HaveOccurred())
defer f.Close()
fm := internal.NewPlainDirectoryContentFormatter()
dc := internal.NewDirectoryContentsWriter(fm, &buf)
Expect(dc.Write("title", path)).To(Succeed())
Expect(buf.String()).To(Equal("title:\n.\ntest-file\n"))
Expect(internal.DirectoryContents{path}.Get()).To(Equal([]string{".", "test-file"}))
})
}

View File

@ -18,6 +18,7 @@ package internal
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
@ -37,14 +38,8 @@ func (w EnvironmentWriter) Write(path string, environment map[string]string) err
for key, value := range environment {
f := filepath.Join(path, key)
// required to support process-specific environment variables
if err := os.MkdirAll(filepath.Dir(f), 0755); err != nil {
return fmt.Errorf("unable to mkdir from key %s\n%w", filepath.Dir(f), err)
}
//nolint:gosec
if err := os.WriteFile(f, []byte(value), 0644); err != nil {
// #nosec
if err := ioutil.WriteFile(f, []byte(value), 0644); err != nil {
return fmt.Errorf("unable to write file %s\n%w", f, err)
}
}

View File

@ -17,6 +17,7 @@
package internal_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -24,10 +25,10 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/internal"
)
func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) {
func testEnvironmentWriter(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
@ -37,7 +38,7 @@ func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) {
it.Before(func() {
var err error
path, err = os.MkdirTemp("", "environment-writer")
path, err = ioutil.TempDir("", "environment-writer")
Expect(err).NotTo(HaveOccurred())
Expect(os.RemoveAll(path)).To(Succeed())
})
@ -53,26 +54,15 @@ func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) {
})
Expect(err).NotTo(HaveOccurred())
content, err := os.ReadFile(filepath.Join(path, "some-name"))
content, err := ioutil.ReadFile(filepath.Join(path, "some-name"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("some-content"))
content, err = os.ReadFile(filepath.Join(path, "other-name"))
content, err = ioutil.ReadFile(filepath.Join(path, "other-name"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("other-content"))
})
it("writes the given environment with process specific envs to a directory", func() {
err := writer.Write(path, map[string]string{
"some-proc/some-name": "some-content",
})
Expect(err).NotTo(HaveOccurred())
content, err := os.ReadFile(filepath.Join(path, "some-proc", "some-name"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("some-content"))
})
it("writes does not create a directory of the env map is empty", func() {
err := writer.Write(path, map[string]string{})
Expect(err).NotTo(HaveOccurred())

View File

@ -1,62 +0,0 @@
/*
* 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)
}

View File

@ -1,55 +0,0 @@
/*
* 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/v2/internal"
)
func testExecDWriter(t *testing.T, _ 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"`))
})
}

View File

@ -24,10 +24,10 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/internal"
)
func testExitHandler(t *testing.T, _ spec.G, it spec.S) {
func testExitHandler(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect

View File

@ -1,38 +0,0 @@
package internal
import (
"fmt"
"os"
"path/filepath"
)
type PlainDirectoryContentFormatter struct {
rootPath string
}
// NewPlainDirectoryContentFormatter returns a formatter applies no formatting
//
// The returned formatter operates as such:
//
// Title -> returns string followed by `:\n`
// File -> returns file name relative to the root followed by `\n`
func NewPlainDirectoryContentFormatter() *PlainDirectoryContentFormatter {
return &PlainDirectoryContentFormatter{}
}
func (p *PlainDirectoryContentFormatter) File(path string, _ os.FileInfo) (string, error) {
rel, err := filepath.Rel(p.rootPath, path)
if err != nil {
return "", fmt.Errorf("unable to calculate relative path %s -> %s\n%w", p.rootPath, path, err)
}
return fmt.Sprintf("%s\n", rel), nil
}
func (p *PlainDirectoryContentFormatter) RootPath(path string) {
p.rootPath = path
}
func (p *PlainDirectoryContentFormatter) Title(title string) string {
return fmt.Sprintf("%s:\n", title)
}

View File

@ -1,67 +0,0 @@
/*
* Copyright 2018-2022 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 (
"fmt"
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
)
func testFormatters(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
path string
)
it.Before(func() {
var err error
path, err = os.MkdirTemp("", "directory-contents")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed())
})
context("directory content formats", func() {
fm := internal.NewPlainDirectoryContentFormatter()
it("formats title", func() {
Expect(fm.Title("foo")).To(Equal("foo:\n"))
})
it("formats a file", func() {
cwd, err := os.Getwd()
Expect(err).ToNot(HaveOccurred())
info, err := os.Stat(cwd)
Expect(err).ToNot(HaveOccurred())
fm.RootPath(filepath.Dir(cwd))
Expect(fm.File(cwd, info)).To(Equal(fmt.Sprintf("%s\n", filepath.Base(cwd))))
})
})
}

View File

@ -26,11 +26,9 @@ import (
func TestUnit(t *testing.T) {
suite := spec.New("libcnb/internal", spec.Report(report.Terminal{}))
suite("ConfigMap", testConfigMap)
suite("DirectoryContents", testDirectoryContentsWriter)
suite("DirectoryContents", testDirectoryContents)
suite("EnvironmentWriter", testEnvironmentWriter)
suite("ExitHandler", testExitHandler)
suite("TOMLWriter", testTOMLWriter)
suite("ExecDWriter", testExecDWriter)
suite("Formatters", testFormatters)
suite.Run(t)
}

18
internal/layer_api_5.go Normal file
View File

@ -0,0 +1,18 @@
package internal
// LayerAPI5 is used for backwards compatibility with serialization/deserialization of the layer toml
type LayerAPI5 struct {
// bool's are inlined to instead of using libcnb.LayerTypes to break a circular package reference, internal -> libcnb -> internal -> ...
// Build indicates that a layer should be used for builds.
Build bool `toml:"build"`
// Cache indicates that a layer should be cached.
Cache bool `toml:"cache"`
// Launch indicates that a layer should be used for launch.
Launch bool `toml:"launch"`
// Metadata is the metadata associated with the layer.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -17,6 +17,7 @@
package internal_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -24,10 +25,10 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/internal"
)
func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) {
func testTOMLWriter(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
@ -38,7 +39,7 @@ func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) {
it.Before(func() {
var err error
parent, err = os.MkdirTemp("", "toml-writer")
parent, err = ioutil.TempDir("", "toml-writer")
Expect(err).NotTo(HaveOccurred())
path = filepath.Join(parent, "text.toml")
@ -55,7 +56,7 @@ func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) {
})
Expect(err).NotTo(HaveOccurred())
Expect(os.ReadFile(path)).To(internal.MatchTOML(`
Expect(ioutil.ReadFile(path)).To(internal.MatchTOML(`
some-field = "some-value"
other-field = "other-value"`))
})

View File

@ -1,26 +0,0 @@
/*
* 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 libcnb
// Label represents an image label.
type Label struct {
// Key is the key of the label.
Key string `toml:"key"`
// Value is the value of the label.
Value string `toml:"value"`
}

View File

@ -1,33 +0,0 @@
/*
* 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 libcnb
// LaunchTOML represents the contents of launch.toml.
type LaunchTOML struct {
// Labels is the collection of image labels contributed by the buildpack.
Labels []Label `toml:"labels"`
// Processes is the collection of process types contributed by the buildpack.
Processes []Process `toml:"processes"`
// Slices is the collection of slices contributed by the buildpack.
Slices []Slice `toml:"slices"`
}
func (l LaunchTOML) isEmpty() bool {
return len(l.Labels) == 0 && len(l.Processes) == 0 && len(l.Slices) == 0
}

131
layer.go
View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2024 the original author or authors.
* 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.
@ -22,20 +22,13 @@ import (
"path/filepath"
"github.com/BurntSushi/toml"
)
const (
BOMFormatCycloneDXExtension = "cdx.json"
BOMFormatSPDXExtension = "spdx.json"
BOMFormatSyftExtension = "syft.json"
BOMMediaTypeCycloneDX = "application/vnd.cyclonedx+json"
BOMMediaTypeSPDX = "application/spdx+json"
BOMMediaTypeSyft = "application/vnd.syft+json"
BOMUnknown = "unknown"
"github.com/buildpacks/libcnb/internal"
)
// Exec represents the exec.d layer location
type Exec struct {
// Path is the path to the exec.d directory.
Path string
}
@ -50,47 +43,34 @@ func (e Exec) ProcessFilePath(processType string, name string) string {
return filepath.Join(e.Path, processType, name)
}
// BOMFormat indicates the format of the SBOM entry
type SBOMFormat int
// Profile is the collection of values to be written into profile.d
type Profile map[string]string
const (
CycloneDXJSON SBOMFormat = iota
SPDXJSON
SyftJSON
UnknownFormat
)
func (b SBOMFormat) String() string {
return []string{
BOMFormatCycloneDXExtension,
BOMFormatSPDXExtension,
BOMFormatSyftExtension,
BOMUnknown}[b]
// Add formats using the default formats for its operands and adds an entry for a .profile.d file. Spaces are added
// between operands when neither is a string.
func (p Profile) Add(name string, a ...interface{}) {
p[name] = fmt.Sprint(a...)
}
func (b SBOMFormat) MediaType() string {
return []string{
BOMMediaTypeCycloneDX,
BOMMediaTypeSPDX,
BOMMediaTypeSyft,
BOMUnknown}[b]
// Addf formats according to a format specifier and adds an entry for a .profile.d file.
func (p Profile) Addf(name string, format string, a ...interface{}) {
p[name] = fmt.Sprintf(format, a...)
}
func SBOMFormatFromString(from string) (SBOMFormat, error) {
switch from {
case CycloneDXJSON.String():
return CycloneDXJSON, nil
case SPDXJSON.String():
return SPDXJSON, nil
case SyftJSON.String():
return SyftJSON, nil
}
// ProcessAdd formats using the default formats for its operands and adds an entry for a .profile.d file. Spaces are
// added between operands when neither is a string.
func (p Profile) ProcessAdd(processType string, name string, a ...interface{}) {
p.Add(filepath.Join(processType, name), a...)
}
return UnknownFormat, fmt.Errorf("unable to translate from %s to SBOMFormat", from)
// ProcessAddf formats according to a format specifier and adds an entry for a .profile.d file.
func (p Profile) ProcessAddf(processType string, name string, format string, a ...interface{}) {
p.Addf(filepath.Join(processType, name), format, a...)
}
// Contribute represents a layer managed by the buildpack.
type Layer struct {
// LayerTypes indicates the type of layer
LayerTypes `toml:"types"`
@ -112,40 +92,13 @@ type Layer struct {
// SharedEnvironment are the environment variables set at both build and launch times.
SharedEnvironment Environment `toml:"-"`
// Profile is the profile.d scripts set in the layer.
Profile Profile `toml:"-"`
// Exec is the exec.d executables set in the layer.
Exec Exec `toml:"-"`
}
func (l Layer) Reset() (Layer, error) {
l.LayerTypes = LayerTypes{
Build: false,
Launch: false,
Cache: false,
}
l.SharedEnvironment = Environment{}
l.BuildEnvironment = Environment{}
l.LaunchEnvironment = Environment{}
l.Metadata = nil
err := os.RemoveAll(l.Path)
if err != nil {
return Layer{}, fmt.Errorf("error could not remove file: %s", err)
}
err = os.MkdirAll(l.Path, os.ModePerm)
if err != nil {
return Layer{}, fmt.Errorf("error could not create directory: %s", err)
}
return l, nil
}
// SBOMPath returns the path to the layer specific SBOM File
func (l Layer) SBOMPath(bt SBOMFormat) string {
return filepath.Join(filepath.Dir(l.Path), fmt.Sprintf("%s.sbom.%s", l.Name, bt))
}
// LayerTypes describes which types apply to a given layer. A layer may have any combination of Launch, Build, and
// Cache types.
type LayerTypes struct {
@ -159,8 +112,21 @@ type LayerTypes struct {
Launch bool `toml:"launch"`
}
//go:generate mockery -name LayerContributor -case=underscore
// LayerContributor is an interface for types that create layers.
type LayerContributor interface {
// Contribute accepts a layer and transforms it, returning a layer.
Contribute(layer Layer) (Layer, error)
// Name is the name of the layer.
Name() string
}
// Layers represents the layers part of the specification.
type Layers struct {
// Path is the layers filesystem location.
Path string
}
@ -173,6 +139,7 @@ func (l *Layers) Layer(name string) (Layer, error) {
BuildEnvironment: Environment{},
LaunchEnvironment: Environment{},
SharedEnvironment: Environment{},
Profile: Profile{},
Exec: Exec{Path: filepath.Join(l.Path, name, "exec.d")},
}
@ -181,15 +148,17 @@ func (l *Layers) Layer(name string) (Layer, error) {
return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err)
}
if !layer.Build && !layer.Cache && !layer.Launch {
// if all three are false, that could mean we have a API <= 0.5 TOML file
// try parsing the <= 0.5 API format where these were top level attributes
buf := internal.LayerAPI5{}
if _, err := toml.DecodeFile(f, &buf); err != nil && !os.IsNotExist(err) {
return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err)
}
layer.Build = buf.Build
layer.Cache = buf.Cache
layer.Launch = buf.Launch
}
return layer, nil
}
// BOMBuildPath returns the full path to the build SBoM file for the buildpack
func (l Layers) BuildSBOMPath(bt SBOMFormat) string {
return filepath.Join(l.Path, fmt.Sprintf("build.sbom.%s", bt))
}
// BOMLaunchPath returns the full path to the launch SBoM file for the buildpack
func (l Layers) LaunchSBOMPath(bt SBOMFormat) string {
return filepath.Join(l.Path, fmt.Sprintf("launch.sbom.%s", bt))
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2024 the original author or authors.
* 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.
@ -17,6 +17,7 @@
package libcnb_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -24,7 +25,7 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb"
)
func testLayer(t *testing.T, context spec.G, it spec.S) {
@ -52,152 +53,38 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
})
})
context("Reset", func() {
var layer libcnb.Layer
context("Profile", func() {
var profile libcnb.Profile
it.Before(func() {
layers = libcnb.Layers{Path: t.TempDir()}
profile = libcnb.Profile{}
})
context("when there is no previous build", func() {
it.Before(func() {
layer = libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: true,
Build: true,
Cache: true,
},
}
})
it("initializes an empty layer", func() {
var err error
layer, err = layer.Reset()
Expect(err).NotTo(HaveOccurred())
Expect(layer).To(Equal(libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: false,
Build: false,
Cache: false,
},
SharedEnvironment: libcnb.Environment{},
BuildEnvironment: libcnb.Environment{},
LaunchEnvironment: libcnb.Environment{},
}))
Expect(filepath.Join(layers.Path, "test-name")).To(BeADirectory())
})
it("adds content", func() {
profile.Add("test-name", "test-value")
Expect(profile).To(Equal(libcnb.Profile{"test-name": "test-value"}))
})
context("when cache is retrieved from previous build", func() {
it.Before(func() {
sharedEnvDir := filepath.Join(layers.Path, "test-name", "env")
Expect(os.MkdirAll(sharedEnvDir, os.ModePerm)).To(Succeed())
err := os.WriteFile(filepath.Join(sharedEnvDir, "OVERRIDE_VAR.override"), []byte("override-value"), 0600)
Expect(err).NotTo(HaveOccurred())
buildEnvDir := filepath.Join(layers.Path, "test-name", "env.build")
Expect(os.MkdirAll(buildEnvDir, os.ModePerm)).To(Succeed())
err = os.WriteFile(filepath.Join(buildEnvDir, "DEFAULT_VAR.default"), []byte("default-value"), 0600)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(filepath.Join(buildEnvDir, "INVALID_VAR.invalid"), []byte("invalid-value"), 0600)
Expect(err).NotTo(HaveOccurred())
launchEnvDir := filepath.Join(layers.Path, "test-name", "env.launch")
Expect(os.MkdirAll(launchEnvDir, os.ModePerm)).To(Succeed())
err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.append"), []byte("append-value"), 0600)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.delim"), []byte("!"), 0600)
Expect(err).NotTo(HaveOccurred())
layer = libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: true,
Build: true,
Cache: true,
},
SharedEnvironment: libcnb.Environment{
"OVERRIDE_VAR.override": "override-value",
},
BuildEnvironment: libcnb.Environment{
"DEFAULT_VAR.default": "default-value",
},
LaunchEnvironment: libcnb.Environment{
"APPEND_VAR.append": "append-value",
"APPEND_VAR.delim": "!",
},
Metadata: map[string]interface{}{
"some-key": "some-value",
},
}
})
context("when Reset is called on a layer", func() {
it("resets all of the layer data and clears the directory", func() {
layer, err := layer.Reset()
Expect(err).NotTo(HaveOccurred())
Expect(layer).To(Equal(libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: false,
Build: false,
Cache: false,
},
SharedEnvironment: libcnb.Environment{},
BuildEnvironment: libcnb.Environment{},
LaunchEnvironment: libcnb.Environment{},
}))
Expect(filepath.Join(layers.Path, "test-name")).To(BeADirectory())
files, err := filepath.Glob(filepath.Join(layers.Path, "test-name", "*"))
Expect(err).NotTo(HaveOccurred())
Expect(files).To(BeEmpty())
})
})
it("adds formatted content", func() {
profile.Addf("test-name", "test-%s", "value")
Expect(profile).To(Equal(libcnb.Profile{"test-name": "test-value"}))
})
context("could not remove files in layer", func() {
it.Before(func() {
Expect(os.Chmod(layers.Path, 0000)).To(Succeed())
})
it("adds process-specific content", func() {
profile.ProcessAdd("test-process", "test-name", "test-value")
Expect(profile).To(Equal(libcnb.Profile{filepath.Join("test-process", "test-name"): "test-value"}))
})
it.After(func() {
Expect(os.Chmod(layers.Path, 0777)).To(Succeed())
})
it("return an error", func() {
layer := libcnb.Layer{
Name: "some-layer",
Path: filepath.Join(layers.Path, "some-layer"),
}
_, err := layer.Reset()
Expect(err).To(MatchError(ContainSubstring("error could not remove file: ")))
Expect(err).To(MatchError(ContainSubstring("permission denied")))
})
it("adds process-specific formatted content", func() {
profile.ProcessAddf("test-process", "test-name", "test-%s", "value")
Expect(profile).To(Equal(libcnb.Profile{filepath.Join("test-process", "test-name"): "test-value"}))
})
})
context("Layers", func() {
it.Before(func() {
var err error
path, err = os.MkdirTemp("", "layers")
path, err = ioutil.TempDir("", "layers")
Expect(err).NotTo(HaveOccurred())
layers = libcnb.Layers{Path: path}
@ -220,40 +107,32 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(l.BuildEnvironment).To(Equal(libcnb.Environment{}))
Expect(l.LaunchEnvironment).To(Equal(libcnb.Environment{}))
Expect(l.SharedEnvironment).To(Equal(libcnb.Environment{}))
Expect(l.Profile).To(Equal(libcnb.Profile{}))
})
it("generates SBOM paths", func() {
it("reads existing 0.5 metadata", func() {
Expect(ioutil.WriteFile(
filepath.Join(path, "test-name.toml"),
[]byte(`
launch = true
build = false
[metadata]
test-key = "test-value"
`),
0600),
).To(Succeed())
l, err := layers.Layer("test-name")
Expect(err).NotTo(HaveOccurred())
Expect(l.Path).To(Equal(filepath.Join(path, "test-name")))
Expect(layers.BuildSBOMPath(libcnb.CycloneDXJSON)).To(Equal(filepath.Join(path, "build.sbom.cdx.json")))
Expect(layers.BuildSBOMPath(libcnb.SPDXJSON)).To(Equal(filepath.Join(path, "build.sbom.spdx.json")))
Expect(layers.BuildSBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "build.sbom.syft.json")))
Expect(layers.LaunchSBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "launch.sbom.syft.json")))
Expect(l.SBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "test-name.sbom.syft.json")))
Expect(l.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"}))
Expect(l.Launch).To(BeTrue())
Expect(l.Build).To(BeFalse())
})
it("maps from string to SBOM Format", func() {
fmt, err := libcnb.SBOMFormatFromString("cdx.json")
Expect(err).ToNot(HaveOccurred())
Expect(fmt).To(Equal(libcnb.CycloneDXJSON))
fmt, err = libcnb.SBOMFormatFromString("spdx.json")
Expect(err).ToNot(HaveOccurred())
Expect(fmt).To(Equal(libcnb.SPDXJSON))
fmt, err = libcnb.SBOMFormatFromString("syft.json")
Expect(err).ToNot(HaveOccurred())
Expect(fmt).To(Equal(libcnb.SyftJSON))
fmt, err = libcnb.SBOMFormatFromString("foobar.json")
Expect(err).To(MatchError("unable to translate from foobar.json to SBOMFormat"))
Expect(fmt).To(Equal(libcnb.UnknownFormat))
})
it("reads existing metadata", func() {
Expect(os.WriteFile(
it("reads existing 0.6 metadata", func() {
Expect(ioutil.WriteFile(
filepath.Join(path, "test-name.toml"),
[]byte(`
[types]
@ -274,8 +153,8 @@ test-key = "test-value"
Expect(l.Build).To(BeFalse())
})
it("reads existing metadata with launch, build and cache all false", func() {
Expect(os.WriteFile(
it("reads existing 0.6 metadata with launch, build and cache all false", func() {
Expect(ioutil.WriteFile(
filepath.Join(path, "test-name.toml"),
[]byte(`
[types]

View File

@ -1,59 +0,0 @@
/*
* Copyright 2018-2022 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 log
import (
"os"
)
//go:generate mockery --name DirectoryContentFormatter --case=underscore
// DirectoryContentFormatter allows customization of logged directory output
//
// When libcnb logs the contents of a directory, each item in the directory
// is passed through a DirectoryContentFormatter.
//
// DirectoryContentsWriter implements this workflow:
// - call RootPath(string) with the root path that's being walked
// - call Title(string) with the given title, the output is logged
// - for each file in the directory:
// - call File(string, os.FileInfo), the output is logged
//
// # A default implementation is provided that returns a formatter applies no formatting
//
// The returned formatter operates as such:
//
// Title -> returns string followed by `:\n`
// File -> returns file name relative to the root followed by `\n`
//
// A buildpack author could provide their own implementation through
// WithDirectoryContentFormatter when calling Detect or Build.
//
// A custom implementation might log in color or might log additional
// information about each file, like permissions. The implementation can
// also control line endings to force all of the files to be logged on a
// single line, or as multiple lines.
type DirectoryContentFormatter interface {
// File takes the full path and os.FileInfo and returns a display string
File(path string, info os.FileInfo) (string, error)
// RootPath provides the root path being iterated
RootPath(path string)
// Title provides a plain string title which can be embellished
Title(title string) string
}

View File

@ -1,102 +0,0 @@
/*
* 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 log
import (
"fmt"
"io"
"os"
"strings"
)
//go:generate mockery --name Logger --case=underscore
// Logger is the interface implement by a type that wishes to write log messages generated by libcnb
type Logger interface {
// Debug formats using the default formats for its operands
Debug(a ...interface{})
// Debugf formats according to a format specifier
Debugf(format string, a ...interface{})
// DebugWriter returns the configured debug writer
DebugWriter() io.Writer
// IsDebugEnabled indicates whether debug logging is enabled
IsDebugEnabled() bool
}
// PlainLogger implements Logger and logs messages to a writer.
type PlainLogger struct {
debug io.Writer
}
// New creates a new instance of PlainLogger. It configures debug logging if $BP_DEBUG or $BP_LOG_LEVEL are set.
func New(debug io.Writer) PlainLogger {
if strings.ToLower(os.Getenv("BP_LOG_LEVEL")) == "debug" || os.Getenv("BP_DEBUG") != "" {
return PlainLogger{debug: debug}
}
return PlainLogger{}
}
// NewDiscard creates a new instance of PlainLogger that discards all log messages. Useful in testing.
func NewDiscard() PlainLogger {
return PlainLogger{debug: io.Discard}
}
// Debug formats using the default formats for its operands and writes to the configured debug writer. Spaces are added
// between operands when neither is a string.
func (l PlainLogger) Debug(a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
s := fmt.Sprint(a...)
if !strings.HasSuffix(s, "\n") {
s += "\n"
}
_, _ = fmt.Fprint(l.debug, s)
}
// Debugf formats according to a format specifier and writes to the configured debug writer.
func (l PlainLogger) Debugf(format string, a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
_, _ = fmt.Fprintf(l.debug, format, a...)
}
// DebugWriter returns the configured debug writer.
func (l PlainLogger) DebugWriter() io.Writer {
if l.IsDebugEnabled() {
return l.debug
}
return io.Discard
}
// IsDebugEnabled indicates whether debug logging is enabled.
func (l PlainLogger) IsDebugEnabled() bool {
return l.debug != nil
}

View File

@ -1,79 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package mocks
import (
fs "io/fs"
mock "github.com/stretchr/testify/mock"
)
// DirectoryContentFormatter is an autogenerated mock type for the DirectoryContentFormatter type
type DirectoryContentFormatter struct {
mock.Mock
}
// File provides a mock function with given fields: path, info
func (_m *DirectoryContentFormatter) File(path string, info fs.FileInfo) (string, error) {
ret := _m.Called(path, info)
if len(ret) == 0 {
panic("no return value specified for File")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string, fs.FileInfo) (string, error)); ok {
return rf(path, info)
}
if rf, ok := ret.Get(0).(func(string, fs.FileInfo) string); ok {
r0 = rf(path, info)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string, fs.FileInfo) error); ok {
r1 = rf(path, info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RootPath provides a mock function with given fields: path
func (_m *DirectoryContentFormatter) RootPath(path string) {
_m.Called(path)
}
// Title provides a mock function with given fields: title
func (_m *DirectoryContentFormatter) Title(title string) string {
ret := _m.Called(title)
if len(ret) == 0 {
panic("no return value specified for Title")
}
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(title)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// NewDirectoryContentFormatter creates a new instance of DirectoryContentFormatter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewDirectoryContentFormatter(t interface {
mock.TestingT
Cleanup(func())
}) *DirectoryContentFormatter {
mock := &DirectoryContentFormatter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,81 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package mocks
import (
io "io"
mock "github.com/stretchr/testify/mock"
)
// Logger is an autogenerated mock type for the Logger type
type Logger struct {
mock.Mock
}
// Debug provides a mock function with given fields: a
func (_m *Logger) Debug(a ...interface{}) {
var _ca []interface{}
_ca = append(_ca, a...)
_m.Called(_ca...)
}
// DebugWriter provides a mock function with given fields:
func (_m *Logger) DebugWriter() io.Writer {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for DebugWriter")
}
var r0 io.Writer
if rf, ok := ret.Get(0).(func() io.Writer); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(io.Writer)
}
}
return r0
}
// Debugf provides a mock function with given fields: format, a
func (_m *Logger) Debugf(format string, a ...interface{}) {
var _ca []interface{}
_ca = append(_ca, format)
_ca = append(_ca, a...)
_m.Called(_ca...)
}
// IsDebugEnabled provides a mock function with given fields:
func (_m *Logger) IsDebugEnabled() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsDebugEnabled")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// NewLogger creates a new instance of Logger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewLogger(t interface {
mock.TestingT
Cleanup(func())
}) *Logger {
mock := &Logger{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

37
main.go
View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2023 the original author or authors.
* 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.
@ -18,38 +18,37 @@ package libcnb
import (
"fmt"
"os"
"path/filepath"
"github.com/buildpacks/libcnb/internal"
)
func main(detect DetectFunc, build BuildFunc, generate GenerateFunc, options ...Option) {
config := NewConfig(options...)
// Main is called by the main function of a buildpack, encapsulating both detection and build in the same binary.
func Main(detector Detector, builder Builder, options ...Option) {
config := Config{
arguments: os.Args,
environmentWriter: internal.EnvironmentWriter{},
exitHandler: internal.NewExitHandler(),
tomlWriter: internal.TOMLWriter{},
}
for _, option := range options {
config = option(config)
}
if len(config.arguments) == 0 {
config.exitHandler.Error(fmt.Errorf("expected command name"))
return
}
config.extension = build == nil && generate != nil
switch c := filepath.Base(config.arguments[0]); c {
case "build":
Build(build, config)
Build(builder, options...)
case "detect":
Detect(detect, config)
case "generate":
Generate(generate, config)
Detect(detector, options...)
default:
config.exitHandler.Error(fmt.Errorf("unsupported command %s", c))
return
}
}
// BuildpackMain is called by the main function of a buildpack, encapsulating both detection and build in the same binary.
func BuildpackMain(detect DetectFunc, build BuildFunc, options ...Option) {
main(detect, build, nil, options...)
}
// ExtensionMain is called by the main function of a extension, encapsulating both detection and generation in the same binary.
func ExtensionMain(detect DetectFunc, generate GenerateFunc, options ...Option) {
main(detect, nil, generate, options...)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2023 the original author or authors.
* 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.
@ -17,6 +17,7 @@
package libcnb_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -25,23 +26,22 @@ import (
"github.com/sclevine/spec"
"github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/v2/mocks"
"github.com/buildpacks/libcnb"
"github.com/buildpacks/libcnb/mocks"
)
func testMain(t *testing.T, _ spec.G, it spec.S) {
func testMain(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
applicationPath string
buildFunc libcnb.BuildFunc
builder *mocks.Builder
buildpackPath string
buildpackPlanPath string
detectFunc libcnb.DetectFunc
buildPlanPath string
detector *mocks.Detector
environmentWriter *mocks.EnvironmentWriter
exitHandler *mocks.ExitHandler
generateFunc libcnb.GenerateFunc
layersPath string
platformPath string
tomlWriter *mocks.TOMLWriter
@ -52,22 +52,20 @@ func testMain(t *testing.T, _ spec.G, it spec.S) {
it.Before(func() {
var err error
applicationPath, err = os.MkdirTemp("", "main-application-path")
applicationPath, err = ioutil.TempDir("", "main-application-path")
Expect(err).NotTo(HaveOccurred())
applicationPath, err = filepath.EvalSymlinks(applicationPath)
Expect(err).NotTo(HaveOccurred())
buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) {
return libcnb.NewBuildResult(), nil
}
builder = &mocks.Builder{}
buildpackPath, err = os.MkdirTemp("", "main-buildpack-path")
buildpackPath, err = ioutil.TempDir("", "main-buildpack-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed())
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
Expect(ioutil.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.8"
api = "0.6"
[buildpack]
id = "test-id"
@ -83,6 +81,7 @@ optional = true
[[stacks]]
id = "test-id"
mixins = ["test-name"]
[metadata]
test-key = "test-value"
@ -90,12 +89,12 @@ test-key = "test-value"
0600),
).To(Succeed())
f, err := os.CreateTemp("", "main-buildpackplan-path")
f, err := ioutil.TempFile("", "main-buildpackplan-path")
Expect(err).NotTo(HaveOccurred())
Expect(f.Close()).NotTo(HaveOccurred())
buildpackPlanPath = f.Name()
Expect(os.WriteFile(buildpackPlanPath,
Expect(ioutil.WriteFile(buildpackPlanPath,
[]byte(`
[[entries]]
name = "test-name"
@ -107,13 +106,12 @@ test-key = "test-value"
0600),
).To(Succeed())
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{}, nil
}
f, err = ioutil.TempFile("", "main-buildplan-path")
Expect(err).NotTo(HaveOccurred())
Expect(f.Close()).NotTo(HaveOccurred())
buildPlanPath = f.Name()
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.GenerateResult{}, nil
}
detector = &mocks.Detector{}
environmentWriter = &mocks.EnvironmentWriter{}
environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
@ -123,10 +121,10 @@ test-key = "test-value"
exitHandler.On("Pass", mock.Anything)
exitHandler.On("Fail", mock.Anything)
layersPath, err = os.MkdirTemp("", "main-layers-path")
layersPath, err = ioutil.TempDir("", "main-layers-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"),
Expect(ioutil.WriteFile(filepath.Join(layersPath, "store.toml"),
[]byte(`
[metadata]
test-key = "test-value"
@ -134,34 +132,30 @@ test-key = "test-value"
0600),
).To(Succeed())
platformPath, err = os.MkdirTemp("", "main-platform-path")
platformPath, err = ioutil.TempDir("", "main-platform-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha", "metadata"), 0755)).To(Succeed())
Expect(os.WriteFile(
Expect(ioutil.WriteFile(
filepath.Join(platformPath, "bindings", "alpha", "metadata", "test-metadata-key"),
[]byte("test-metadata-value"),
0600,
)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha", "secret"), 0755)).To(Succeed())
Expect(os.WriteFile(
Expect(ioutil.WriteFile(
filepath.Join(platformPath, "bindings", "alpha", "secret", "test-secret-key"),
[]byte("test-secret-value"),
0600,
)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
Expect(ioutil.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
To(Succeed())
tomlWriter = &mocks.TOMLWriter{}
tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed())
Expect(os.Setenv("CNB_LAYERS_DIR", layersPath)).To(Succeed())
Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed())
Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed())
Expect(os.Setenv("CNB_BUILD_PLAN_PATH", buildpackPlanPath)).To(Succeed())
workingDir, err = os.Getwd()
Expect(err).NotTo(HaveOccurred())
@ -172,10 +166,6 @@ test-key = "test-value"
Expect(os.Chdir(workingDir)).To(Succeed())
Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed())
Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed())
Expect(os.RemoveAll(applicationPath)).To(Succeed())
Expect(os.RemoveAll(buildpackPath)).To(Succeed())
@ -185,78 +175,55 @@ test-key = "test-value"
})
it("encounters the wrong number of arguments", func() {
libcnb.BuildpackMain(detectFunc, buildFunc,
libcnb.Main(detector, builder,
libcnb.WithArguments([]string{}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected command name"))
})
it("calls builder for build command", func() {
builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
commandPath := filepath.Join("bin", "build")
libcnb.BuildpackMain(detectFunc, buildFunc,
libcnb.WithArguments([]string{commandPath}),
libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
Expect(exitHandler.Calls).To(BeEmpty())
})
it("calls detector for detect command", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{Pass: true}, nil
}
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
commandPath := filepath.Join("bin", "detect")
libcnb.BuildpackMain(detectFunc, buildFunc,
libcnb.WithArguments([]string{commandPath}),
libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
})
it("calls generator for generate command", func() {
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.GenerateResult{}, nil
}
commandPath := filepath.Join("bin", "generate")
libcnb.ExtensionMain(nil, generateFunc,
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
})
it("calls exitHandler.Pass() on detection pass", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{Pass: true}, nil
}
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
commandPath := filepath.Join("bin", "detect")
libcnb.BuildpackMain(detectFunc, buildFunc,
libcnb.WithArguments([]string{commandPath}),
libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Pass"))
})
it("calls exitHandler.Fail() on detection fail", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) {
return libcnb.DetectResult{Pass: false}, nil
}
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: false}, nil)
commandPath := filepath.Join("bin", "detect")
libcnb.BuildpackMain(detectFunc, buildFunc,
libcnb.WithArguments([]string{commandPath}),
libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Fail"))
@ -265,10 +232,9 @@ test-key = "test-value"
it("encounters an unknown command", func() {
commandPath := filepath.Join("bin", "test-command")
libcnb.BuildpackMain(detectFunc, buildFunc,
libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unsupported command test-command"))

35
mocks/builder.go Normal file
View File

@ -0,0 +1,35 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
libcnb "github.com/buildpacks/libcnb"
)
// Builder is an autogenerated mock type for the Builder type
type Builder struct {
mock.Mock
}
// Build provides a mock function with given fields: context
func (_m *Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
ret := _m.Called(context)
var r0 libcnb.BuildResult
if rf, ok := ret.Get(0).(func(libcnb.BuildContext) libcnb.BuildResult); ok {
r0 = rf(context)
} else {
r0 = ret.Get(0).(libcnb.BuildResult)
}
var r1 error
if rf, ok := ret.Get(1).(func(libcnb.BuildContext) error); ok {
r1 = rf(context)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

35
mocks/detector.go Normal file
View File

@ -0,0 +1,35 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
libcnb "github.com/buildpacks/libcnb"
)
// Detector is an autogenerated mock type for the Detector type
type Detector struct {
mock.Mock
}
// Detect provides a mock function with given fields: context
func (_m *Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) {
ret := _m.Called(context)
var r0 libcnb.DetectResult
if rf, ok := ret.Get(0).(func(libcnb.DetectContext) libcnb.DetectResult); ok {
r0 = rf(context)
} else {
r0 = ret.Get(0).(libcnb.DetectResult)
}
var r1 error
if rf, ok := ret.Get(1).(func(libcnb.DetectContext) error); ok {
r1 = rf(context)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
@ -13,10 +13,6 @@ type EnvironmentWriter struct {
func (_m *EnvironmentWriter) Write(dir string, environment map[string]string) error {
ret := _m.Called(dir, environment)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok {
r0 = rf(dir, environment)
@ -26,17 +22,3 @@ func (_m *EnvironmentWriter) Write(dir string, environment map[string]string) er
return r0
}
// NewEnvironmentWriter creates a new instance of EnvironmentWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewEnvironmentWriter(t interface {
mock.TestingT
Cleanup(func())
}) *EnvironmentWriter {
mock := &EnvironmentWriter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,54 +0,0 @@
// Code generated by mockery v2.43.2. 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()
if len(ret) == 0 {
panic("no return value specified for Execute")
}
var r0 map[string]string
var r1 error
if rf, ok := ret.Get(0).(func() (map[string]string, error)); ok {
return rf()
}
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)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewExecD creates a new instance of ExecD. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewExecD(t interface {
mock.TestingT
Cleanup(func())
}) *ExecD {
mock := &ExecD{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,42 +0,0 @@
// Code generated by mockery v2.43.2. 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)
if len(ret) == 0 {
panic("no return value specified for Write")
}
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
}
// NewExecDWriter creates a new instance of ExecDWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewExecDWriter(t interface {
mock.TestingT
Cleanup(func())
}) *ExecDWriter {
mock := &ExecDWriter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
@ -23,17 +23,3 @@ func (_m *ExitHandler) Fail() {
func (_m *ExitHandler) Pass() {
_m.Called()
}
// NewExitHandler creates a new instance of ExitHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewExitHandler(t interface {
mock.TestingT
Cleanup(func())
}) *ExitHandler {
mock := &ExitHandler{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,49 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
libcnb "github.com/buildpacks/libcnb"
)
// LayerContributor is an autogenerated mock type for the LayerContributor type
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)
var r0 libcnb.Layer
if rf, ok := ret.Get(0).(func(libcnb.Layer) libcnb.Layer); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(libcnb.Layer)
}
var r1 error
if rf, ok := ret.Get(1).(func(libcnb.Layer) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Name provides a mock function with given fields:
func (_m *LayerContributor) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
@ -13,10 +13,6 @@ type TOMLWriter struct {
func (_m *TOMLWriter) Write(path string, value interface{}) error {
ret := _m.Called(path, value)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
r0 = rf(path, value)
@ -26,17 +22,3 @@ func (_m *TOMLWriter) Write(path string, value interface{}) error {
return r0
}
// NewTOMLWriter creates a new instance of TOMLWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewTOMLWriter(t interface {
mock.TestingT
Cleanup(func())
}) *TOMLWriter {
mock := &TOMLWriter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018-2023 the original author or authors.
* 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.
@ -17,19 +17,19 @@
package libcnb
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/internal"
)
const (
// BindingKind is the metadata key for a binding's kind.
BindingKind = "kind"
// BindingProvider is the key for a binding's provider.
BindingProvider = "provider"
@ -41,52 +41,11 @@ const (
// See the Service Binding Specification for Kubernetes for more details - https://k8s-service-bindings.github.io/spec/
EnvServiceBindings = "SERVICE_BINDING_ROOT"
// EnvBuildpackDirectory is the name of the environment variable that contains the path to the buildpack
EnvBuildpackDirectory = "CNB_BUILDPACK_DIR"
// EnvExtensionDirectory is the name of the environment variable that contains the path to the extension
EnvExtensionDirectory = "CNB_EXTENSION_DIR"
// EnvVcapServices is the name of the environment variable that contains the bindings in cloudfoundry
EnvVcapServices = "VCAP_SERVICES"
// EnvLayersDirectory is the name of the environment variable that contains the root path to all buildpack layers
EnvLayersDirectory = "CNB_LAYERS_DIR"
// EnvOutputDirectory is the name of the environment variable that contains the path to the output directory
EnvOutputDirectory = "CNB_OUTPUT_DIR"
// EnvPlatformDirectory is the name of the environment variable that contains the path to the platform directory
EnvPlatformDirectory = "CNB_PLATFORM_DIR"
// EnvDetectBuildPlanPath is the name of the environment variable that contains the path to the build plan
EnvDetectPlanPath = "CNB_BUILD_PLAN_PATH"
// EnvBuildPlanPath is the name of the environment variable that contains the path to the build plan
EnvBuildPlanPath = "CNB_BP_PLAN_PATH"
// Deprecated: EnvStackID is the name of the environment variable that contains the stack id
EnvStackID = "CNB_STACK_ID"
// EnvTargetOS contains the name of the os
EnvTargetOS = "CNB_TARGET_OS"
// EnvTargetArch contains the architecture
EnvTargetArch = "CNB_TARGET_ARCH"
// EnvTargetOS contains the variant of the architecture
EnvTargetArchVariant = "CNB_TARGET_ARCH_VARIANT"
// EnvTargetDistroName contains the name of the ditro
EnvTargetDistroName = "CNB_TARGET_DISTRO_NAME"
// EnvTargetDistroVersion contains the version of the distro
EnvTargetDistroVersion = "CNB_TARGET_DISTRO_VERSION"
// DefaultPlatformBindingsLocation is the typical location for bindings, which exists under the platform directory
//
// Not guaranteed to exist, but often does. This should only be used as a fallback if EnvServiceBindings and EnvPlatformDirectory are not set
DefaultPlatformBindingsLocation = "/platform/bindings"
// EnvCNBBindings is the name of the environment variable that contains the path to the CNB bindings directory.
// See the CNB bindings extension spec for more details - https://github.com/buildpacks/spec/blob/main/extensions/bindings.md
// Deprecated: Use the Service Binding Specification for Kubernetes instead -
// https://github.com/buildpacks/rfcs/blob/main/text/0055-deprecate-service-bindings.md.
EnvCNBBindings = "CNB_BINDINGS"
)
// Binding is a projection of metadata about an external entity to be bound to.
@ -118,7 +77,7 @@ func NewBinding(name string, path string, secret map[string]string) Binding {
for k, v := range secret {
switch k {
case BindingType:
case BindingType, BindingKind: // TODO: Remove as CNB_BINDINGS ages out
b.Type = strings.TrimSpace(v)
case BindingProvider:
b.Provider = strings.TrimSpace(v)
@ -137,6 +96,19 @@ func NewBindingFromPath(path string) (Binding, error) {
return Binding{}, fmt.Errorf("unable to create new config map from %s\n%w", path, err)
}
// TODO: Remove as CNB_BINDINGS ages out
for _, d := range []string{"metadata", "secret"} {
file := filepath.Join(path, d)
cm, err := internal.NewConfigMapFromPath(file)
if err != nil {
return Binding{}, fmt.Errorf("unable to create new config map from %s\n%w", file, err)
}
for k, v := range cm {
secret[k] = v
}
}
return NewBinding(filepath.Base(path), path, secret), nil
}
@ -157,30 +129,52 @@ func (b Binding) SecretFilePath(name string) (string, bool) {
return "", false
}
// TODO: Remove as CNB_BINDINGS ages out
for _, d := range []string{"metadata", "secret"} {
file := filepath.Join(b.Path, d, name)
if _, err := os.Stat(file); err == nil {
return file, true
}
}
return filepath.Join(b.Path, name), true
}
// Bindings is a collection of bindings keyed by their name.
type Bindings []Binding
// NewBindingsFromEnvironment creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT
// or $CNB_BINDINGS if it does not exist. If neither is defined, returns an empty collection of Bindings.
// Note - This API is deprecated. Please use NewBindingsForLaunch instead.
func NewBindingsFromEnvironment() (Bindings, error) {
return NewBindingsForLaunch()
}
// NewBindingsForLaunch creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT
// or $CNB_BINDINGS if it does not exist. If neither is defined, returns an empty collection of Bindings.
func NewBindingsForLaunch() (Bindings, error) {
if path, ok := os.LookupEnv(EnvServiceBindings); ok {
return NewBindingsFromPath(path)
}
// TODO: Remove as CNB_BINDINGS ages out
if path, ok := os.LookupEnv(EnvCNBBindings); ok {
return NewBindingsFromPath(path)
}
return Bindings{}, nil
}
// NewBindingsFromPath creates a new instance from all the bindings at a given path.
func NewBindingsFromPath(path string) (Bindings, error) {
files, err := os.ReadDir(path)
if err != nil && errors.Is(err, fs.ErrNotExist) {
return Bindings{}, nil
} else if err != nil {
return nil, fmt.Errorf("unable to list directory %s\n%w", path, err)
files, err := filepath.Glob(filepath.Join(path, "*"))
if err != nil {
return nil, fmt.Errorf("unable to glob %s\n%w", path, err)
}
bindings := Bindings{}
for _, file := range files {
bindingPath := filepath.Join(path, file.Name())
if strings.HasPrefix(filepath.Base(bindingPath), ".") {
// ignore hidden files
continue
}
binding, err := NewBindingFromPath(bindingPath)
binding, err := NewBindingFromPath(file)
if err != nil {
return nil, fmt.Errorf("unable to create new binding from %s\n%w", file, err)
}
@ -191,73 +185,17 @@ func NewBindingsFromPath(path string) (Bindings, error) {
return bindings, nil
}
type vcapServicesBinding struct {
Name string `json:"name"`
Label string `json:"label"`
Credentials map[string]interface{} `json:"credentials"`
}
func toJSONString(input interface{}) (string, error) {
switch in := input.(type) {
case string:
return in, nil
default:
jsonProperty, err := json.Marshal(in)
if err != nil {
return "", err
}
return string(jsonProperty), nil
}
}
// NewBindingsFromVcapServicesEnv creates a new instance from all the bindings given from the VCAP_SERVICES.
func NewBindingsFromVcapServicesEnv(content string) (Bindings, error) {
var contentTyped map[string][]vcapServicesBinding
err := json.Unmarshal([]byte(content), &contentTyped)
if err != nil {
return Bindings{}, err
}
bindings := Bindings{}
for p, bArray := range contentTyped {
for _, b := range bArray {
secret := map[string]string{}
for k, v := range b.Credentials {
secret[k], err = toJSONString(v)
if err != nil {
return nil, err
}
}
bindings = append(bindings, Binding{
Name: b.Name,
Type: b.Label,
Provider: p,
Secret: secret,
})
}
}
return bindings, nil
}
// NewBindings creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT.
// If that isn't defined, bindings are read from <platform>/bindings.
// If that isn't defined, bindings are read from $VCAP_SERVICES.
// If that isn't defined, the specified platform path will be used
func NewBindings(platformDir string) (Bindings, error) {
// NewBindingsForBuild creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT
// or $CNB_BINDINGS if it does not exist. If neither is defined, bindings are read from <platform>/bindings, the default
// path defined in the CNB Binding extension specification.
func NewBindingsForBuild(platformDir string) (Bindings, error) {
if path, ok := os.LookupEnv(EnvServiceBindings); ok {
return NewBindingsFromPath(path)
}
if path, ok := os.LookupEnv(EnvPlatformDirectory); ok {
return NewBindingsFromPath(filepath.Join(path, "bindings"))
// TODO: Remove as CNB_BINDINGS ages out
if path, ok := os.LookupEnv(EnvCNBBindings); ok {
return NewBindingsFromPath(path)
}
if content, ok := os.LookupEnv(EnvVcapServices); ok {
return NewBindingsFromVcapServicesEnv(content)
}
return NewBindingsFromPath(filepath.Join(platformDir, "bindings"))
}

View File

@ -18,6 +18,7 @@ package libcnb_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -25,7 +26,7 @@ import (
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb"
)
func testPlatform(t *testing.T, context spec.G, it spec.S) {
@ -37,7 +38,7 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
it.Before(func() {
var err error
platformPath, err = os.MkdirTemp("", "platform")
platformPath, err = ioutil.TempDir("", "platform")
path = filepath.Join(platformPath, "bindings")
Expect(err).NotTo(HaveOccurred())
})
@ -46,74 +47,164 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
Expect(os.RemoveAll(path)).To(Succeed())
})
context("Cloudfoundry VCAP_SERVICES", func() {
it("creates a bindings from VCAP_SERVICES", func() {
content, err := os.ReadFile("testdata/vcap_services.json")
Expect(err).NotTo(HaveOccurred())
t.Setenv(libcnb.EnvVcapServices, string(content))
context("CNB Bindings", func() {
it.Before(func() {
Expect(os.MkdirAll(filepath.Join(path, "alpha", "metadata"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "metadata", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "metadata", "test-metadata-key"), []byte("test-metadata-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "metadata", "test-metadata-key-trimmed"), []byte(" test-metadata-value-trimmed \n"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, "alpha", "secret"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "secret", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "secret", "test-secret-key-trimmed"), []byte(" test-secret-value-trimmed \n"), 0600)).To(Succeed())
bindings, err := libcnb.NewBindings("")
Expect(err).NotTo(HaveOccurred())
Expect(bindings).To(ConsistOf(libcnb.Bindings{
{
Name: "elephantsql-binding-c6c60",
Type: "elephantsql-type",
Provider: "elephantsql-provider",
Secret: map[string]string{
"bool": "true",
"int": "1",
"uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser",
},
},
{
Name: "mysendgrid",
Type: "sendgrid-type",
Provider: "sendgrid-provider",
Secret: map[string]string{
"username": "QvsXMbJ3rK",
"password": "HCHMOYluTv",
"hostname": "smtp.example.com",
},
},
{
Name: "postgres",
Type: "postgres",
Provider: "postgres",
Secret: map[string]string{
"urls": "{\"example\":\"http://example.com\"}",
"username": "foo",
"password": "bar",
},
},
}))
Expect(os.MkdirAll(filepath.Join(path, "bravo", "metadata"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "metadata", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "metadata", "test-metadata-key"), []byte("test-metadata-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "metadata", "test-metadata-key-trimmed"), []byte(" test-metadata-value-trimmed \n"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, "bravo", "secret"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "secret", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "secret", "test-secret-key-trimmed"), []byte(" test-secret-value-trimmed \n"), 0600)).To(Succeed())
})
it("creates empty bindings from empty VCAP_SERVICES", func() {
t.Setenv(libcnb.EnvVcapServices, "{}")
context("Binding", func() {
it("creates an empty binding", func() {
Expect(libcnb.NewBinding("test-name", "test-path", map[string]string{
libcnb.BindingKind: "test-kind",
libcnb.BindingProvider: "test-provider",
"test-key": "test-value",
})).To(Equal(libcnb.Binding{
Name: "test-name",
Path: "test-path",
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{"test-key": "test-value"},
}))
})
bindings, err := libcnb.NewBindings("")
Expect(err).NotTo(HaveOccurred())
it("creates a binding from a path", func() {
path := filepath.Join(path, "alpha")
Expect(bindings).To(HaveLen(0))
binding, err := libcnb.NewBindingFromPath(path)
Expect(binding, err).To(Equal(libcnb.Binding{
Name: filepath.Base(path),
Path: path,
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
}))
metadataFilePath, ok := binding.SecretFilePath("test-metadata-key")
Expect(ok).To(BeTrue())
Expect(metadataFilePath).To(Equal(filepath.Join(path, "metadata", "test-metadata-key")))
secretFilePath, ok := binding.SecretFilePath("test-secret-key")
Expect(ok).To(BeTrue())
Expect(secretFilePath).To(Equal(filepath.Join(path, "secret", "test-secret-key")))
})
it("sanitizes secrets", func() {
path := filepath.Join(path, "alpha")
b, err := libcnb.NewBindingFromPath(path)
Expect(err).NotTo(HaveOccurred())
Expect(b.String()).To(Equal(fmt.Sprintf("{Name: alpha Path: %s Type: test-kind Provider: test-provider Secret: [test-metadata-key test-metadata-key-trimmed test-secret-key test-secret-key-trimmed]}", path)))
})
})
context("Bindings", func() {
it("creates a bindings from a path", func() {
Expect(libcnb.NewBindingsFromPath(path)).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
}))
})
it("returns empty bindings if environment variable is not set", func() {
Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{}))
})
context("from environment", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvCNBBindings, path))
})
it.After(func() {
Expect(os.Unsetenv(libcnb.EnvCNBBindings))
})
it("creates bindings from path in $CNB_BINDINGS", func() {
Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
}))
})
})
})
})
context("Kubernetes Service Bindings", func() {
it.Before(func() {
Expect(os.MkdirAll(filepath.Join(path, "alpha"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "type"), []byte("test-type"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "type"), []byte("test-type"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "alpha", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, "bravo"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "type"), []byte("test-type"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, ".hidden", "metadata"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, ".hidden", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, ".hiddenFile"), []byte("test-kind"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "type"), []byte("test-type"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "bravo", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
})
context("Binding", func() {
@ -180,25 +271,21 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
}))
})
it("creates an empty binding if path does not exist", func() {
Expect(libcnb.NewBindingsFromPath("/path/doesnt/exist")).To(Equal(libcnb.Bindings{}))
})
it("returns empty bindings if SERVICE_BINDING_ROOT and CNB_PLATFORM_DIR are not set and /platform/bindings does not exist", func() {
Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{}))
it("returns empty bindings if environment variable is not set", func() {
Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{}))
})
context("from environment", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvServiceBindings, path))
})
it.After(func() {
Expect(os.Unsetenv(libcnb.EnvServiceBindings))
Expect(os.Unsetenv("CNB_PLATFORM_DIR"))
})
it("creates bindings from path in SERVICE_BINDING_ROOT if both set", func() {
Expect(os.Setenv(libcnb.EnvServiceBindings, path))
Expect(os.Setenv("CNB_PLATFORM_DIR", "/does/not/exist"))
Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{
it("creates bindings from path in SERVICE_BINDING_ROOT", func() {
Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
@ -215,26 +302,94 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
},
}))
})
})
it("creates bindings from path in SERVICE_BINDING_ROOT if SERVICE_BINDING_ROOT not set", func() {
Expect(os.Setenv("CNB_PLATFORM_DIR", filepath.Dir(path)))
context("from environment or path", func() {
context("when SERVICE_BINDING_ROOT is defined but CNB_BINDINGS or the passed path does not exist", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvServiceBindings, path))
Expect(os.Setenv(libcnb.EnvCNBBindings, "does not exist"))
})
Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
it.After(func() {
Expect(os.Unsetenv(libcnb.EnvServiceBindings))
Expect(os.Unsetenv(libcnb.EnvCNBBindings))
})
it("creates bindings from path in SERVICE_BINDING_ROOT", func() {
Expect(libcnb.NewBindingsForBuild("random-path-that-does-not-exist")).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
})
})
context("when CNB_BINDINGS is defined but the path does not exist", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvCNBBindings, path))
})
it.After(func() {
Expect(os.Unsetenv(libcnb.EnvCNBBindings))
})
it("creates bindings from path in CNB_BINDINGS", func() {
Expect(libcnb.NewBindingsForBuild("random-path-that-does-not-exist")).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
})
})
context("when SERVICE_BINDING_ROOT and CNB_BINDINGS is not defined but the path exists", func() {
it("creates bindings from the given path", func() {
Expect(libcnb.NewBindingsForBuild(platformPath)).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
})
})
context("when no valid binding variable is set", func() {
it("returns an an empty binding", func() {
Expect(libcnb.NewBindingsForBuild("does-not-exist")).To(Equal(libcnb.Bindings{}))
})
})
})
})

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package log_test
package poet_test
import (
"testing"
@ -24,7 +24,7 @@ import (
)
func TestUnit(t *testing.T) {
suite := spec.New("libcnb/log", spec.Report(report.Terminal{}))
suite("PlainLogger", testLogger)
suite := spec.New("libcnb/poet", spec.Report(report.Terminal{}))
suite("Logger", testLogger)
suite.Run(t)
}

141
poet/logger.go Normal file
View File

@ -0,0 +1,141 @@
/*
* 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 poet
import (
"fmt"
"io"
"os"
"strings"
)
// Logger logs messages to a writer.
type Logger struct {
debug io.Writer
info io.Writer
}
// Option is a function that configures a Logger.
type Option func(Logger) Logger
// WithDebug configures the debug Writer.
func WithDebug(writer io.Writer) Option {
return func(logger Logger) Logger {
logger.debug = writer
return logger
}
}
// NewLoggerWithOptions create a new instance of Logger. It configures the Logger with options.
func NewLoggerWithOptions(writer io.Writer, options ...Option) Logger {
l := Logger{
info: writer,
}
for _, option := range options {
l = option(l)
}
return l
}
// NewLogger creates a new instance of Logger. It configures debug logging if $BP_DEBUG is set.
func NewLogger(writer io.Writer) Logger {
var options []Option
if _, ok := os.LookupEnv("BP_DEBUG"); ok {
options = append(options, WithDebug(writer))
}
return NewLoggerWithOptions(writer, options...)
}
// Debug formats using the default formats for its operands and writes to the configured debug writer. Spaces are added
// between operands when neither is a string.
func (l Logger) Debug(a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
l.print(l.debug, a...)
}
// Debugf formats according to a format specifier and writes to the configured debug writer.
func (l Logger) Debugf(format string, a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
l.printf(l.debug, format, a...)
}
// DebugWriter returns the configured debug writer.
func (l Logger) DebugWriter() io.Writer {
return l.debug
}
// IsDebugEnabled indicates whether debug logging is enabled.
func (l Logger) IsDebugEnabled() bool {
return l.debug != nil
}
// Info formats using the default formats for its operands and writes to the configured info writer. Spaces are added
// between operands when neither is a string.
func (l Logger) Info(a ...interface{}) {
if !l.IsInfoEnabled() {
return
}
l.print(l.info, a...)
}
// Infof formats according to a format specifier and writes to the configured info writer.
func (l Logger) Infof(format string, a ...interface{}) {
if !l.IsInfoEnabled() {
return
}
l.printf(l.info, format, a...)
}
// InfoWriter returns the configured info writer.
func (l Logger) InfoWriter() io.Writer {
return l.info
}
// IsInfoEnabled indicates whether info logging is enabled.
func (l Logger) IsInfoEnabled() bool {
return l.info != nil
}
func (Logger) print(writer io.Writer, a ...interface{}) {
s := fmt.Sprint(a...)
if !strings.HasSuffix(s, "\n") {
s += "\n"
}
_, _ = fmt.Fprint(writer, s)
}
func (Logger) printf(writer io.Writer, format string, a ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
_, _ = fmt.Fprintf(writer, format, a...)
}

View File

@ -14,18 +14,17 @@
* limitations under the License.
*/
package log_test
package poet_test
import (
"bytes"
"io"
"os"
"testing"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/poet"
)
func testLogger(t *testing.T, context spec.G, it spec.S) {
@ -33,7 +32,7 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
Expect = NewWithT(t).Expect
b *bytes.Buffer
l log.PlainLogger
l poet.Logger
)
it.Before(func() {
@ -42,26 +41,18 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
context("without BP_DEBUG", func() {
it.Before(func() {
l = log.New(b)
l = poet.NewLogger(b)
})
it("does not configure debug", func() {
Expect(l.IsDebugEnabled()).To(BeFalse())
})
it("does not return nil debug writer", func() {
Expect(l.DebugWriter()).To(Not(BeNil()))
})
it("does not return non-discard writer", func() {
Expect(l.DebugWriter()).To(Equal(io.Discard))
})
})
context("with BP_DEBUG", func() {
it.Before(func() {
Expect(os.Setenv("BP_DEBUG", "")).To(Succeed())
l = log.New(b)
l = poet.NewLogger(b)
})
it.After(func() {
@ -69,33 +60,55 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
})
it("configures debug", func() {
Expect(l.IsDebugEnabled()).To(BeFalse())
Expect(l.IsDebugEnabled()).To(BeTrue())
})
})
context("with BP_LOG_LEVEL set to DEBUG", func() {
context("with debug disabled", func() {
it.Before(func() {
Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed())
l = log.New(b)
l = poet.NewLoggerWithOptions(b)
})
it.After(func() {
Expect(os.Unsetenv("BP_LOG_LEVEL")).To(Succeed())
it("does not write debug log", func() {
l.Debug("test-message")
Expect(b.String()).To(Equal(""))
})
it("configures debug", func() {
Expect(l.IsDebugEnabled()).To(BeTrue())
it("does not write debugf log", func() {
l.Debugf("test-%s", "message")
Expect(b.String()).To(Equal(""))
})
it("does not return debug writer", func() {
Expect(l.DebugWriter()).To(BeNil())
})
it("indicates that debug is not enabled", func() {
Expect(l.IsDebugEnabled()).To(BeFalse())
})
it("writes info log", func() {
l.Info("test-message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("writes infof log", func() {
l.Infof("test-%s", "message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("returns info writer", func() {
Expect(l.InfoWriter()).NotTo(BeNil())
})
it("indicates that info is enabled", func() {
Expect(l.IsInfoEnabled()).To(BeTrue())
})
})
context("with debug enabled", func() {
it.Before(func() {
Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed())
l = log.New(b)
})
it.After(func() {
Expect(os.Unsetenv("BP_LOG_LEVEL")).To(Succeed())
l = poet.NewLoggerWithOptions(b, poet.WithDebug(b))
})
it("writes debug log", func() {
@ -108,14 +121,30 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
Expect(b.String()).To(Equal("test-message\n"))
})
it("writes debug directly", func() {
_, err := l.DebugWriter().Write([]byte("test-message\n"))
Expect(err).NotTo(HaveOccurred())
Expect(b.String()).To(Equal("test-message\n"))
it("returns debug writer", func() {
Expect(l.DebugWriter()).NotTo(BeNil())
})
it("indicates that debug is enabled", func() {
Expect(l.IsDebugEnabled()).To(BeTrue())
})
it("writes info log", func() {
l.Info("test-message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("writes infof log", func() {
l.Infof("test-%s", "message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("returns info writer", func() {
Expect(l.InfoWriter()).NotTo(BeNil())
})
it("indicates that info is enabled", func() {
Expect(l.IsInfoEnabled()).To(BeTrue())
})
})
}

View File

@ -1,36 +0,0 @@
/*
* 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 libcnb
// Process represents metadata about a type of command that can be run.
type Process struct {
// Type is the type of the process.
Type string `toml:"type"`
// Command is the command of the process.
Command []string `toml:"command"`
// Arguments are arguments to the command.
Arguments []string `toml:"args"`
// WorkingDirectory is a directory to execute the command in, removes the need to use a shell environment to CD into working directory
WorkingDirectory string `toml:"working-dir,omitempty"`
// Default can be set to true to indicate that the process
// type being defined should be the default process type for the app image.
Default bool `toml:"default,omitempty"`
}

View File

@ -1,24 +0,0 @@
/*
* 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 libcnb
// Slice represents metadata about a slice.
type Slice struct {
// Paths are the contents of the slice.
Paths []string `toml:"paths"`
}

View File

@ -1,24 +0,0 @@
/*
* 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 libcnb
// Store represents the contents of store.toml
type Store struct {
// Metadata represents the persistent metadata.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -1,68 +0,0 @@
{
"elephantsql-provider": [
{
"name": "elephantsql-binding-c6c60",
"binding_guid": "44ceb72f-100b-4f50-87a2-7809c8b42b8d",
"binding_name": "elephantsql-binding-c6c60",
"instance_guid": "391308e8-8586-4c42-b464-c7831aa2ad22",
"instance_name": "elephantsql-c6c60",
"label": "elephantsql-type",
"tags": [
"postgres",
"postgresql",
"relational"
],
"plan": "turtle",
"credentials": {
"uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser",
"int": 1,
"bool": true
},
"syslog_drain_url": null,
"volume_mounts": []
}
],
"sendgrid-provider": [
{
"name": "mysendgrid",
"binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081",
"binding_name": null,
"instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d",
"instance_name": "mysendgrid",
"label": "sendgrid-type",
"tags": [
"smtp"
],
"plan": "free",
"credentials": {
"hostname": "smtp.example.com",
"username": "QvsXMbJ3rK",
"password": "HCHMOYluTv"
},
"syslog_drain_url": null,
"volume_mounts": []
}
],
"postgres": [
{
"name": "postgres",
"label": "postgres",
"plan": "default",
"tags": [
"postgres"
],
"binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081",
"binding_name": null,
"instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d",
"credentials": {
"username": "foo",
"password": "bar",
"urls": {
"example": "http://example.com"
}
},
"syslog_drain_url": null,
"volume_mounts": []
}
]
}

View File

@ -1,190 +1,7 @@
module github.com/buildpacks/libcnb/tools/v2
module github.com/buildpacks/libcnb/tools
go 1.22.1
go 1.15
toolchain go1.23.2
require golang.org/x/tools v0.1.4
require golang.org/x/tools v0.26.0
require github.com/golangci/golangci-lint v1.61.0
require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect
github.com/4meepo/tagalign v1.3.4 // indirect
github.com/Abirdcfly/dupword v0.1.1 // indirect
github.com/Antonboom/errname v0.1.13 // indirect
github.com/Antonboom/nilnil v0.1.9 // indirect
github.com/Antonboom/testifylint v1.4.3 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/Crocmagnon/fatcontext v0.5.2 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.1 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.4.1 // indirect
github.com/breml/bidichk v0.2.7 // indirect
github.com/breml/errchkjson v0.3.6 // indirect
github.com/butuzov/ireturn v0.3.0 // indirect
github.com/butuzov/mirror v1.2.0 // indirect
github.com/catenacyber/perfsprint v0.7.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.2.0 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.13.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.6 // indirect
github.com/go-critic/go-critic v0.11.4 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect
github.com/golangci/misspell v0.6.0 // indirect
github.com/golangci/modinfo v0.3.4 // indirect
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/revgrep v0.5.3 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jjti/go-spancheck v0.6.2 // indirect
github.com/julz/importas v0.1.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect
github.com/kisielk/errcheck v1.7.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.5 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.10 // indirect
github.com/kyoh86/exportloopref v0.1.11 // indirect
github.com/lasiar/canonicalheader v1.1.1 // indirect
github.com/ldez/gomoddirectives v0.2.4 // indirect
github.com/ldez/tagliatelle v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lufeee/execinquery v1.2.1 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgechev/revive v1.3.9 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.16.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.6.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/ryancurrah/gomodguard v1.3.5 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect
github.com/securego/gosec/v2 v2.21.2 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/tenv v1.10.0 // indirect
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tdakkota/asciicheck v0.2.0 // indirect
github.com/tetafro/godot v1.4.17 // indirect
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.1.1 // indirect
github.com/uudashr/gocognit v1.1.3 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.12.2 // indirect
go-simpler.org/sloglint v0.7.2 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.5.1 // indirect
mvdan.cc/gofumpt v0.7.0 // indirect
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
)
require github.com/golangci/golangci-lint v1.41.1

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
//go:build tools
// +build tools
package tools