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

Enable setting custom values during porter build. #1900

Merged
Merged
6 changes: 5 additions & 1 deletion cmd/porter/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func buildBundleBuildCommand(p *porter.Porter) *cobra.Command {
porter build --version 0.1.0
porter build --file path/to/porter.yaml
porter build --dir path/to/build/context
porter build --custom version=0.2.0 --custom myapp.version=0.1.2
`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.Validate(p)
Expand All @@ -84,9 +85,12 @@ func buildBundleBuildCommand(p *porter.Porter) *cobra.Command {
f.StringArrayVar(&opts.SSH, "ssh", nil,
"SSH agent socket or keys to expose to the build (format: default|<id>[=<socket>|<key>[,<key>]]). May be specified multiple times.")
f.StringArrayVar(&opts.Secrets, "secret", nil,
"Secret file to expose to the build (format: id=mysecret,src=/local/secret). May be specified multiple times.")
"Secret file to expose to the build (format: id=mysecret,src=/local/secret). Custom values are assessible as build arguments in the template Dockerfile and in the manifest using template variables. May be specified multiple times.")
f.BoolVar(&opts.NoCache, "no-cache", false,
"Do not use the Docker cache when building the bundle's invocation image.")
f.StringArrayVar(&opts.Customs, "custom", nil,
"Define an individual key-value pair for the custom section in the form of NAME=VALUE. Use dot notation to specify a nested custom field. May be specified multiple times.")
carolynvs marked this conversation as resolved.
Show resolved Hide resolved

// Allow configuring the --driver flag with build-driver, to avoid conflicts with other commands
cmd.Flag("driver").Annotations = map[string][]string{
"viper-key": {"build-driver"},
Expand Down
7 changes: 7 additions & 0 deletions docs/content/bundle/manifest/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ data.

```yaml
custom:
custom-config: "custom-value"
some-custom-config:
item: "value"
more-custom-config:
Expand All @@ -511,6 +512,12 @@ You can access custom data at runtime using the `bundle.custom.KEY.SUBKEY` templ
For example, `{{ bundle.custom.more-custom-config.enabled}}` allows you to
access nested values from the custom section.

Multiple custom values that were defined in the manifest can also be injected with new values during build time using the \--custom values tied to the `porter build` command. Currently only supports string values. You can use dot notation to specify a nested field:

```
porter build --custom custom-config=new-custom-value --custom some-custom-config.item=edited-value
```

See the [Custom Extensions](https://github.com/cnabio/cnab-spec/blob/master/101-bundle-json.md#custom-extensions)
section of the CNAB Specification for more details.

Expand Down
4 changes: 3 additions & 1 deletion docs/content/cli/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@ porter build [flags]
porter build --version 0.1.0
porter build --file path/to/porter.yaml
porter build --dir path/to/build/context
porter build --custom version=0.2.0 --custom myapp.version=0.1.2
```

### Options

```
--build-arg stringArray Set build arguments in the template Dockerfile (format: NAME=VALUE). May be specified multiple times.
--custom stringArray Define an individual key-value pair for the custom section in the form of NAME=VALUE. Use dot notation to specify a nested custom field. May be specified multiple times.
-d, --dir string Path to the build context directory where all bundle assets are located.
-f, --file porter.yaml Path to the Porter manifest. Defaults to porter.yaml in the current directory.
-h, --help help for build
--name string Override the bundle name
--no-cache Do not use the Docker cache when building the bundle's invocation image.
--no-lint Do not run the linter
--secret stringArray Secret file to expose to the build (format: id=mysecret,src=/local/secret). May be specified multiple times.
--secret stringArray Secret file to expose to the build (format: id=mysecret,src=/local/secret). Custom values are assessible as build arguments in the template Dockerfile and in the manifest using template variables. May be specified multiple times.
--ssh stringArray SSH agent socket or keys to expose to the build (format: default|<id>[=<socket>|<key>[,<key>]]). May be specified multiple times.
-v, --verbose Enable verbose logging
--version string Override the bundle version
Expand Down
4 changes: 3 additions & 1 deletion docs/content/cli/bundles_build.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@ porter bundles build [flags]
porter build --version 0.1.0
porter build --file path/to/porter.yaml
porter build --dir path/to/build/context
porter build --custom version=0.2.0 --custom myapp.version=0.1.2
```

### Options

```
--build-arg stringArray Set build arguments in the template Dockerfile (format: NAME=VALUE). May be specified multiple times.
--custom stringArray Define an individual key-value pair for the custom section in the form of NAME=VALUE. Use dot notation to specify a nested custom field. May be specified multiple times.
-d, --dir string Path to the build context directory where all bundle assets are located.
-f, --file porter.yaml Path to the Porter manifest. Defaults to porter.yaml in the current directory.
-h, --help help for build
--name string Override the bundle name
--no-cache Do not use the Docker cache when building the bundle's invocation image.
--no-lint Do not run the linter
--secret stringArray Secret file to expose to the build (format: id=mysecret,src=/local/secret). May be specified multiple times.
--secret stringArray Secret file to expose to the build (format: id=mysecret,src=/local/secret). Custom values are assessible as build arguments in the template Dockerfile and in the manifest using template variables. May be specified multiple times.
--ssh stringArray SSH agent socket or keys to expose to the build (format: default|<id>[=<socket>|<key>[,<key>]]). May be specified multiple times.
-v, --verbose Enable verbose logging
--version string Override the bundle version
Expand Down
38 changes: 38 additions & 0 deletions pkg/build/buildkit/buildx.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ func (b *Builder) BuildInvocationImage(ctx context.Context, manifest *manifest.M
parseBuildArgs(opts.BuildArgs, args)
args["BUNDLE_DIR"] = build.BUNDLE_DIR

convertedCustomInput := make(map[string]string)
convertedCustomInput, err = flattenMap(manifest.Custom)
if err != nil {
return err
}

for k, v := range convertedCustomInput {
args[strings.ToUpper(strings.Replace(k, ".", "_", -1))] = v
}

buildxOpts := map[string]buildx.Options{
"default": {
Tags: []string{manifest.Image},
Expand Down Expand Up @@ -198,3 +208,31 @@ func (b *Builder) TagInvocationImage(ctx context.Context, origTag, newTag string
}
return nil
}

// flattenMap recursively walks through nested map and flattent it
// to one-level map of key-value with string type.
func flattenMap(mapInput map[string]interface{}) (map[string]string, error) {
out := make(map[string]string)

for key, value := range mapInput {
switch v := value.(type) {
case string:
out[key] = v
case map[string]interface{}:
tmp, err := flattenMap(v)
if err != nil {
return nil, err
}
for innerKey, innerValue := range tmp {
out[key+"."+innerKey] = innerValue
}
case map[string]string:
for innerKey, innerValue := range v {
out[key+"."+innerKey] = innerValue
}
default:
return nil, errors.Errorf("Unknown type %#v: %t", v, v)
}
}
return out, nil
}
84 changes: 84 additions & 0 deletions pkg/build/buildkit/buildx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_parseBuildArgs(t *testing.T) {
Expand All @@ -26,3 +27,86 @@ func Test_parseBuildArgs(t *testing.T) {
})
}
}

func Test_flattenMap(t *testing.T) {
tt := []struct {
desc string
inp map[string]interface{}
out map[string]string
err bool
}{
{
desc: "one pair",
inp: map[string]interface{}{
"key": "value",
},
out: map[string]string{
"key": "value",
},
err: false,
},
{
desc: "nested input",
inp: map[string]interface{}{
"key": map[string]string{
"nestedKey": "value",
},
},
out: map[string]string{
"key.nestedKey": "value",
},
err: false,
},
{
desc: "nested input",
inp: map[string]interface{}{
"key1": map[string]interface{}{
"key2": map[string]string{
"key3": "value",
},
},
},
out: map[string]string{
"key1.key2.key3": "value",
},
err: false,
},
{
desc: "multiple nested input",
inp: map[string]interface{}{
"key11": map[string]interface{}{
"key12": map[string]string{
"key13": "value1",
},
},
"key21": map[string]string{
"key22": "value2",
},
},
out: map[string]string{
"key11.key12.key13": "value1",
"key21.key22": "value2",
},
err: false,
},
{
desc: "empty interface value other than map[string]interface{}, map[string]string or string",
inp: map[string]interface{}{
"a": 1,
},
err: true,
},
}

for _, tc := range tt {
t.Run(tc.desc, func(t *testing.T) {
out, err := flattenMap(tc.inp)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, out, tc.out)
})
}
}
23 changes: 23 additions & 0 deletions pkg/porter/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"get.porter.sh/porter/pkg/config"
"get.porter.sh/porter/pkg/manifest"
"get.porter.sh/porter/pkg/mixin"
"get.porter.sh/porter/pkg/parameters"
"get.porter.sh/porter/pkg/printer"
"github.com/Masterminds/semver/v3"
"github.com/opencontainers/go-digest"
Expand All @@ -29,6 +30,12 @@ type BuildOptions struct {

// Driver to use when building the invocation image.
Driver string

// Custom is the unparsed list of NAME=VALUE custom inputs set on the command line.
Customs []string

// parsedCustoms is the parsed set of custom inputs from Customs.
parsedCustoms map[string]string
}

const BuildDriverDefault = config.BuildDriverBuildkit
Expand Down Expand Up @@ -56,6 +63,11 @@ func (o *BuildOptions) Validate(p *Porter) error {
// This would be less awkward if we didn't do an automatic build during publish
p.Data.BuildDriver = o.Driver

err := o.parseCustomInputs()
if err != nil {
return err
}

return o.bundleFileOptions.Validate(p.Context)
}

Expand All @@ -68,6 +80,17 @@ func stringSliceContains(allowedValues []string, value string) bool {
return false
}

func (o *BuildOptions) parseCustomInputs() error {
p, err := parameters.ParseVariableAssignments(o.Customs)
if err != nil {
return err
}

o.parsedCustoms = p

return nil
}

func (p *Porter) Build(ctx context.Context, opts BuildOptions) error {
opts.Apply(p.Context)

Expand Down
24 changes: 24 additions & 0 deletions pkg/porter/build_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,27 @@ func TestBuildOptions_Defaults(t *testing.T) {
assert.Equal(t, config.BuildDriverBuildkit, opts.Driver)
})
}

