blob: f15e4e1ca412058dabfc2f40ab71505045a49e22 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package providers
import (
"fmt"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/lang/ephemeral"
"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.
//
// This is distinct from the testing.MockProvider, which is a mock provider
// that is used by the Terraform core itself to test it's own behavior.
type Mock struct {
Provider Interface
Data *configs.MockData
schema *GetProviderSchemaResponse
identitySchema *GetResourceIdentitySchemasResponse
}
func (m *Mock) GetProviderSchema() GetProviderSchemaResponse {
if m.schema == nil {
// Cache the schema, it's not changing.
schema := m.Provider.GetProviderSchema()
// Override the provider schema with the constant mock provider schema.
// This is empty at the moment, check configs/mock_provider.go for the
// actual schema.
//
// The GetProviderSchemaResponse is returned by value, so it should be
// safe for us to modify directly, without affecting any shared state
// that could be in use elsewhere.
schema.Provider = Schema{
Version: schema.Provider.Version,
Body: nil, // Empty - we support no blocks or attributes in mock provider configurations.
}
// Note, we leave the resource and data source schemas as they are since
// we want to be able to validate those configurations against the real
// provider schemas.
m.schema = &schema
}
return *m.schema
}
func (m *Mock) GetResourceIdentitySchemas() GetResourceIdentitySchemasResponse {
if m.identitySchema == nil {
// Cache the schema, it's not changing.
schema := m.Provider.GetResourceIdentitySchemas()
m.identitySchema = &schema
}
return *m.identitySchema
}
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 and we can call
// validate without needing to configure the provider first.
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 and we can
// call validate without needing to configure the provider first.
return m.Provider.ValidateDataResourceConfig(request)
}
func (m *Mock) UpgradeResourceState(request UpgradeResourceStateRequest) (response UpgradeResourceStateResponse) {
// We can't do this from a mocked provider, so we just return whatever state
// is in the request back unchanged.
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))
}
schemaType := resource.Body.ImpliedType()
var value cty.Value
var err error
switch {
case request.RawStateFlatmap != nil:
value, err = hcl2shim.HCL2ValueFromFlatmap(request.RawStateFlatmap, schemaType)
case len(request.RawStateJSON) > 0:
value, err = ctyjson.Unmarshal(request.RawStateJSON, schemaType)
}
if err != nil {
// Generally, we shouldn't get an error here. The mocked providers are
// only used in tests, and we can't use different versions of providers
// within/between tests so the types should always match up. As such,
// we're not gonna return a super detailed error here.
response.Diagnostics = response.Diagnostics.Append(err)
return response
}
response.UpgradedState = ephemeral.StripWriteOnlyAttributes(value, resource.Body)
return response
}
func (m *Mock) UpgradeResourceIdentity(request UpgradeResourceIdentityRequest) (response UpgradeResourceIdentityResponse) {
// We can't do this from a mocked provider, so we just return whatever identity
// is in the request back unchanged.
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 identity schema for resource %s", request.TypeName))
}
schemaType := resource.Identity.ImpliedType()
value, err := ctyjson.Unmarshal(request.RawIdentityJSON, schemaType)
if err != nil {
// Generally, we shouldn't get an error here. The mocked providers are
// only used in tests, and we can't use different versions of providers
// within/between tests so the types should always match up. As such,
// we're not gonna return a super detailed error here.
response.Diagnostics = response.Diagnostics.Append(err)
return response
}
response.UpgradedIdentity = value
return response
}
func (m *Mock) ConfigureProvider(request ConfigureProviderRequest) (response ConfigureProviderResponse) {
// Do nothing here, we don't have anything to configure within the mocked
// providers. We don't want to call the original providers from here as
// they may try to talk to their underlying cloud providers and we
// definitely don't have the right configuration or credentials for this.
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.
return ReadResourceResponse{
NewState: request.PriorState,
Identity: request.CurrentIdentity,
}
}
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"),
}
}
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))
}
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.
replacement := &mocking.MockedData{
Value: cty.NilVal, // If we have no data then we use cty.NilVal.
ComputedAsUnknown: true,
}
// if we are allowed to use the mock defaults for plan, we can populate the computed fields with the mock defaults.
if mockedResource, exists := m.Data.MockResources[request.TypeName]; exists && mockedResource.UseForPlan {
replacement.Value = mockedResource.Defaults
replacement.Range = mockedResource.DefaultsRange
replacement.ComputedAsUnknown = false
}
value, diags := mocking.PlanComputedValuesForResource(request.ProposedNewState, replacement, resource.Body)
response.Diagnostics = response.Diagnostics.Append(diags)
response.PlannedState = ephemeral.StripWriteOnlyAttributes(value, resource.Body)
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.
response.PlannedState = ephemeral.StripWriteOnlyAttributes(request.ProposedNewState, resource.Body)
response.PlannedPrivate = []byte("update")
return response
}
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.MockedData{
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.Body)
response.Diagnostics = response.Diagnostics.Append(diags)
response.NewState = value
response.NewIdentity = request.PlannedIdentity
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,
NewIdentity: request.PlannedIdentity,
}
}
}
func (m *Mock) ImportResourceState(request ImportResourceStateRequest) (response ImportResourceStateResponse) {
// You can't import via mock providers. The users should write specific
// `override_resource` blocks for any resources they want to import, so we
// just make them think about it rather than performing a blanket import
// of all resources that are backed by mock providers.
response.Diagnostics = response.Diagnostics.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid import request", "Cannot import resources from mock providers. Use an `override_resource` block to targeting the specific resource being imported instead."))
return response
}
func (m *Mock) MoveResourceState(request MoveResourceStateRequest) MoveResourceStateResponse {
// The MoveResourceState operation happens offline, so we can just hand this
// off to the underlying provider.
return m.Provider.MoveResourceState(request)
}
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.MockedData{
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.Body)
response.Diagnostics = response.Diagnostics.Append(diags)
response.State = ephemeral.StripWriteOnlyAttributes(value, datasource.Body)
return response
}
func (m *Mock) ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfigRequest) ValidateEphemeralResourceConfigResponse {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return ValidateEphemeralResourceConfigResponse{
Diagnostics: diags,
}
}
func (m *Mock) OpenEphemeralResource(OpenEphemeralResourceRequest) OpenEphemeralResourceResponse {
// FIXME: Design some means to mock an ephemeral resource type.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return OpenEphemeralResourceResponse{
Diagnostics: diags,
}
}
func (m *Mock) RenewEphemeralResource(RenewEphemeralResourceRequest) RenewEphemeralResourceResponse {
// FIXME: Design some means to mock an ephemeral resource type.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return RenewEphemeralResourceResponse{
Diagnostics: diags,
}
}
func (m *Mock) CloseEphemeralResource(CloseEphemeralResourceRequest) CloseEphemeralResourceResponse {
// FIXME: Design some means to mock an ephemeral resource type.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return CloseEphemeralResourceResponse{
Diagnostics: diags,
}
}
func (m *Mock) CallFunction(request CallFunctionRequest) CallFunctionResponse {
return m.Provider.CallFunction(request)
}
func (m *Mock) Close() error {
return m.Provider.Close()
}