-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Inject secrets from
provider:"secret"
tags (#252)
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
Showing
11 changed files
with
573 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
}) | ||
} | ||
} |
Oops, something went wrong.