Skip to content

Commit

Permalink
Treat object parameters as objects in templating (#3225)
Browse files Browse the repository at this point in the history
* Treat object parameters as objects in templating

This change makes it possible to access a parameter in templating, not
only to the top-level, i.e. the parameter itself, but also -- if the
parameter is of type "object" -- to access properties of the object
value. For example:

```yaml
parameters:
  - name: foo
    type: object
    default:
      a: 1
      b: 2

install:
  mymixin:
    some_property: ${ bundle.parameters.foo.a }
```

In this case, the `.a` was not possible before as `foo` was merely a
JSON string at the point of template resolution.

Signed-off-by: Leo Bergnéhr <[email protected]>

* Interpolate object parameters as JSON strings

When trating object parameters as `map`s, the interpolation of the
object itself changes to the string representation of `map` instead of
the previous JSON string representation.

In order to keep the old behaviour of interpolating the object parameter
to a JSON string, this change introduces a new type, `FormattedObject`,
which implements the [`fmt.Formatter`](https://pkg.go.dev/fmt#Formatter)
interface. The implemented `Format` function turns the object's string
representation into a JSON string, instad of the default Go
representation of a `map`.

Example:

```yaml
parameter:
  - name: foo
    type: object
    default:
      bar: 1

install:
  mymixin:
    # Interpolating a nested property of the object parameter.
    val: "${ bundle.parameter.foo.bar }" # -> `val: '1'`

    # Interpolating the object parameter directly.
    obj: "${ bundle.parameter.foo }"     # -> `obj: '{"bar":1}'`
```

A shortcoming of this implementation is that interpolating nested
properties which also are `map` types will result in the interpolated
value being the string representation of `map`.

Signed-off-by: Leo Bergnéhr <[email protected]>

* Use format string for `Fprintf`

Fixes lint error `SA1006`.

Signed-off-by: Leo Bergnéhr <[email protected]>

* Support `sensitive` for object parameters

This adds support for tracking object parameters as sensitive. However,
if interpolating sub-properties of an object in templating, those will
not get tracked as sensitive.

Signed-off-by: Leo Bergnéhr <[email protected]>

---------

Signed-off-by: Leo Bergnéhr <[email protected]>
Signed-off-by: Kim Christensen <[email protected]>
Co-authored-by: Kim Christensen <[email protected]>
  • Loading branch information
lbergnehr and kichristensen authored Sep 30, 2024
1 parent 28abae5 commit 2a084ea
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 9 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ and we will add you. **All** contributors belong here. 💯
- [guangwu guo](https://github.com/testwill)
- [Eric Herrmann](https://github.com/egherrmann)
- [Alex Dejanu](https://github.com/dejanu)
- [Leo Bergnéhr](https://github.com/lbergnehr)
- [John Cudd](https://github.com/jmcudd)


41 changes: 35 additions & 6 deletions pkg/runtime/runtime_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -131,15 +132,40 @@ func (m *RuntimeManifest) loadDependencyDefinitions() error {
return nil
}

func (m *RuntimeManifest) resolveParameter(pd manifest.ParameterDefinition) string {
// This wrapper type serve as a way of formatting a `map[string]interface{}` as
// JSON when the templating by mustache is done. It makes it possible to
// maintain the JSON string representation of the map while still allowing the
// map to be used as a context in the templating, allowing for accessing the
// map's keys in the template.
type FormattedObject map[string]interface{}

// Format the `FormattedObject` as a JSON string.
func (fo FormattedObject) Format(f fmt.State, c rune) {
jsonStr, _ := json.Marshal(fo)
fmt.Fprintf(f, "%s", string(jsonStr))
}

func (m *RuntimeManifest) resolveParameter(pd manifest.ParameterDefinition) (interface{}, error) {
getValue := func(envVar string) (interface{}, error) {
value := m.config.Getenv(envVar)
if pd.Type == "object" {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(value), &obj); err != nil {
return nil, err
}
return FormattedObject(obj), nil
}
return value, nil
}

if pd.Destination.EnvironmentVariable != "" {
return m.config.Getenv(pd.Destination.EnvironmentVariable)
return getValue(pd.Destination.EnvironmentVariable)
}
if pd.Destination.Path != "" {
return pd.Destination.Path
return pd.Destination.Path, nil
}
envVar := manifest.ParamToEnvVar(pd.Name)
return m.config.Getenv(envVar)
return getValue(envVar)
}

func (m *RuntimeManifest) resolveCredential(cd manifest.CredentialDefinition) (string, error) {
Expand Down Expand Up @@ -271,9 +297,12 @@ func (m *RuntimeManifest) buildSourceData() (map[string]interface{}, error) {
}

pe := param.Name
val := m.resolveParameter(param)
val, err := m.resolveParameter(param)
if err != nil {
return nil, err
}
if param.Sensitive {
m.setSensitiveValue(val)
m.setSensitiveValue(fmt.Sprint(val))
}
params[pe] = val
}
Expand Down
30 changes: 27 additions & 3 deletions pkg/runtime/runtime_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@ func TestResolveMapParam(t *testing.T) {
ctx := context.Background()
testConfig := config.NewTestConfig(t)
testConfig.Setenv("PERSON", "Ralpha")
testConfig.Setenv("CONTACT", "{ \"name\": \"Breta\" }")

mContent := `schemaVersion: 1.0.0-alpha.2
parameters:
- name: person
- name: place
applyTo: [install]
- name: contact
type: object
install:
- mymixin:
Parameters:
Thing: ${ bundle.parameters.person }
ObjectName: ${ bundle.parameters.contact.name }
Object: '${ bundle.parameters.contact }'
`
rm := runtimeManifestFromStepYaml(t, testConfig, mContent)
s := rm.Install[0]
Expand All @@ -62,6 +67,19 @@ install:
assert.Equal(t, "Ralpha", val)
assert.NotContains(t, "place", pms, "parameters that don't apply to the current action should not be resolved")

// Asserting `bundle.parameters.contact.name` works.
require.IsType(t, "string", pms["ObjectName"], "Data.mymixin.Parameters.ObjectName has incorrect type")
contactName := pms["ObjectName"].(string)
require.IsType(t, "string", contactName, "Data.mymixin.Parameters.ObjectName.name has incorrect type")
assert.Equal(t, "Breta", contactName)

// Asserting `bundle.parameters.contact` evaluates to the JSON string
// representation of the object.
require.IsType(t, "string", pms["Object"], "Data.mymixin.Parameters.Object has incorrect type")
contact := pms["Object"].(string)
require.IsType(t, "string", contact, "Data.mymixin.Parameters.Object has incorrect type")
assert.Equal(t, "{\"name\":\"Breta\"}", contact)

err = rm.Initialize(ctx)
require.NoError(t, err)
}
Expand Down Expand Up @@ -276,18 +294,23 @@ func TestResolveSensitiveParameter(t *testing.T) {
ctx := context.Background()
testConfig := config.NewTestConfig(t)
testConfig.Setenv("SENSITIVE_PARAM", "deliciou$dubonnet")
testConfig.Setenv("SENSITIVE_OBJECT", "{ \"secret\": \"this_is_secret\" }")
testConfig.Setenv("REGULAR_PARAM", "regular param value")

mContent := `schemaVersion: 1.0.0
parameters:
- name: sensitive_param
sensitive: true
- name: sensitive_object
sensitive: true
type: object
- name: regular_param
install:
- mymixin:
Arguments:
- ${ bundle.parameters.sensitive_param }
- '${ bundle.parameters.sensitive_object }'
- ${ bundle.parameters.regular_param }
`
rm := runtimeManifestFromStepYaml(t, testConfig, mContent)
Expand All @@ -304,12 +327,13 @@ install:
require.IsType(t, mixin["Arguments"], []interface{}{}, "Data.mymixin.Arguments has incorrect type")
args := mixin["Arguments"].([]interface{})

require.Len(t, args, 2)
require.Len(t, args, 3)
assert.Equal(t, "deliciou$dubonnet", args[0])
assert.Equal(t, "regular param value", args[1])
assert.Equal(t, "{\"secret\":\"this_is_secret\"}", args[1])
assert.Equal(t, "regular param value", args[2])

// There should now be one sensitive value tracked under the manifest
assert.Equal(t, []string{"deliciou$dubonnet"}, rm.GetSensitiveValues())
assert.Equal(t, []string{"deliciou$dubonnet", "{\"secret\":\"this_is_secret\"}"}, rm.GetSensitiveValues())
}

func TestResolveCredential(t *testing.T) {
Expand Down

0 comments on commit 2a084ea

Please sign in to comment.