[testing framework] prepare for beta phase of development (#33445)

This commit is contained in:
Liam Cervante 2023-07-06 15:53:18 +02:00 committed by GitHub
parent 3613e972c5
commit ea162f6ab5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 831 additions and 83 deletions

View File

@ -63,7 +63,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
loader, err := c.initConfigLoader()
diags = diags.Append(err)
if err != nil {
c.View.Diagnostics(diags)
view.Diagnostics(nil, nil, diags)
return 1
}
c.loader = loader
@ -71,7 +71,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
config, configDiags := loader.LoadConfigWithTests(".", "tests")
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
c.View.Diagnostics(diags)
view.Diagnostics(nil, nil, diags)
return 1
}
@ -113,7 +113,7 @@ func (c *TestCommand) ExecuteTestSuite(suite *moduletest.Suite, config *configs.
diags = diags.Append(err)
if err != nil {
suite.Status = suite.Status.Merge(moduletest.Error)
c.View.Diagnostics(diags)
view.Diagnostics(nil, nil, diags)
return
}
@ -121,10 +121,10 @@ func (c *TestCommand) ExecuteTestSuite(suite *moduletest.Suite, config *configs.
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
suite.Status = suite.Status.Merge(moduletest.Error)
c.View.Diagnostics(diags)
view.Diagnostics(nil, nil, diags)
return
}
c.View.Diagnostics(diags) // Print out any warnings from the setup.
view.Diagnostics(nil, nil, diags) // Print out any warnings from the setup.
var files []string
for name := range suite.Files {
@ -148,7 +148,7 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F
if diags.HasErrors() {
file.Status = file.Status.Merge(moduletest.Error)
view.File(file)
c.View.Diagnostics(diags)
view.Diagnostics(nil, file, diags)
return
}
@ -166,48 +166,13 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F
if planDiags.HasErrors() {
// This is bad, we need to tell the user that we couldn't clean up
// and they need to go and manually delete some resources.
c.Streams.Eprintf("Terraform encountered an error destroying resources created during the test.\n\n")
c.View.Diagnostics(planDiags)
if state.HasManagedResourceInstanceObjects() {
c.Streams.Eprintf("Terraform left the following resources in state, they need to be cleaned up manually:\n\n")
for _, resource := range state.AllResourceInstanceObjectAddrs() {
if resource.DeposedKey != states.NotDeposed {
c.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
continue
}
c.Streams.Eprintf(" - %s\n", resource.Instance)
}
}
view.DestroySummary(planDiags, file, state)
return
}
c.View.Diagnostics(planDiags) // Print out any warnings from the destroy plan.
view.Diagnostics(nil, file, planDiags) // Print out any warnings from the destroy plan.
finalState, applyDiags := ctx.Apply(plan, config)
if applyDiags.HasErrors() {
// This is bad, we need to tell the user that we couldn't clean up
// and they need to go and manually delete some resources.
c.Streams.Eprintf("Terraform encountered an error destroying resources created during the test.\n\n")
}
c.View.Diagnostics(applyDiags) // Print out any warnings from the destroy apply.
if finalState.HasManagedResourceInstanceObjects() {
// Then we need to print dialog telling the user they need to clean
// things up, and we should mark the overall test as errored.
c.Streams.Eprintf("Terraform left the following resources in state, they need to be cleaned up manually:\n\n")
for _, resource := range state.AllResourceInstanceObjectAddrs() {
if resource.DeposedKey != states.NotDeposed {
c.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
continue
}
c.Streams.Eprintf(" - %s\n", resource.Instance)
}
}
view.DestroySummary(applyDiags, file, finalState)
}()
file.Status = file.Status.Merge(moduletest.Pass)
@ -222,7 +187,7 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F
}
view.File(file)
c.View.Diagnostics(diags)
view.Diagnostics(nil, file, diags)
for _, run := range file.Runs {
view.Run(run, file)

View File

@ -4,6 +4,15 @@ import (
"path"
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
testing_command "github.com/hashicorp/terraform/internal/command/testing"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/terminal"
)
func TestTest(t *testing.T) {
@ -11,6 +20,7 @@ func TestTest(t *testing.T) {
args []string
expected string
code int
skip bool
}{
"simple_pass": {
expected: "1 passed, 0 failed.",
@ -24,6 +34,10 @@ func TestTest(t *testing.T) {
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_outputs": {
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_variables": {
expected: "2 passed, 0 failed.",
code: 0,
@ -32,23 +46,75 @@ func TestTest(t *testing.T) {
expected: "2 passed, 0 failed.",
code: 0,
},
"expect_failures_checks": {
expected: "1 passed, 0 failed.",
code: 0,
// TODO(liamcervante): Enable this when support for expect_failures
// has been added.
skip: true,
},
"expect_failures_inputs": {
expected: "1 passed, 0 failed.",
code: 0,
// TODO(liamcervante): Enable this when support for expect_failures
// has been added.
skip: true,
},
"expect_failures_outputs": {
expected: "1 passed, 0 failed.",
code: 0,
// TODO(liamcervante): Enable this when support for expect_failures
// has been added.
skip: true,
},
"expect_failures_resources": {
expected: "1 passed, 0 failed.",
code: 0,
// TODO(liamcervante): Enable this when support for expect_failures
// has been added.
skip: true,
},
"simple_fail": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_checks": {
expected: "0 passed, 1 failed.",
code: 1,
// TODO(liamcervante): Enable this, at the moment checks aren't
// causing the tests to fail when they should. Also, it's not
// skipping warnings during the plan when it should.
skip: true,
},
"custom_condition_inputs": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_outputs": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_resources": {
expected: "0 passed, 1 failed.",
code: 1,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", name)), td)
defer testChdir(t, td)()
p := planFixtureProvider()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
@ -63,6 +129,158 @@ func TestTest(t *testing.T) {
if !strings.Contains(output.Stdout(), tc.expected) {
t.Errorf("output didn't contain expected string:\n\n%s", output.All())
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
})
}
}
func TestTest_ProviderAlias(t *testing.T) {
// TODO(liamcervante): Enable this test once we have added support for
// provider aliasing and customisation into the testing framework.
t.Skip()
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_provider_alias")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run(nil)
output := done(t)
printedOutput := false
if code != 0 {
printedOutput = true
t.Errorf("expected status code 0 but got %d: %s", code, output.All())
}
if provider.ResourceCount() > 0 {
if !printedOutput {
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString())
}
}
}
func TestTest_ModuleDependencies(t *testing.T) {
// TODO(liamcervante): Enable this test once we have added support for
// module customisation into the testing framework.
t.Skip()
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_setup_module")), td)
defer testChdir(t, td)()
// Our two providers will share a common set of values to make things
// easier.
store := &testing_command.ResourceStore{
Data: make(map[string]cty.Value),
}
// We set it up so the module provider will update the data sources
// available to the core mock provider.
test := testing_command.NewProvider(store)
setup := testing_command.NewProvider(store)
test.SetDataPrefix("data")
test.SetResourcePrefix("resource")
// Let's make the setup provider write into the data for test provider.
setup.SetResourcePrefix("data")
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
"setup": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(test.Provider),
addrs.NewDefaultProvider("setup"): providers.FactoryFixed(setup.Provider),
},
},
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run(nil)
output := done(t)
printedOutput := false
if code != 0 {
printedOutput = true
t.Errorf("expected status code 0 but got %d: %s", code, output.All())
}
if test.ResourceCount() > 0 {
if !printedOutput {
printedOutput = true
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", test.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", test.ResourceString())
}
}
if setup.ResourceCount() > 0 {
if !printedOutput {
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", setup.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", setup.ResourceString())
}
}
}

