Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f4bbe82
WIP - adding flag to set the CNB_EXEC_ENV
jjbustamante Jan 26, 2025
f0a88d8
adding . and - to the regular expression to validate and cnn exec env
jjbustamante Jan 28, 2025
05a35c9
WIP - adding exec-env on project.toml schema verion 0.3, it is still …
jjbustamante Jan 29, 2025
29124cc
fixing some lint errors
jjbustamante Jan 29, 2025
6c4ac6e
WIP - refacting test case
jjbustamante Jan 30, 2025
52fdc9c
adding more test cases
jjbustamante Jan 31, 2025
75be829
adding exec-env to builder.toml
jjbustamante Jan 31, 2025
2c71fad
adding exec-env to builder.toml
jjbustamante Jan 31, 2025
716a1f8
Adding unit test for exec-env in buildpack.toml
jjbustamante Jan 31, 2025
452250d
Removing for now the platform API version validation
jjbustamante Feb 3, 2025
865625e
Fixing unit tests
jjbustamante Feb 3, 2025
dad1243
fixing some unit tests
jjbustamante Feb 3, 2025
d0088d5
Merge branch 'main' into jjbustamante/execution-environment-poc
jjbustamante Feb 12, 2025
e3e6e59
Merge branch 'main' into jjbustamante/execution-environment-poc
jjbustamante Mar 5, 2025
49a7a50
Merge branch 'main' into jjbustamante/execution-environment-poc
jjbustamante Apr 7, 2025
67f4855
Merge branch 'main' into jjbustamante/execution-environment-poc
jjbustamante Jun 3, 2025
153edfb
Fixing formatting issue
jjbustamante Jun 3, 2025
0285c59
Merge branch 'main' into jjbustamante/execution-environment-poc
jjbustamante Oct 26, 2025
ff26375
Test Coverage Improvements Summary
jjbustamante Oct 26, 2025
b4262d9
Merge branch 'main' into jjbustamante/execution-environment-poc
jjbustamante Nov 23, 2025
b808c42
Fixing Acceptance Tests API Platform expected versions
jjbustamante Nov 23, 2025
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
2 changes: 1 addition & 1 deletion acceptance/testdata/pack_fixtures/report_output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Pack:

Default Lifecycle Version: 0.20.11

Supported Platform APIs: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13
Supported Platform APIs: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15

Config:
default-builder-image = "{{ .DefaultBuilder }}"
Expand Down
9 changes: 5 additions & 4 deletions builder/config_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ const (
)

type BuildConfigEnv struct {
Name string `toml:"name"`
Value string `toml:"value"`
Suffix Suffix `toml:"suffix,omitempty"`
Delim string `toml:"delim,omitempty"`
Name string `toml:"name"`
Value string `toml:"value"`
Suffix Suffix `toml:"suffix,omitempty"`
Delim string `toml:"delim,omitempty"`
ExecEnv []string `toml:"exec-env,omitempty"`
}