func TestPorter_BuildWithCustomValues(t *testing.T) {
p := NewTestPorter(t)
defer p.Teardown()

p.TestConfig.TestContext.AddTestFile("./testdata/porter.yaml", config.Name)

m, err := manifest.LoadManifestFrom(context.Background(), p.Config, config.Name)
require.NoError(t, err)

err = p.buildBundle(m, "digest")
require.NoError(t, err)

opts := BuildOptions{Customs: []string{"customKey1=editedCustomValue1"}}
require.NoError(t, opts.Validate(p.Porter), "Validate failed")

err = p.Build(context.Background(), opts)
require.NoError(t, err)

bun, err := p.CNAB.LoadBundle(build.LOCAL_BUNDLE)
require.NoError(t, err)

assert.Equal(t, bun.Custom["customKey1"], "editedCustomValue1")
}
6 changes: 6 additions & 0 deletions pkg/porter/generateManifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,11 @@ func (p *Porter) generateInternalManifest(opts BuildOptions) error {
}
}

for k, v := range opts.parsedCustoms {
if err = e.SetValue("custom."+k, v); err != nil {
return err
}
}

return e.WriteFile(build.LOCAL_MANIFEST)
}
4 changes: 4 additions & 0 deletions pkg/porter/generateManifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func Test_generateInternalManifest(t *testing.T) {
name: "name and value set",
opts: BuildOptions{metadataOpts: metadataOpts{Name: "newname", Version: "1.0.0"}},
wantManifest: "all-fields.yaml",
}, {
name: "custom input set",
opts: BuildOptions{Customs: []string{"key1=editedValue1", "key2.nestedKey2=editedValue2"}},
wantManifest: "custom-input.yaml",
}}

