Files
opentf/internal/tofu/node_resource_abstract_test.go
Martin Atkins 32082321bf providers: Interface now requires context.Context arguments
Continuing our work to gradually plumb context.Context to everywhere that
we want to generate OpenTelemetry traces, this completes the call path
for most (but not all) of the gRPC requests to provider plugins, so that
we can add OpenTelemetry trace instrumentation in a future commit.

Unfortunately there are still a few providers.Interface callers left in
functions that don't have context.Context plumbed to them yet, and so
those are temporarily stubbed as context.TODO() here so we can more easily
find and complete them later.

The two gRPC implementations of providers.Interface were previously making
provider requests using a single context.Context established at the time
the provider process was started, but that isn't an appropriate context
to use for per-request concerns like tracing, so that context is now
unused and could potentially be removed in a future commit, but this change
already got pretty large and so I intend to deal with that separately
later.

This now exposes the gRPC provider calls to potential context cancellation
that they would previously observe only indirectly though the Stop method.
Since Stop is primarily used for graceful shutdown of ApplyResourceChange,
the changes here explicitly disconnect the cancellation signal for
ApplyResourceChange in particular, while letting the others get canceled
in the normal way since they are expected to be free of significant
side-effects. In future work we could consider removing Stop from the
internal API entirely and keeping it only as an implementation detail of
the gRPC implementation of this interface, with ApplyResourceChange
directly reacting to context cancellation and sending the gRPC Stop call
itself, but again that's too much change for this already-large commit.

The internal/legacy package currently contains some legacy code preserved
for the benefit of the backends, and unfortunately contains more than is
strictly necessary to support those callers, and so there was some dead
code there that also needed updating. provider_mock.go is removed entirely
because it's just an older copy of the similar file in package tofu. The
few calls to providers in schemas.go are updated to use
context.Background() rather than context.TODO() because we have no
intention of plumbing context.Context into that legacy code, and will
hopefully just delete it wholesale one day.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-05-23 08:58:23 -07:00

