mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
476 lines
18 KiB
476 lines
18 KiB
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) ValidateListResourceConfig(request ValidateListResourceConfigRequest) ValidateListResourceConfigResponse {
|
|
// 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.ValidateListResourceConfig(request)
|
|
}
|
|
|
|
func (m *Mock) ValidateActionConfig(request ValidateActionConfigRequest) ValidateActionConfigResponse {
|
|
// 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.ValidateActionConfig(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) GenerateResourceConfig(request GenerateResourceConfigRequest) (response GenerateResourceConfigResponse) {
|
|
panic("not implemented")
|
|
}
|
|
|
|
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) ListResource(request ListResourceRequest) ListResourceResponse {
|
|
return m.Provider.ListResource(request)
|
|
}
|
|
|
|
func (m *Mock) ValidateStateStoreConfig(req ValidateStateStoreConfigRequest) ValidateStateStoreConfigResponse {
|
|
return m.Provider.ValidateStateStoreConfig(req)
|
|
}
|
|
|
|
func (m *Mock) ConfigureStateStore(req ConfigureStateStoreRequest) ConfigureStateStoreResponse {
|
|
return m.Provider.ConfigureStateStore(req)
|
|
}
|
|
|
|
func (m *Mock) ReadStateBytes(req ReadStateBytesRequest) ReadStateBytesResponse {
|
|
return m.Provider.ReadStateBytes(req)
|
|
}
|
|
|
|
func (m *Mock) WriteStateBytes(req WriteStateBytesRequest) WriteStateBytesResponse {
|
|
return m.Provider.WriteStateBytes(req)
|
|
}
|
|
|
|
func (m *Mock) LockState(req LockStateRequest) LockStateResponse {
|
|
return m.Provider.LockState(req)
|
|
}
|
|
|
|
func (m *Mock) UnlockState(req UnlockStateRequest) UnlockStateResponse {
|
|
return m.Provider.UnlockState(req)
|
|
}
|
|
|
|
func (m *Mock) GetStates(req GetStatesRequest) GetStatesResponse {
|
|
return m.Provider.GetStates(req)
|
|
}
|
|
|
|
func (m *Mock) DeleteState(req DeleteStateRequest) DeleteStateResponse {
|
|
return m.Provider.DeleteState(req)
|
|
}
|
|
|
|
func (m *Mock) PlanAction(request PlanActionRequest) PlanActionResponse {
|
|
return PlanActionResponse{}
|
|
}
|
|
|
|
func (m *Mock) InvokeAction(request InvokeActionRequest) InvokeActionResponse {
|
|
return InvokeActionResponse{
|
|
Events: func(yield func(InvokeActionEvent) bool) {
|
|
yield(InvokeActionEvent_Completed{})
|
|
},
|
|
Diagnostics: nil,
|
|
}
|
|
}
|
|
|
|
func (m *Mock) Close() error {
|
|
return m.Provider.Close()
|
|
}
|