From 7ec8c57b8efd8b5a2a10878ec9fe855682752024 Mon Sep 17 00:00:00 2001 From: Bryce Lampe Date: Wed, 20 Dec 2023 14:34:35 -0800 Subject: [PATCH] Fix an issue where provider config didn't handle complex types --- internal/configencoding/configencoding.go | 242 ++++++++++++++++++ provider.go | 30 ++- .../grpc/config/consumer/ts/Pulumi.test.yaml | 1 - 3 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 internal/configencoding/configencoding.go delete mode 100644 tests/grpc/config/consumer/ts/Pulumi.test.yaml diff --git a/internal/configencoding/configencoding.go b/internal/configencoding/configencoding.go new file mode 100644 index 00000000..c0793159 --- /dev/null +++ b/internal/configencoding/configencoding.go @@ -0,0 +1,242 @@ +package configencoding + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" + "google.golang.org/protobuf/types/known/structpb" +) + +type configEncoding struct { + schema schema.ConfigSpec +} + +func New(s schema.ConfigSpec) *configEncoding { + return &configEncoding{schema: s} +} + +func (*configEncoding) tryUnwrapSecret(encoded any) (any, bool) { + m, ok := encoded.(map[string]any) + if !ok { + return nil, false + } + sig, ok := m["4dabf18193072939515e22adb298388d"] + if !ok { + return nil, false + } + ss, ok := sig.(string) + if !ok { + return nil, false + } + if ss != "1b47061264138c4ac30d75fd1eb44270" { + return nil, false + } + value, ok := m["value"] + return value, ok +} + +func (enc *configEncoding) convertStringToPropertyValue(s string, prop schema.PropertySpec) (resource.PropertyValue, error) { + // If the schema expects a string, we can just return this as-is. + if prop.Type == "string" { + return resource.NewStringProperty(s), nil + } + + // Otherwise, we will attempt to deserialize the input string as JSON and convert the result into a Pulumi + // property. If the input string is empty, we will return an appropriate zero value. + if s == "" { + return enc.zeroValue(prop.Type), nil + } + + var jsonValue interface{} + if err := json.Unmarshal([]byte(s), &jsonValue); err != nil { + return resource.PropertyValue{}, err + } + + opts := enc.unmarshalOpts() + + // Instead of using resource.NewPropertyValue, specialize it to detect nested json-encoded secrets. + var replv func(encoded any) (resource.PropertyValue, bool) + replv = func(encoded any) (resource.PropertyValue, bool) { + encodedSecret, isSecret := enc.tryUnwrapSecret(encoded) + if !isSecret { + return resource.NewNullProperty(), false + } + + v := resource.NewPropertyValueRepl(encodedSecret, nil, replv) + if opts.KeepSecrets { + v = resource.MakeSecret(v) + } + + return v, true + } + + return resource.NewPropertyValueRepl(jsonValue, nil, replv), nil +} + +func (*configEncoding) zeroValue(typ string) resource.PropertyValue { + switch typ { + case "bool": + return resource.NewPropertyValue(false) + case "int", "float": + return resource.NewPropertyValue(0) + case "list", "set": + return resource.NewPropertyValue([]interface{}{}) + default: + return resource.NewPropertyValue(map[string]interface{}{}) + } +} + +func (enc *configEncoding) unmarshalOpts() plugin.MarshalOptions { + return plugin.MarshalOptions{ + Label: "config", + KeepUnknowns: true, + SkipNulls: true, + RejectAssets: true, + } +} + +// Like plugin.UnmarshalPropertyValue but overrides string parsing with convertStringToPropertyValue. +func (enc *configEncoding) unmarshalPropertyValue(key resource.PropertyKey, + v *structpb.Value, +) (*resource.PropertyValue, error) { + opts := enc.unmarshalOpts() + + pv, err := plugin.UnmarshalPropertyValue(key, v, enc.unmarshalOpts()) + if err != nil { + return nil, fmt.Errorf("error unmarshalling property %q: %w", key, err) + } + + prop, ok := enc.schema.Variables[string(key)] + + // Only apply JSON-encoded recognition for known fields. + if !ok { + return pv, nil + } + + var jsonString string + var jsonStringDetected, jsonStringSecret bool + + if pv.IsString() { + jsonString = pv.StringValue() + jsonStringDetected = true + } + + if opts.KeepSecrets && pv.IsSecret() && pv.SecretValue().Element.IsString() { + jsonString = pv.SecretValue().Element.StringValue() + jsonStringDetected = true + jsonStringSecret = true + } + + if jsonStringDetected { + v, err := enc.convertStringToPropertyValue(jsonString, prop) + if err != nil { + return nil, fmt.Errorf("error unmarshalling property %q: %w", key, err) + } + if jsonStringSecret { + s := resource.MakeSecret(v) + return &s, nil + } + return &v, nil + } + + // Computed sentinels are coming in as always having an empty string, but the encoding coerses them to a zero + // value of the appropriate type. + if pv.IsComputed() { + el := pv.V.(resource.Computed).Element + if el.IsString() && el.StringValue() == "" { + res := resource.MakeComputed(enc.zeroValue(prop.Type)) + return &res, nil + } + } + + return pv, nil +} + +// Inline from plugin.UnmarshalProperties substituting plugin.UnmarshalPropertyValue. +func (enc *configEncoding) UnmarshalProperties(props *structpb.Struct) (resource.PropertyMap, error) { + opts := enc.unmarshalOpts() + + result := make(resource.PropertyMap) + + // First sort the keys so we enumerate them in order (in case errors happen, we want determinism). + var keys []string + if props != nil { + for k := range props.Fields { + keys = append(keys, k) + } + sort.Strings(keys) + } + + // And now unmarshal every field it into the map. + for _, key := range keys { + pk := resource.PropertyKey(key) + v, err := enc.unmarshalPropertyValue(pk, props.Fields[key]) + if err != nil { + return nil, err + } else if v != nil { + if opts.SkipNulls && v.IsNull() { + continue + } + if opts.SkipInternalKeys && resource.IsInternalPropertyKey(pk) { + continue + } + result[pk] = *v + } + } + + return result, nil +} + +// Inverse of UnmarshalProperties, with additional support for secrets. Since the encoding cannot represent nested +// secrets, any nested secrets will be approximated by making the entire top-level property secret. +// func (enc *ConfigEncoding) MarshalProperties(props resource.PropertyMap) (*structpb.Struct, error) { +// opts := plugin.MarshalOptions{ +// Label: "config", +// KeepUnknowns: true, +// SkipNulls: true, +// RejectAssets: true, +// KeepSecrets: true, +// } + +// copy := make(resource.PropertyMap) +// for k, v := range props { +// var err error +// copy[k], err = enc.jsonEncodePropertyValue(k, v) +// if err != nil { +// return nil, err +// } +// } +// return plugin.MarshalProperties(copy, opts) +// } + +// func (enc *ConfigEncoding) jsonEncodePropertyValue(k resource.PropertyKey, +// v resource.PropertyValue, +// ) (resource.PropertyValue, error) { +// if v.ContainsUnknowns() { +// return resource.NewStringProperty(plugin.UnknownStringValue), nil +// } +// if v.ContainsSecrets() { +// encoded, err := enc.jsonEncodePropertyValue(k, propertyvalue.RemoveSecrets(v)) +// if err != nil { +// return v, err +// } +// return resource.MakeSecret(encoded), err +// } +// _, knownKey := enc.schema.Variables[string(k)] +// switch { +// case knownKey && v.IsNull(): +// return resource.NewStringProperty(""), nil +// case knownKey && !v.IsNull() && !v.IsString(): +// encoded, err := json.Marshal(v.Mappable()) +// if err != nil { +// return v, fmt.Errorf("JSON encoding error while marshalling property %q: %w", k, err) +// } +// return resource.NewStringProperty(string(encoded)), nil +// default: +// return v, nil +// } +// } diff --git a/provider.go b/provider.go index 2ce24a94..43726395 100644 --- a/provider.go +++ b/provider.go @@ -23,6 +23,7 @@ import ( "time" "github.com/hashicorp/go-multierror" + "github.com/pulumi/pulumi-go-provider/internal/configencoding" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" pprovider "github.com/pulumi/pulumi/pkg/v3/resource/provider" "github.com/pulumi/pulumi/sdk/v3/go/common/diag" @@ -502,12 +503,14 @@ func (p *pkgContext) Log(severity diag.Severity, msg string) { func (p *pkgContext) Logf(severity diag.Severity, msg string, args ...any) { p.Log(severity, fmt.Sprintf(msg, args...)) } + func (p *pkgContext) LogStatus(severity diag.Severity, msg string) { err := p.provider.host.LogStatus(p, severity, p.urn, msg) if err != nil { fmt.Fprintf(os.Stderr, "failed to log %s status: %s", severity, msg) } } + func (p *pkgContext) LogStatusf(severity diag.Severity, msg string, args ...any) { p.LogStatus(severity, fmt.Sprintf(msg, args...)) } @@ -647,12 +650,12 @@ func (d detailedDiff) rpc() map[string]*rpc.PropertyDiff { } func (p *provider) CheckConfig(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { - olds, err := p.getMap(req.Olds) + olds, err := p.decodeConfig(ctx, req.Olds) if err != nil { return nil, err } - news, err := p.getMap(req.News) + news, err := p.decodeConfig(ctx, req.News) if err != nil { return nil, err } @@ -661,7 +664,6 @@ func (p *provider) CheckConfig(ctx context.Context, req *rpc.CheckRequest) (*rpc Olds: olds, News: news, }) - if err != nil { return nil, err } @@ -686,11 +688,11 @@ func getIgnoreChanges(l []string) []presource.PropertyKey { } func (p *provider) DiffConfig(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { - olds, err := p.getMap(req.GetOlds()) + olds, err := p.decodeConfig(ctx, req.GetOlds()) if err != nil { return nil, err } - news, err := p.getMap(req.GetNews()) + news, err := p.decodeConfig(ctx, req.GetNews()) if err != nil { return nil, err } @@ -707,14 +709,24 @@ func (p *provider) DiffConfig(ctx context.Context, req *rpc.DiffRequest) (*rpc.D return r.rpc(), nil } +func (p *provider) decodeConfig(ctx context.Context, args *structpb.Struct) (presource.PropertyMap, error) { + spec, err := GetSchema(ctx, p.name, p.version, p.client) + if err != nil { + return nil, err + } + ce := configencoding.New(spec.Config) + return ce.UnmarshalProperties(args) +} + func (p *provider) Configure(ctx context.Context, req *rpc.ConfigureRequest) (*rpc.ConfigureResponse, error) { - argMap, err := p.getMap(req.GetArgs()) + args, err := p.decodeConfig(ctx, req.GetArgs()) if err != nil { return nil, err } + err = p.client.Configure(p.ctx(ctx, ""), ConfigureRequest{ Variables: req.GetVariables(), - Args: argMap, + Args: args, }) if err != nil { return nil, err @@ -784,7 +796,6 @@ func (p *provider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.Check Inputs: inputs, Failures: checkFailureList(r.Failures).rpc(), }, nil - } func (p *provider) Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { @@ -897,7 +908,6 @@ func (p *provider) Update(ctx context.Context, req *rpc.UpdateRequest) (*rpc.Upd return &rpc.UpdateResponse{ Properties: props, }, nil - } func (p *provider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) { @@ -915,7 +925,6 @@ func (p *provider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb return nil, err } return &emptypb.Empty{}, nil - } type ConstructRequest struct { @@ -974,7 +983,6 @@ func (p *provider) Cancel(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty return nil, err } return &emptypb.Empty{}, nil - } func (p *provider) GetPluginInfo(context.Context, *emptypb.Empty) (*rpc.PluginInfo, error) { diff --git a/tests/grpc/config/consumer/ts/Pulumi.test.yaml b/tests/grpc/config/consumer/ts/Pulumi.test.yaml deleted file mode 100644 index 19b29d3e..00000000 --- a/tests/grpc/config/consumer/ts/Pulumi.test.yaml +++ /dev/null @@ -1 +0,0 @@ -encryptionsalt: v1:7KRXRxSuQNY=:v1:i4cvRY+Qq1JYHRu/:jIthm4H0r+JEgjhJAzDz1sVs866qEw==