diff --git a/infer/configuration.go b/infer/configuration.go index 7de2fa8..edeab51 100644 --- a/infer/configuration.go +++ b/infer/configuration.go @@ -107,7 +107,7 @@ func (c *config[T]) checkConfig(ctx context.Context, req p.CheckRequest) (p.Chec return p.CheckResponse{}, err } return p.CheckResponse{ - Inputs: inputs, + Inputs: applySecrets[T](inputs), Failures: failures, }, nil } diff --git a/infer/resource.go b/infer/resource.go index 9e7bc94..2978231 100644 --- a/infer/resource.go +++ b/infer/resource.go @@ -865,19 +865,6 @@ func (*derivedResourceController[R, I, O]) getInstance() *R { } func (rc *derivedResourceController[R, I, O]) Check(ctx context.Context, req p.CheckRequest) (p.CheckResponse, error) { - encoder, i, failures, err := decodeCheckingMapErrors[I](req.News) - if err != nil { - return p.CheckResponse{}, err - } - if len(failures) > 0 { - return p.CheckResponse{ - // If we failed to decode, we apply secrets pro-actively to ensure - // that they don't leak into previews. - Inputs: applySecrets[I](req.News), - Failures: failures, - }, nil - } - var r R if r, ok := ((interface{})(r)).(CustomCheck[I]); ok { // The user implemented check manually, so call that. @@ -885,21 +872,43 @@ func (rc *derivedResourceController[R, I, O]) Check(ctx context.Context, req p.C // We do not apply defaults or secrets if the user has implemented Check // themselves. Defaults and secrets are applied by [DefaultCheck]. - defCheckEnc, i, failures, err := callCustomCheck(ctx, r, req.Urn.Name(), req.Olds, req.News) + backupEncoder, _, _, _ := decodeCheckingMapErrors[I](req.News) + + encoder, i, failures, err := callCustomCheck(ctx, r, req.Urn.Name(), req.Olds, req.News) if err != nil { return p.CheckResponse{}, err } - if defCheckEnc != nil { - encoder = *defCheckEnc + + // callCustomCheck will have an encoder if and only if the custom check + // calls [DefaultCheck]. + // + // If it doesn't have an encoder, but no error was returned, we do our + // best to recover secrets, unknowns, etc by calling + // decodeCheckingMapErrors to re-derive an encoder to use. + if encoder == nil { + encoder = &backupEncoder } inputs, err := encoder.Encode(i) return p.CheckResponse{ - Inputs: inputs, + Inputs: applySecrets[I](inputs), Failures: failures, }, err } + encoder, i, failures, err := decodeCheckingMapErrors[I](req.News) + if err != nil { + return p.CheckResponse{}, err + } + if len(failures) > 0 { + return p.CheckResponse{ + // If we failed to decode, we apply secrets pro-actively to ensure + // that they don't leak into previews. + Inputs: applySecrets[I](req.News), + Failures: failures, + }, nil + } + if i, err = defaultCheck(i); err != nil { return p.CheckResponse{}, fmt.Errorf("unable to apply defaults: %w", err) } @@ -933,7 +942,6 @@ func callCustomCheck[T any]( // // It also adds defaults to inputs as necessary, as defined by [Annotator.SetDefault]. func DefaultCheck[I any](ctx context.Context, inputs resource.PropertyMap) (I, []p.CheckFailure, error) { - inputs = applySecrets[I](inputs) enc, i, failures, err := decodeCheckingMapErrors[I](inputs) if v, ok := ctx.Value(defaultCheckEncoderKey{}).(*defaultCheckEncoderValue); ok { diff --git a/infer/tests/check_test.go b/infer/tests/check_test.go index 2b8daee..b21878c 100644 --- a/infer/tests/check_test.go +++ b/infer/tests/check_test.go @@ -15,8 +15,10 @@ package tests import ( + "context" "testing" + "github.com/pulumi/pulumi-go-provider/infer" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -257,3 +259,45 @@ func TestCheckDefaultsRecursive(t *testing.T) { }, resp.Inputs) } + +func TestCheckAlwaysAppliesSecrets(t *testing.T) { + t.Parallel() + + prov := provider() + resp, err := prov.Check(p.CheckRequest{ + Urn: urn("CustomCheckNoDefaults", "check-env"), + News: resource.PropertyMap{ + "input": resource.NewProperty("value"), + }, + }) + require.NoError(t, err) + + assert.Equal(t, resource.PropertyMap{ + "input": resource.MakeSecret(resource.NewProperty("value")), + }, resp.Inputs) +} + +var ( + _ infer.CustomResource[CustomCheckNoDefaultsArgs, CustomCheckNoDefaultsOutput] = &CustomCheckNoDefaults{} + _ infer.CustomCheck[CustomCheckNoDefaultsArgs] = &CustomCheckNoDefaults{} +) + +type CustomCheckNoDefaults struct{} + +func (w *CustomCheckNoDefaults) Check(_ context.Context, + _ string, _ resource.PropertyMap, m resource.PropertyMap, +) (CustomCheckNoDefaultsArgs, []p.CheckFailure, error) { + return CustomCheckNoDefaultsArgs{Input: m["input"].StringValue()}, nil, nil +} + +type CustomCheckNoDefaultsArgs struct { + Input string `pulumi:"input" provider:"secret"` +} + +type CustomCheckNoDefaultsOutput struct{ CustomCheckNoDefaultsArgs } + +func (w *CustomCheckNoDefaults) Create( + ctx context.Context, name string, inputs CustomCheckNoDefaultsArgs, preview bool, +) (string, CustomCheckNoDefaultsOutput, error) { + return "id", CustomCheckNoDefaultsOutput{inputs}, nil +} diff --git a/infer/tests/provider.go b/infer/tests/provider.go index b2b340a..f6a1bb9 100644 --- a/infer/tests/provider.go +++ b/infer/tests/provider.go @@ -399,6 +399,7 @@ func providerOpts(config infer.InferredConfig) infer.Options { infer.Resource[*Recursive, RecursiveArgs, RecursiveOutput](), infer.Resource[*ReadConfig, ReadConfigArgs, ReadConfigOutput](), infer.Resource[*ReadConfigCustom, ReadConfigCustomArgs, ReadConfigCustomOutput](), + infer.Resource[*CustomCheckNoDefaults, CustomCheckNoDefaultsArgs, CustomCheckNoDefaultsOutput](), }, Functions: []infer.InferredFunction{ infer.Function[*GetJoin, JoinArgs, JoinResult](), diff --git a/middleware/schema/schema.go b/middleware/schema/schema.go index a1e9351..b765bbc 100644 --- a/middleware/schema/schema.go +++ b/middleware/schema/schema.go @@ -23,6 +23,7 @@ import ( "fmt" "reflect" "strings" + "sync" "github.com/hashicorp/go-multierror" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" @@ -92,6 +93,8 @@ type state struct { lowerSchema *cache combinedSchema *cache innerGetSchema func(ctx context.Context, req p.GetSchemaRequest) (p.GetSchemaResponse, error) + + m sync.Mutex } // Options sets the schema options used by [Wrap]. @@ -179,6 +182,9 @@ func Wrap(provider p.Provider, opts Options) p.Provider { } func (s *state) GetSchema(ctx context.Context, req p.GetSchemaRequest) (p.GetSchemaResponse, error) { + s.m.Lock() + defer s.m.Unlock() + if s.schema.isEmpty() { spec, err := s.generateSchema(ctx) if err != nil { diff --git a/tests/config_test.go b/tests/config_test.go index 0e200ab..af73d02 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -192,6 +192,7 @@ func TestInferCustomCheckConfig(t *testing.T) { })) t.Run("with-default-check", func(t *testing.T) { + t.Parallel() resp, err := s.CheckConfig(p.CheckRequest{ Urn: resource.CreateURN("p", "pulumi:providers:test", "", "test", "dev"), News: resource.PropertyMap{ @@ -210,6 +211,7 @@ func TestInferCustomCheckConfig(t *testing.T) { }) t.Run("without-default-check", func(t *testing.T) { + t.Parallel() resp, err := s.CheckConfig(p.CheckRequest{ News: resource.PropertyMap{ "field": resource.NewProperty("value"),