Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of Concept for Execution Environment RFC #2324

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
9 changes: 5 additions & 4 deletions builder/config_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,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"]
`
1 change: 1 addition & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type LifecycleOptions struct {
Termui Termui
DockerHost string
Cache cache.CacheOpts
ExecutionEnvironment string
CacheImage string
HTTPProxy string
HTTPSProxy string
Expand Down
3 changes: 3 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 @@ -57,6 +58,8 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops

ops = append(ops,
WithEnv(fmt.Sprintf("%s=%s", platformAPIEnvVar, lifecycleExec.platformAPI.String())),
// TODO: replace the Platform API Version for the correct value
If(lifecycleExec.platformAPI.AtLeast("0.13"), WithEnv(fmt.Sprintf("%s=%s", executionEnvVar, lifecycleExec.opts.ExecutionEnvironment))),
WithLifecycleProxy(lifecycleExec),
WithBinds([]string{
fmt.Sprintf("%s:%s", lifecycleExec.layersVolume, lifecycleExec.mountPaths.layersDir()),
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 @@ -36,6 +37,7 @@ type BuildFlags struct {
Cache cache.CacheOpts
AppPath string
Builder string
ExecutionEnv string
Registry string
RunImage string
Platform string
Expand Down Expand Up @@ -215,6 +217,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
PreviousInputImage: inputPreviousImage,
LayoutRepoDir: cfg.LayoutRepositoryDir,
},
CNBExecutionEnv: flags.ExecutionEnv,
}); err != nil {
return errors.Wrap(err, "failed to build")
}
Expand Down Expand Up @@ -276,6 +279,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 @@ -338,6 +342,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 @@ -982,6 +982,86 @@ builder = "my-builder"
h.AssertError(t, err, "Exporting to OCI layout is currently experimental.")
})
})

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("export to OCI layout is expected", func() {
Expand Down Expand Up @@ -1243,6 +1323,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
},
}
}

type buildOptionsMatcher struct {
equals func(client.BuildOptions) bool
description string
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 @@ -643,6 +646,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
CreationTime: opts.CreationTime,
Layout: opts.Layout(),
Keychain: c.keychain,
ExecutionEnvironment: opts.CNBExecutionEnv,
}

switch {
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
3 changes: 3 additions & 0 deletions pkg/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"path/filepath"
"strings"

v03 "github.com/buildpacks/pack/pkg/project/v03"

"github.com/BurntSushi/toml"
"github.com/pkg/errors"

Expand All @@ -26,6 +28,7 @@ type VersionDescriptor struct {
var parsers = map[string]func(string) (types.Descriptor, toml.MetaData, error){
"0.1": v01.NewDescriptor,
"0.2": v02.NewDescriptor,
"0.3": v03.NewDescriptor,
}

func ReadProjectDescriptor(pathToFile string, logger logging.Logger) (types.Descriptor, error) {
Expand Down
Loading
Loading