Merge pull request #10 from hashicorp/f-config-helpers
Complex configuration (non-string) works with Diff helpers
This commit is contained in:
commit
e1a0f6f929
|
@ -0,0 +1,73 @@
|
|||
package flatmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Expand takes a map and a key (prefix) and expands that value into
|
||||
// a more complex structure. This is the reverse of the Flatten operation.
|
||||
func Expand(m map[string]string, key string) interface{} {
|
||||
// If the key is exactly a key in the map, just return it
|
||||
if v, ok := m[key]; ok {
|
||||
if num, err := strconv.ParseInt(v, 0, 0); err == nil {
|
||||
return int(num)
|
||||
} else if v == "true" {
|
||||
return true
|
||||
} else if v == "false" {
|
||||
return false
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Check if the key is an array, and if so, expand the array
|
||||
if _, ok := m[key+".#"]; ok {
|
||||
return expandArray(m, key)
|
||||
}
|
||||
|
||||
// Check if this is a prefix in the map
|
||||
prefix := key + "."
|
||||
for k, _ := range m {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
return expandMap(m, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandArray(m map[string]string, prefix string) []interface{} {
|
||||
num, err := strconv.ParseInt(m[prefix+".#"], 0, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := make([]interface{}, num)
|
||||
for i := 0; i < int(num); i++ {
|
||||
result[i] = Expand(m, fmt.Sprintf("%s.%d", prefix, i))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func expandMap(m map[string]string, prefix string) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, _ := range m {
|
||||
if !strings.HasPrefix(k, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := k[len(prefix):]
|
||||
idx := strings.Index(key, ".")
|
||||
if idx == -1 {
|
||||
idx = len(k)
|
||||
}
|
||||
|
||||
// It contains a period, so it is a more complex structure
|
||||
result[key] = Expand(m, k[:idx])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package flatmap
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpand(t *testing.T) {
|
||||
cases := []struct {
|
||||
Map map[string]string
|
||||
Key string
|
||||
Output interface{}
|
||||
}{
|
||||
{
|
||||
Map: map[string]string{
|
||||
"foo": "bar",
|
||||
"bar": "baz",
|
||||
},
|
||||
Key: "foo",
|
||||
Output: "bar",
|
||||
},
|
||||
|
||||
{
|
||||
Map: map[string]string{
|
||||
"foo.#": "2",
|
||||
"foo.0": "one",
|
||||
"foo.1": "two",
|
||||
},
|
||||
Key: "foo",
|
||||
Output: []interface{}{
|
||||
"one",
|
||||
"two",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Map: map[string]string{
|
||||
"foo.#": "1",
|
||||
"foo.0.name": "bar",
|
||||
"foo.0.port": "3000",
|
||||
"foo.0.enabled": "true",
|
||||
},
|
||||
Key: "foo",
|
||||
Output: []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "bar",
|
||||
"port": 3000,
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
actual := Expand(tc.Map, tc.Key)
|
||||
if !reflect.DeepEqual(actual, tc.Output) {
|
||||
t.Fatalf(
|
||||
"Key: %v\nMap:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n",
|
||||
tc.Key,
|
||||
tc.Map,
|
||||
actual,
|
||||
tc.Output)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package flatmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Flatten takes a structure and turns into a flat map[string]string.
|
||||
//
|
||||
// Within the "thing" parameter, only primitive values are allowed. Structs are
|
||||
// not supported. Therefore, it can only be slices, maps, primitives, and
|
||||
// any combination of those together.
|
||||
//
|
||||
// See the tests for examples of what inputs are turned into.
|
||||
func Flatten(thing map[string]interface{}) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
for k, raw := range thing {
|
||||
flatten(result, k, reflect.ValueOf(raw))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func flatten(result map[string]string, prefix string, v reflect.Value) {
|
||||
if v.Kind() == reflect.Interface {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
result[prefix] = "true"
|
||||
} else {
|
||||
result[prefix] = "false"
|
||||
}
|
||||
case reflect.Int:
|
||||
result[prefix] = fmt.Sprintf("%d", v.Int())
|
||||
case reflect.Map:
|
||||
flattenMap(result, prefix, v)
|
||||
case reflect.Slice:
|
||||
flattenSlice(result, prefix, v)
|
||||
case reflect.String:
|
||||
result[prefix] = v.String()
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown: %s", v))
|
||||
}
|
||||
}
|
||||
|
||||
func flattenMap(result map[string]string, prefix string, v reflect.Value) {
|
||||
for _, k := range v.MapKeys() {
|
||||
if k.Kind() == reflect.Interface {
|
||||
k = k.Elem()
|
||||
}
|
||||
|
||||
if k.Kind() != reflect.String {
|
||||
panic(fmt.Sprintf("%s: map key is not string: %s", prefix, k))
|
||||
}
|
||||
|
||||
flatten(result, fmt.Sprintf("%s.%s", prefix, k.String()), v.MapIndex(k))
|
||||
}
|
||||
}
|
||||
|
||||
func flattenSlice(result map[string]string, prefix string, v reflect.Value) {
|
||||
prefix = prefix + "."
|
||||
|
||||
result[prefix+"#"] = fmt.Sprintf("%d", v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
flatten(result, fmt.Sprintf("%s%d", prefix, i), v.Index(i))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package flatmap
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input map[string]interface{}
|
||||
Output map[string]string
|
||||
}{
|
||||
{
|
||||
Input: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
"bar": "baz",
|
||||
},
|
||||
Output: map[string]string{
|
||||
"foo": "bar",
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Input: map[string]interface{}{
|
||||
"foo": []string{
|
||||
"one",
|
||||
"two",
|
||||
},
|
||||
},
|
||||
Output: map[string]string{
|
||||
"foo.#": "2",
|
||||
"foo.0": "one",
|
||||
"foo.1": "two",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Input: map[string]interface{}{
|
||||
"foo": []map[interface{}]interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"name": "bar",
|
||||
"port": 3000,
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Output: map[string]string{
|
||||
"foo.#": "1",
|
||||
"foo.0.name": "bar",
|
||||
"foo.0.port": "3000",
|
||||
"foo.0.enabled": "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
actual := Flatten(tc.Input)
|
||||
if !reflect.DeepEqual(actual, tc.Output) {
|
||||
t.Fatalf(
|
||||
"Input:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n",
|
||||
tc.Input,
|
||||
actual,
|
||||
tc.Output)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,43 @@
|
|||
package diff
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/flatmap"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// AttrType is an enum that tells the ResourceBuilder what type of attribute
|
||||
// an attribute is, affecting the overall diff output.
|
||||
//
|
||||
// The valid values are:
|
||||
//
|
||||
// * AttrTypeCreate - This attribute can only be set or updated on create.
|
||||
// This means that if this attribute is changed, it will require a new
|
||||
// resource to be created if it is already created.
|
||||
//
|
||||
// * AttrTypeUpdate - This attribute can be set at create time or updated
|
||||
// in-place. Changing this attribute does not require a new resource.
|
||||
//
|
||||
type AttrType byte
|
||||
|
||||
const (
|
||||
AttrTypeUnknown AttrType = iota
|
||||
AttrTypeCreate
|
||||
AttrTypeUpdate
|
||||
)
|
||||
|
||||
// ResourceBuilder is a helper that knows about how a single resource
|
||||
// changes and how those changes affect the diff.
|
||||
type ResourceBuilder struct {
|
||||
CreateComputedAttrs []string
|
||||
RequiresNewAttrs []string
|
||||
// Attrs are the mapping of attributes that can be set from the
|
||||
// configuration, and the affect they have. See the documentation for
|
||||
// AttrType for more info.
|
||||
Attrs map[string]AttrType
|
||||
|
||||
// ComputedAttrs are the attributes that are computed at
|
||||
// resource creation time.
|
||||
ComputedAttrs []string
|
||||
}
|
||||
|
||||
// Diff returns the ResourceDiff for a resource given its state and
|
||||
|
@ -18,45 +47,52 @@ func (b *ResourceBuilder) Diff(
|
|||
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
|
||||
attrs := make(map[string]*terraform.ResourceAttrDiff)
|
||||
|
||||
requiresNewSet := make(map[string]struct{})
|
||||
for _, k := range b.RequiresNewAttrs {
|
||||
requiresNewSet[k] = struct{}{}
|
||||
}
|
||||
|
||||
// We require a new resource if the ID is empty. Or, later, we set
|
||||
// this to true if any configuration changed that triggers a new resource.
|
||||
requiresNew := s.ID == ""
|
||||
|
||||
// Go through the configuration and find the changed attributes
|
||||
for k, v := range c.Raw {
|
||||
newV := v.(string)
|
||||
// Flatten the raw and processed configuration
|
||||
flatRaw := flatmap.Flatten(c.Raw)
|
||||
flatConfig := flatmap.Flatten(c.Config)
|
||||
|
||||
for k, v := range flatRaw {
|
||||
// Make sure this is an attribute that actually affects
|
||||
// the diff in some way.
|
||||
var attr AttrType
|
||||
for ak, at := range b.Attrs {
|
||||
if strings.HasPrefix(k, ak) {
|
||||
attr = at
|
||||
break
|
||||
}
|
||||
}
|
||||
if attr == AttrTypeUnknown {
|
||||
continue
|
||||
}
|
||||
|
||||
// If this key is in the cleaned config, then use that value
|
||||
// because it'll have its variables properly interpolated
|
||||
if cleanV, ok := c.Config[k]; ok {
|
||||
newV = cleanV.(string)
|
||||
if cleanV, ok := flatConfig[k]; ok {
|
||||
v = cleanV
|
||||
}
|
||||
|
||||
var oldV string
|
||||
var ok bool
|
||||
if oldV, ok = s.Attributes[k]; ok {
|
||||
// Old value exists! We check to see if there is a change
|
||||
if oldV == newV {
|
||||
continue
|
||||
}
|
||||
oldV, ok := s.Attributes[k]
|
||||
|
||||
// If there is an old value and they're the same, no change
|
||||
if ok && oldV == v {
|
||||
continue
|
||||
}
|
||||
|
||||
// There has been a change. Record it
|
||||
// Record the change
|
||||
attrs[k] = &terraform.ResourceAttrDiff{
|
||||
Old: oldV,
|
||||
New: newV,
|
||||
Old: oldV,
|
||||
New: v,
|
||||
Type: terraform.DiffAttrInput,
|
||||
}
|
||||
|
||||
// If this requires a new resource, record that and flag our
|
||||
// boolean.
|
||||
if _, ok := requiresNewSet[k]; ok {
|
||||
if attr == AttrTypeCreate {
|
||||
attrs[k].RequiresNew = true
|
||||
attrs[k].Type = terraform.DiffAttrInput
|
||||
requiresNew = true
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +100,7 @@ func (b *ResourceBuilder) Diff(
|
|||
// If we require a new resource, then process all the attributes
|
||||
// that will be changing due to the creation of the resource.
|
||||
if requiresNew {
|
||||
for _, k := range b.CreateComputedAttrs {
|
||||
for _, k := range b.ComputedAttrs {
|
||||
old := s.Attributes[k]
|
||||
attrs[k] = &terraform.ResourceAttrDiff{
|
||||
Old: old,
|
||||
|
|
|
@ -7,9 +7,51 @@ import (
|
|||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestResourceBuilder_complex(t *testing.T) {
|
||||
rb := &ResourceBuilder{
|
||||
Attrs: map[string]AttrType{
|
||||
"listener": AttrTypeUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
state := &terraform.ResourceState{
|
||||
ID: "foo",
|
||||
Attributes: map[string]string{
|
||||
"ignore": "1",
|
||||
"listener.#": "1",
|
||||
"listener.0.port": "80",
|
||||
},
|
||||
}
|
||||
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"listener": []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"port": 3000,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
diff, err := rb.Diff(state, c)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if diff == nil {
|
||||
t.Fatal("should not be nil")
|
||||
}
|
||||
|
||||
actual := testResourceDiffStr(diff)
|
||||
expected := testRBComplexDiff
|
||||
if actual != expected {
|
||||
t.Fatalf("bad: %s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceBuilder_new(t *testing.T) {
|
||||
rb := &ResourceBuilder{
|
||||
CreateComputedAttrs: []string{"private_ip"},
|
||||
Attrs: map[string]AttrType{
|
||||
"foo": AttrTypeUpdate,
|
||||
},
|
||||
ComputedAttrs: []string{"private_ip"},
|
||||
}
|
||||
|
||||
state := &terraform.ResourceState{}
|
||||
|
@ -35,8 +77,10 @@ func TestResourceBuilder_new(t *testing.T) {
|
|||
|
||||
func TestResourceBuilder_requiresNew(t *testing.T) {
|
||||
rb := &ResourceBuilder{
|
||||
CreateComputedAttrs: []string{"private_ip"},
|
||||
RequiresNewAttrs: []string{"ami"},
|
||||
ComputedAttrs: []string{"private_ip"},
|
||||
Attrs: map[string]AttrType{
|
||||
"ami": AttrTypeCreate,
|
||||
},
|
||||
}
|
||||
|
||||
state := &terraform.ResourceState{
|
||||
|
@ -68,7 +112,7 @@ func TestResourceBuilder_requiresNew(t *testing.T) {
|
|||
|
||||
func TestResourceBuilder_same(t *testing.T) {
|
||||
rb := &ResourceBuilder{
|
||||
CreateComputedAttrs: []string{"private_ip"},
|
||||
ComputedAttrs: []string{"private_ip"},
|
||||
}
|
||||
|
||||
state := &terraform.ResourceState{
|
||||
|
@ -92,7 +136,11 @@ func TestResourceBuilder_same(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResourceBuilder_unknown(t *testing.T) {
|
||||
rb := &ResourceBuilder{}
|
||||
rb := &ResourceBuilder{
|
||||
Attrs: map[string]AttrType{
|
||||
"foo": AttrTypeUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
state := &terraform.ResourceState{}
|
||||
|
||||
|
@ -119,7 +167,11 @@ func TestResourceBuilder_unknown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResourceBuilder_vars(t *testing.T) {
|
||||
rb := &ResourceBuilder{}
|
||||
rb := &ResourceBuilder{
|
||||
Attrs: map[string]AttrType{
|
||||
"foo": AttrTypeUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
state := &terraform.ResourceState{}
|
||||
|
||||
|
@ -144,6 +196,10 @@ func TestResourceBuilder_vars(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
const testRBComplexDiff = `UPDATE
|
||||
IN listener.0.port: "80" => "3000"
|
||||
`
|
||||
|
||||
const testRBNewDiff = `UPDATE
|
||||
IN foo: "" => "bar"
|
||||
OUT private_ip: "" => "<computed>"
|
||||
|
|
Loading…
Reference in New Issue