Skip to content

Commit

Permalink
Merge 1615996 into b76866c
Browse files Browse the repository at this point in the history
  • Loading branch information
t0yv0 committed Feb 7, 2023
2 parents b76866c + 1615996 commit 3ab62fc
Show file tree
Hide file tree
Showing 19 changed files with 888 additions and 105 deletions.
28 changes: 28 additions & 0 deletions pf/internal/pfutils/raw_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2016-2023, 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 pfutils

import (
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func NewRawState(t tftypes.Type, v tftypes.Value) (*tfprotov6.RawState, error) {
json, err := ValueToJSON(t, v)
if err != nil {
return nil, err
}
return &tfprotov6.RawState{JSON: json}, nil
}
44 changes: 30 additions & 14 deletions pf/internal/pfutils/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,43 @@ type Schema interface {

DeprecationMessage() string
AttributeAtPath(context.Context, path.Path) (Attr, diag.Diagnostics)

// Resource schemas are versioned for [State Upgrade].
//
// [State Upgrade]: https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade
ResourceSchemaVersion() int64
}

func FromProviderSchema(x pschema.Schema) Schema {
attrs := convertMap(FromProviderAttribute, x.Attributes)
blocks := convertMap(FromProviderBlock, x.Blocks)
return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, x.AttributeAtPath)
// Provider schemas cannot be versioned, see also x.GetVersion() always returning 0.
version := int64(0)
return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, x.AttributeAtPath, version)
}

func FromDataSourceSchema(x dschema.Schema) Schema {
attrs := convertMap(FromDataSourceAttribute, x.Attributes)
blocks := convertMap(FromDataSourceBlock, x.Blocks)
return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, x.AttributeAtPath)
// Data source schemas cannot be versioned, see also x.GetVersion() always returning 0.
version := int64(0)
return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, x.AttributeAtPath, version)
}

func FromResourceSchema(x rschema.Schema) Schema {
attrs := convertMap(FromResourceAttribute, x.Attributes)
blocks := convertMap(FromResourceBlock, x.Blocks)
return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, x.AttributeAtPath)
return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, x.AttributeAtPath, x.Version)
}

type schemaAdapter[T any] struct {
tftypes.AttributePathStepper
attrType attr.Type
deprecationMessage string
attrs map[string]Attr
blocks map[string]Block
attributeAtPath func(context.Context, path.Path) (T, diag.Diagnostics)
attrType attr.Type
deprecationMessage string
attrs map[string]Attr
blocks map[string]Block
attributeAtPath func(context.Context, path.Path) (T, diag.Diagnostics)
resourceSchemaVersion int64
}

var _ Schema = (*schemaAdapter[interface{}])(nil)
Expand All @@ -78,17 +88,23 @@ func newSchemaAdapter[T any](
attrs map[string]Attr,
blocks map[string]Block,
atPath func(context.Context, path.Path) (T, diag.Diagnostics),
resourceSchemaVersion int64,
) *schemaAdapter[T] {
return &schemaAdapter[T]{
AttributePathStepper: stepper,
attrType: t,
deprecationMessage: deprecationMessage,
attributeAtPath: atPath,
attrs: attrs,
blocks: blocks,
AttributePathStepper: stepper,
attrType: t,
deprecationMessage: deprecationMessage,
attributeAtPath: atPath,
attrs: attrs,
blocks: blocks,
resourceSchemaVersion: resourceSchemaVersion,
}
}

func (a *schemaAdapter[T]) ResourceSchemaVersion() int64 {
return a.resourceSchemaVersion
}

func (a *schemaAdapter[T]) DeprecationMessage() string {
return a.deprecationMessage
}
Expand Down
189 changes: 189 additions & 0 deletions pf/internal/pfutils/value_to_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Copyright 2016-2023, 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 pfutils

