Skip to content

Commit

Permalink
linting and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
EronWright committed Feb 9, 2024
1 parent 98f005a commit 8e76a1a
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 54 deletions.
172 changes: 143 additions & 29 deletions resourcex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@ The `resourcex` package extends the `github.com/pulumi/pulumi/sdk/v3/go/common/r
for working with property values.

1. `Extract` - Extract structured values from a property map, with tracking of unknownness and secretness.
2. `DecodeValues` - Decode a property map into a JSON-like structure containing only values.
2. `Decode` - Decode a property map into a JSON-like structure containing only values.
3. `Traverse` - Traverse a property path, visiting each property value.

## Extraction

The `Extract` function is designed to extract subsets of values from property map using structs.
Information about the unknownness and secretness of the extracted values is provided, e.g. to annotate output properties.
Information about the unknownness and secretness of the extracted values is provided, e.g. to annotate output properties.

Here's a real-world example:
Here's an example of decoding a property map into various structures, observing how unknownness and secretness varies:

```go

func Test_Extract_Example(t *testing.T) {

res1 := resource.URN("urn:pulumi:test::test::kubernetes:core/v1:Namespace::some-namespace")
res1 := resource.URN("urn:pulumi:test::test::kubernetes:core/v1:Namespace::some-namespace")

props := resource.PropertyMap{
"chart": resource.NewStringProperty("nginx"),
Expand All @@ -38,20 +35,20 @@ func Test_Extract_Example(t *testing.T) {
Dependencies: []resource.URN{res1},
}),
"args": resource.NewArrayProperty([]resource.PropertyValue{
resource.MakeComputed(resource.NewObjectProperty(resource.PropertyMap{})),
resource.NewObjectProperty(resource.PropertyMap{
"name": resource.NewStringProperty("a"),
"value": resource.MakeComputed(resource.NewStringProperty("")),
"value": resource.MakeSecret(resource.NewStringProperty("a")),
}),
resource.MakeComputed(resource.NewObjectProperty(resource.PropertyMap{})),
resource.NewObjectProperty(resource.PropertyMap{
"name": resource.NewStringProperty("b"),
"value": resource.MakeSecret(resource.NewStringProperty("b")),
"name": resource.NewStringProperty("c"),
"value": resource.MakeSecret(resource.NewStringProperty("c")),
}),
}),
}

type RepositoryOpts struct {
// Repository where to locate the requested chart. If is a URL the chart is installed without installing the repository.
// Repository where to locate the requested chart.
Repo string `json:"repo,omitempty"`
// The Repositories CA File
CAFile string `json:"caFile,omitempty"`
Expand All @@ -65,11 +62,6 @@ func Test_Extract_Example(t *testing.T) {
Username string `json:"username,omitempty"`
}

type Arg struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}

