From 87a488092cb535d45038f4ffd9a55572914994d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Aug 2014 20:19:44 -0700 Subject: [PATCH] helper/schema: support partial states --- helper/schema/resource_data.go | 64 ++++++-- helper/schema/resource_data_test.go | 236 +++++++++++++++++++++++++++- 2 files changed, 284 insertions(+), 16 deletions(-) diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index fdd8dc11a..093c24a7b 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -42,9 +42,11 @@ type ResourceData struct { diff *terraform.ResourceDiff diffing bool - setMap map[string]string - newState *terraform.ResourceState - once sync.Once + setMap map[string]string + newState *terraform.ResourceState + partial bool + partialMap map[string]struct{} + once sync.Once } // Get returns the data for the given key, or nil if the key doesn't exist @@ -72,17 +74,17 @@ func (d *ResourceData) GetChange(key string) (interface{}, interface{}) { // existed or not in the configuration. The second boolean result will also // be false if a key is given that isn't in the schema at all. func (d *ResourceData) GetOk(key string) (interface{}, bool) { - r := d.getRaw(key) + r := d.getRaw(key, getSourceSet) return r.Value, r.Exists } -func (d *ResourceData) getRaw(key string) getResult { +func (d *ResourceData) getRaw(key string, level getSource) getResult { var parts []string if key != "" { parts = strings.Split(key, ".") } - return d.getObject("", parts, d.schema, getSourceSet) + return d.getObject("", parts, d.schema, level) } // HasChange returns whether or not the given key has been changed. @@ -91,6 +93,20 @@ func (d *ResourceData) HasChange(key string) bool { return !reflect.DeepEqual(o, n) } +// Partial turns partial state mode on/off. +// +// When partial state mode is enabled, then only key prefixes specified +// by SetPartial will be in the final state. This allows providers to return +// partial states for partially applied resources (when errors occur). +func (d *ResourceData) Partial(on bool) { + d.partial = on + if on { + d.partialMap = make(map[string]struct{}) + } else { + d.partialMap = nil + } +} + // Set sets the value for the given key. // // If the key is invalid or the value is not a correct type, an error @@ -104,6 +120,17 @@ func (d *ResourceData) Set(key string, value interface{}) error { return d.setObject("", parts, d.schema, value) } +// SetPartial adds the key prefix to the final state output while +// in partial state mode. +// +// If partial state mode is disabled, then this has no effect. Additionally, +// whenever partial state mode is toggled, the partial data is cleared. +func (d *ResourceData) SetPartial(k string) { + if d.partial { + d.partialMap[k] = struct{}{} + } +} + // Id returns the ID of the resource. func (d *ResourceData) Id() string { var result string @@ -799,7 +826,7 @@ func (d *ResourceData) setSet( func (d *ResourceData) stateList( prefix string, schema *Schema) map[string]string { - countRaw := d.get(prefix, []string{"#"}, schema, getSourceSet) + countRaw := d.get(prefix, []string{"#"}, schema, d.stateSource(prefix)) if !countRaw.Exists { return nil } @@ -831,7 +858,7 @@ func (d *ResourceData) stateList( func (d *ResourceData) stateMap( prefix string, schema *Schema) map[string]string { - v := d.getMap(prefix, nil, schema, getSourceSet) + v := d.getMap(prefix, nil, schema, d.stateSource(prefix)) if !v.Exists { return nil } @@ -869,7 +896,7 @@ func (d *ResourceData) stateObject( func (d *ResourceData) statePrimitive( prefix string, schema *Schema) map[string]string { - raw := d.getRaw(prefix) + raw := d.getRaw(prefix, d.stateSource(prefix)) if !raw.Exists { return nil } @@ -899,7 +926,7 @@ func (d *ResourceData) statePrimitive( func (d *ResourceData) stateSet( prefix string, schema *Schema) map[string]string { - raw := d.get(prefix, nil, schema, getSourceSet) + raw := d.get(prefix, nil, schema, d.stateSource(prefix)) if !raw.Exists { return nil } @@ -947,3 +974,20 @@ func (d *ResourceData) stateSingle( panic(fmt.Sprintf("%s: unknown type %#v", prefix, schema.Type)) } } + +func (d *ResourceData) stateSource(prefix string) getSource { + // If we're not doing a partial apply, then get the set level + if !d.partial { + return getSourceSet + } + + // Otherwise, only return getSourceSet if its in the partial map. + // Otherwise we use state level only. + for k, _ := range d.partialMap { + if strings.HasPrefix(prefix, k) { + return getSourceSet + } + } + + return getSourceState +} diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index f1773ed06..3b630459d 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -1363,11 +1363,12 @@ func TestResourceDataSet(t *testing.T) { func TestResourceDataState(t *testing.T) { cases := []struct { - Schema map[string]*Schema - State *terraform.ResourceState - Diff *terraform.ResourceDiff - Set map[string]interface{} - Result *terraform.ResourceState + Schema map[string]*Schema + State *terraform.ResourceState + Diff *terraform.ResourceDiff + Set map[string]interface{} + Result *terraform.ResourceState + Partial []string }{ // Basic primitive in diff { @@ -1728,6 +1729,221 @@ func TestResourceDataState(t *testing.T) { }, }, }, + + /* + * PARTIAL STATES + */ + + // Basic primitive + { + Schema: map[string]*Schema{ + "availability_zone": &Schema{ + Type: TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + + State: nil, + + Diff: &terraform.ResourceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "availability_zone": &terraform.ResourceAttrDiff{ + Old: "", + New: "foo", + RequiresNew: true, + }, + }, + }, + + Partial: []string{}, + + Result: &terraform.ResourceState{ + Attributes: map[string]string{}, + }, + }, + + // List + { + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeList, + Required: true, + Elem: &Schema{Type: TypeInt}, + }, + }, + + State: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "1", + "ports.0": "80", + }, + }, + + Diff: &terraform.ResourceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "ports.#": &terraform.ResourceAttrDiff{ + Old: "1", + New: "2", + }, + "ports.1": &terraform.ResourceAttrDiff{ + Old: "", + New: "100", + }, + }, + }, + + Partial: []string{}, + + Result: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "1", + "ports.0": "80", + }, + }, + }, + + // List of resources + { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "from": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + }, + + State: &terraform.ResourceState{ + Attributes: map[string]string{ + "ingress.#": "1", + "ingress.0.from": "80", + }, + }, + + Diff: &terraform.ResourceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "ingress.#": &terraform.ResourceAttrDiff{ + Old: "1", + New: "2", + }, + "ingress.0.from": &terraform.ResourceAttrDiff{ + Old: "80", + New: "150", + }, + "ingress.1.from": &terraform.ResourceAttrDiff{ + Old: "", + New: "100", + }, + }, + }, + + Partial: []string{}, + + Result: &terraform.ResourceState{ + Attributes: map[string]string{ + "ingress.#": "1", + "ingress.0.from": "80", + }, + }, + }, + + // List of maps + { + Schema: map[string]*Schema{ + "config_vars": &Schema{ + Type: TypeList, + Optional: true, + Computed: true, + Elem: &Schema{ + Type: TypeMap, + }, + }, + }, + + State: &terraform.ResourceState{ + Attributes: map[string]string{ + "config_vars.#": "2", + "config_vars.0.foo": "bar", + "config_vars.0.bar": "bar", + "config_vars.1.bar": "baz", + }, + }, + + Diff: &terraform.ResourceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "config_vars.0.bar": &terraform.ResourceAttrDiff{ + NewRemoved: true, + }, + }, + }, + + Set: map[string]interface{}{ + "config_vars.1": map[string]interface{}{ + "baz": "bang", + }, + }, + + Partial: []string{}, + + Result: &terraform.ResourceState{ + Attributes: map[string]string{ + "config_vars.#": "2", + "config_vars.0.foo": "bar", + "config_vars.0.bar": "bar", + "config_vars.1.bar": "baz", + }, + }, + }, + + // Sets + { + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeSet, + Optional: true, + Computed: true, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + }, + + State: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "3", + "ports.0": "100", + "ports.1": "80", + "ports.2": "80", + }, + }, + + Diff: &terraform.ResourceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "ports.1": &terraform.ResourceAttrDiff{ + New: "120", + }, + }, + }, + + Partial: []string{}, + + Result: &terraform.ResourceState{ + Attributes: map[string]string{ + "ports.#": "2", + "ports.0": "80", + "ports.1": "100", + }, + }, + }, } for i, tc := range cases { @@ -1749,6 +1965,14 @@ func TestResourceDataState(t *testing.T) { d.SetId("foo") } + // If we have partial, then enable partial state mode. + if tc.Partial != nil { + d.Partial(true) + for _, k := range tc.Partial { + d.SetPartial(k) + } + } + actual := d.State() // If we set an ID, then undo what we did so the comparison works @@ -1758,7 +1982,7 @@ func TestResourceDataState(t *testing.T) { } if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("Bad: %d\n\n%#v", i, actual) + t.Fatalf("Bad: %d\n\n%#v\n\nExpected:\n\n%#v", i, actual, tc.Result) } } }