helper/schema: support partial states

This commit is contained in:
Mitchell Hashimoto 2014-08-26 20:19:44 -07:00
parent 40e5608fa9
commit 87a488092c
2 changed files with 284 additions and 16 deletions

View File

@ -44,6 +44,8 @@ type ResourceData struct {
setMap map[string]string
newState *terraform.ResourceState
partial bool
partialMap map[string]struct{}
once sync.Once
}
@ -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
}

View File

@ -1368,6 +1368,7 @@ func TestResourceDataState(t *testing.T) {
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)
}
}
}