terraform test: implement mock providers (#34167)

pull/33037/merge
Liam Cervante 3 years ago committed by GitHub
parent 05f877166d
commit f435706bfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,231 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package providers
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/moduletest/mocking"
"github.com/hashicorp/terraform/internal/tfdiags"
)
var _ Interface = (*Mock)(nil)
// Mock is a mock provider that can be used by Terraform authors during test
// executions.
//
// The mock provider wraps an instance of an actual provider so it can return
// the correct schema and validate the configuration accurately. But, it
// intercepts calls to create resources or read data sources and instead reads
// and write the data to/from the state directly instead of needing to
// communicate with actual cloud providers.
//
// Callers can also specify the configs.MockData field to provide some preset
// data to return for any computed fields within the provider schema. The
// provider will make up random / junk data for any computed fields for which
// preset data is not available.
type Mock struct {
Provider Interface
Data *configs.MockData
schema *GetProviderSchemaResponse
}
func (m *Mock) GetProviderSchema() GetProviderSchemaResponse {
if m.schema == nil {
// Cache the schema, it's not changing.
schema := m.Provider.GetProviderSchema()
m.schema = &schema
}
return *m.schema
}
func (m *Mock) ValidateProviderConfig(request ValidateProviderConfigRequest) (response ValidateProviderConfigResponse) {
// The config for the mocked providers is consistent, and validated when we
// parse the HCL directly. So we'll just make no change here.
return ValidateProviderConfigResponse{
PreparedConfig: request.Config,
}
}
func (m *Mock) ValidateResourceConfig(request ValidateResourceConfigRequest) ValidateResourceConfigResponse {
// We'll just pass this through to the underlying provider. The mock should
// support the same resource syntax as the original provider.
return m.Provider.ValidateResourceConfig(request)
}
func (m *Mock) ValidateDataResourceConfig(request ValidateDataResourceConfigRequest) ValidateDataResourceConfigResponse {
// We'll just pass this through to the underlying provider. The mock should
// support the same data source syntax as the original provider.
return m.Provider.ValidateDataResourceConfig(request)
}
func (m *Mock) UpgradeResourceState(request UpgradeResourceStateRequest) UpgradeResourceStateResponse {
// It's unlikely this will ever be called on a mocked provider, given they
// can only execute from inside tests. But we don't need to anything special
// here, let's just have the original provider handle it.
return m.Provider.UpgradeResourceState(request)
}
func (m *Mock) ConfigureProvider(request ConfigureProviderRequest) (response ConfigureProviderResponse) {
// Do nothing here, we don't have anything to configure within the mocked
// providers and we don't want to call the original providers from here as
// they may try to talk to their underlying cloud providers.
return response
}
func (m *Mock) Stop() error {
// Just stop the original resource.
return m.Provider.Stop()
}
func (m *Mock) ReadResource(request ReadResourceRequest) ReadResourceResponse {
// For a mocked provider, reading a resource is just reading it from the
// state. So we'll return what we have.
// TODO(liamcervante): Can we do more than just say the state of resources
// never changes? What if we recomputed the values, so we can have drift
// if the value in the mocked data has changed?
return ReadResourceResponse{
NewState: request.PriorState,
}
}
func (m *Mock) PlanResourceChange(request PlanResourceChangeRequest) PlanResourceChangeResponse {
if request.ProposedNewState.IsNull() {
// Then we are deleting this resource - we don't need to do anything.
return PlanResourceChangeResponse{
PlannedState: request.ProposedNewState,
PlannedPrivate: []byte("destroy"),
}
}
if request.PriorState.IsNull() {
// Then we are creating this resource - we need to populate the computed
// null fields with unknowns so Terraform will render them properly.
var response PlanResourceChangeResponse
schema := m.GetProviderSchema()
response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics)
if schema.Diagnostics.HasErrors() {
// We couldn't retrieve the schema for some reason, so the mock
// provider can't really function.
return response
}
resource, exists := schema.ResourceTypes[request.TypeName]
if !exists {
// This means something has gone wrong much earlier, we should have
// failed a validation somewhere if a resource type doesn't exist.
panic(fmt.Errorf("failed to retrieve schema for resource %s", request.TypeName))
}
value, diags := mocking.PlanComputedValuesForResource(request.ProposedNewState, resource.Block)
response.Diagnostics = response.Diagnostics.Append(diags)
response.PlannedState = value
response.PlannedPrivate = []byte("create")
return response
}
// Otherwise, we're just doing a simple update and we don't need to populate
// any values for that.
return PlanResourceChangeResponse{
PlannedState: request.ProposedNewState,
PlannedPrivate: []byte("update"),
}
}
func (m *Mock) ApplyResourceChange(request ApplyResourceChangeRequest) ApplyResourceChangeResponse {
switch string(request.PlannedPrivate) {
case "create":
// A new resource that we've created might have computed fields we need
// to populate.
var response ApplyResourceChangeResponse
schema := m.GetProviderSchema()
response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics)
if schema.Diagnostics.HasErrors() {
// We couldn't retrieve the schema for some reason, so the mock
// provider can't really function.
return response
}
resource, exists := schema.ResourceTypes[request.TypeName]
if !exists {
// This means something has gone wrong much earlier, we should have
// failed a validation somewhere if a resource type doesn't exist.
panic(fmt.Errorf("failed to retrieve schema for resource %s", request.TypeName))
}
replacement := mocking.ReplacementValue{
Value: cty.NilVal, // If we have no data then we use cty.NilVal.
}
if mockedResource, exists := m.Data.MockResources[request.TypeName]; exists {
replacement.Value = mockedResource.Defaults
replacement.Range = mockedResource.DefaultsRange
}
value, diags := mocking.ApplyComputedValuesForResource(request.PlannedState, replacement, resource.Block)
response.Diagnostics = response.Diagnostics.Append(diags)
response.NewState = value
return response
default:
// For update or destroy operations, we don't have to create any values
// so we'll just return the planned state directly.
return ApplyResourceChangeResponse{
NewState: request.PlannedState,
}
}
}
func (m *Mock) ImportResourceState(request ImportResourceStateRequest) (response ImportResourceStateResponse) {
// Given mock providers only execute from within the test framework and it
// doesn't make a lot of sense why someone would want to import something
// during a test, we just don't support this at the moment.
// TODO(liamcervante): Find use cases for this? The existing syntax for
// mocks does make this possible but let's find a reason to do it first.
response.Diagnostics = response.Diagnostics.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid import request", "Cannot import resources from mock providers."))
return response
}
func (m *Mock) ReadDataSource(request ReadDataSourceRequest) ReadDataSourceResponse {
var response ReadDataSourceResponse
schema := m.GetProviderSchema()
response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics)
if schema.Diagnostics.HasErrors() {
// We couldn't retrieve the schema for some reason, so the mock
// provider can't really function.
return response
}
datasource, exists := schema.DataSources[request.TypeName]
if !exists {
// This means something has gone wrong much earlier, we should have
// failed a validation somewhere if a data source type doesn't exist.
panic(fmt.Errorf("failed to retrieve schema for data source %s", request.TypeName))
}
mockedData := mocking.ReplacementValue{
Value: cty.NilVal, // If we have no mocked data we use cty.NilVal.
}
if mockedDataSource, exists := m.Data.MockDataSources[request.TypeName]; exists {
mockedData.Value = mockedDataSource.Defaults
mockedData.Range = mockedDataSource.DefaultsRange
}
value, diags := mocking.ComputedValuesForDataSource(request.Config, mockedData, datasource.Block)
response.Diagnostics = response.Diagnostics.Append(diags)
response.State = value
return response
}
func (m *Mock) Close() error {
return m.Provider.Close()
}

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
@ -2324,3 +2325,134 @@ locals {
t.Errorf("expected local value to be \"foo\" but was \"%s\"", module.LocalValues["local_value"].AsString())
}
}
func TestContext2Apply_mockProvider(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
provider "test" {}
data "test_object" "foo" {}
resource "test_object" "foo" {
value = data.test_object.foo.output
}
`,
})
// Manually mark the provider config as being mocked.
m.Module.ProviderConfigs["test"].Mock = true
m.Module.ProviderConfigs["test"].MockData = &configs.MockData{
MockDataSources: map[string]*configs.MockResource{
"test_object": {
Mode: addrs.DataResourceMode,
Type: "test_object",
Defaults: cty.ObjectVal(map[string]cty.Value{
"output": cty.StringVal("expected data output"),
}),
},
},
MockResources: map[string]*configs.MockResource{
"test_object": {
Mode: addrs.ManagedResourceMode,
Type: "test_object",
Defaults: cty.ObjectVal(map[string]cty.Value{
"output": cty.StringVal("expected resource output"),
}),
},
},
}
testProvider := &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_object": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
"output": {
Type: cty.String,
Computed: true,
},
},
},
},
},
DataSources: map[string]providers.Schema{
"test_object": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"output": {
Type: cty.String,
Computed: true,
},
},
},
},
},
},
}
reachedReadDataSourceFn := false
reachedPlanResourceChangeFn := false
reachedApplyResourceChangeFn := false
testProvider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) {
reachedReadDataSourceFn = true
cfg := request.Config.AsValueMap()
cfg["output"] = cty.StringVal("unexpected data output")
resp.State = cty.ObjectVal(cfg)
return resp
}
testProvider.PlanResourceChangeFn = func(request providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
reachedPlanResourceChangeFn = true
cfg := request.Config.AsValueMap()
cfg["output"] = cty.UnknownVal(cty.String)
resp.PlannedState = cty.ObjectVal(cfg)
return resp
}
testProvider.ApplyResourceChangeFn = func(request providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
reachedApplyResourceChangeFn = true
cfg := request.Config.AsValueMap()
cfg["output"] = cty.StringVal("unexpected resource output")
resp.NewState = cty.ObjectVal(cfg)
return resp
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
})
if diags.HasErrors() {
t.Fatalf("expected no errors, but got %s", diags)
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("expected no errors, but got %s", diags)
}
// Check we never made it to the actual provider.
if reachedReadDataSourceFn {
t.Errorf("read the data source in the provider when it should have been mocked")
}
if reachedPlanResourceChangeFn {
t.Errorf("planned the resource in the provider when it should have been mocked")
}
if reachedApplyResourceChangeFn {
t.Errorf("applied the resource in the provider when it should have been mocked")
}
// Check we got the right data back from our mocked provider.
instance := state.ResourceInstance(mustResourceInstanceAddr("test_object.foo"))
expected := "{\"output\":\"expected resource output\",\"value\":\"expected data output\"}"
if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 {
t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff)
}
}

@ -5,8 +5,11 @@ package terraform
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/lang"
@ -16,7 +19,6 @@ import (
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// EvalContext is the interface that is given to eval nodes to execute.
@ -41,7 +43,7 @@ type EvalContext interface {
// It is an error to initialize the same provider more than once. This
// method will panic if the module instance address of the given provider
// configuration does not match the Path() of the EvalContext.
InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error)
InitProvider(addr addrs.AbsProviderConfig, configs *configs.Provider) (providers.Interface, error)
// Provider gets the provider instance with the given address (already
// initialized) or returns nil if the provider isn't initialized.

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/lang"
@ -118,7 +119,7 @@ func (ctx *BuiltinEvalContext) Input() UIInput {
return ctx.InputValue
}
func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) {
func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig, config *configs.Provider) (providers.Interface, error) {
// If we already initialized, it is an error
if p := ctx.Provider(addr); p != nil {
return nil, fmt.Errorf("%s is already initialized", addr)
@ -137,6 +138,17 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (provi
}
log.Printf("[TRACE] BuiltinEvalContext: Initialized %q provider for %s", addr.String(), addr)
// The config might be nil, if there was no config block defined for this
// provider.
if config != nil && config.Mock {
log.Printf("[TRACE] BuiltinEvalContext: Mocked %q provider for %s", addr.String(), addr)
p = &providers.Mock{
Provider: p,
Data: config.MockData,
}
}
ctx.ProviderCache[key] = p
return p, nil

@ -8,9 +8,11 @@ import (
"sync"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/zclconf/go-cty/cty"
)
func TestBuiltinEvalContextProviderInput(t *testing.T) {
@ -75,15 +77,27 @@ func TestBuildingEvalContextInitProvider(t *testing.T) {
Provider: addrs.NewDefaultProvider("test"),
Alias: "foo",
}
providerAddrMock := addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
Alias: "mock",
}
_, err := ctx.InitProvider(providerAddrDefault)
_, err := ctx.InitProvider(providerAddrDefault, nil)
if err != nil {
t.Fatalf("error initializing provider test: %s", err)
}
_, err = ctx.InitProvider(providerAddrAlias)
_, err = ctx.InitProvider(providerAddrAlias, nil)
if err != nil {
t.Fatalf("error initializing provider test.foo: %s", err)
}
_, err = ctx.InitProvider(providerAddrMock, &configs.Provider{
Mock: true,
})
if err != nil {
t.Fatalf("error initializing provider test.mock: %s", err)
}
}
func testBuiltinEvalContext(t *testing.T) *BuiltinEvalContext {

@ -6,8 +6,12 @@ package terraform
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/lang"
@ -17,8 +21,6 @@ import (
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// MockEvalContext is a mock version of EvalContext that can be used
@ -177,7 +179,7 @@ func (c *MockEvalContext) Input() UIInput {
return c.InputInput
}
func (c *MockEvalContext) InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) {
func (c *MockEvalContext) InitProvider(addr addrs.AbsProviderConfig, _ *configs.Provider) (providers.Interface, error) {
c.InitProviderCalled = true
c.InitProviderType = addr.String()
c.InitProviderAddr = addr

@ -8,10 +8,11 @@ import (
"log"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// NodeApplyableProvider represents a provider during an apply.
@ -25,7 +26,7 @@ var (
// GraphNodeExecutable
func (n *NodeApplyableProvider) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
_, err := ctx.InitProvider(n.Addr)
_, err := ctx.InitProvider(n.Addr, n.Config)
diags = diags.Append(err)
if diags.HasErrors() {
return diags

@ -17,6 +17,6 @@ var _ GraphNodeExecutable = (*NodeEvalableProvider)(nil)
// GraphNodeExecutable
func (n *NodeEvalableProvider) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
_, err := ctx.InitProvider(n.Addr)
_, err := ctx.InitProvider(n.Addr, n.Config)
return diags.Append(err)
}

Loading…
Cancel
Save