diff --git a/pf/go.mod b/pf/go.mod index 7925c3c9b1..8132e92961 100644 --- a/pf/go.mod +++ b/pf/go.mod @@ -19,6 +19,7 @@ require ( github.com/pulumi/pulumi-terraform-bridge/v3 v3.91.0 github.com/pulumi/pulumi-terraform-bridge/x/muxer v0.0.8 github.com/stretchr/testify v1.9.0 + pgregory.net/rapid v0.6.1 ) require ( diff --git a/pf/internal/plugin/provider_server.go b/pf/internal/plugin/provider_server.go index e0c9615b07..a49a6723aa 100644 --- a/pf/internal/plugin/provider_server.go +++ b/pf/internal/plugin/provider_server.go @@ -17,6 +17,7 @@ package plugin import ( "context" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" @@ -80,5 +81,41 @@ func (p providerThunk) Configure( if ctx.Value(setupConfigureKey) != nil { return plugin.ConfigureResponse{}, nil } + req.Inputs = removeSecrets(req.Inputs) + contract.Assertf(!req.Inputs.ContainsSecrets(), + "Inputs to configure should not contain secrets") return p.GrpcProvider.Configure(ctx, req) } + +func removeSecrets(v resource.PropertyMap) resource.PropertyMap { + var remove func(resource.PropertyValue) resource.PropertyValue + remove = func(v resource.PropertyValue) resource.PropertyValue { + switch { + case v.IsArray(): + arr := make([]resource.PropertyValue, 0, len(v.ArrayValue())) + for _, v := range v.ArrayValue() { + arr = append(arr, remove(v)) + } + return resource.NewProperty(arr) + case v.IsObject(): + obj := make(resource.PropertyMap, len(v.ObjectValue())) + for k, v := range v.ObjectValue() { + obj[k] = remove(v) + } + return resource.NewProperty(obj) + case v.IsComputed(): + return resource.MakeComputed(remove(v.Input().Element)) + case v.IsOutput(): + o := v.OutputValue() + o.Secret = false + o.Element = remove(o.Element) + return resource.NewProperty(o) + case v.IsSecret(): + return remove(v.SecretValue().Element) + default: + return v + } + } + + return remove(resource.NewProperty(v)).ObjectValue() +} diff --git a/pf/internal/plugin/provider_server_test.go b/pf/internal/plugin/provider_server_test.go new file mode 100644 index 0000000000..1c40e8722b --- /dev/null +++ b/pf/internal/plugin/provider_server_test.go @@ -0,0 +1,123 @@ +// Copyright 2016-2024, 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 plugin + +import ( + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + prapid "github.com/pulumi/pulumi/sdk/v3/go/property/testing" + "github.com/stretchr/testify/assert" + "pgregory.net/rapid" +) + +// TestRemoveSecrets checks that removeSecrets removes [resource.Secret] values and unsets +// [resource.Output.Secret] fields without making any other changes. +func TestRemoveSecrets(t *testing.T) { + + // These functions validate that a diff does not contain any non-secret changes. + var ( + validateObjectDiff func(assert.TestingT, resource.ObjectDiff) + validateArrayDiff func(assert.TestingT, resource.ArrayDiff) + validateValueDiff func(assert.TestingT, resource.ValueDiff) + ) + + validateValueDiff = func(t assert.TestingT, v resource.ValueDiff) { + switch { + case v.Old.IsOutput(): + oOld := v.Old.OutputValue() + oOld.Secret = false + if d := resource.NewProperty(oOld).DiffIncludeUnknowns(v.New); d != nil { + validateValueDiff(t, *d) + } + case v.Old.IsSecret(): + if d := v.Old.SecretValue().Element.DiffIncludeUnknowns(v.New); d != nil { + validateValueDiff(t, *d) + } + case v.Old.IsObject(): + validateObjectDiff(t, *v.Object) + case v.Old.IsArray(): + validateArrayDiff(t, *v.Array) + default: + assert.Failf(t, "", "unexpected Update.Old type %q", v.Old.TypeString()) + } + } + + validateArrayDiff = func(t assert.TestingT, diff resource.ArrayDiff) { + assert.Empty(t, diff.Adds) + assert.Empty(t, diff.Deletes) + + for _, v := range diff.Updates { + validateValueDiff(t, v) + } + } + + validateObjectDiff = func(t assert.TestingT, diff resource.ObjectDiff) { + assert.Empty(t, diff.Adds) + + // Diff does not distinguish from a missing key and a null property, so + // when we go from a map{k: secret(null)} to a map{k: null}, the diff + // machinery shows a delete. + // + // We have an explicit test for this behavior. + for _, v := range diff.Deletes { + assert.Equal(t, resource.MakeSecret(resource.NewNullProperty()), v) + } + + for _, v := range diff.Updates { + validateValueDiff(t, v) + } + } + + t.Run("rapid", rapid.MakeCheck(func(t *rapid.T) { + m := resource.ToResourcePropertyValue(prapid.Map(5).Draw(t, "top-level")).ObjectValue() + if m.ContainsSecrets() { + unsecreted := removeSecrets(m) + assert.False(t, unsecreted.ContainsSecrets()) + + // We need to assert that the only change between m and unsecreted + // is that secret values went to their element values. + if d := m.DiffIncludeUnknowns(unsecreted); d != nil { + validateObjectDiff(t, *d) + } + } else { + assert.Equal(t, m, removeSecrets(m)) + } + })) + + t.Run("map-null-secrets", func(t *testing.T) { + assert.Equal(t, + resource.PropertyMap{ + "null": resource.NewNullProperty(), + }, + removeSecrets(resource.PropertyMap{ + "null": resource.MakeSecret(resource.NewNullProperty()), + }), + ) + }) +} + +func TestDiffNull(t *testing.T) { + m1 := resource.PropertyMap{ + "k": resource.MakeSecret(resource.NewProperty("v")), + "null": resource.MakeSecret(resource.NewNullProperty()), + } + m2 := resource.PropertyMap{ + "k": resource.NewProperty("v"), + "null": resource.NewNullProperty(), + } + diff := m1.DiffIncludeUnknowns(m2) + t.Log(diff.Deletes) +}