Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ var (
// Platform is a pair of lists of Platform API versions:
// 1. All supported versions (including deprecated versions)
// 2. The versions that are deprecated
Platform = newApisMustParse([]string{"0.7", "0.8", "0.9", "0.10", "0.11", "0.12", "0.13", "0.14"}, []string{})
Platform = newApisMustParse([]string{"0.7", "0.8", "0.9", "0.10", "0.11", "0.12", "0.13", "0.14", "0.15"}, []string{})
// Buildpack is a pair of lists of Buildpack API versions:
// 1. All supported versions (including deprecated versions)
// 2. The versions that are deprecated
Buildpack = newApisMustParse([]string{"0.7", "0.8", "0.9", "0.10", "0.11"}, []string{})
Buildpack = newApisMustParse([]string{"0.7", "0.8", "0.9", "0.10", "0.11", "0.12"}, []string{})
)

type APIs struct {
Expand Down
6 changes: 5 additions & 1 deletion buildpack/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
// EnvLayersDir is the absolute path of the buildpack layers directory (read-write); a different copy is provided for each buildpack;
// contents may be saved to either or both of: the final output image or the cache
EnvLayersDir = "CNB_LAYERS_DIR"
// Also provided during build: EnvBuildpackDir, EnvPlatformDir (see detect.go)
// Also provided during build: EnvBuildpackDir, EnvPlatformDir, EnvExecEnv (see detect.go)
)

type BuildInputs struct {
Expand All @@ -35,6 +35,7 @@ type BuildInputs struct {
PlatformDir string
Env BuildEnv
TargetEnv []string
ExecEnv string
Out, Err io.Writer
Plan Plan
}
Expand Down Expand Up @@ -154,6 +155,9 @@ func runBuildCmd(d BpDescriptor, bpLayersDir, planPath string, inputs BuildInput
if api.MustParse(d.API()).AtLeast("0.10") {
cmd.Env = append(cmd.Env, inputs.TargetEnv...)
}
if api.MustParse(d.API()).AtLeast("0.12") && inputs.ExecEnv != "" {
cmd.Env = append(cmd.Env, "CNB_EXEC_ENV="+inputs.ExecEnv)
}

if err = cmd.Run(); err != nil {
return NewError(err, ErrTypeBuildpack)
Expand Down
77 changes: 77 additions & 0 deletions buildpack/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,57 @@ func testBuild(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, isUnset(actual), true)
})

when("CNB_EXEC_ENV", func() {
when("buildpack API >= 0.12", func() {
it.Before(func() {
descriptor.WithAPI = "0.12"
})

it("sets CNB_EXEC_ENV when ExecEnv is provided", func() {
inputs.ExecEnv = "test"

if _, err := executor.Build(descriptor, inputs, logger); err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}

actual := h.Rdfile(t, filepath.Join(appDir, "build-env-cnb-exec-env-A-v1"))
h.AssertEq(t, actual, "test")
})

it("does not set CNB_EXEC_ENV when ExecEnv is empty", func() {
inputs.ExecEnv = ""

if _, err := executor.Build(descriptor, inputs, logger); err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}

actual := h.Rdfile(t, filepath.Join(appDir, "build-env-cnb-exec-env-A-v1"))
if !isUnset(actual) {
t.Fatalf("expected CNB_EXEC_ENV to be unset, but was: %s", actual)
}
})
})

when("buildpack API < 0.12", func() {
it.Before(func() {
descriptor.WithAPI = "0.11"
})

it("does not set CNB_EXEC_ENV even when ExecEnv is provided", func() {
inputs.ExecEnv = "test"

if _, err := executor.Build(descriptor, inputs, logger); err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}

actual := h.Rdfile(t, filepath.Join(appDir, "build-env-cnb-exec-env-A-v1"))
if !isUnset(actual) {
t.Fatalf("expected CNB_EXEC_ENV to be unset for API < 0.12, but was: %s", actual)
}
})
})
})