View File

@ -0,0 +1,15 @@
variable "input" {
type = string
}
resource "test_resource" "resource" {
value = var.input
}
check "expected_to_fail" {
assert {
condition = test_resource.resource.value != var.input
error_message = "this really should fail"
}
}

View File

@ -0,0 +1,5 @@
variables {
input = "some value"
}
run "test" {}

View File

@ -0,0 +1,13 @@
variable "input" {
type = string
validation {
condition = var.input == "something very specific"
error_message = "this should definitely fail"
}
}
resource "test_resource" "resource" {
value = var.input
}

View File

@ -0,0 +1,5 @@
variables {
input = "some value"
}
run "test" {}

View File

@ -0,0 +1,13 @@
variable "input" {
type = string
}
output "output" {
value = var.input
precondition {
condition = var.input == "something incredibly specific"
error_message = "this should fail"
}
}

View File

@ -0,0 +1,5 @@
variables {
input = "some value"
}
run "test" {}

View File

@ -0,0 +1,15 @@
variable "input" {
type = string
}
resource "test_resource" "resource" {
value = var.input
lifecycle {
postcondition {
condition = self.value != var.input
error_message = "this really should fail"
}
}
}

View File

@ -0,0 +1,5 @@
variables {
input = "some value"
}
run "test" {}