// ReadConfig reads a builder configuration from the file path provided and returns the
Expand Down
18 changes: 18 additions & 0 deletions builder/config_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ func testConfig(t *testing.T, when spec.G, it spec.S) {
[[order]]
[[order.group]]
id = "buildpack/1"
exec-env = ["production"]

[[build.env]]
name = "key1"
value = "value1"
suffix = "append"
delim = "%"
exec-env = ["test"]

`), 0666))
})

Expand All @@ -76,6 +85,15 @@ func testConfig(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, builderConfig.Buildpacks[2].ImageName, "")

h.AssertEq(t, builderConfig.Order[0].Group[0].ID, "buildpack/1")
h.AssertTrue(t, len(builderConfig.Order[0].Group[0].ExecEnv) == 1)
h.AssertEq(t, builderConfig.Order[0].Group[0].ExecEnv[0], "production")

h.AssertTrue(t, len(builderConfig.Build.Env) == 1)
h.AssertEq(t, builderConfig.Build.Env[0].Name, "key1")
h.AssertEq(t, builderConfig.Build.Env[0].Value, "value1")
h.AssertEq(t, string(builderConfig.Build.Env[0].Suffix), "append")
h.AssertTrue(t, len(builderConfig.Build.Env[0].ExecEnv) == 1)
h.AssertEq(t, builderConfig.Build.Env[0].ExecEnv[0], "test")
})
})

Expand Down
54 changes: 54 additions & 0 deletions buildpackage/config_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,39 @@ func testBuildpackageConfigReader(t *testing.T, when spec.G, it spec.S) {
h.AssertError(t, err, "missing 'buildpack.uri' configuration")
})
})

when("#ReadBuildpackDescriptor", func() {
var tmpDir string

it.Before(func() {
var err error
tmpDir, err = os.MkdirTemp("", "buildpack-descriptor-test")
h.AssertNil(t, err)
})

it.After(func() {
_ = os.RemoveAll(tmpDir)
})

it("returns exec-env when a composite buildpack toml file is provided", func() {
buildPackTomlFilePath := filepath.Join(tmpDir, "buildpack-1.toml")

err := os.WriteFile(buildPackTomlFilePath, []byte(validCompositeBuildPackTomlWithExecEnv), os.ModePerm)
h.AssertNil(t, err)

packageConfigReader := buildpackage.NewConfigReader()

buildpackDescriptor, err := packageConfigReader.ReadBuildpackDescriptor(buildPackTomlFilePath)
h.AssertNil(t, err)

h.AssertTrue(t, len(buildpackDescriptor.Order()) == 1)
h.AssertTrue(t, len(buildpackDescriptor.Order()[0].Group) == 2)
h.AssertTrue(t, len(buildpackDescriptor.Order()[0].Group[0].ExecEnv) == 1)
h.AssertTrue(t, len(buildpackDescriptor.Order()[0].Group[1].ExecEnv) == 1)
h.AssertEq(t, buildpackDescriptor.Order()[0].Group[0].ExecEnv[0], "production.1")
h.AssertEq(t, buildpackDescriptor.Order()[0].Group[1].ExecEnv[0], "production.2")
})
})
}

const validPackageToml = `
Expand Down Expand Up @@ -306,3 +339,24 @@ const missingBuildpackPackageToml = `
[[dependencies]]
uri = "bp/b"
`

const validCompositeBuildPackTomlWithExecEnv = `
api = "0.15"

[buildpack]
id = "samples/hello-universe"
version = "0.0.1"
name = "Hello Universe Buildpack"

# Order used for detection
[[order]]
[[order.group]]
id = "samples/hello-world"
version = "0.0.1"
exec-env = ["production.1"]

[[order.group]]
id = "samples/hello-moon"
version = "0.0.1"
exec-env = ["production.2"]
`
7 changes: 7 additions & 0 deletions internal/build/fakes/fake_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,10 @@ func WithEnableUsernsHost() func(*build.LifecycleOptions) {
opts.EnableUsernsHost = true
}
}

// WithExecutionEnvironment creates a LifecycleOptions option that sets the execution environment
func WithExecutionEnvironment(execEnv string) func(*build.LifecycleOptions) {
return func(opts *build.LifecycleOptions) {
opts.ExecutionEnvironment = execEnv
}
}
3 changes: 3 additions & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var (
api.MustParse("0.11"),
api.MustParse("0.12"),
api.MustParse("0.13"),
api.MustParse("0.14"),
api.MustParse("0.15"),
}
)

Expand Down Expand Up @@ -87,6 +89,7 @@ type LifecycleOptions struct {
Termui Termui
DockerHost string
Cache cache.CacheOpts
ExecutionEnvironment string
CacheImage string
HTTPProxy string
HTTPSProxy string
Expand Down
2 changes: 2 additions & 0 deletions internal/build/phase_config_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
linuxContainerAdmin = "root"
windowsContainerAdmin = "ContainerAdministrator"
platformAPIEnvVar = "CNB_PLATFORM_API"
executionEnvVar = "CNB_EXEC_ENV"
)

type PhaseConfigProviderOperation func(*PhaseConfigProvider)
Expand Down Expand Up @@ -59,6 +60,7 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops

