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.
389 lines
12 KiB
389 lines
12 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package testing
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime/debug"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/zclconf/go-cty/cty"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
var (
|
|
TestingResourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
TestingEphemeralResourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"value": {
|
|
Type: cty.String,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
DeferredResourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
"deferred": {Type: cty.Bool, Required: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
FailedResourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
"fail_plan": {Type: cty.Bool, Optional: true, Computed: true},
|
|
"fail_apply": {Type: cty.Bool, Optional: true, Computed: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
BlockedResourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
"required_resources": {Type: cty.Set(cty.String), Optional: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
WriteOnlyResourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
"write_only": {Type: cty.String, WriteOnly: true, Optional: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
TestingDataSourceSchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Required: true},
|
|
"value": {Type: cty.String, Computed: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
WriteOnlyDataSourceSchema = providers.Schema{
|
|
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, WriteOnly: true, Optional: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
TestingResourceWithIdentitySchema = providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
Identity: &configschema.Object{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Required: true},
|
|
},
|
|
Nesting: configschema.NestingSingle,
|
|
},
|
|
}
|
|
)
|
|
|
|
// MockProvider wraps the standard MockProvider with a simple in-memory
|
|
// data store for resources and data sources.
|
|
type MockProvider struct {
|
|
*testing_provider.MockProvider
|
|
|
|
ResourceStore *ResourceStore
|
|
|
|
// If set, authentication means the configuration must provide a value
|
|
// that matches the value here otherwise the Configure function will
|
|
// fail.
|
|
Authentication string
|
|
}
|
|
|
|
// NewProvider returns a new MockProvider with an empty data store.
|
|
func NewProvider(t *testing.T) *MockProvider {
|
|
provider := NewProviderWithData(t, NewResourceStore())
|
|
return provider
|
|
}
|
|
|
|
// NewProviderWithData returns a new MockProvider with the given data store.
|
|
func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
|
|
if store == nil {
|
|
store = NewResourceStore()
|
|
}
|
|
|
|
// grab the current stack trace so we know where the provider was created
|
|
// in case it isn't being cleaned up properly
|
|
currentStackTrace := debug.Stack()
|
|
|
|
provider := &MockProvider{
|
|
MockProvider: &testing_provider.MockProvider{
|
|
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
|
|
Provider: providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
// if the configuration sets require_auth then it
|
|
// must also provide the correct value for
|
|
// authentication
|
|
"authentication": {
|
|
Type: cty.String,
|
|
Sensitive: true,
|
|
Optional: true,
|
|
},
|
|
"require_auth": {
|
|
Type: cty.Bool,
|
|
Optional: true,
|
|
},
|
|
|
|
// If this value is provider, the Configure
|
|
// function call will fail and return the value
|
|
// here as part of the error.
|
|
"configure_error": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
|
|
// ignored allows the configuration to create
|
|
// dependencies from this provider to component
|
|
// blocks and inputs without affecting behaviour.
|
|
"ignored": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"testing_resource": {
|
|
Body: TestingResourceSchema.Body,
|
|
},
|
|
"testing_deferred_resource": {
|
|
Body: DeferredResourceSchema.Body,
|
|
},
|
|
"testing_failed_resource": {
|
|
Body: FailedResourceSchema.Body,
|
|
},
|
|
"testing_blocked_resource": {
|
|
Body: BlockedResourceSchema.Body,
|
|
},
|
|
"testing_resource_with_identity": {
|
|
Body: TestingResourceSchema.Body,
|
|
Identity: TestingResourceWithIdentitySchema.Identity,
|
|
},
|
|
"testing_write_only_resource": {
|
|
Body: WriteOnlyResourceSchema.Body,
|
|
},
|
|
},
|
|
DataSources: map[string]providers.Schema{
|
|
"testing_data_source": {
|
|
Body: TestingDataSourceSchema.Body,
|
|
},
|
|
"testing_write_only_data_source": {
|
|
Body: WriteOnlyDataSourceSchema.Body,
|
|
},
|
|
},
|
|
EphemeralResourceTypes: map[string]providers.Schema{
|
|
"testing_resource": {
|
|
Body: TestingEphemeralResourceSchema.Body,
|
|
},
|
|
},
|
|
Functions: map[string]providers.FunctionDecl{
|
|
"echo": {
|
|
Parameters: []providers.FunctionParam{
|
|
{Name: "value", Type: cty.DynamicPseudoType},
|
|
},
|
|
ReturnType: cty.DynamicPseudoType,
|
|
},
|
|
},
|
|
ServerCapabilities: providers.ServerCapabilities{
|
|
MoveResourceState: true,
|
|
},
|
|
},
|
|
PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
return getResource(request.TypeName).Plan(request, store)
|
|
},
|
|
ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return getResource(request.TypeName).Apply(request, store)
|
|
},
|
|
ReadResourceFn: func(request providers.ReadResourceRequest) providers.ReadResourceResponse {
|
|
return getResource(request.TypeName).Read(request, store)
|
|
},
|
|
ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
id := request.Config.GetAttr("id")
|
|
if id.IsNull() {
|
|
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "missing id", "id is required"))
|
|
return providers.ReadDataSourceResponse{
|
|
Diagnostics: diags,
|
|
}
|
|
}
|
|
|
|
value, exists := store.Get(id.AsString())
|
|
if !exists {
|
|
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%q not found", id)))
|
|
}
|
|
return providers.ReadDataSourceResponse{
|
|
State: value,
|
|
Diagnostics: diags,
|
|
}
|
|
},
|
|
ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
|
|
id := request.ID
|
|
value, exists := store.Get(id)
|
|
if !exists {
|
|
return providers.ImportResourceStateResponse{
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%q not found", id)),
|
|
},
|
|
}
|
|
}
|
|
return providers.ImportResourceStateResponse{
|
|
ImportedResources: []providers.ImportedResource{
|
|
{
|
|
TypeName: request.TypeName,
|
|
State: value,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
MoveResourceStateFn: func(request providers.MoveResourceStateRequest) providers.MoveResourceStateResponse {
|
|
if request.SourceTypeName != "testing_resource" && request.TargetTypeName != "testing_deferred_resource" {
|
|
return providers.MoveResourceStateResponse{
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.Sourceless(tfdiags.Error, "unsupported", "unsupported move"),
|
|
},
|
|
}
|
|
}
|
|
// So, we know we're moving from `testing_resource` to
|
|
// `testing_deferred_resource`.
|
|
|
|
source, err := ctyjson.Unmarshal(request.SourceStateJSON, cty.Object(map[string]cty.Type{
|
|
"id": cty.String,
|
|
"value": cty.String,
|
|
}))
|
|
if err != nil {
|
|
return providers.MoveResourceStateResponse{
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.Sourceless(tfdiags.Error, "invalid source state", err.Error()),
|
|
},
|
|
}
|
|
}
|
|
|
|
target := cty.ObjectVal(map[string]cty.Value{
|
|
"id": source.GetAttr("id"),
|
|
"value": source.GetAttr("value"),
|
|
"deferred": cty.False,
|
|
})
|
|
store.Set(source.GetAttr("id").AsString(), target)
|
|
|
|
return providers.MoveResourceStateResponse{
|
|
TargetState: target,
|
|
}
|
|
},
|
|
CallFunctionFn: func(request providers.CallFunctionRequest) providers.CallFunctionResponse {
|
|
// Just echo the first argument back as the result.
|
|
return providers.CallFunctionResponse{
|
|
Result: request.Arguments[0],
|
|
}
|
|
},
|
|
OpenEphemeralResourceFn: func(request providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse {
|
|
return providers.OpenEphemeralResourceResponse{
|
|
Result: cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("secret"),
|
|
}),
|
|
}
|
|
},
|
|
},
|
|
ResourceStore: store,
|
|
}
|
|
|
|
// We want to use internal fields in this function so we have to set it
|
|
// like this.
|
|
provider.ConfigureProviderFn = provider.configure
|
|
|
|
t.Cleanup(func() {
|
|
// Fail the test if this provider is not closed.
|
|
if !provider.CloseCalled {
|
|
t.Log(string(currentStackTrace))
|
|
t.Fatalf("provider.Close was not called")
|
|
}
|
|
})
|
|
|
|
return provider
|
|
}
|
|
|
|
func (provider *MockProvider) configure(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
|
|
// If configure_error is set, return an error.
|
|
err := request.Config.GetAttr("configure_error")
|
|
if err.IsKnown() && !err.IsNull() {
|
|
return providers.ConfigureProviderResponse{
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.AttributeValue(tfdiags.Error, err.AsString(), "configure_error attribute was set", cty.GetAttrPath("configure_error")),
|
|
},
|
|
}
|
|
}
|
|
|
|
// We deliberately only check the authentication if the configuration
|
|
// is providing it. It's entirely up to the config to opt into the
|
|
// authentication which would be crazy for a real provider but just
|
|
// makes things so much simpler for us in testing world.
|
|
requireAuth := request.Config.GetAttr("require_auth")
|
|
if requireAuth.True() {
|
|
authn := request.Config.GetAttr("authentication")
|
|
if authn.IsNull() || !authn.IsKnown() {
|
|
return providers.ConfigureProviderResponse{
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field is required", cty.GetAttrPath("authentication")),
|
|
},
|
|
}
|
|
}
|
|
if authn.AsString() != provider.Authentication {
|
|
return providers.ConfigureProviderResponse{
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field did not match expected", cty.GetAttrPath("authentication")),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
return providers.ConfigureProviderResponse{}
|
|
}
|
|
|
|
// mustGenerateUUID is a helper to generate a UUID and panic if it fails.
|
|
func mustGenerateUUID() string {
|
|
val, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return val
|
|
}
|