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