ops = append(ops,
WithEnv(fmt.Sprintf("%s=%s", platformAPIEnvVar, lifecycleExec.platformAPI.String())),
If(lifecycleExec.platformAPI.AtLeast("0.15"), WithEnv(fmt.Sprintf("%s=%s", executionEnvVar, lifecycleExec.opts.ExecutionEnvironment))),
WithLifecycleProxy(lifecycleExec),
WithBinds([]string{
fmt.Sprintf("%s:%s", lifecycleExec.layersVolume, lifecycleExec.mountPaths.layersDir()),
Expand Down
41 changes: 41 additions & 0 deletions internal/build/phase_config_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

ifakes "github.com/buildpacks/imgutil/fakes"
"github.com/buildpacks/lifecycle/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
Expand Down Expand Up @@ -204,6 +205,46 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) {
})
})

when("execution environment is set", func() {
when("platform API >= 0.15", func() {
it("sets CNB_EXEC_ENV environment variable", func() {
expectedBuilderImage := ifakes.NewImage("some-builder-name", "", nil)
fakeBuilder, err := fakes.NewFakeBuilder(
fakes.WithImage(expectedBuilderImage),
fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.15")}),
)
h.AssertNil(t, err)
lifecycle := newTestLifecycleExec(t, false, "some-temp-dir",
fakes.WithBuilder(fakeBuilder),
fakes.WithExecutionEnvironment("test"),
)

phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle)

h.AssertSliceContains(t, phaseConfigProvider.ContainerConfig().Env, "CNB_EXEC_ENV=test")
})
})

when("platform API < 0.15", func() {
it("does not set CNB_EXEC_ENV environment variable", func() {
expectedBuilderImage := ifakes.NewImage("some-builder-name", "", nil)
fakeBuilder, err := fakes.NewFakeBuilder(
fakes.WithImage(expectedBuilderImage),
fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.14")}),
)
h.AssertNil(t, err)
lifecycle := newTestLifecycleExec(t, false, "some-temp-dir",
fakes.WithBuilder(fakeBuilder),
fakes.WithExecutionEnvironment("test"),
)

phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle)

h.AssertSliceNotContains(t, phaseConfigProvider.ContainerConfig().Env, "CNB_EXEC_ENV=test")
})
})
})

when("called with WithImage", func() {
it("sets the image on the config", func() {
lifecycle := newTestLifecycleExec(t, false, "some-temp-dir")
Expand Down
12 changes: 12 additions & 0 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -38,6 +39,7 @@ type BuildFlags struct {
Cache cache.CacheOpts
AppPath string
Builder string
ExecutionEnv string
Registry string
RunImage string
Platform string
Expand Down Expand Up @@ -220,6 +222,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
PreviousInputImage: inputPreviousImage,
LayoutRepoDir: cfg.LayoutRepositoryDir,
},
CNBExecutionEnv: flags.ExecutionEnv,
InsecureRegistries: flags.InsecureRegistries,
}); err != nil {
return errors.Wrap(err, "failed to build")
Expand Down Expand Up @@ -284,6 +287,7 @@ This option may set DOCKER_HOST environment variable for the build container if
cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`)
cmd.Flags().StringVar(&buildFlags.Platform, "platform", "", `Platform to build on (e.g., "linux/amd64").`)
cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`)
cmd.Flags().StringVar(&buildFlags.ExecutionEnv, "exec-env", "production", `Execution environment to use. (default "production"`)
cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name")
cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)")
cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to.\nTags should be in the format 'image:tag' or 'repository/image:tag'."+stringSliceHelp("tag"))
Expand Down Expand Up @@ -347,6 +351,14 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef clie
inputImageRef.Name(), inputImageRef.Name())
}