it("loads env vars from <platform>/env", func() {
h.Mkfile(t, "some-data",
filepath.Join(platformDir, "env", "SOME_VAR"),
Expand Down Expand Up @@ -843,6 +894,32 @@ func testBuild(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].WorkingDirectory, "/working-directory")
})

it("sets the execution environment", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = ["some-cmd"]`+"\n"+
`exec-env = ["development", "test"]`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := executor.Build(descriptor, inputs, logger)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
h.AssertEq(t, br.Processes[0].ExecEnv, []string{"development", "test"})
})

it("handles empty execution environment", func() {
h.Mkfile(t,
"[[processes]]\n"+
`command = ["some-cmd"]`,
filepath.Join(appDir, "launch-A-v1.toml"),
)
br, err := executor.Build(descriptor, inputs, logger)
h.AssertNil(t, err)
h.AssertEq(t, len(br.Processes), 1)
var expectedExecEnv []string
h.AssertEq(t, br.Processes[0].ExecEnv, expectedExecEnv)
})
})

when("slices", func() {
Expand Down
11 changes: 6 additions & 5 deletions buildpack/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ type Descriptor interface {
// BaseInfo is information shared by both buildpacks and extensions.
// For buildpacks it winds up under the toml `buildpack` key along with SBOM info, but extensions have no SBOMs.
type BaseInfo struct {
ClearEnv bool `toml:"clear-env,omitempty"`
Homepage string `toml:"homepage,omitempty"`
ID string `toml:"id"`
Name string `toml:"name"`
Version string `toml:"version"`
ClearEnv bool `toml:"clear-env,omitempty"`
Homepage string `toml:"homepage,omitempty"`
ID string `toml:"id"`
Name string `toml:"name"`
Version string `toml:"version"`
ExecEnv []string `toml:"exec-env,omitempty"`
}
6 changes: 6 additions & 0 deletions buildpack/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const (
EnvExtensionDir = "CNB_EXTENSION_DIR"
// EnvPlatformDir is the absolute path of the platform directory (read-only); a single copy is provided for all buildpacks
EnvPlatformDir = "CNB_PLATFORM_DIR"
// EnvExecEnv is the target execution environment. Standard values include "production", "test", and "development".
EnvExecEnv = "CNB_EXEC_ENV"
)

type DetectInputs struct {
Expand All @@ -31,6 +33,7 @@ type DetectInputs struct {
PlatformDir string
Env BuildEnv
TargetEnv []string
ExecEnv string
}

type DetectOutputs struct {
Expand Down Expand Up @@ -182,6 +185,9 @@ func runDetect(d detectable, inputs DetectInputs, planPath string, envRootDirKey
if api.MustParse(d.API()).AtLeast("0.10") {
cmd.Env = append(cmd.Env, inputs.TargetEnv...)
}
if api.MustParse(d.API()).AtLeast("0.12") && inputs.ExecEnv != "" {
cmd.Env = append(cmd.Env, EnvExecEnv+"="+inputs.ExecEnv)
}

if err := cmd.Run(); err != nil {
if err, ok := err.(*exec.ExitError); ok {
Expand Down
48 changes: 48 additions & 0 deletions buildpack/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,54 @@ func testDetect(t *testing.T, when spec.G, it spec.S) {
}
})

when("CNB_EXEC_ENV", func() {
when("buildpack API >= 0.12", func() {
it.Before(func() {
descriptor.WithAPI = "0.12"
})

it("sets CNB_EXEC_ENV when ExecEnv is provided", func() {
mockEnv.EXPECT().WithOverrides(platformDir, buildConfigDir).Return(append(os.Environ(), someEnv), nil)
inputs.ExecEnv = "test"

executor.Detect(descriptor, inputs, logger)

actual := rdappfile("detect-env-cnb-exec-env-A-v1")
h.AssertEq(t, actual, "test")
})

it("does not set CNB_EXEC_ENV when ExecEnv is empty", func() {
mockEnv.EXPECT().WithOverrides(platformDir, buildConfigDir).Return(append(os.Environ(), someEnv), nil)
inputs.ExecEnv = ""

executor.Detect(descriptor, inputs, logger)

actual := rdappfile("detect-env-cnb-exec-env-A-v1")
if !isUnset(actual) {
t.Fatalf("expected CNB_EXEC_ENV to be unset, but was: %s", actual)
}
})
})

when("buildpack API < 0.12", func() {
it.Before(func() {
descriptor.WithAPI = "0.11"
})

it("does not set CNB_EXEC_ENV even when ExecEnv is provided", func() {
mockEnv.EXPECT().WithOverrides(platformDir, buildConfigDir).Return(append(os.Environ(), someEnv), nil)
inputs.ExecEnv = "test"

executor.Detect(descriptor, inputs, logger)

actual := rdappfile("detect-env-cnb-exec-env-A-v1")
if !isUnset(actual) {
t.Fatalf("expected CNB_EXEC_ENV to be unset for API < 0.12, but was: %s", actual)
}
})
})
})

it("errors when <platform>/env cannot be loaded", func() {
mockEnv.EXPECT().WithOverrides(platformDir, buildConfigDir).Return(nil, errors.New("some error"))

Expand Down
2 changes: 2 additions & 0 deletions buildpack/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type ProcessEntry struct {
Direct *bool `toml:"direct" json:"direct"`
Default bool `toml:"default,omitempty" json:"default,omitempty"`
WorkingDirectory string `toml:"working-dir,omitempty" json:"working-dir,omitempty"`
ExecEnv []string `toml:"exec-env,omitempty" json:"exec-env,omitempty"`
}

// DecodeLaunchTOML reads a launch.toml file
Expand Down Expand Up @@ -91,6 +92,7 @@ func (p *ProcessEntry) ToLaunchProcess(bpID string) launch.Process {
Default: p.Default,
BuildpackID: bpID,
WorkingDirectory: p.WorkingDirectory,
ExecEnv: p.ExecEnv,
}
}

Expand Down
1 change: 1 addition & 0 deletions buildpack/testdata/buildpack/bin/build
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ echo -n "${CNB_BUILDPACK_DIR:-unset}" > "build-env-cnb-buildpack-dir-${bp_id}-${
echo -n "${CNB_LAYERS_DIR:-unset}" > "build-env-cnb-layers-dir-${bp_id}-${bp_version}"
echo -n "${CNB_OUTPUT_DIR:-unset}" > "build-env-cnb-output-dir-${bp_id}-${bp_version}"
echo -n "${CNB_PLATFORM_DIR:-unset}" > "build-env-cnb-platform-dir-${bp_id}-${bp_version}"
echo -n "${CNB_EXEC_ENV:-unset}" > "build-env-cnb-exec-env-${bp_id}-${bp_version}"

cp -a "$platform_dir/env" "build-env-${bp_id}-${bp_version}"

Expand Down
1 change: 1 addition & 0 deletions buildpack/testdata/buildpack/bin/detect
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ echo -n "$ENV_TYPE" > "detect-env-type-${bp_id}-${bp_version}"
echo -n "${CNB_BUILDPACK_DIR:-unset}" > "detect-env-cnb-buildpack-dir-${bp_id}-${bp_version}"
echo -n "${CNB_BUILD_PLAN_PATH:-unset}" > "detect-env-cnb-build-plan-path-${bp_id}-${bp_version}"
echo -n "${CNB_PLATFORM_DIR:-unset}" > "detect-env-cnb-platform-dir-${bp_id}-${bp_version}"
echo -n "${CNB_EXEC_ENV:-unset}" > "detect-env-cnb-exec-env-${bp_id}-${bp_version}"

if [[ -f detect-plan-${bp_id}-${bp_version}.toml ]]; then
cat "detect-plan-${bp_id}-${bp_version}.toml" > "$plan_path"
Expand Down
1 change: 1 addition & 0 deletions cmd/launcher/cli/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func RunLaunch() error {
DefaultProcessType: defaultProcessType,
LayersDir: cmd.EnvOrDefault(platform.EnvLayersDir, platform.DefaultLayersDir),
AppDir: cmd.EnvOrDefault(platform.EnvAppDir, platform.DefaultAppDir),
ExecEnv: cmd.EnvOrDefault(platform.EnvExecEnv, platform.DefaultExecEnv),
PlatformAPI: p.API(),
Processes: md.Processes,
Buildpacks: md.Buildpacks,
Expand Down
1 change: 1 addition & 0 deletions cmd/lifecycle/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func (b *buildCmd) build(group buildpack.Group, plan files.Plan, analyzedMD file
BuildConfigDir: b.BuildConfigDir,
LayersDir: b.LayersDir,
PlatformDir: b.PlatformDir,
ExecEnv: b.ExecEnv,
BuildExecutor: &buildpack.DefaultBuildExecutor{},
DirStore: platform.NewDirStore(b.BuildpacksDir, ""),
Group: group,
Expand Down
1 change: 1 addition & 0 deletions cmd/lifecycle/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore phase.Cache, analyz
AdditionalNames: e.AdditionalTags,
AppDir: e.AppDir,
DefaultProcessType: e.DefaultProcessType,
ExecEnv: e.ExecEnv,
ExtendedDir: e.ExtendedDir,
LauncherConfig: launcherConfig(e.LauncherPath, e.LauncherSBOMDir),
LayersDir: e.LayersDir,
Expand Down
1 change: 1 addition & 0 deletions launch/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Process struct {
Default bool `toml:"default,omitempty" json:"default,omitempty"`
BuildpackID string `toml:"buildpack-id" json:"buildpackID"`
WorkingDirectory string `toml:"working-dir,omitempty" json:"working-dir,omitempty"`
ExecEnv []string `toml:"exec-env,omitempty" json:"exec-env,omitempty"`
PlatformAPI *api.Version `toml:"-" json:"-"`
}

Expand Down
1 change: 1 addition & 0 deletions launch/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Launcher struct {
Env Env
Exec ExecFunc
ExecD ExecD
ExecEnv string
Shell Shell
LayersDir string
PlatformAPI *api.Version
Expand Down
36 changes: 35 additions & 1 deletion launch/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,48 @@ func (l *Launcher) processForLegacy(cmd []string) (Process, error) {

func (l *Launcher) findProcessType(pType string) (Process, bool) {
for _, p := range l.Processes {
if p.Type == pType {
if p.Type == pType && l.isProcessEligibleForExecEnv(p) {
return p, true
}
}

return Process{}, false
}

// isProcessEligibleForExecEnv checks if a process is eligible for the current execution environment
// According to the spec:
// - A process is eligible if it has no exec-env specified, OR
// - Its exec-env includes the current execution environment, OR
// - Its exec-env includes the special value "*" which indicates compatibility with all environments
func (l *Launcher) isProcessEligibleForExecEnv(p Process) bool {
// Note: This is gated by Platform API 0.15, not Buildpack API
// The platform controls the Platform API version and determines when this is available
if l.PlatformAPI == nil || !l.PlatformAPI.AtLeast("0.15") {
return true // Skip execution environment filtering for older Platform APIs
}

// If no exec-env specified, process applies to all execution environments
if len(p.ExecEnv) == 0 {
return true
}

// Check if process supports all execution environments
for _, env := range p.ExecEnv {
if env == "*" {
return true
}
}

// Check if process supports the current execution environment
for _, env := range p.ExecEnv {
if env == l.ExecEnv {
return true
}
}

return false
}

func (l *Launcher) userProvidedProcess(cmd []string) (Process, error) {
if len(cmd) == 0 {
return Process{}, errors.New("when there is no default process a command is required")
Expand Down
Loading
Loading