412 lines
14 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 tofu
import (
"fmt"
"testing"
"github.com/opentofu/opentofu/internal/refactoring"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states"
"github.com/zclconf/go-cty/cty"
)
func TestNodeAbstractResourceProvider(t *testing.T) {
tests := []struct {
Addr addrs.ConfigResource
Config *configs.Resource
Want addrs.Provider
}{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "baz",
}.InModule(addrs.RootModule),
Want: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "null",
},
},
{
Addr: addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "terraform_remote_state",
Name: "baz",
}.InModule(addrs.RootModule),
Want: addrs.Provider{
// As a special case, the type prefix "terraform_" maps to
// the builtin provider, not the default one.
Hostname: addrs.BuiltInProviderHost,
Namespace: addrs.BuiltInProviderNamespace,
Type: "terraform",
},
},
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "baz",
}.InModule(addrs.RootModule),
Config: &configs.Resource{
// Just enough configs.Resource for the Provider method. Not
// actually valid for general use.
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "awesomecorp",
Type: "happycloud",
},
},
// The config overrides the default behavior.
Want: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "awesomecorp",
Type: "happycloud",
},
},
{
Addr: addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "terraform_remote_state",
Name: "baz",
}.InModule(addrs.RootModule),
Config: &configs.Resource{
// Just enough configs.Resource for the Provider method. Not
// actually valid for general use.
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "awesomecorp",
Type: "happycloud",
},
},
// The config overrides the default behavior.
Want: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "awesomecorp",
Type: "happycloud",
},
},
}
for _, test := range tests {
var name string
if test.Config != nil {
name = fmt.Sprintf("%s with configured %s", test.Addr, test.Config.Provider)
} else {
name = fmt.Sprintf("%s with no configuration", test.Addr)
}
t.Run(name, func(t *testing.T) {
node := &NodeAbstractResource{
// Just enough NodeAbstractResource for the Provider function.
// (This would not be valid for some other functions.)
Addr: test.Addr,
Config: test.Config,
}
got := node.Provider()
if got != test.Want {
t.Errorf("wrong result\naddr: %s\nconfig: %#v\ngot: %s\nwant: %s", test.Addr, test.Config, got, test.Want)
}
})
}
}
// Make sure ProvideBy returns the final resolved provider
func TestNodeAbstractResourceSetProvider(t *testing.T) {
node := &NodeAbstractResource{
// Just enough NodeAbstractResource for the Provider function.
// (This would not be valid for some other functions.)
Addr: addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "terraform_remote_state",
Name: "baz",
}.InModule(addrs.RootModule),
Config: &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_remote_state",
Name: "baz",
// Just enough configs.Resource for the Provider method. Not
// actually valid for general use.
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "awesomecorp",
Type: "happycloud",
},
},
}
p := node.ProvidedBy()
// the implied non-exact provider should be "terraform"
lpc, ok := p.ProviderConfig.(addrs.LocalProviderConfig)
if !ok {
t.Fatalf("expected LocalProviderConfig, got %#v\n", p)
}
if lpc.LocalName != "terraform" {
t.Fatalf("expected non-exact provider of 'terraform', got %q", lpc.LocalName)
}
// now set a resolved provider for the resource
resolved := addrs.AbsProviderConfig{
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "awesomecorp",
Type: "happycloud",
},
Module: addrs.RootModule,
Alias: "test",
}
node.SetProvider(ResolvedProvider{ProviderConfig: resolved})
p = node.ProvidedBy()
apc, ok := p.ProviderConfig.(addrs.AbsProviderConfig)
if !ok {
t.Fatalf("expected AbsProviderConfig, got %#v\n", p)
}
if apc.String() != resolved.String() {
t.Fatalf("incorrect resolved config: got %#v, wanted %#v\n", apc, resolved)
}
}
type readResourceInstanceStateTest struct {
Name string
State *states.State
Node *NodeAbstractResourceInstance
MoveResults refactoring.MoveResults
Provider *MockProvider
ExpectedInstanceId string
WantErrorStr string
}
func getMockProviderForReadResourceInstanceState() *MockProvider {
return &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"aws_instance": constructProviderSchemaForTesting(map[string]*configschema.Attribute{
"id": {
Type: cty.String,
},
}),
"aws_instance0": constructProviderSchemaForTesting(map[string]*configschema.Attribute{
"id": {
Type: cty.String,
},
}),
},
},
}
}
func getReadResourceInstanceStateTests(stateBuilder func(s *states.SyncState)) []readResourceInstanceStateTest {
mockProvider := getMockProviderForReadResourceInstanceState()
mockProviderWithStateChange := getMockProviderForReadResourceInstanceState()
// Changes id to i-abc1234
mockProviderWithStateChange.MoveResourceStateResponse = &providers.MoveResourceStateResponse{
TargetState: cty.ObjectVal(map[string]cty.Value{"id": cty.StringVal("i-abc1234")}),
}
mockProviderWithMoveUnsupported := getMockProviderForReadResourceInstanceState()
mockProviderWithMoveUnsupported.MoveResourceStateFn = func(req providers.MoveResourceStateRequest) providers.MoveResourceStateResponse {
return providers.MoveResourceStateResponse{
Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "move not supported", "")},
}
}
// This test does not configure the provider, but the mock provider will
// check that this was called and report errors.
mockProvider.ConfigureProviderCalled = true
mockProviderWithStateChange.ConfigureProviderCalled = true
mockProviderWithMoveUnsupported.ConfigureProviderCalled = true
tests := []readResourceInstanceStateTest{
{
Name: "gets primary instance state",
Provider: mockProvider,
State: states.BuildState(stateBuilder),
Node: &NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: ResolvedProvider{ProviderConfig: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`)},
},
// Otherwise prevRunAddr fails, since we have no current Addr in the state
Addr: mustResourceInstanceAddr("aws_instance.bar"),
},
ExpectedInstanceId: "i-abc123",
},
{
Name: "resource moved to another type without state change",
Provider: mockProvider,
MoveResults: refactoring.MoveResults{
Changes: addrs.MakeMap[addrs.AbsResourceInstance, refactoring.MoveSuccess](
addrs.MakeMapElem(mustResourceInstanceAddr("aws_instance.bar"), refactoring.MoveSuccess{
From: mustResourceInstanceAddr("aws_instance0.baz"),
To: mustResourceInstanceAddr("aws_instance.bar"),
})),
},
State: states.BuildState(stateBuilder),
Node: &NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: ResolvedProvider{ProviderConfig: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`)},
},
// Otherwise prevRunAddr fails, since we have no current Addr in the state
Addr: mustResourceInstanceAddr("aws_instance.bar"),
},
ExpectedInstanceId: "i-abc123",
},
{
Name: "resource moved to another type with state change",
Provider: mockProviderWithStateChange,
MoveResults: refactoring.MoveResults{
Changes: addrs.MakeMap[addrs.AbsResourceInstance, refactoring.MoveSuccess](
addrs.MakeMapElem(mustResourceInstanceAddr("aws_instance.bar"), refactoring.MoveSuccess{
From: mustResourceInstanceAddr("aws_instance0.baz"),
To: mustResourceInstanceAddr("aws_instance.bar"),
})),
},
State: states.BuildState(stateBuilder),
Node: &NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: ResolvedProvider{ProviderConfig: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`)},
},
// Otherwise prevRunAddr fails, since we have no current Addr in the state
Addr: mustResourceInstanceAddr("aws_instance.bar"),
},
// The state change should have been applied
ExpectedInstanceId: "i-abc1234",
},
{
Name: "resource moved to another type but move not supported by provider",
Provider: mockProviderWithMoveUnsupported,
MoveResults: refactoring.MoveResults{
Changes: addrs.MakeMap[addrs.AbsResourceInstance, refactoring.MoveSuccess](
addrs.MakeMapElem(mustResourceInstanceAddr("aws_instance.bar"), refactoring.MoveSuccess{
From: mustResourceInstanceAddr("aws_instance0.baz"),
}),
),
},
State: states.BuildState(stateBuilder),
Node: &NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: ResolvedProvider{ProviderConfig: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`)},
},
Addr: mustResourceInstanceAddr("aws_instance.bar"),
},
WantErrorStr: "move not supported",
},
}
return tests
}
// TestNodeAbstractResource_ReadResourceInstanceState tests the readResourceInstanceState and readResourceInstanceStateDeposed methods of NodeAbstractResource.
// Those are quite similar in behavior, so we can test them together.
func TestNodeAbstractResource_ReadResourceInstanceState(t *testing.T) {
tests := getReadResourceInstanceStateTests(func(s *states.SyncState) {
providerAddr := addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
}
oneAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Absolute(addrs.RootModuleInstance)
s.SetResourceProvider(oneAddr, providerAddr)
s.SetResourceInstanceCurrent(oneAddr.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
}, providerAddr, addrs.NoKey)
})
for _, test := range tests {
t.Run("ReadState "+test.Name, func(t *testing.T) {
evalCtx := new(MockEvalContext)
evalCtx.StateState = test.State.SyncWrapper()
evalCtx.PathPath = addrs.RootModuleInstance
evalCtx.ProviderSchemaSchema = test.Provider.GetProviderSchema(t.Context())
evalCtx.MoveResultsResults = test.MoveResults
evalCtx.ProviderProvider = providers.Interface(test.Provider)
got, readDiags := test.Node.readResourceInstanceState(t.Context(), evalCtx, test.Node.Addr)
if test.WantErrorStr != "" {
if !readDiags.HasErrors() {
t.Fatalf("[%s] Expected error, got none", test.Name)
}
if readDiags.Err().Error() != test.WantErrorStr {
t.Fatalf("[%s] Expected error %q, got %q", test.Name, test.WantErrorStr, readDiags.Err().Error())
}
return
}
if readDiags.HasErrors() {
t.Fatalf("[%s] Got err: %#v", test.Name, readDiags.Err())
}
expected := test.ExpectedInstanceId
if got == nil || got.Value.GetAttr("id") != cty.StringVal(expected) {
t.Fatalf("[%s] Expected output with ID %#v, got: %#v", test.Name, expected, got)
}
})
}
// Deposed tests
deposedTests := getReadResourceInstanceStateTests(func(s *states.SyncState) {
providerAddr := addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
}
oneAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Absolute(addrs.RootModuleInstance)
s.SetResourceProvider(oneAddr, providerAddr)
s.SetResourceInstanceDeposed(oneAddr.Instance(addrs.NoKey), "00000001", &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
}, providerAddr, addrs.NoKey)
})
for _, test := range deposedTests {
t.Run("ReadStateDeposed "+test.Name, func(t *testing.T) {
evalCtx := new(MockEvalContext)
evalCtx.StateState = test.State.SyncWrapper()
evalCtx.PathPath = addrs.RootModuleInstance
evalCtx.ProviderSchemaSchema = test.Provider.GetProviderSchema(t.Context())
evalCtx.MoveResultsResults = test.MoveResults
evalCtx.ProviderProvider = providers.Interface(test.Provider)
key := states.DeposedKey("00000001") // shim from legacy state assigns 0th deposed index this key
got, readDiags := test.Node.readResourceInstanceStateDeposed(t.Context(), evalCtx, test.Node.Addr, key)
if test.WantErrorStr != "" {
if !readDiags.HasErrors() {
t.Fatalf("[%s] Expected error, got none", test.Name)
}
if readDiags.Err().Error() != test.WantErrorStr {
t.Fatalf("[%s] Expected error %q, got %q", test.Name, test.WantErrorStr, readDiags.Err().Error())
}
return
}
if readDiags.HasErrors() {
t.Fatalf("[%s] Got err: %#v", test.Name, readDiags.Err())
}
expected := test.ExpectedInstanceId
if got == nil || got.Value.GetAttr("id") != cty.StringVal(expected) {
t.Fatalf("[%s] Expected output with ID %#v, got: %#v", test.Name, expected, got)
}
})
}
}