if flags.ExecutionEnv != "" && flags.ExecutionEnv != "production" && flags.ExecutionEnv != "test" {
// RFC: the / character is reserved in case we need to introduce namespacing in the future.
var executionEnvRegex = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`)
if ok := executionEnvRegex.MatchString(flags.ExecutionEnv); !ok {
return errors.New("exec-env MUST only contain numbers, letters, and the characters: . or -")
}
}

return nil
}

Expand Down
89 changes: 89 additions & 0 deletions internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,86 @@ builder = "my-builder"
})
})

when("--exec-env", func() {
when("is not provided", func() {
it("set 'production' as default value", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("production")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder"})
h.AssertNil(t, command.Execute())
})
})

when("is provided", func() {
when("contains valid characters", func() {
it("forwards the exec-value (only letters) into the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("something")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "something"})
h.AssertNil(t, command.Execute())
})

it("forwards the exec-value (only numbers) into the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("1234")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "1234"})
h.AssertNil(t, command.Execute())
})

it("forwards the exec-value (mix letters and numbers) into the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("env1")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env1"})
h.AssertNil(t, command.Execute())
})

it("forwards the exec-value (mix letters, numbers and .) into the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("env1.1")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env1.1"})
h.AssertNil(t, command.Execute())
})

it("forwards the exec-value (mix letters, numbers and -) into the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("env-1")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env-1"})
h.AssertNil(t, command.Execute())
})

it("forwards the exec-value (mix letters, numbers, . and -) into the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithExecEnv("env-1.1")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "env-1.1"})
h.AssertNil(t, command.Execute())
})
})

when("contains invalid characters", func() {
it("errors with a descriptive message", func() {
command.SetArgs([]string{"image", "--builder", "my-builder", "--exec-env", "$production"})
err := command.Execute()
h.AssertNotNil(t, err)
h.AssertError(t, err, "exec-env MUST only contain numbers, letters, and the characters: . or -")
})
})
})
})

when("--insecure-registry is provided", func() {
it("sets one insecure registry", func() {
mockClient.EXPECT().
Expand Down Expand Up @@ -1268,6 +1348,15 @@ func EqBuildOptionsWithLayoutConfig(image, previousImage string, sparse bool, la
}
}

func EqBuildOptionsWithExecEnv(s string) interface{} {
return buildOptionsMatcher{
description: fmt.Sprintf("exec-env=%s", s),
equals: func(o client.BuildOptions) bool {
return o.CNBExecutionEnv == s
},
}
}

func EqBuildOptionsWithInsecureRegistries(insecureRegistries []string) gomock.Matcher {
return buildOptionsMatcher{
description: fmt.Sprintf("Insercure Registries=%s", insecureRegistries),
Expand Down
4 changes: 4 additions & 0 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type BuildOptions struct {
// e.g. tcp://example.com:1234, unix:///run/user/1000/podman/podman.sock
DockerHost string

// the target environment the OCI image is expected to be run in, i.e. production, test, development.
CNBExecutionEnv string

// Used to determine a run-image mirror if Run Image is empty.
// Used in combination with Builder metadata to determine to the 'best' mirror.
// 'best' is defined as:
Expand Down Expand Up @@ -667,6 +670,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
Layout: opts.Layout(),
Keychain: c.keychain,
EnableUsernsHost: opts.EnableUsernsHost,
ExecutionEnvironment: opts.CNBExecutionEnv,
InsecureRegistries: opts.InsecureRegistries,
}

Expand Down
1 change: 1 addition & 0 deletions pkg/dist/buildmodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ModuleInfo struct {
Description string `toml:"description,omitempty" json:"description,omitempty" yaml:"description,omitempty"`
Homepage string `toml:"homepage,omitempty" json:"homepage,omitempty" yaml:"homepage,omitempty"`
Keywords []string `toml:"keywords,omitempty" json:"keywords,omitempty" yaml:"keywords,omitempty"`
ExecEnv []string `toml:"exec-env,omitempty" json:"exec-env,omitempty" yaml:"exec-env,omitempty"`
Licenses []License `toml:"licenses,omitempty" json:"licenses,omitempty" yaml:"licenses,omitempty"`
ClearEnv bool `toml:"clear-env,omitempty" json:"clear-env,omitempty" yaml:"clear-env,omitempty"`
}
Expand Down
Loading
Loading