Skip to content

Commit

Permalink
Inject secrets from provider:"secret" tags (#252)
Browse files Browse the repository at this point in the history
This PR add explicit secret injection to `infer`. This is necessary for
explicit providers in YAML, which does not correctly apply schema based
secrets to provider config. This is what the bridge does.

To guard against misbehaved or partially implemented SDKs, I have also
added explicit secret injection to resources.

Fixes #251
  • Loading branch information
iwahbe authored Jul 26, 2024
1 parent b1fe34e commit 73b74eb
Show file tree
Hide file tree
Showing 11 changed files with 573 additions and 47 deletions.
1 change: 0 additions & 1 deletion infer/apply_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ func applyDefaults[T any](value *T) error {
contract.Assertf(v.CanSet(), "Cannot accept an un-editable pointer")

var walker defaultsWalker

_, err := walker.walk(v)
return err
}
Expand Down
127 changes: 127 additions & 0 deletions infer/apply_secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package infer

import (
"errors"
"reflect"

"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"

"github.com/pulumi/pulumi-go-provider/infer/internal/ende"
"github.com/pulumi/pulumi-go-provider/internal/introspect"
)

func applySecrets[I any](inputs resource.PropertyMap) resource.PropertyMap {
var walker secretsWalker
result := walker.walk(typeFor[I](), resource.NewProperty(inputs))
contract.AssertNoErrorf(errors.Join(walker.errs...),
`secretsWalker only produces errors when the type it walks has invalid property tags
I can't have invalid property tags because we have gotten to runtime, and it would have failed at
schema generation time already.`)
return result.ObjectValue()
}

// The object that controls secrets application.
type secretsWalker struct{ errs []error }

func (w *secretsWalker) walk(t reflect.Type, p resource.PropertyValue) (out resource.PropertyValue) {
// If t is nil, we have no type information, so return.
if t == nil {
return p
}

// Ensure we are working in raw value types for p

if ende.IsSecret(p) {
p = ende.MakePublic(p)
defer func() { out = ende.MakeSecret(p) }()
}

if ende.IsComputed(p) {
p = ende.MakeKnown(p)
defer func() { out = ende.MakeComputed(p) }()
}

// Ensure we are working in raw value types for t

for t.Kind() == reflect.Pointer {
t = t.Elem()
}

// Here is where we attempt to apply secrets from type information.
//
// If the shape of p does not match the type of t, we will simply return
// p. Because secrets are applied in Check (which may have failed), we can't
// assume that p conforms to the type of t.

switch t.Kind() {
// Structs can carry secret information, so this is where we add secrets.
case reflect.Struct:
if !p.IsObject() {
return p // p and t mismatch, so return early
}
obj := p.ObjectValue()

for _, field := range reflect.VisibleFields(t) {
info, err := introspect.ParseTag(field)
if err != nil {
w.errs = append(w.errs, err)
continue
}

if info.Internal {
continue
}

v, ok := obj[resource.PropertyKey(info.Name)]
if !ok {
// Since info.Name is missing from obj, we don't need to
// worry about if field should be secret.
continue
}
v = w.walk(field.Type, v)
if info.Secret {
v = ende.MakeSecret(v)
}
obj[resource.PropertyKey(info.Name)] = v
}
return resource.NewProperty(obj)
// Collection types
case reflect.Slice, reflect.Array:
if !p.IsArray() {
return p // p and t mismatch, so return early
}
arr := p.ArrayValue()
for i, v := range arr {
arr[i] = w.walk(t.Elem(), v)
}
return resource.NewProperty(arr)
case reflect.Map:
if !p.IsObject() {
return p // p and t mismatch, so return early
}
m := p.ObjectValue()
for k, v := range m {
m[k] = w.walk(t.Elem(), v)
}
return resource.NewProperty(m)
// Primitive types can't have tags, so there's nothing to apply here
default:
return p
}

}
213 changes: 213 additions & 0 deletions infer/apply_secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package infer

import (
"reflect"
"testing"

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

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

type nested struct {
F1 string `pulumi:"f1"`
}

tests := []struct {
name string
input, expected resource.PropertyMap
typ reflect.Type
}{
{
name: "no-secrets",
typ: typeFor[struct {
F1 string `pulumi:"f1"`
F2 map[string]string `pulumi:"f2"`
F3 map[string]nested `pulumi:"F3"`
F4 []string `pulumi:"F4"`
F5 []nested `pulumi:"F5"`
}](),
input: resource.NewPropertyMapFromMap(map[string]any{
"f1": "v1",
"f2": map[string]any{
"n1": "v2",
},
"f3": map[string]any{
"k1": map[string]any{"f1": "v3"},
},
"f4": []any{
"v4",
"v5",
},
"f5": []any{
map[string]any{
"k1": map[string]any{
"f1": "v3",
},
},
},
}),
expected: resource.PropertyMap{
"f1": resource.NewProperty("v1"),
"f2": resource.NewProperty(resource.PropertyMap{
"n1": resource.NewProperty("v2"),
}),
"f3": resource.NewProperty(resource.PropertyMap{
"k1": resource.NewProperty(resource.PropertyMap{
"f1": resource.NewProperty("v3"),
}),
}),
"f4": resource.NewProperty([]resource.PropertyValue{
resource.NewProperty("v4"),
resource.NewProperty("v5"),
}),
"f5": resource.NewProperty([]resource.PropertyValue{
resource.NewProperty(resource.PropertyMap{
"k1": resource.NewProperty(resource.PropertyMap{
"f1": resource.NewProperty("v3"),
}),
}),
}),
},
},
{
name: "nested-secrets",
typ: typeFor[struct {
F1 struct {
F1 string `pulumi:"f1" provider:"secret"`
} `pulumi:"f1"`
F2 []struct {
F1 string `pulumi:"f1" provider:"secret"`
} `pulumi:"f2"`
F3 map[string]struct {
F1 string `pulumi:"f1" provider:"secret"`
} `pulumi:"f3"`
F4 struct {
F1 struct {
F1 string `pulumi:"f1" provider:"secret"`
} `pulumi:"f1"`
} `pulumi:"f4"`
}](),
input: resource.NewPropertyMapFromMap(map[string]any{
"f1": map[string]any{
"f1": "secret1",
},
"f2": []any{
map[string]any{
"f1": "secret2",
},
},
"f3": map[string]any{
"key1": map[string]any{
"f1": "secret3",
},
},
"f4": map[string]any{
"f1": map[string]any{
"f1": "secret4",
},
},
}),
expected: resource.PropertyMap{
"f1": resource.NewProperty(resource.PropertyMap{
"f1": resource.MakeSecret(resource.NewProperty("secret1")),
}),
"f2": resource.NewProperty([]resource.PropertyValue{
resource.NewProperty(resource.PropertyMap{
"f1": resource.MakeSecret(resource.NewProperty("secret2")),
}),
}),
"f3": resource.NewProperty(resource.PropertyMap{
"key1": resource.NewProperty(resource.PropertyMap{
"f1": resource.MakeSecret(resource.NewProperty("secret3")),
}),
}),
"f4": resource.NewProperty(resource.PropertyMap{
"f1": resource.NewProperty(resource.PropertyMap{
"f1": resource.MakeSecret(resource.NewProperty("secret4")),
}),
}),
},
},
{
name: "already-secret",
typ: typeFor[struct {
F1 string `pulumi:"f1" provider:"secret"`
F2 string `pulumi:"f2"`
}](),
input: resource.PropertyMap{
"f1": resource.MakeSecret(resource.NewProperty("v1")),
"f2": resource.MakeSecret(resource.NewProperty("v2")),
},
expected: resource.PropertyMap{
"f1": resource.MakeSecret(resource.NewProperty("v1")),
"f2": resource.MakeSecret(resource.NewProperty("v2")),
},
},
{
name: "computed-input",
typ: typeFor[struct {
F1 string `pulumi:"f1" provider:"secret"`
}](),
input: resource.PropertyMap{
"f1": resource.MakeComputed(resource.NewProperty("")),
},
expected: resource.PropertyMap{
"f1": resource.NewProperty(resource.Output{
Element: resource.NewProperty(""),
Secret: true,
Known: false,
}),
},
},
{
name: "mismatched-types",
typ: typeFor[struct {
F1 string `pulumi:"f1" provider:"secret"`
F2 []struct {
F1 string `pulumi:"f1" provider:"secret"`
} `pulumi:"f2"`
}](),
input: resource.PropertyMap{
"f2": resource.NewProperty([]resource.PropertyValue{
resource.NewProperty("v2"),
}),
"f3": resource.NewProperty("v3"),
},
expected: resource.PropertyMap{
"f2": resource.NewProperty([]resource.PropertyValue{
resource.NewProperty("v2"),
}),
"f3": resource.NewProperty("v3"),
},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var walker secretsWalker
result := walker.walk(tt.typ, resource.NewProperty(tt.input))
require.Empty(t, walker.errs)
assert.Equal(t, tt.expected, result.ObjectValue())
})
}
}
Loading

0 comments on commit 73b74eb

Please sign in to comment.