mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
457 lines
14 KiB
Go
457 lines
14 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package testing
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/providers/testing"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
var (
|
|
ProviderSchema = &providers.GetProviderSchemaResponse{
|
|
Provider: providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"data_prefix": {Type: cty.String, Optional: true},
|
|
"resource_prefix": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_resource": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
"interrupt_count": {Type: cty.Number, Optional: true},
|
|
"destroy_fail": {Type: cty.Bool, Optional: true, Computed: true},
|
|
"create_wait_seconds": {Type: cty.Number, Optional: true},
|
|
"destroy_wait_seconds": {Type: cty.Number, Optional: true},
|
|
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},
|
|
"defer": {Type: cty.Bool, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DataSources: map[string]providers.Schema{
|
|
"test_data_source": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Required: true},
|
|
"value": {Type: cty.String, Computed: true},
|
|
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},
|
|
|
|
// We never actually reference these values from a data
|
|
// source, but we have tests that use the same cty.Value
|
|
// to represent a test_resource and a test_data_source
|
|
// so the schemas have to match.
|
|
|
|
"interrupt_count": {Type: cty.Number, Computed: true},
|
|
"destroy_fail": {Type: cty.Bool, Computed: true},
|
|
"create_wait_seconds": {Type: cty.Number, Computed: true},
|
|
"destroy_wait_seconds": {Type: cty.Number, Computed: true},
|
|
"defer": {Type: cty.Bool, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
EphemeralResourceTypes: map[string]providers.Schema{
|
|
"test_ephemeral_resource": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"value": {
|
|
Type: cty.String,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Functions: map[string]providers.FunctionDecl{
|
|
"is_true": {
|
|
Parameters: []providers.FunctionParam{
|
|
{
|
|
Name: "input",
|
|
Type: cty.Bool,
|
|
AllowNullValue: false,
|
|
AllowUnknownValues: false,
|
|
},
|
|
},
|
|
ReturnType: cty.Bool,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// TestProvider is a wrapper around terraform.MockProvider that defines dynamic
|
|
// schemas, and keeps track of the resources and data sources that it contains.
|
|
type TestProvider struct {
|
|
Provider *testing.MockProvider
|
|
|
|
data, resource cty.Value
|
|
|
|
Interrupt chan<- struct{}
|
|
|
|
Store *ResourceStore
|
|
}
|
|
|
|
// NewProvider creates a new TestProvider for use in tests.
|
|
//
|
|
// If you provide an empty or nil *ResourceStore argument this is equivalent to the provider
|
|
// not having provisioned any remote objects prior to the test's events.
|
|
//
|
|
// If you provide a *ResourceStore containing values, those cty.Values represent remote objects
|
|
// that the provider has 'already' provisioned and can return information about immediately in a test.
|
|
func NewProvider(store *ResourceStore) *TestProvider {
|
|
if store == nil {
|
|
store = &ResourceStore{
|
|
Data: make(map[string]cty.Value),
|
|
}
|
|
}
|
|
|
|
provider := &TestProvider{
|
|
Provider: new(testing.MockProvider),
|
|
Store: store,
|
|
}
|
|
|
|
provider.Provider.GetProviderSchemaResponse = ProviderSchema
|
|
provider.Provider.ConfigureProviderFn = provider.ConfigureProvider
|
|
provider.Provider.PlanResourceChangeFn = provider.PlanResourceChange
|
|
provider.Provider.ApplyResourceChangeFn = provider.ApplyResourceChange
|
|
provider.Provider.ReadResourceFn = provider.ReadResource
|
|
provider.Provider.ReadDataSourceFn = provider.ReadDataSource
|
|
provider.Provider.CallFunctionFn = provider.CallFunction
|
|
provider.Provider.OpenEphemeralResourceFn = provider.OpenEphemeralResource
|
|
provider.Provider.CloseEphemeralResourceFn = provider.CloseEphemeralResource
|
|
|
|
return provider
|
|
}
|
|
|
|
func (provider *TestProvider) DataPrefix() string {
|
|
var prefix string
|
|
if !provider.data.IsNull() && provider.data.IsKnown() {
|
|
prefix = provider.data.AsString()
|
|
}
|
|
return prefix
|
|
}
|
|
|
|
func (provider *TestProvider) SetDataPrefix(prefix string) {
|
|
provider.data = cty.StringVal(prefix)
|
|
}
|
|
|
|
func (provider *TestProvider) GetDataKey(id string) string {
|
|
if !provider.data.IsNull() && provider.data.IsKnown() {
|
|
return path.Join(provider.data.AsString(), id)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func (provider *TestProvider) ResourcePrefix() string {
|
|
var prefix string
|
|
if !provider.resource.IsNull() && provider.resource.IsKnown() {
|
|
prefix = provider.resource.AsString()
|
|
}
|
|
return prefix
|
|
}
|
|
|
|
func (provider *TestProvider) SetResourcePrefix(prefix string) {
|
|
provider.resource = cty.StringVal(prefix)
|
|
}
|
|
|
|
func (provider *TestProvider) GetResourceKey(id string) string {
|
|
if !provider.resource.IsNull() && provider.resource.IsKnown() {
|
|
return path.Join(provider.resource.AsString(), id)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func (provider *TestProvider) ResourceString() string {
|
|
return provider.string(provider.ResourcePrefix())
|
|
}
|
|
|
|
func (provider *TestProvider) ResourceCount() int {
|
|
return provider.count(provider.ResourcePrefix())
|
|
}
|
|
|
|
func (provider *TestProvider) DataSourceString() string {
|
|
return provider.string(provider.DataPrefix())
|
|
}
|
|
|
|
func (provider *TestProvider) DataSourceCount() int {
|
|
return provider.count(provider.DataPrefix())
|
|
}
|
|
|
|
func (provider *TestProvider) count(prefix string) int {
|
|
defer provider.Store.beginRead()()
|
|
|
|
if len(prefix) == 0 {
|
|
return len(provider.Store.Data)
|
|
}
|
|
|
|
count := 0
|
|
for key := range provider.Store.Data {
|
|
if strings.HasPrefix(key, prefix) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (provider *TestProvider) string(prefix string) string {
|
|
defer provider.Store.beginRead()()
|
|
|
|
var keys []string
|
|
for key := range provider.Store.Data {
|
|
if strings.HasPrefix(key, prefix) {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
return strings.Join(keys, ", ")
|
|
}
|
|
|
|
func (provider *TestProvider) ConfigureProvider(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
|
|
provider.resource = request.Config.GetAttr("resource_prefix")
|
|
provider.data = request.Config.GetAttr("data_prefix")
|
|
return providers.ConfigureProviderResponse{}
|
|
}
|
|
|
|
func (provider *TestProvider) PlanResourceChange(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
if request.ProposedNewState.IsNull() {
|
|
|
|
var deferred *providers.Deferred
|
|
if shouldBeDeferred := request.PriorState.GetAttr("defer"); !shouldBeDeferred.IsNull() && shouldBeDeferred.True() {
|
|
deferred = &providers.Deferred{
|
|
Reason: providers.DeferredReasonResourceConfigUnknown,
|
|
}
|
|
}
|
|
|
|
// Then this is a delete operation.
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: request.ProposedNewState,
|
|
Deferred: deferred,
|
|
}
|
|
}
|
|
|
|
resource := request.ProposedNewState
|
|
if id := resource.GetAttr("id"); !id.IsKnown() || id.IsNull() {
|
|
vals := resource.AsValueMap()
|
|
vals["id"] = cty.UnknownVal(cty.String)
|
|
resource = cty.ObjectVal(vals)
|
|
}
|
|
|
|
if destroyFail := resource.GetAttr("destroy_fail"); !destroyFail.IsKnown() || destroyFail.IsNull() {
|
|
vals := resource.AsValueMap()
|
|
vals["destroy_fail"] = cty.UnknownVal(cty.Bool)
|
|
resource = cty.ObjectVal(vals)
|
|
}
|
|
|
|
if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() {
|
|
vals := resource.AsValueMap()
|
|
vals["write_only"] = cty.NullVal(cty.String)
|
|
resource = cty.ObjectVal(vals)
|
|
}
|
|
|
|
var deferred *providers.Deferred
|
|
if shouldBeDeferred := resource.GetAttr("defer"); !shouldBeDeferred.IsKnown() || (!shouldBeDeferred.IsNull() && shouldBeDeferred.True()) {
|
|
deferred = &providers.Deferred{
|
|
Reason: providers.DeferredReasonResourceConfigUnknown,
|
|
}
|
|
}
|
|
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: resource,
|
|
Deferred: deferred,
|
|
}
|
|
}
|
|
|
|
func (provider *TestProvider) ApplyResourceChange(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
if request.PlannedState.IsNull() {
|
|
// Then this is a delete operation.
|
|
|
|
if destroyFail := request.PriorState.GetAttr("destroy_fail"); destroyFail.IsKnown() && destroyFail.True() {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to destroy resource", "destroy_fail is set to true"))
|
|
return providers.ApplyResourceChangeResponse{
|
|
Diagnostics: diags,
|
|
}
|
|
}
|
|
|
|
if wait := request.PriorState.GetAttr("destroy_wait_seconds"); !wait.IsNull() && wait.IsKnown() {
|
|
waitTime, _ := wait.AsBigFloat().Int64()
|
|
time.Sleep(time.Second * time.Duration(waitTime))
|
|
}
|
|
|
|
provider.Store.Delete(provider.GetResourceKey(request.PriorState.GetAttr("id").AsString()))
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: request.PlannedState,
|
|
}
|
|
}
|
|
|
|
resource := request.PlannedState
|
|
id := resource.GetAttr("id")
|
|
if !id.IsKnown() {
|
|
val, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to generate uuid: %v", err))
|
|
}
|
|
|
|
id = cty.StringVal(val)
|
|
|
|
vals := resource.AsValueMap()
|
|
vals["id"] = id
|
|
resource = cty.ObjectVal(vals)
|
|
}
|
|
|
|
if interrupts := resource.GetAttr("interrupt_count"); !interrupts.IsNull() && interrupts.IsKnown() && provider.Interrupt != nil {
|
|
count, _ := interrupts.AsBigFloat().Int64()
|
|
for ix := 0; ix < int(count); ix++ {
|
|
provider.Interrupt <- struct{}{}
|
|
}
|
|
|
|
// Wait for a second to make sure the interrupts are processed by
|
|
// Terraform before the provider finishes. This is an attempt to ensure
|
|
// the output of any tests that rely on this behaviour is deterministic.
|
|
time.Sleep(time.Second)
|
|
}
|
|
|
|
if wait := resource.GetAttr("create_wait_seconds"); !wait.IsNull() && wait.IsKnown() {
|
|
waitTime, _ := wait.AsBigFloat().Int64()
|
|
time.Sleep(time.Second * time.Duration(waitTime))
|
|
}
|
|
|
|
if destroyFail := resource.GetAttr("destroy_fail"); !destroyFail.IsKnown() {
|
|
vals := resource.AsValueMap()
|
|
vals["destroy_fail"] = cty.False
|
|
resource = cty.ObjectVal(vals)
|
|
}
|
|
|
|
provider.Store.Put(provider.GetResourceKey(id.AsString()), resource)
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: resource,
|
|
}
|
|
}
|
|
|
|
func (provider *TestProvider) ReadResource(request providers.ReadResourceRequest) providers.ReadResourceResponse {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
id := request.PriorState.GetAttr("id").AsString()
|
|
resource := provider.Store.Get(provider.GetResourceKey(id))
|
|
if resource == cty.NilVal {
|
|
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id)))
|
|
}
|
|
|
|
return providers.ReadResourceResponse{
|
|
NewState: resource,
|
|
Diagnostics: diags,
|
|
}
|
|
}
|
|
|
|
func (provider *TestProvider) ReadDataSource(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
id := request.Config.GetAttr("id").AsString()
|
|
resource := provider.Store.Get(provider.GetDataKey(id))
|
|
if resource == cty.NilVal {
|
|
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id)))
|
|
}
|
|
|
|
if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() {
|
|
vals := resource.AsValueMap()
|
|
vals["write_only"] = cty.NullVal(cty.String)
|
|
resource = cty.ObjectVal(vals)
|
|
}
|
|
|
|
return providers.ReadDataSourceResponse{
|
|
State: resource,
|
|
Diagnostics: diags,
|
|
}
|
|
}
|
|
|
|
func (provider *TestProvider) CallFunction(request providers.CallFunctionRequest) providers.CallFunctionResponse {
|
|
switch request.FunctionName {
|
|
case "is_true":
|
|
return providers.CallFunctionResponse{
|
|
Result: request.Arguments[0],
|
|
}
|
|
default:
|
|
return providers.CallFunctionResponse{
|
|
Err: fmt.Errorf("unknown function %q", request.FunctionName),
|
|
}
|
|
}
|
|
}
|
|
|
|
func (provider *TestProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) {
|
|
resp.Result = cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("bar"),
|
|
})
|
|
return resp
|
|
}
|
|
|
|
func (provider *TestProvider) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) {
|
|
return resp
|
|
}
|
|
|
|
// ResourceStore manages a set of cty.Value resources that can be shared between
|
|
// TestProvider providers.
|
|
//
|
|
// A ResourceStore represents the remote objects that a test provider is managing.
|
|
// For example, when the test provider gets a ReadResource request it will search
|
|
// the store for a resource with a matching ID. See (*TestProvider).ReadResource.
|
|
type ResourceStore struct {
|
|
mutex sync.RWMutex
|
|
|
|
Data map[string]cty.Value
|
|
}
|
|
|
|
func (store *ResourceStore) Delete(key string) cty.Value {
|
|
defer store.beginWrite()()
|
|
|
|
if resource, ok := store.Data[key]; ok {
|
|
delete(store.Data, key)
|
|
return resource
|
|
}
|
|
return cty.NilVal
|
|
}
|
|
|
|
func (store *ResourceStore) Get(key string) cty.Value {
|
|
defer store.beginRead()()
|
|
|
|
return store.get(key)
|
|
}
|
|
|
|
func (store *ResourceStore) Put(key string, resource cty.Value) cty.Value {
|
|
defer store.beginWrite()()
|
|
|
|
old := store.get(key)
|
|
store.Data[key] = resource
|
|
return old
|
|
}
|
|
|
|
func (store *ResourceStore) get(key string) cty.Value {
|
|
if resource, ok := store.Data[key]; ok {
|
|
return resource
|
|
}
|
|
return cty.NilVal
|
|
}
|
|
|
|
func (store *ResourceStore) beginWrite() func() {
|
|
store.mutex.Lock()
|
|
return store.mutex.Unlock
|
|
|
|
}
|
|
func (store *ResourceStore) beginRead() func() {
|
|
store.mutex.RLock()
|
|
return store.mutex.RUnlock
|
|
}
|