Skip to content

Commit

Permalink
[PF] Allow overriding Number with String for PF fields (#2155)
Browse files Browse the repository at this point in the history
This is motivated by [`terraform-provider-vantage blocked on numeric ID
support
#1198`](#1198).
My plan for #1198 is to override the "id" fields of each resource to be
a `"string"`, which is how we currently handle SDK based providers with
the same problem.
  • Loading branch information
iwahbe authored Jul 16, 2024
1 parent 16b5c17 commit c975862
Show file tree
Hide file tree
Showing 12 changed files with 390 additions and 6 deletions.
15 changes: 12 additions & 3 deletions pf/internal/check/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"

"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"

"github.com/pulumi/pulumi-terraform-bridge/pf/internal/muxer"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
Expand Down Expand Up @@ -57,7 +58,7 @@ func checkIDProperties(sink diag.Sink, info tfbridge.ProviderInfo, isPFResource
if resourceHasComputeID(info, rname) {
return true
}
ok, reason := resourceHasRegularID(resource)
ok, reason := resourceHasRegularID(resource, info.Resources[rname])
if ok {
return true
}
Expand All @@ -77,12 +78,20 @@ func checkIDProperties(sink diag.Sink, info tfbridge.ProviderInfo, isPFResource
return nil
}

func resourceHasRegularID(resource shim.Resource) (bool, string) {
func resourceHasRegularID(resource shim.Resource, resourceInfo *tfbridge.ResourceInfo) (bool, string) {
idSchema, gotID := resource.Schema().GetOk("id")
if !gotID {
return false, `no "id" attribute`
}
if idSchema.Type() != shim.TypeString {
var typeOverride tokens.Type
if resourceInfo != nil {
if id := resourceInfo.Fields["id"]; id != nil {
typeOverride = id.Type
}
}

// If the user over-rode the type to be a string, don't reject.
if idSchema.Type() != shim.TypeString && typeOverride != "string" {
return false, `"id" attribute is not of type String`
}
if idSchema.Sensitive() {
Expand Down
4 changes: 3 additions & 1 deletion pf/internal/check/not_supported.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ func (u *notSupportedUtil) resource(path string, res *tfbridge.ResourceInfo) {
}

func (u *notSupportedUtil) schema(path string, schema *tfbridge.SchemaInfo) {
u.assertIsZero(path+".Type", schema.Type)
if schema.Type != "string" {
u.assertIsZero(path+".Type", schema.Type)
}
u.assertIsZero(path+".AltTypes", schema.AltTypes)
u.assertIsZero(path+".NestedType", schema.NestedType)
u.assertIsZero(path+".Transform", schema.Transform)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,30 @@
}
},
"resources": {
"testbridge:index/intID:IntID": {
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"inputProperties": {
"name": {
"type": "string"
}
},
"stateInputs": {
"description": "Input properties used for looking up and filtering IntID resources.\n",
"properties": {
"name": {
"type": "string"
}
},
"type": "object"
}
},
"testbridge:index/testnest:Testnest": {
"properties": {
"rules": {
Expand Down
7 changes: 7 additions & 0 deletions pf/tests/internal/testprovider/testbridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ func SyntheticTestBridgeProvider() tfbridge.ProviderInfo {

"testbridge_privst": {Tok: "testbridge:index/testres:Privst"},
"testbridge_autoname_res": {Tok: "testbridge:index/testres:AutoNameRes"},
"testbridge_int_id_res": {
Tok: "testbridge:index/intID:IntID",
Fields: map[string]*tfbridge.SchemaInfo{
"id": {Type: "string"},
},
},
},

DataSources: map[string]*tfbridge.DataSourceInfo{
Expand Down Expand Up @@ -208,5 +214,6 @@ func (p *syntheticProvider) Resources(context.Context) []func() resource.Resourc
newTestDefaultInfoRes,
newPrivst,
newAutoNameRes,
newIntIDRes,
}
}
109 changes: 109 additions & 0 deletions pf/tests/internal/testprovider/testbridge_resource_int_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 testprovider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

type intIDRes struct{}

var _ resource.Resource = &intIDRes{}

func newIntIDRes() resource.Resource {
return &intIDRes{}
}

func (*intIDRes) schema() rschema.Schema {
return rschema.Schema{
Attributes: map[string]rschema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Required: true,
},
},
}
}

func (e *intIDRes) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_int_id_res"
}

func (e *intIDRes) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = e.schema()
}

func (e *intIDRes) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
resp.State.Raw = req.Plan.Raw.Copy() // Copy plan to state.
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), 1234)...)
}

func (e *intIDRes) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
}

func (e *intIDRes) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var id int64
resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...)
if resp.Diagnostics.HasError() {
return
}
if id != 1234 {
resp.Diagnostics.AddAttributeError(path.Root("id"), "unexpected value",
fmt.Sprintf("expected 1234, found %d", id))
}

resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("id"), &id)...)
if resp.Diagnostics.HasError() {
return
}
if id != 5678 {
resp.Diagnostics.AddAttributeError(path.Root("id"), "unexpected value",
fmt.Sprintf("expected 5678, found %d", id))
}

resp.State.Raw = req.Plan.Raw.Copy() // Copy plan to state.
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), 90)...)
}

func (e *intIDRes) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
resp.State = e.nilState(ctx)
}

