Merge pull request #19233 from hashicorp/jbardin/requires-new
fix instance replacement
This commit is contained in:
commit
e74f46d622
|
@ -22,6 +22,7 @@ func Provider() terraform.ResourceProvider {
|
||||||
"test_resource_with_custom_diff": testResourceCustomDiff(),
|
"test_resource_with_custom_diff": testResourceCustomDiff(),
|
||||||
"test_resource_timeout": testResourceTimeout(),
|
"test_resource_timeout": testResourceTimeout(),
|
||||||
"test_resource_diff_suppress": testResourceDiffSuppress(),
|
"test_resource_diff_suppress": testResourceDiffSuppress(),
|
||||||
|
"test_resource_force_new": testResourceForceNew(),
|
||||||
},
|
},
|
||||||
DataSourcesMap: map[string]*schema.Resource{
|
DataSourcesMap: map[string]*schema.Resource{
|
||||||
"test_data_source": testDataSource(),
|
"test_data_source": testDataSource(),
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testResourceForceNew() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: testResourceForceNewCreate,
|
||||||
|
Read: testResourceForceNewRead,
|
||||||
|
Delete: testResourceForceNewDelete,
|
||||||
|
|
||||||
|
Importer: &schema.ResourceImporter{
|
||||||
|
State: schema.ImportStatePassthrough,
|
||||||
|
},
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"triggers": {
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResourceForceNewCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
d.SetId("testId")
|
||||||
|
return testResourceForceNewRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResourceForceNewRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResourceForceNewDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResourceForceNew_create(t *testing.T) {
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckResourceDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource_force_new" "foo" {
|
||||||
|
triggers = {
|
||||||
|
"a" = "foo"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func TestResourceForceNew_update(t *testing.T) {
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckResourceDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource_force_new" "foo" {
|
||||||
|
triggers = {
|
||||||
|
"a" = "foo"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource_force_new" "foo" {
|
||||||
|
triggers = {
|
||||||
|
"a" = "bar"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource_force_new" "foo" {
|
||||||
|
triggers = {
|
||||||
|
"b" = "bar"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceForceNew_remove(t *testing.T) {
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckResourceDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource_force_new" "foo" {
|
||||||
|
triggers = {
|
||||||
|
"a" = "bar"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource_force_new" "foo" {
|
||||||
|
} `),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -443,3 +443,33 @@ output "value_from_map_from_list" {
|
||||||
func testAccCheckResourceDestroy(s *terraform.State) error {
|
func testAccCheckResourceDestroy(s *terraform.State) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResource_removeForceNew(t *testing.T) {
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckResourceDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource" "foo" {
|
||||||
|
required = "yep"
|
||||||
|
required_map = {
|
||||||
|
key = "value"
|
||||||
|
}
|
||||||
|
optional_force_new = "here"
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: strings.TrimSpace(`
|
||||||
|
resource "test_resource" "foo" {
|
||||||
|
required = "yep"
|
||||||
|
required_map = {
|
||||||
|
key = "value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,10 @@ func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) {
|
||||||
paths = append(paths, p)
|
paths = append(paths, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now trim off any trailing paths that aren't GetAttrSteps, since only an
|
||||||
|
// attribute itself can require replacement
|
||||||
|
paths = trimPaths(paths)
|
||||||
|
|
||||||
// There may be redundant paths due to set elements or index attributes
|
// There may be redundant paths due to set elements or index attributes
|
||||||
// Do some ugly n^2 filtering, but these are always fairly small sets.
|
// Do some ugly n^2 filtering, but these are always fairly small sets.
|
||||||
for i := 0; i < len(paths)-1; i++ {
|
for i := 0; i < len(paths)-1; i++ {
|
||||||
|
@ -44,6 +48,30 @@ func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) {
|
||||||
return paths, nil
|
return paths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trimPaths removes any trailing steps that aren't of type GetAttrSet, since
|
||||||
|
// only an attribute itself can require replacement
|
||||||
|
func trimPaths(paths []cty.Path) []cty.Path {
|
||||||
|
var trimmed []cty.Path
|
||||||
|
for _, path := range paths {
|
||||||
|
path = trimPath(path)
|
||||||
|
if len(path) > 0 {
|
||||||
|
trimmed = append(trimmed, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimPath(path cty.Path) cty.Path {
|
||||||
|
for len(path) > 0 {
|
||||||
|
_, isGetAttr := path[len(path)-1].(cty.GetAttrStep)
|
||||||
|
if isGetAttr {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
// requiresReplacePath takes a key from a flatmap along with the cty.Type
|
// requiresReplacePath takes a key from a flatmap along with the cty.Type
|
||||||
// describing the structure, and returns the cty.Path that would be used to
|
// describing the structure, and returns the cty.Path that would be used to
|
||||||
// reference the nested value in the data structure.
|
// reference the nested value in the data structure.
|
||||||
|
|
|
@ -6,9 +6,18 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ignoreUnexported = cmpopts.IgnoreUnexported(cty.GetAttrStep{}, cty.IndexStep{})
|
||||||
|
valueComparer = cmp.Comparer(cty.Value.RawEquals)
|
||||||
|
)
|
||||||
|
|
||||||
func TestPathFromFlatmap(t *testing.T) {
|
func TestPathFromFlatmap(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Flatmap string
|
Flatmap string
|
||||||
|
@ -221,3 +230,140 @@ func TestPathFromFlatmap(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequiresReplace(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
attrs []string
|
||||||
|
expected []cty.Path
|
||||||
|
ty cty.Type
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
attrs: []string{
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.String,
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two",
|
||||||
|
attrs: []string{
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.String,
|
||||||
|
"bar": cty.String,
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}},
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "bar"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested object",
|
||||||
|
attrs: []string{
|
||||||
|
"foo.bar",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.Object(map[string]cty.Type{
|
||||||
|
"bar": cty.String,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested objects",
|
||||||
|
attrs: []string{
|
||||||
|
"foo.bar.baz",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.Object(map[string]cty.Type{
|
||||||
|
"bar": cty.Object(map[string]cty.Type{
|
||||||
|
"baz": cty.String,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}, cty.GetAttrStep{Name: "baz"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested map",
|
||||||
|
attrs: []string{
|
||||||
|
"foo.%",
|
||||||
|
"foo.bar",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.Map(cty.String),
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested list",
|
||||||
|
attrs: []string{
|
||||||
|
"foo.#",
|
||||||
|
"foo.1",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.Map(cty.String),
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object in map",
|
||||||
|
attrs: []string{
|
||||||
|
"foo.bar.baz",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.Map(cty.Object(
|
||||||
|
map[string]cty.Type{
|
||||||
|
"baz": cty.String,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.GetAttrStep{Name: "baz"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object in list",
|
||||||
|
attrs: []string{
|
||||||
|
"foo.1.baz",
|
||||||
|
},
|
||||||
|
ty: cty.Object(map[string]cty.Type{
|
||||||
|
"foo": cty.List(cty.Object(
|
||||||
|
map[string]cty.Type{
|
||||||
|
"baz": cty.String,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
expected: []cty.Path{
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "baz"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rp, err := RequiresReplace(tc.attrs, tc.ty)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !cmp.Equal(tc.expected, rp, ignoreUnexported, valueComparer) {
|
||||||
|
t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, rp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -399,12 +399,12 @@ func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadReso
|
||||||
// The old provider API used an empty id to signal that the remote
|
// The old provider API used an empty id to signal that the remote
|
||||||
// object appears to have been deleted, but our new protocol expects
|
// object appears to have been deleted, but our new protocol expects
|
||||||
// to see a null value (in the cty sense) in that case.
|
// to see a null value (in the cty sense) in that case.
|
||||||
newConfigMP, err := msgpack.Marshal(cty.NullVal(block.ImpliedType()), block.ImpliedType())
|
newStateMP, err := msgpack.Marshal(cty.NullVal(block.ImpliedType()), block.ImpliedType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||||
}
|
}
|
||||||
resp.NewState = &proto.DynamicValue{
|
resp.NewState = &proto.DynamicValue{
|
||||||
Msgpack: newConfigMP,
|
Msgpack: newStateMP,
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,11 +310,15 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
// from known prior values to unknown values, unless the provider is
|
// from known prior values to unknown values, unless the provider is
|
||||||
// able to predict new values for any of these computed attributes.
|
// able to predict new values for any of these computed attributes.
|
||||||
nullPriorVal := cty.NullVal(schema.ImpliedType())
|
nullPriorVal := cty.NullVal(schema.ImpliedType())
|
||||||
|
|
||||||
|
// create a new proposed value from the null state and the config
|
||||||
|
proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, configVal)
|
||||||
|
|
||||||
resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
||||||
TypeName: n.Addr.Resource.Type,
|
TypeName: n.Addr.Resource.Type,
|
||||||
Config: configVal,
|
Config: configVal,
|
||||||
PriorState: nullPriorVal,
|
PriorState: nullPriorVal,
|
||||||
ProposedNewState: configVal,
|
ProposedNewState: proposedNewVal,
|
||||||
PriorPrivate: plannedPrivate,
|
PriorPrivate: plannedPrivate,
|
||||||
})
|
})
|
||||||
// We need to tread carefully here, since if there are any warnings
|
// We need to tread carefully here, since if there are any warnings
|
||||||
|
|
Loading…
Reference in New Issue