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() }