func (e *intIDRes) nilState(ctx context.Context) tfsdk.State {
typ := e.terraformType(ctx)
return tfsdk.State{
Raw: tftypes.NewValue(typ, nil),
Schema: e.schema(),
}
}

func (e *intIDRes) terraformType(ctx context.Context) tftypes.Type {
return e.schema().Type().TerraformType(ctx)
}
24 changes: 24 additions & 0 deletions pf/tests/provider_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

testutils "github.com/pulumi/providertest/replay"
"github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/providerbuilder"
"github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/testprovider"
"github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge"
tfbridge0 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
)
Expand Down Expand Up @@ -352,3 +353,26 @@ func TestCheck(t *testing.T) {
})
}
}

func TestCheckWithIntID(t *testing.T) {
server := newProviderServer(t, testprovider.SyntheticTestBridgeProvider())
testCase := `
{
"method": "/pulumirpc.ResourceProvider/Check",
"request": {
"urn": "urn:pulumi:test-stack::basicprogram::testbridge:index/intID:IntID::r1",
"news": {
"name": "name"
},
"olds": {},
"randomSeed": "wqZZaHWVfsS1ozo3bdauTfZmjslvWcZpUjn7BzpS79c="
},
"response": {
"inputs": {
"name": "name"
}
}
}
`
testutils.Replay(t, server, testCase)
}
21 changes: 21 additions & 0 deletions pf/tests/provider_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ func TestCreateWithComputedOptionals(t *testing.T) {
testutils.Replay(t, server, testCase)
}

func TestCreateWithIntID(t *testing.T) {
server := newProviderServer(t, testprovider.SyntheticTestBridgeProvider())
testCase := `
{
"method": "/pulumirpc.ResourceProvider/Create",
"request": {
"urn": "urn:pulumi:test-stack::basicprogram::testbridge:index/intID:IntID::r1",
"properties": {},
"preview": false
},
"response": {
"id": "1234",
"properties": {
"id": "1234"
}
}
}
`
testutils.Replay(t, server, testCase)
}

func TestCreateWritesSchemaVersion(t *testing.T) {
server := newProviderServer(t, testprovider.RandomProvider())

Expand Down
26 changes: 26 additions & 0 deletions pf/tests/provider_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,29 @@ func TestUpdateWritesSchemaVersion(t *testing.T) {
}
`)
}

func TestUpdateWithIntID(t *testing.T) {
server := newProviderServer(t, testprovider.SyntheticTestBridgeProvider())
testCase := `
{
"method": "/pulumirpc.ResourceProvider/Update",
"request": {
"id": "1234",
"olds": {
"id": "1234"
},
"news": {
"id": "5678"
},
"urn": "urn:pulumi:test-stack::basicprogram::testbridge:index/intID:IntID::r1",
"preview": false
},
"response": {
"properties": {
"id": "90"
}
}
}
`
testutils.Replay(t, server, testCase)
}
89 changes: 89 additions & 0 deletions pkg/convert/adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 convert

import (
"fmt"
"math/big"
"strconv"

"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)

// adaptedEncoder wraps an encoder in an adapter during encoding.
//
// Given [resource.PropertyValue] types P_i and P_j and an encoder P_j -> T, the adapter
// function should translate P_i -> P_j.
type adaptedEncoder[T Encoder] struct {
adapter func(resource.PropertyValue) (resource.PropertyValue, error)
encoder T
}

func (e adaptedEncoder[T]) fromPropertyValue(v resource.PropertyValue) (tftypes.Value, error) {
adapted, err := e.adapter(v)
if err != nil {
return tftypes.Value{}, fmt.Errorf("failed to adapt for %T: %w", e.encoder, err)
}
return e.encoder.fromPropertyValue(adapted)
}

type adaptedDecoder[T Decoder] struct {
adapter func(tftypes.Value) (tftypes.Value, error)
decoder T
}

func (d adaptedDecoder[T]) toPropertyValue(v tftypes.Value) (resource.PropertyValue, error) {
adapted, err := d.adapter(v)
if err != nil {
return resource.PropertyValue{}, fmt.Errorf("failed to adapt for %T: %w", d.decoder, err)
}
return d.decoder.toPropertyValue(adapted)
}

func newIntOverrideStringEncoder() Encoder {
return adaptedEncoder[*numberEncoder]{
adapter: func(v resource.PropertyValue) (resource.PropertyValue, error) {
if v.IsString() {
f, err := strconv.ParseFloat(v.StringValue(), 64)
if err != nil {
return resource.PropertyValue{}, err
}
return resource.NewProperty(f), nil
}
return v, nil
},
encoder: &numberEncoder{},
}
}

func newStringOverIntDecoder() Decoder {
return adaptedDecoder[*stringDecoder]{
adapter: func(v tftypes.Value) (tftypes.Value, error) {
if !v.Type().Is(tftypes.Number) {
return v, nil
}
if !v.IsKnown() {
return tftypes.NewValue(tftypes.String, tftypes.UnknownValue), nil
}
var f big.Float
if err := v.As(&f); err != nil {
return tftypes.Value{}, err
}
return tftypes.NewValue(tftypes.String, f.Text('f', -1)), nil
},
decoder: &stringDecoder{},
}
}
Loading

0 comments on commit c975862

Please sign in to comment.