Skip to content

Commit

Permalink
Fix an issue where provider config didn't handle complex types
Browse files Browse the repository at this point in the history
  • Loading branch information
blampe committed Dec 22, 2023
1 parent 7cbf0e3 commit 7ec8c57
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 12 deletions.
242 changes: 242 additions & 0 deletions internal/configencoding/configencoding.go
Original file line number Diff line number Diff line change
@@ -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
// }
// }
30 changes: 19 additions & 11 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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...))
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion tests/grpc/config/consumer/ts/Pulumi.test.yaml

This file was deleted.

0 comments on commit 7ec8c57

Please sign in to comment.