import (
"encoding/json"
"fmt"
"math/big"

"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// Inverse of tftypes.ValueFromJson.
func ValueToJSON(typ tftypes.Type, v tftypes.Value) ([]byte, error) {
raw, err := jsonMarshal(v, typ, tftypes.NewAttributePath())
if err != nil {
return nil, err
}
return json.Marshal(raw)
}

func jsonMarshal(v tftypes.Value, typ tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
if v.IsNull() {
return nil, nil
}
if !v.IsKnown() {
return nil, p.NewErrorf("unknown values cannot be serialized to JSON")
}
switch {
case typ.Is(tftypes.String):
return jsonMarshalString(v, typ, p)
case typ.Is(tftypes.Number):
return jsonMarshalNumber(v, typ, p)
case typ.Is(tftypes.Bool):
return jsonMarshalBool(v, typ, p)
case typ.Is(tftypes.DynamicPseudoType):
return jsonMarshalDynamicPseudoType(v, typ, p)
case typ.Is(tftypes.List{}):
return jsonMarshalList(v, typ.(tftypes.List).ElementType, p)
case typ.Is(tftypes.Set{}):
return jsonMarshalSet(v, typ.(tftypes.Set).ElementType, p)
case typ.Is(tftypes.Map{}):
return jsonMarshalMap(v, typ.(tftypes.Map).ElementType, p)
case typ.Is(tftypes.Tuple{}):
return jsonMarshalTuple(v, typ.(tftypes.Tuple).ElementTypes, p)
case typ.Is(tftypes.Object{}):
return jsonMarshalObject(v, typ.(tftypes.Object).AttributeTypes, p)
}

return nil, p.NewErrorf("unknown type %s", typ)
}

func jsonMarshalString(v tftypes.Value, typ tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var stringValue string
err := v.As(&stringValue)
if err != nil {
return nil, p.NewError(err)
}
return stringValue, nil
}

func jsonMarshalNumber(v tftypes.Value, typ tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var n big.Float
err := v.As(&n)
if err != nil {
return nil, p.NewError(err)
}
f64, _ := n.Float64()
return f64, nil
}

func jsonMarshalBool(v tftypes.Value, typ tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var b bool
err := v.As(&b)
if err != nil {
return nil, p.NewError(err)
}
return b, nil
}

func jsonMarshalDynamicPseudoType(v tftypes.Value, typ tftypes.Type, p *tftypes.AttributePath) ([]byte, error) {
return nil, fmt.Errorf("DynamicPseudoType is not yet supported")
}

func jsonMarshalList(v tftypes.Value, elementType tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var vs []tftypes.Value
err := v.As(&vs)
if err != nil {
return nil, p.NewError(err)
}
var res []interface{}
for i, v := range vs {
ep := p.WithElementKeyInt(i)
e, err := jsonMarshal(v, elementType, ep)
if err != nil {
return nil, ep.NewError(err)
}
res = append(res, e)
}
return res, nil
}

func jsonMarshalSet(v tftypes.Value, elementType tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var vs []tftypes.Value
err := v.As(&vs)
if err != nil {
return nil, p.NewError(err)
}
var res []interface{}
for _, v := range vs {
ep := p.WithElementKeyValue(v)
e, err := jsonMarshal(v, elementType, ep)
if err != nil {
return nil, ep.NewError(err)
}
res = append(res, e)
}
return res, nil
}

func jsonMarshalMap(v tftypes.Value, elementType tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var vs map[string]tftypes.Value
err := v.As(&vs)
if err != nil {
return nil, p.NewError(err)
}
var res map[string]interface{}
for k, v := range vs {
ep := p.WithElementKeyValue(v)
e, err := jsonMarshal(v, elementType, ep)
if err != nil {
return nil, ep.NewError(err)
}
res[k] = e
}
return res, nil
}

func jsonMarshalTuple(v tftypes.Value, elementTypes []tftypes.Type, p *tftypes.AttributePath) (interface{}, error) {
var vs []tftypes.Value
err := v.As(&vs)
if err != nil {
return nil, p.NewError(err)
}
var res []interface{}
for i, v := range vs {
ep := p.WithElementKeyInt(i)
e, err := jsonMarshal(v, elementTypes[i], ep)
if err != nil {
return nil, ep.NewError(err)
}
res = append(res, e)
}
return res, nil
}

func jsonMarshalObject(
v tftypes.Value,
elementTypes map[string]tftypes.Type,
p *tftypes.AttributePath,
) (interface{}, error) {
var vs map[string]tftypes.Value
err := v.As(&vs)
if err != nil {
return nil, p.NewError(err)
}
res := map[string]interface{}{}
for k, v := range vs {
ep := p.WithAttributeName(k)
e, err := jsonMarshal(v, elementTypes[k], ep)
if err != nil {
return nil, ep.NewError(err)
}
res[k] = e
}
return res, nil
}
24 changes: 24 additions & 0 deletions pf/tests/internal/testing/replay.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ func ReplaySequence(t *testing.T, server pulumirpc.ResourceProviderServer, jsonL
}
}

func NewCreateRequest(t *testing.T, encoded string) *pulumirpc.CreateRequest {
return newRequest(t, new(pulumirpc.CreateRequest), encoded)
}

func NewUpdateRequest(t *testing.T, encoded string) *pulumirpc.UpdateRequest {
return newRequest(t, new(pulumirpc.UpdateRequest), encoded)
}

func newRequest[Req proto.Message](t *testing.T, req Req, jsonRequest string) Req {
err := jsonpb.Unmarshal(bytes.NewBuffer([]byte(jsonRequest)), req)
require.NoError(t, err)
return req
}

func ParseResponse[Resp proto.Message, Parsed any](t *testing.T, resp Resp, parsed Parsed) Parsed {
m := jsonpb.Marshaler{}
buf := bytes.Buffer{}
err := m.Marshal(&buf, resp)
require.NoError(t, err)
err = json.Unmarshal(buf.Bytes(), parsed)
require.NoError(t, err)
return parsed
}

func replay[Req proto.Message, Resp proto.Message](
t *testing.T,
entry jsonLogEntry,
Expand Down
23 changes: 23 additions & 0 deletions pf/tests/provider_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
package tfbridgetests

import (
"context"
"testing"

testutils "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/testing"
"github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/testprovider"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCreateWithComputedOptionals(t *testing.T) {
Expand All @@ -44,3 +47,23 @@ func TestCreateWithComputedOptionals(t *testing.T) {
`
testutils.Replay(t, server, testCase)
}

func TestCreateWritesSchemaVersion(t *testing.T) {
server := newProviderServer(t, testprovider.RandomProvider())
ctx := context.Background()
resp, err := server.Create(ctx, testutils.NewCreateRequest(t, `
{
"urn": "urn:pulumi:dev::repro-pulumi-random::random:index/randomString:RandomString::s",
"properties": {
"length": 1
}
}
`))
require.NoError(t, err)
response := testutils.ParseResponse(t, resp, new(struct {
Properties struct {
META interface{} `json:"__meta"`
} `json:"properties"`
}))
assert.Equal(t, `{"schema_version":"2"}`, response.Properties.META)
}
Loading

0 comments on commit 3ab62fc

Please sign in to comment.