Merge pull request #19536 from hashicorp/jbardin/state-funcs

Handle StateFunc in plugin shims
This commit is contained in:
James Bardin 2018-12-04 11:01:39 -05:00 committed by GitHub
commit 7cec01a883
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 228 additions and 14 deletions

View File

@ -25,6 +25,7 @@ func Provider() terraform.ResourceProvider {
"test_resource_force_new": testResourceForceNew(), "test_resource_force_new": testResourceForceNew(),
"test_resource_nested": testResourceNested(), "test_resource_nested": testResourceNested(),
"test_resource_nested_set": testResourceNestedSet(), "test_resource_nested_set": testResourceNestedSet(),
"test_resource_state_func": testResourceStateFunc(),
}, },
DataSourcesMap: map[string]*schema.Resource{ DataSourcesMap: map[string]*schema.Resource{
"test_data_source": testDataSource(), "test_data_source": testDataSource(),

View File

@ -0,0 +1,74 @@
package test
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"math/rand"
"github.com/hashicorp/terraform/helper/schema"
)
func testResourceStateFunc() *schema.Resource {
return &schema.Resource{
Create: testResourceStateFuncCreate,
Read: testResourceStateFuncRead,
Update: testResourceStateFuncUpdate,
Delete: testResourceStateFuncDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
Optional: true,
},
"state_func": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
StateFunc: stateFuncHash,
},
"state_func_value": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
func stateFuncHash(v interface{}) string {
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
}
func testResourceStateFuncCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fmt.Sprintf("%x", rand.Int63()))
// if we have a reference for the actual data in the state_func field,
// compare it
if data, ok := d.GetOk("state_func_value"); ok {
expected := data.(string)
got := d.Get("state_func").(string)
if expected != got {
return fmt.Errorf("expected state_func value:%q, got%q", expected, got)
}
}
return testResourceStateFuncRead(d, meta)
}
func testResourceStateFuncRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceStateFuncUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceStateFuncDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -0,0 +1,60 @@
package test
import (
"strings"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestResourceStateFunc_basic(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_state_func" "foo" {
}
`),
Check: resource.TestCheckNoResourceAttr("test_resource_state_func.foo", "state_func"),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_state_func" "foo" {
state_func = "data"
state_func_value = "data"
}
`),
Check: resource.TestCheckResourceAttr("test_resource_state_func.foo", "state_func", stateFuncHash("data")),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_state_func" "foo" {
}
`),
Check: resource.TestCheckNoResourceAttr("test_resource_state_func.foo", "state_func"),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_state_func" "foo" {
optional = "added"
state_func = "data"
state_func_value = "data"
}
`),
Check: resource.TestCheckResourceAttr("test_resource_state_func.foo", "state_func", stateFuncHash("data")),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_state_func" "foo" {
optional = "added"
state_func = "changed"
state_func_value = "changed"
}
`),
Check: resource.TestCheckResourceAttr("test_resource_state_func.foo", "state_func", stateFuncHash("changed")),
},
},
})
}

View File

@ -22,6 +22,8 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
const newExtraKey = "_new_extra_shim"
// NewGRPCProviderServerShim wraps a terraform.ResourceProvider in a // NewGRPCProviderServerShim wraps a terraform.ResourceProvider in a
// proto.ProviderServer implementation. If the provided provider is not a // proto.ProviderServer implementation. If the provided provider is not a
// *schema.Provider, this will return nil, // *schema.Provider, this will return nil,
@ -563,16 +565,30 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
Msgpack: plannedMP, Msgpack: plannedMP,
} }
// the Meta field gets encoded into PlannedPrivate // Now we need to store any NewExtra values, which are where any actual
if diff.Meta != nil { // StateFunc modified config fields are hidden.
plannedPrivate, err := json.Marshal(diff.Meta) privateMap := diff.Meta
if err != nil { if privateMap == nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) privateMap = map[string]interface{}{}
return resp, nil
}
resp.PlannedPrivate = plannedPrivate
} }
newExtra := map[string]interface{}{}
for k, v := range diff.Attributes {
if v.NewExtra != nil {
newExtra[k] = v.NewExtra
}
}
privateMap[newExtraKey] = newExtra
// the Meta field gets encoded into PlannedPrivate
plannedPrivate, err := json.Marshal(privateMap)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
resp.PlannedPrivate = plannedPrivate
// collect the attributes that require instance replacement, and convert // collect the attributes that require instance replacement, and convert
// them to cty.Paths. // them to cty.Paths.
var requiresNew []string var requiresNew []string
@ -653,7 +669,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
Destroy: true, Destroy: true,
} }
} else { } else {
diff, err = schema.DiffFromValues(priorStateVal, plannedStateVal, res) diff, err = schema.DiffFromValues(priorStateVal, plannedStateVal, stripResourceModifiers(res))
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil return resp, nil
@ -667,9 +683,23 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
} }
} }
// add NewExtra Fields that may have been stored in the private data
if newExtra := private[newExtraKey]; newExtra != nil {
for k, v := range newExtra.(map[string]interface{}) {
d := diff.Attributes[k]
if d == nil {
d = &terraform.ResourceAttrDiff{}
}
d.NewExtra = v
diff.Attributes[k] = d
}
}
// strip out non-diffs // strip out non-diffs
for k, v := range diff.Attributes { for k, v := range diff.Attributes {
if v.New == v.Old && !v.NewComputed && !v.NewRemoved { if v.New == v.Old && !v.NewComputed && !v.NewRemoved && v.NewExtra == "" {
delete(diff.Attributes, k) delete(diff.Attributes, k)
} }
} }
@ -684,11 +714,10 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
return resp, nil return resp, nil
} }
// here we use the planned state to check for unknown/zero containers values
// when normalizing the flatmap.
plannedState := hcl2shim.FlatmapValueFromHCL2(plannedStateVal)
if newInstanceState != nil { if newInstanceState != nil {
// here we use the planned state to check for unknown/zero containers values
// when normalizing the flatmap.
plannedState := hcl2shim.FlatmapValueFromHCL2(plannedStateVal)
newInstanceState.Attributes = normalizeFlatmapContainers(plannedState, newInstanceState.Attributes) newInstanceState.Attributes = normalizeFlatmapContainers(plannedState, newInstanceState.Attributes)
} }
@ -972,3 +1001,45 @@ func copyTimeoutValues(to cty.Value, from cty.Value) cty.Value {
return cty.ObjectVal(toAttrs) return cty.ObjectVal(toAttrs)
} }
// stripResourceModifiers takes a *schema.Resource and returns a deep copy with all
// StateFuncs and CustomizeDiffs removed. This will be used during apply to
// create a diff from a planned state where the diff modifications have already
// been applied.
func stripResourceModifiers(r *schema.Resource) *schema.Resource {
if r == nil {
return nil
}
// start with a shallow copy
newResource := new(schema.Resource)
*newResource = *r
newResource.CustomizeDiff = nil
newResource.Schema = map[string]*schema.Schema{}
for k, s := range r.Schema {
newResource.Schema[k] = stripSchema(s)
}
return newResource
}
func stripSchema(s *schema.Schema) *schema.Schema {
if s == nil {
return nil
}
// start with a shallow copy
newSchema := new(schema.Schema)
*newSchema = *s
newSchema.StateFunc = nil
switch e := newSchema.Elem.(type) {
case *schema.Schema:
newSchema.Elem = stripSchema(e)
case *schema.Resource:
newSchema.Elem = stripResourceModifiers(e)
}
return newSchema
}

View File

@ -358,6 +358,13 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(err)
return resp return resp
} }
configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Block.ImpliedType()) propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Block.ImpliedType())
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(err)
@ -367,6 +374,7 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
protoReq := &proto.PlanResourceChange_Request{ protoReq := &proto.PlanResourceChange_Request{
TypeName: r.TypeName, TypeName: r.TypeName,
PriorState: &proto.DynamicValue{Msgpack: priorMP}, PriorState: &proto.DynamicValue{Msgpack: priorMP},
Config: &proto.DynamicValue{Msgpack: configMP},
ProposedNewState: &proto.DynamicValue{Msgpack: propMP}, ProposedNewState: &proto.DynamicValue{Msgpack: propMP},
PriorPrivate: r.PriorPrivate, PriorPrivate: r.PriorPrivate,
} }