View File

@ -0,0 +1,15 @@
variable "input" {
type = string
}
resource "test_resource" "resource" {
value = var.input
}
check "expected_to_fail" {
assert {
condition = test_resource.resource.value != var.input
error_message = "this really should fail"
}
}

View File

@ -0,0 +1,9 @@
variables {
input = "some value"
}
run "test" {
expect_failures = [
check.expected_to_fail
]
}

View File

@ -0,0 +1,13 @@
variable "input" {
type = string
validation {
condition = var.input == "something very specific"
error_message = "this should definitely fail"
}
}
resource "test_resource" "resource" {
value = var.input
}

View File

@ -0,0 +1,9 @@
variables {
input = "some value"
}
run "test" {
expect_failures = [
var.input
]
}

View File

@ -0,0 +1,13 @@
variable "input" {
type = string
}
output "output" {
value = var.input
precondition {
condition = var.input == "something incredibly specific"
error_message = "this should fail"
}
}

View File

@ -0,0 +1,9 @@
variables {
input = "some value"
}
run "test" {
expect_failures = [
output.output
]
}

View File

@ -0,0 +1,15 @@
variable "input" {
type = string
}
resource "test_resource" "resource" {
value = var.input
lifecycle {
postcondition {
condition = self.value != var.input
error_message = "this really should fail"
}
}
}

View File

@ -0,0 +1,15 @@
variables {
input = "some value"
}
run "test" {
assert {
condition = test_resource.resource.value == "some value"
error_message = "since we used a postcondition, it should still have actually created the resource"
}
expect_failures = [
test_resource.resource
]
}

View File