type Loader struct {
Chart string `json:"chart,omitempty"`
DependencyUpdate *bool `json:"dependencyUpdate,omitempty"`
Expand All @@ -82,7 +74,6 @@ func Test_Extract_Example(t *testing.T) {
result, err := Extract(loader, props, ExtractOptions{RejectUnknowns: false})
assert.NoError(t, err)
assert.Equal(t, ExtractResult{ContainsUnknowns: false, ContainsSecrets: true}, result)
t.Logf("\n%+v", result)

// EXAMPLE: anonymous struct (version)
version := struct {
Expand All @@ -92,37 +83,160 @@ func Test_Extract_Example(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "1.24.0", version.Version)
assert.Equal(t, ExtractResult{ContainsUnknowns: false, ContainsSecrets: false}, result)
t.Logf("\n%+v\n%+v", version, result)

// EXAMPLE: anonymous struct (namespace)
// EXAMPLE: anonymous struct ("namespace")
namespace := struct {
Namespace string `json:"namespace"`
}{}
result, err = Extract(&namespace, props, ExtractOptions{RejectUnknowns: false})
assert.NoError(t, err)
assert.Equal(t, "", namespace.Namespace)
assert.Equal(t, ExtractResult{ContainsUnknowns: true, ContainsSecrets: true, Dependencies: []resource.URN{res1}}, result)
t.Logf("\n%+v\n%+v", namespace, result)
assert.Equal(t,
ExtractResult{ContainsUnknowns: true, ContainsSecrets: true, Dependencies: []resource.URN{res1}}, result)

// EXAMPLE: not present (dependencyUpdate, optional)
// EXAMPLE: unset property ("dependencyUpdate")
dependencyUpdate := struct {
DependencyUpdate *bool `json:"dependencyUpdate"`
}{}
result, err = Extract(&dependencyUpdate, props, ExtractOptions{RejectUnknowns: false})
assert.NoError(t, err)
assert.Nil(t, dependencyUpdate.DependencyUpdate)
assert.Equal(t, ExtractResult{ContainsUnknowns: false, ContainsSecrets: false}, result)
t.Logf("\n%+v\n%+v", dependencyUpdate, result)

// EXAMPLE: arrays
type Arg struct {
Name string `json:"name"`
Value string `json:"value"`
}
args := struct {
Args []Arg `json:"args"`
Args []*Arg `json:"args"`
}{}
result, err = Extract(&args, props, ExtractOptions{RejectUnknowns: false})
assert.NoError(t, err)
assert.Equal(t, []Arg{{Name: "", Value: ""}, {Name: "a", Value: ""}, {Name: "b", Value: "b"}}, args.Args)
assert.Equal(t, []*Arg{{Name: "a", Value: "a"}, nil, {Name: "c", Value: "c"}}, args.Args)
assert.Equal(t, ExtractResult{ContainsUnknowns: true, ContainsSecrets: true}, result)
t.Logf("\n%+v\n%+v", args, result)

}
```
// EXAMPLE: arrays (names only)
type ArgNames struct {
Name string `json:"name"`
}
argNames := struct {
Args []*ArgNames `json:"args"`
}{}
result, err = Extract(&argNames, props, ExtractOptions{RejectUnknowns: false})
assert.NoError(t, err)
assert.Equal(t, []*ArgNames{{Name: "a"}, nil, {Name: "c"}}, argNames.Args)
assert.Equal(t, ExtractResult{ContainsUnknowns: true, ContainsSecrets: false}, result)

```

## Decoding

The `Decode` function decodes a property map into a JSON-like map structure containing pure values.
Unknown and computed values are decoded to `null`, both for objects and for arrays.

The following property value types are supported: `Bool`, `Number`, `String`, `Array`, `Computed`,
`Output`, `Secret`, `Object`.

The following property value types are NOT supported: `Asset`, `Archive`, `ResourceReference`.

Here's an example:

```go
res1 := resource.URN("urn:pulumi:test::test::kubernetes:core/v1:Namespace::some-namespace")

props := resource.PropertyMap{
"chart": resource.NewStringProperty("nginx"),
"version": resource.NewStringProperty("1.24.0"),
"repositoryOpts": resource.NewObjectProperty(resource.PropertyMap{
"repo": resource.NewStringProperty("https://charts.bitnami.com/bitnami"),
"username": resource.NewStringProperty("username"),
"password": resource.NewSecretProperty(&resource.Secret{
Element: resource.NewStringProperty("password"),
}),
"other": resource.MakeComputed(resource.NewStringProperty("")),
}),
"namespace": resource.NewOutputProperty(resource.Output{
Element: resource.NewStringProperty(""),
Known: false,
Secret: true,
Dependencies: []resource.URN{res1},
}),
"args": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"name": resource.NewStringProperty("a"),
"value": resource.MakeSecret(resource.NewStringProperty("a")),
}),
resource.MakeComputed(resource.NewObjectProperty(resource.PropertyMap{})),
resource.NewObjectProperty(resource.PropertyMap{
"name": resource.NewStringProperty("c"),
"value": resource.MakeSecret(resource.NewStringProperty("c")),
}),
}),
}

decoded := Decode(props)
assert.Equal(t, map[string]interface{}{
"chart": "nginx",
"version": "1.24.0",
"repositoryOpts": map[string]interface{}{
"repo": "https://charts.bitnami.com/bitnami",
"username": "username",
"password": "password",
"other": nil,
},
"namespace": nil,
"args": []interface{}{
map[string]interface{}{
"name": "a",
"value": "a",
},
nil,
map[string]interface{}{
"name": "c",
"value": "c",
},
},
}, decoded)
```

## Traversal

The `Traverse` function traverses a property map along the given property path,
invoking a callback function for each property value it encounters, including the map itself.

A wildcard may be used as an array index to traverse all elements of the array.

Examples of valid paths:
- root
- root.nested
- root.double.nest
- root.array[0]
- root.array[100]
- root.array[0].nested
- root.array[0][1].nested
- root.nested.array[0].double[1]
- root.array[*]
- root.array[*].field

For example, given this property map:

```go
props := /* A */ resource.NewObjectProperty(resource.PropertyMap{
"chart": resource.NewStringProperty("nginx"),
"version": resource.NewStringProperty("1.24.0"),
"repositoryOpts": /* B */ resource.NewObjectProperty(resource.PropertyMap{
"repo": resource.NewStringProperty("https://charts.bitnami.com/bitnami"),
"username": resource.NewStringProperty("username"),
"password": /* C */ resource.NewSecretProperty(&resource.Secret{
Element: /* D */ resource.NewStringProperty("password"),
}),
}),
})
```

Traversing the path `repositoryOpts.password` would invoke the callback function for each of the following values:
1. the root-level property value (A)
2. the "object" property value (B)
3. the "secret" property value (C)
4. the "string" property value (D)
4 changes: 2 additions & 2 deletions resourcex/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

// DecodeValues decodes a property map into a JSON-like structure containing only values.
// Decode decodes a property map into a JSON-like structure containing only values.
// Unknown values are decoded as nil, both in maps and arrays.
// Secrets are collapsed into their underlying values.
func DecodeValues(props resource.PropertyMap) interface{} {
func Decode(props resource.PropertyMap) interface{} {
return decodeM(props)
}

Expand Down
67 changes: 66 additions & 1 deletion resourcex/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import (
"testing"

"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_Decode(t *testing.T) {
t.Parallel()
tests := []struct {
name string
props resource.PropertyMap
Expand Down Expand Up @@ -198,9 +200,72 @@ func Test_Decode(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
actual := DecodeValues(tt.props)
t.Parallel()
actual := Decode(tt.props)
require.Equal(t, tt.expected, actual, "expected result")
})
}
}

func Test_Decode_Example(t *testing.T) {
t.Parallel()

res1 := resource.URN("urn:pulumi:test::test::kubernetes:core/v1:Namespace::some-namespace")

props := resource.PropertyMap{
"chart": resource.NewStringProperty("nginx"),
"version": resource.NewStringProperty("1.24.0"),
"repositoryOpts": resource.NewObjectProperty(resource.PropertyMap{
"repo": resource.NewStringProperty("https://charts.bitnami.com/bitnami"),
"username": resource.NewStringProperty("username"),
"password": resource.NewSecretProperty(&resource.Secret{
Element: resource.NewStringProperty("password"),
}),
"other": resource.MakeComputed(resource.NewStringProperty("")),
}),
"namespace": resource.NewOutputProperty(resource.Output{
Element: resource.NewStringProperty(""),
Known: false,
Secret: true,
Dependencies: []resource.URN{res1},
}),
"args": resource.NewArrayProperty([]resource.PropertyValue{
resource.NewObjectProperty(resource.PropertyMap{
"name": resource.NewStringProperty("a"),
"value": resource.MakeSecret(resource.NewStringProperty("a")),
}),
resource.MakeComputed(resource.NewObjectProperty(resource.PropertyMap{})),
resource.NewObjectProperty(resource.PropertyMap{
"name": resource.NewStringProperty("c"),
"value": resource.MakeSecret(resource.NewStringProperty("c")),
}),
}),
}

decoded := Decode(props)
assert.Equal(t, map[string]interface{}{
"chart": "nginx",
"version": "1.24.0",
"repositoryOpts": map[string]interface{}{
"repo": "https://charts.bitnami.com/bitnami",
"username": "username",
"password": "password",
"other": nil,
},
"namespace": nil,
"args": []interface{}{
map[string]interface{}{
"name": "a",
"value": "a",
},
nil,
map[string]interface{}{
"name": "c",
"value": "c",
},
},
}, decoded)
t.Logf("\n%+v", printJSON(decoded))
}
4 changes: 2 additions & 2 deletions resourcex/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func Extract(target interface{}, props resource.PropertyMap, opts ExtractOptions
}

// decode the property map into a JSON-like structure containing only values.
stripped := DecodeValues(props)
decoded := Decode(props)

// deserialize the JSON-like structure into a strongly typed struct.
config := &mapstructure.DecoderConfig{
Expand All @@ -59,7 +59,7 @@ func Extract(target interface{}, props resource.PropertyMap, opts ExtractOptions
if err != nil {
return ExtractResult{}, err
}
err = decoder.Decode(stripped)
err = decoder.Decode(decoded)
if err != nil {
return ExtractResult{}, err
}
Expand Down
Loading

0 comments on commit 8e76a1a

Please sign in to comment.