p := NewTestPorter(t)
Expand Down
4 changes: 4 additions & 0 deletions pkg/porter/testdata/generateManifest/all-fields.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: newname
version: 1.0.0
description: "An example Porter configuration"
registry: "localhost:5000"
custom:
key1: value1
key2:
nestedKey2: value2
mixins:
- exec
install:
Expand Down
30 changes: 30 additions & 0 deletions pkg/porter/testdata/generateManifest/custom-input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
schemaVersion: 1.0.0-alpha.1
name: porter-hello
version: 0.1.0
description: "An example Porter configuration"
registry: "localhost:5000"
custom:
key1: editedValue1
key2:
nestedKey2: editedValue2
mixins:
- exec
install:
- exec:
description: "Install Hello World"
command: ./helpers.sh
arguments:
- install
status:
- exec:
description: "World Status"
command: ./helpers.sh
arguments:
- status
uninstall:
- exec:
description: "Uninstall Hello World"
command: ./helpers.sh
arguments:
- uninstall
# comments n stuff
4 changes: 4 additions & 0 deletions pkg/porter/testdata/generateManifest/new-name.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: newname
version: 0.1.0
description: "An example Porter configuration"
registry: "localhost:5000"
custom:
key1: value1
key2:
nestedKey2: value2
mixins:
- exec
install:
Expand Down
Loading