mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
It seems that a small number of providers are now able to return a special signal when they find that they are unable to perform an operation due to unknown values in the provider or resource configuration. This is a uses that new signal to recommend a workaround in that situation, giving a more actionable error message than would've been returned by the provider otherwise. We've not yet decided how OpenTofu might make use of these new signals in the long term, and so this is intentionally implemented in a way where most of the logic is centralized in the provider-related packages rather than sprawled all over "package tofu". It's likely that a future incarnation of this will plumb this idea in more deeply, but this is just a temporary stop-gap to give slightly better error messages in the meantime and so it's better to keep it relatively contained for now until we have a longer-term plan for what OpenTofu Core might do with this information. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
1006 lines
26 KiB
Go
1006 lines
26 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs/hcl2shim"
|
|
mockproto "github.com/opentofu/opentofu/internal/plugin/mock_proto"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
proto "github.com/opentofu/opentofu/internal/tfplugin5"
|
|
)
|
|
|
|
var _ providers.Interface = (*GRPCProvider)(nil)
|
|
|
|
func mockProviderClient(t *testing.T) *mockproto.MockProviderClient {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
// we always need a GetSchema method
|
|
client.EXPECT().GetSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(providerProtoSchema(), nil)
|
|
|
|
return client
|
|
}
|
|
|
|
func checkDiags(t *testing.T, d tfdiags.Diagnostics) {
|
|
t.Helper()
|
|
if d.HasErrors() {
|
|
t.Fatal(d.Err())
|
|
}
|
|
}
|
|
|
|
// checkDiagsHasError ensures error diagnostics are present or fails the test.
|
|
func checkDiagsHasError(t *testing.T, d tfdiags.Diagnostics) {
|
|
t.Helper()
|
|
|
|
if !d.HasErrors() {
|
|
t.Fatal("expected error diagnostics")
|
|
}
|
|
}
|
|
|
|
func providerProtoSchema() *proto.GetProviderSchema_Response {
|
|
return &proto.GetProviderSchema_Response{
|
|
Provider: &proto.Schema{
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ResourceSchemas: map[string]*proto.Schema{
|
|
"resource": &proto.Schema{
|
|
Version: 1,
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DataSourceSchemas: map[string]*proto.Schema{
|
|
"data": &proto.Schema{
|
|
Version: 1,
|
|
Block: &proto.Schema_Block{
|
|
Attributes: []*proto.Schema_Attribute{
|
|
{
|
|
Name: "attr",
|
|
Type: []byte(`"string"`),
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Functions: map[string]*proto.Function{
|
|
"fn": &proto.Function{
|
|
Parameters: []*proto.Function_Parameter{{
|
|
Name: "par_a",
|
|
Type: []byte(`"string"`),
|
|
AllowNullValue: false,
|
|
AllowUnknownValues: false,
|
|
}},
|
|
VariadicParameter: &proto.Function_Parameter{
|
|
Name: "par_var",
|
|
Type: []byte(`"string"`),
|
|
AllowNullValue: true,
|
|
AllowUnknownValues: false,
|
|
},
|
|
Return: &proto.Function_Return{
|
|
Type: []byte(`"string"`),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_GetSchema(t *testing.T) {
|
|
p := &GRPCProvider{
|
|
client: mockProviderClient(t),
|
|
}
|
|
|
|
resp := p.GetProviderSchema(t.Context())
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
// Ensure that gRPC errors are returned early.
|
|
// Reference: https://github.com/hashicorp/terraform/issues/31047
|
|
func TestGRPCProvider_GetSchema_GRPCError(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetProviderSchema_Response{}, fmt.Errorf("test error"))
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema(t.Context())
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_GetSchema_GlobalCacheEnabled(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
// The SchemaCache is global and is saved between test runs
|
|
providers.SchemaCache = providers.NewMockSchemaCache()
|
|
|
|
providerAddr := addrs.Provider{
|
|
Namespace: "namespace",
|
|
Type: "type",
|
|
}
|
|
|
|
mockedProviderResponse := &proto.Schema{Version: 2, Block: &proto.Schema_Block{}}
|
|
|
|
client.EXPECT().GetSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Times(1).Return(&proto.GetProviderSchema_Response{
|
|
Provider: mockedProviderResponse,
|
|
ServerCapabilities: &proto.ServerCapabilities{GetProviderSchemaOptional: true},
|
|
}, nil)
|
|
|
|
// Run GetProviderTwice, expect GetSchema to be called once
|
|
// Re-initialize the provider before each run to avoid usage of the local cache
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
Addr: providerAddr,
|
|
}
|
|
resp := p.GetProviderSchema(t.Context())
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
|
|
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
|
|
}
|
|
|
|
p = &GRPCProvider{
|
|
client: client,
|
|
Addr: providerAddr,
|
|
}
|
|
resp = p.GetProviderSchema(t.Context())
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
|
|
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_GetSchema_GlobalCacheDisabled(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
// The SchemaCache is global and is saved between test runs
|
|
providers.SchemaCache = providers.NewMockSchemaCache()
|
|
|
|
providerAddr := addrs.Provider{
|
|
Namespace: "namespace",
|
|
Type: "type",
|
|
}
|
|
|
|
mockedProviderResponse := &proto.Schema{Version: 2, Block: &proto.Schema_Block{}}
|
|
|
|
client.EXPECT().GetSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Times(2).Return(&proto.GetProviderSchema_Response{
|
|
Provider: mockedProviderResponse,
|
|
ServerCapabilities: &proto.ServerCapabilities{GetProviderSchemaOptional: false},
|
|
}, nil)
|
|
|
|
// Run GetProviderTwice, expect GetSchema to be called once
|
|
// Re-initialize the provider before each run to avoid usage of the local cache
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
Addr: providerAddr,
|
|
}
|
|
resp := p.GetProviderSchema(t.Context())
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
|
|
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
|
|
}
|
|
|
|
p = &GRPCProvider{
|
|
client: client,
|
|
Addr: providerAddr,
|
|
}
|
|
resp = p.GetProviderSchema(t.Context())
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
if !cmp.Equal(resp.Provider.Version, mockedProviderResponse.Version) {
|
|
t.Fatal(cmp.Diff(resp.Provider.Version, mockedProviderResponse.Version))
|
|
}
|
|
}
|
|
|
|
// Ensure that provider error diagnostics are returned early.
|
|
// Reference: https://github.com/hashicorp/terraform/issues/31047
|
|
func TestGRPCProvider_GetSchema_ResponseErrorDiagnostic(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
|
|
client.EXPECT().GetSchema(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.GetProviderSchema_Response{
|
|
Diagnostics: []*proto.Diagnostic{
|
|
{
|
|
Severity: proto.Diagnostic_ERROR,
|
|
Summary: "error summary",
|
|
Detail: "error detail",
|
|
},
|
|
},
|
|
// Trigger potential panics
|
|
Provider: &proto.Schema{},
|
|
}, nil)
|
|
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
resp := p.GetProviderSchema(t.Context())
|
|
|
|
checkDiagsHasError(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_PrepareProviderConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().PrepareProviderConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PrepareProviderConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
|
|
resp := p.ValidateProviderConfig(t.Context(), providers.ValidateProviderConfigRequest{Config: cfg})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateResourceConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ValidateResourceTypeConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateResourceTypeConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
|
|
resp := p.ValidateResourceConfig(t.Context(), providers.ValidateResourceConfigRequest{
|
|
TypeName: "resource",
|
|
Config: cfg,
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_ValidateDataSourceConfig(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ValidateDataSourceConfig(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ValidateDataSourceConfig_Response{}, nil)
|
|
|
|
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
|
|
resp := p.ValidateDataResourceConfig(t.Context(), providers.ValidateDataResourceConfigRequest{
|
|
TypeName: "data",
|
|
Config: cfg,
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_UpgradeResourceState(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().UpgradeResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.UpgradeResourceState_Response{
|
|
UpgradedState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.UpgradeResourceState(t.Context(), providers.UpgradeResourceStateRequest{
|
|
TypeName: "resource",
|
|
Version: 0,
|
|
RawStateJSON: []byte(`{"old_attr":"bar"}`),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().UpgradeResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.UpgradeResourceState_Response{
|
|
UpgradedState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.UpgradeResourceState(t.Context(), providers.UpgradeResourceStateRequest{
|
|
TypeName: "resource",
|
|
Version: 0,
|
|
RawStateJSON: []byte(`{"old_attr":"bar"}`),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_MoveResourceState(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().MoveResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.MoveResourceState_Response{
|
|
TargetState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
TargetPrivate: []byte(`{"meta": "data"}`),
|
|
}, nil)
|
|
|
|
resp := p.MoveResourceState(t.Context(), providers.MoveResourceStateRequest{
|
|
SourceTypeName: "resource_old",
|
|
SourceSchemaVersion: 0,
|
|
TargetTypeName: "resource",
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
if !cmp.Equal(expectedState, resp.TargetState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.TargetState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
if !bytes.Equal(expectedPrivate, resp.TargetPrivate) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.TargetPrivate)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_Configure(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().Configure(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.Configure_Response{}, nil)
|
|
|
|
resp := p.ConfigureProvider(t.Context(), providers.ConfigureProviderRequest{
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
checkDiags(t, resp.Diagnostics)
|
|
}
|
|
|
|
func TestGRPCProvider_Stop(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
client := mockproto.NewMockProviderClient(ctrl)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().Stop(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.Stop_Response{}, nil)
|
|
|
|
err := p.Stop(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadResource(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadResource(t.Context(), providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadResourceJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadResource(t.Context(), providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadEmptyJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadResource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadResource_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Json: []byte(``),
|
|
},
|
|
}, nil)
|
|
|
|
obj := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
})
|
|
resp := p.ReadResource(t.Context(), providers.ReadResourceRequest{
|
|
TypeName: "resource",
|
|
PriorState: obj,
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.NullVal(obj.Type())
|
|
|
|
if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_PlanResourceChange(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().PlanResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanResourceChange_Response{
|
|
PlannedState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
RequiresReplace: []*proto.AttributePath{
|
|
{
|
|
Steps: []*proto.AttributePath_Step{
|
|
{
|
|
Selector: &proto.AttributePath_Step_AttributeName{
|
|
AttributeName: "attr",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
PlannedPrivate: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.PlanResourceChange(t.Context(), providers.PlanResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
ProposedNewState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}`
|
|
replace := fmt.Sprintf("%#v", resp.RequiresReplace)
|
|
if expectedReplace != replace {
|
|
t.Fatalf("expected %q, got %q", expectedReplace, replace)
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_PlanResourceChange_deferred(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().PlanResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanResourceChange_Response{
|
|
PlannedState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Deferred: &proto.Deferred{
|
|
Reason: proto.Deferred_PROVIDER_CONFIG_UNKNOWN,
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.PlanResourceChange(t.Context(), providers.PlanResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
ProposedNewState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
})
|
|
|
|
if len(resp.Diagnostics) != 1 {
|
|
t.Fatal("wrong number of diagnostics; want one\n" + spew.Sdump(resp.Diagnostics))
|
|
}
|
|
desc := resp.Diagnostics[0].Description()
|
|
if got, want := desc.Summary, `Provider configuration is incomplete`; got != want {
|
|
t.Errorf("wrong error summary\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if got, want := desc.Detail, `The provider was unable to work with this resource because the associated provider configuration makes use of values from other resources that will not be known until after apply.`; got != want {
|
|
t.Errorf("wrong error detail\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if !providers.IsDeferralDiagnostic(resp.Diagnostics[0]) {
|
|
t.Errorf("diagnostic is not marked as being a \"deferral diagnostic\"")
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_PlanResourceChangeJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().PlanResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.PlanResourceChange_Response{
|
|
PlannedState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
RequiresReplace: []*proto.AttributePath{
|
|
{
|
|
Steps: []*proto.AttributePath_Step{
|
|
{
|
|
Selector: &proto.AttributePath_Step_AttributeName{
|
|
AttributeName: "attr",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
PlannedPrivate: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.PlanResourceChange(t.Context(), providers.PlanResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
ProposedNewState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}`
|
|
replace := fmt.Sprintf("%#v", resp.RequiresReplace)
|
|
if expectedReplace != replace {
|
|
t.Fatalf("expected %q, got %q", expectedReplace, replace)
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ApplyResourceChange(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ApplyResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ApplyResourceChange_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Private: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.ApplyResourceChange(t.Context(), providers.ApplyResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
PlannedPrivate: expectedPrivate,
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.Private) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private)
|
|
}
|
|
}
|
|
func TestGRPCProvider_ApplyResourceChangeJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ApplyResourceChange(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ApplyResourceChange_Response{
|
|
NewState: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
Private: expectedPrivate,
|
|
}, nil)
|
|
|
|
resp := p.ApplyResourceChange(t.Context(), providers.ApplyResourceChangeRequest{
|
|
TypeName: "resource",
|
|
PriorState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
PlannedPrivate: expectedPrivate,
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedState := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
|
|
if !bytes.Equal(expectedPrivate, resp.Private) {
|
|
t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private)
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ImportResourceState(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ImportResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ImportResourceState_Response{
|
|
ImportedResources: []*proto.ImportResourceState_ImportedResource{
|
|
{
|
|
TypeName: "resource",
|
|
State: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
Private: expectedPrivate,
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ImportResourceState(t.Context(), providers.ImportResourceStateRequest{
|
|
TypeName: "resource",
|
|
ID: "foo",
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedResource := providers.ImportedResource{
|
|
TypeName: "resource",
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Private: expectedPrivate,
|
|
}
|
|
|
|
imported := resp.ImportedResources[0]
|
|
if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
expectedPrivate := []byte(`{"meta": "data"}`)
|
|
|
|
client.EXPECT().ImportResourceState(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ImportResourceState_Response{
|
|
ImportedResources: []*proto.ImportResourceState_ImportedResource{
|
|
{
|
|
TypeName: "resource",
|
|
State: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
Private: expectedPrivate,
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ImportResourceState(t.Context(), providers.ImportResourceStateRequest{
|
|
TypeName: "resource",
|
|
ID: "foo",
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expectedResource := providers.ImportedResource{
|
|
TypeName: "resource",
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
}),
|
|
Private: expectedPrivate,
|
|
}
|
|
|
|
imported := resp.ImportedResources[0]
|
|
if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadDataSource(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadDataSource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadDataSource_Response{
|
|
State: &proto.DynamicValue{
|
|
Msgpack: []byte("\x81\xa4attr\xa3bar"),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadDataSource(t.Context(), providers.ReadDataSourceRequest{
|
|
TypeName: "data",
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_ReadDataSourceJSON(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().ReadDataSource(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.ReadDataSource_Response{
|
|
State: &proto.DynamicValue{
|
|
Json: []byte(`{"attr":"bar"}`),
|
|
},
|
|
}, nil)
|
|
|
|
resp := p.ReadDataSource(t.Context(), providers.ReadDataSourceRequest{
|
|
TypeName: "data",
|
|
Config: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("foo"),
|
|
}),
|
|
})
|
|
|
|
checkDiags(t, resp.Diagnostics)
|
|
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.StringVal("bar"),
|
|
})
|
|
|
|
if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) {
|
|
t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty))
|
|
}
|
|
}
|
|
|
|
func TestGRPCProvider_CallFunction(t *testing.T) {
|
|
client := mockProviderClient(t)
|
|
p := &GRPCProvider{
|
|
client: client,
|
|
}
|
|
|
|
client.EXPECT().CallFunction(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&proto.CallFunction_Response{
|
|
Result: &proto.DynamicValue{Json: []byte(`"foo"`)},
|
|
}, nil)
|
|
|
|
resp := p.CallFunction(t.Context(), providers.CallFunctionRequest{
|
|
Name: "fn",
|
|
Arguments: []cty.Value{cty.StringVal("bar"), cty.NilVal},
|
|
})
|
|
|
|
if resp.Error != nil {
|
|
t.Fatal(resp.Error)
|
|
}
|
|
if resp.Result != cty.StringVal("foo") {
|
|
t.Fatalf("%v", resp.Result)
|
|
}
|
|
}
|