opentofu/internal/command/add_test.go

600 lines
18 KiB
Go
Raw Normal View History

package command
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
)
// simple test cases with a simple resource schema
func TestAdd_basic(t *testing.T) {
td := tempDir(t)
testCopyDir(t, testFixturePath("add/basic"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true, Description: "the ami to use"},
"value": {Type: cty.String, Required: true, Description: "a value of a thing"},
},
},
},
},
}
overrides := &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(p),
addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p),
},
}
t.Run("basic", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
value = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("basic to file", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
outPath := "add.tf"
args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
value = null # REQUIRED string
}
`
result, err := os.ReadFile(outPath)
if err != nil {
t.Fatalf("error reading result file %s: %s", outPath, err.Error())
}
// While the entire directory will get removed once the whole test suite
// is done, we remove this lest it gets in the way of another (not yet
// written) test.
os.Remove(outPath)
if !cmp.Equal(expected, string(result)) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, string(result)))
}
})
t.Run("optionals", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"-optional", "test_instance.new"}
code := c.Run(args)
if code != 0 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
output := done(t)
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
ami = null # OPTIONAL string
id = null # OPTIONAL string
value = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("alternate provider for resource", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"-provider=provider[\"registry.terraform.io/happycorp/test\"].alias", "test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
// The provider happycorp/test has a localname "othertest" in the provider configuration.
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
provider = othertest.alias
value = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("resource exists error", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"test_instance.exists"}
code := c.Run(args)
if code != 1 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
output := done(t)
if !strings.Contains(output.Stderr(), "The resource test_instance.exists is already in this configuration") {
t.Fatalf("missing expected error message: %s", output.Stderr())
}
})
t.Run("provider not in configuration", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"toast_instance.new"}
code := c.Run(args)
if code != 1 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
output := done(t)
if !strings.Contains(output.Stderr(), "No schema found for provider registry.terraform.io/hashicorp/toast.") {
t.Fatalf("missing expected error message: %s", output.Stderr())
}
})
t.Run("no schema for resource", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"test_pet.meow"}
code := c.Run(args)
if code != 1 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
output := done(t)
if !strings.Contains(output.Stderr(), "No resource schema found for test_pet.") {
t.Fatalf("missing expected error message: %s", output.Stderr())
}
})
}
func TestAdd(t *testing.T) {
td := tempDir(t)
testCopyDir(t, testFixturePath("add/module"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// a simple hashicorp/test provider, and a more complex happycorp/test provider
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Required: true},
},
},
},
},
}
happycorp := testProvider()
happycorp.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true, Description: "the ami to use"},
"value": {Type: cty.String, Required: true, Description: "a value of a thing"},
"disks": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"size": {Type: cty.String, Optional: true},
"mount_point": {Type: cty.String, Required: true},
},
},
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network_interface": {
Nesting: configschema.NestingList,
MinItems: 1,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"device_index": {Type: cty.String, Optional: true},
"description": {Type: cty.String, Optional: true},
},
},
},
},
},
},
},
}
providerSource, psClose := newMockProviderSource(t, map[string][]string{
"registry.terraform.io/happycorp/test": {"1.0.0"},
"registry.terraform.io/hashicorp/test": {"1.0.0"},
})
defer psClose()
overrides := &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(happycorp),
addrs.NewDefaultProvider("test"): providers.FactoryFixed(p),
},
}
// the test fixture uses a module, so we need to run init.
m := Meta{
testingOverrides: overrides,
ProviderSource: providerSource,
Ui: new(cli.MockUi),
}
init := &InitCommand{
Meta: m,
}
code := init.Run([]string{})
if code != 0 {
t.Fatal("init failed")
}
t.Run("optional", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"-optional", "test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
ami = null # OPTIONAL string
disks = [{ # OPTIONAL list of object
mount_point = null # REQUIRED string
size = null # OPTIONAL string
}]
id = null # OPTIONAL string
value = null # REQUIRED string
network_interface { # REQUIRED block
description = null # OPTIONAL string
device_index = null # OPTIONAL string
}
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("chooses correct provider for root module", func(t *testing.T) {
// in the root module of this test fixture, "test" is the local name for "happycorp/test"
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
value = null # REQUIRED string
network_interface { # REQUIRED block
}
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("chooses correct provider for child module", func(t *testing.T) {
// in the child module of this test fixture, "test" is a default "hashicorp/test" provider
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"module.child.test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
id = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("chooses correct provider for an unknown module", func(t *testing.T) {
// it's weird but ok to use a new/unknown module name; terraform will
// fall back on default providers (unless a -provider argument is
// supplied)
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"module.madeup.test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
id = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
}
func TestAdd_from_state(t *testing.T) {
td := tempDir(t)
testCopyDir(t, testFixturePath("add/basic"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// write some state
testState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "new",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte("{\"id\":\"bar\",\"ami\":\"ami-123456\",\"disks\":[{\"mount_point\":\"diska\",\"size\":null}],\"value\":\"bloop\"}"),
Status: states.ObjectReady,
Dependencies: []addrs.ConfigResource{},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
})
f, err := os.Create("terraform.tfstate")
if err != nil {
t.Fatalf("failed to create temporary state file: %s", err)
}
defer f.Close()
err = writeStateForTesting(testState, f)
if err != nil {
t.Fatalf("failed to write state file: %s", err)
}
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true, Description: "the ami to use"},
"value": {Type: cty.String, Required: true, Description: "a value of a thing"},
"disks": {
NestedType: &configschema.Object{
Nesting: configschema.NestingList,
Attributes: map[string]*configschema.Attribute{
"size": {Type: cty.String, Optional: true},
"mount_point": {Type: cty.String, Required: true},
},
},
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network_interface": {
Nesting: configschema.NestingList,
MinItems: 1,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"device_index": {Type: cty.String, Optional: true},
"description": {Type: cty.String, Optional: true},
},
},
},
},
},
},
},
}
overrides := &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(p),
addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p),
},
}
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"-from-state", "test_instance.new"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
ami = "ami-123456"
disks = [
{
mount_point = "diska"
size = null
},
]
id = "bar"
value = "bloop"
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
if _, err := os.Stat(filepath.Join(td, ".terraform.tfstate.lock.info")); !os.IsNotExist(err) {
t.Fatal("state left locked after add")
}
}