@ -1,7 +1,7 @@
resource "test_instance" "foo" {
ami = "bar"
resource "test_resource" "foo" {
value = "bar"
}
locals {
value = test_instance.foo.ami
value = test_resource.foo.value
}

View File

@ -1,6 +1,6 @@
run "validate_test_instance" {
run "validate_test_resource" {
assert {
condition = local.value == "bar"
error_message = "invalid ami value"
error_message = "invalid value"
}
}

View File

@ -0,0 +1,7 @@
resource "test_resource" "foo" {
value = "bar"
}
output "value" {
value = test_resource.foo.value
}

View File

@ -0,0 +1,6 @@
run "validate_test_resource" {
assert {
condition = output.value == "bar"
error_message = "invalid value"
}
}

View File

@ -2,6 +2,6 @@ variable "input" {
type = string
}
resource "test_instance" "foo" {
ami = var.input
resource "test_resource" "foo" {
value = var.input
}

View File

@ -2,20 +2,20 @@ variables {
input = "bar"
}
run "validate_test_instance" {
run "validate_test_resource" {
assert {
condition = test_instance.foo.ami == "bar"
error_message = "invalid ami value"
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}
run "validate_test_instance" {
run "validate_test_resource" {
variables {
input = "zap"
}
assert {
condition = test_instance.foo.ami == "zap"
error_message = "invalid ami value"
condition = test_resource.foo.value == "zap"
error_message = "invalid value"
}
}

View File

@ -1,3 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
resource "test_resource" "foo" {
value = "bar"
}

View File

@ -1,16 +1,16 @@
run "validate_test_instance" {
run "validate_test_resource" {
command = plan
assert {
condition = test_instance.foo.ami == "bar"
error_message = "invalid ami value"
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}
run "validate_test_instance" {
run "validate_test_resource" {
assert {
condition = test_instance.foo.ami == "bar"
error_message = "invalid ami value"
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -1,3 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
resource "test_resource" "foo" {
value = "bar"
}

View File

@ -1,6 +1,6 @@
run "validate_test_instance" {
run "validate_test_resource" {
assert {
condition = test_instance.foo.ami == "zap"
error_message = "invalid ami value"
condition = test_resource.foo.value == "zap"
error_message = "invalid value"
}
}

View File

@ -1,3 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
resource "test_resource" "foo" {
value = "bar"
}

View File

@ -1,6 +1,6 @@
run "validate_test_instance" {
run "validate_test_resource" {
assert {
condition = test_instance.foo.ami == "bar"
error_message = "invalid ami value"
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -1,3 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
resource "test_resource" "foo" {
value = "bar"
}

View File

@ -1,6 +1,6 @@
run "validate_test_instance" {
run "validate_test_resource" {
assert {
condition = test_instance.foo.ami == "bar"
error_message = "invalid ami value"
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -0,0 +1,12 @@
variable "managed_id" {
type = string
}
data "test_data_source" "managed_data" {
id = var.managed_id
}
resource "test_resource" "created" {
value = data.test_data_source.managed_data.value
}

View File

@ -0,0 +1,37 @@
provider "test" {
data_prefix = "data"
resource_prefix = "resource"
}
provider "test" {
alias = "setup"
# The setup provider will write into the main providers data sources.
resource_prefix = "data"
}
variables {
managed_id = "B853C121"
}
run "setup" {
module {
source = "./setup"
}
variables {
value = "Hello, world!"
id = "B853C121"
}
providers = {
test = test.setup
}
}
run "test" {
assert {
condition = test_resource.created.value == "Hello, world!"
error_message = "bad value"
}
}

View File

@ -0,0 +1,12 @@
variable "value" {
type = string
}
variable "id" {
type = string
}
resource "test_resource" "managed" {
id = var.id
value = var.value
}

View File

@ -0,0 +1,12 @@
variable "managed_id" {
type = string
}
data "test_data_source" "managed_data" {
id = var.managed_id
}
resource "test_resource" "created" {
value = data.test_data_source.managed_data.value
}

View File

@ -0,0 +1,21 @@
variables {
managed_id = "B853C121"
}
run "setup" {
module {
source = "./setup"
}
variables {
value = "Hello, world!"
id = "B853C121"
}
}
run "test" {
assert {
condition = test_resource.created.value == "Hello, world!"
error_message = "bad value"
}
}

View File

@ -0,0 +1,13 @@
variable "value" {
type = string
}
variable "id" {
type = string
}
resource "test_resource" "managed" {
provider = setup
id = var.id
value = var.value
}

View File

@ -0,0 +1,271 @@
package testing
import (
"fmt"
"path"
"strings"
"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/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
var (
ProviderSchema = &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Block: &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": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"value": {Type: cty.String, Optional: true},
},
},
},
},
DataSources: map[string]providers.Schema{
"test_data_source": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Required: true},
"value": {Type: cty.String, Computed: true},
},
},
},
},
}
)
// 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 *terraform.MockProvider
data, resource cty.Value
Store *ResourceStore
}
func NewProvider(store *ResourceStore) *TestProvider {
if store == nil {
store = &ResourceStore{
Data: make(map[string]cty.Value),
}
}
provider := &TestProvider{
Provider: new(terraform.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
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 {
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 {
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() {
// Then this is a delete operation.
return providers.PlanResourceChangeResponse{
PlannedState: request.ProposedNewState,
}
}
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)
}
return providers.PlanResourceChangeResponse{
PlannedState: resource,
}
}
func (provider *TestProvider) ApplyResourceChange(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
if request.PlannedState.IsNull() {
// Then this is a delete operation.
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)
}
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)))
}
return providers.ReadDataSourceResponse{
State: resource,
Diagnostics: diags,
}
}
// ResourceStore manages a set of cty.Value resources that can be shared between
// TestProvider providers.
type ResourceStore struct {
Data map[string]cty.Value
}
func (store *ResourceStore) Delete(key string) cty.Value {
if resource, ok := store.Data[key]; ok {
delete(store.Data, key)
return resource
}
return cty.NilVal
}
func (store *ResourceStore) Get(key string) cty.Value {
if resource, ok := store.Data[key]; ok {
return resource
}
return cty.NilVal
}
func (store *ResourceStore) Put(key string, resource cty.Value) cty.Value {
old := store.Get(key)
store.Data[key] = resource
return old
}