diff --git a/helper/resource/map.go b/helper/resource/map.go deleted file mode 100644 index 02a993d6922..00000000000 --- a/helper/resource/map.go +++ /dev/null @@ -1,140 +0,0 @@ -package resource - -import ( - "fmt" - "sort" - - "github.com/hashicorp/terraform-plugin-sdk/terraform" -) - -// Map is a map of resources that are supported, and provides helpers for -// more easily implementing a ResourceProvider. -type Map struct { - Mapping map[string]Resource -} - -func (m *Map) Validate( - t string, c *terraform.ResourceConfig) ([]string, []error) { - r, ok := m.Mapping[t] - if !ok { - return nil, []error{fmt.Errorf("Unknown resource type: %s", t)} - } - - // If there is no validator set, then it is valid - if r.ConfigValidator == nil { - return nil, nil - } - - return r.ConfigValidator.Validate(c) -} - -// Apply performs a create or update depending on the diff, and calls -// the proper function on the matching Resource. -func (m *Map) Apply( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - d *terraform.InstanceDiff, - meta interface{}) (*terraform.InstanceState, error) { - r, ok := m.Mapping[info.Type] - if !ok { - return nil, fmt.Errorf("Unknown resource type: %s", info.Type) - } - - if d.Destroy || d.RequiresNew() { - if s.ID != "" { - // Destroy the resource if it is created - err := r.Destroy(s, meta) - if err != nil { - return s, err - } - - s.ID = "" - } - - // If we're only destroying, and not creating, then return now. - // Otherwise, we continue so that we can create a new resource. - if !d.RequiresNew() { - return nil, nil - } - } - - var result *terraform.InstanceState - var err error - if s.ID == "" { - result, err = r.Create(s, d, meta) - } else { - if r.Update == nil { - return s, fmt.Errorf( - "Resource type '%s' doesn't support update", - info.Type) - } - - result, err = r.Update(s, d, meta) - } - if result != nil { - if result.Attributes == nil { - result.Attributes = make(map[string]string) - } - - result.Attributes["id"] = result.ID - } - - return result, err -} - -// Diff performs a diff on the proper resource type. -func (m *Map) Diff( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - c *terraform.ResourceConfig, - meta interface{}) (*terraform.InstanceDiff, error) { - r, ok := m.Mapping[info.Type] - if !ok { - return nil, fmt.Errorf("Unknown resource type: %s", info.Type) - } - - return r.Diff(s, c, meta) -} - -// Refresh performs a Refresh on the proper resource type. -// -// Refresh on the Resource won't be called if the state represents a -// non-created resource (ID is blank). -// -// An error is returned if the resource isn't registered. -func (m *Map) Refresh( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - // If the resource isn't created, don't refresh. - if s.ID == "" { - return s, nil - } - - r, ok := m.Mapping[info.Type] - if !ok { - return nil, fmt.Errorf("Unknown resource type: %s", info.Type) - } - - return r.Refresh(s, meta) -} - -// Resources returns all the resources that are supported by this -// resource map and can be used to satisfy the Resources method of -// a ResourceProvider. -func (m *Map) Resources() []terraform.ResourceType { - ks := make([]string, 0, len(m.Mapping)) - for k, _ := range m.Mapping { - ks = append(ks, k) - } - sort.Strings(ks) - - rs := make([]terraform.ResourceType, 0, len(m.Mapping)) - for _, k := range ks { - rs = append(rs, terraform.ResourceType{ - Name: k, - }) - } - - return rs -} diff --git a/helper/resource/map_test.go b/helper/resource/map_test.go deleted file mode 100644 index ea131fb0d88..00000000000 --- a/helper/resource/map_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package resource - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/internal/helper/config" - "github.com/hashicorp/terraform-plugin-sdk/terraform" -) - -func TestMapResources(t *testing.T) { - m := &Map{ - Mapping: map[string]Resource{ - "aws_elb": Resource{}, - "aws_instance": Resource{}, - }, - } - - rts := m.Resources() - - expected := []terraform.ResourceType{ - terraform.ResourceType{ - Name: "aws_elb", - }, - terraform.ResourceType{ - Name: "aws_instance", - }, - } - - if !reflect.DeepEqual(rts, expected) { - t.Fatalf("bad: %#v", rts) - } -} - -func TestMapValidate(t *testing.T) { - m := &Map{ - Mapping: map[string]Resource{ - "aws_elb": Resource{ - ConfigValidator: &config.Validator{ - Required: []string{"foo"}, - }, - }, - }, - } - - var c *terraform.ResourceConfig - var ws []string - var es []error - - // Valid - c = testConfigForMap(t, map[string]interface{}{"foo": "bar"}) - ws, es = m.Validate("aws_elb", c) - if len(ws) > 0 { - t.Fatalf("bad: %#v", ws) - } - if len(es) > 0 { - t.Fatalf("bad: %#v", es) - } - - // Invalid - c = testConfigForMap(t, map[string]interface{}{}) - ws, es = m.Validate("aws_elb", c) - if len(ws) > 0 { - t.Fatalf("bad: %#v", ws) - } - if len(es) == 0 { - t.Fatalf("bad: %#v", es) - } -} - -func testConfigForMap(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { - return terraform.NewResourceConfigRaw(c) -} diff --git a/helper/resource/resource.go b/helper/resource/resource.go deleted file mode 100644 index 80782413b43..00000000000 --- a/helper/resource/resource.go +++ /dev/null @@ -1,49 +0,0 @@ -package resource - -import ( - "github.com/hashicorp/terraform-plugin-sdk/internal/helper/config" - "github.com/hashicorp/terraform-plugin-sdk/terraform" -) - -type Resource struct { - ConfigValidator *config.Validator - Create CreateFunc - Destroy DestroyFunc - Diff DiffFunc - Refresh RefreshFunc - Update UpdateFunc -} - -// CreateFunc is a function that creates a resource that didn't previously -// exist. -type CreateFunc func( - *terraform.InstanceState, - *terraform.InstanceDiff, - interface{}) (*terraform.InstanceState, error) - -// DestroyFunc is a function that destroys a resource that previously -// exists using the state. -type DestroyFunc func( - *terraform.InstanceState, - interface{}) error - -// DiffFunc is a function that performs a diff of a resource. -type DiffFunc func( - *terraform.InstanceState, - *terraform.ResourceConfig, - interface{}) (*terraform.InstanceDiff, error) - -// RefreshFunc is a function that performs a refresh of a specific type -// of resource. -type RefreshFunc func( - *terraform.InstanceState, - interface{}) (*terraform.InstanceState, error) - -// UpdateFunc is a function that is called to update a resource that -// previously existed. The difference between this and CreateFunc is that -// the diff is guaranteed to only contain attributes that don't require -// a new resource. -type UpdateFunc func( - *terraform.InstanceState, - *terraform.InstanceDiff, - interface{}) (*terraform.InstanceState, error) diff --git a/internal/flatmap/expand.go b/internal/flatmap/expand.go deleted file mode 100644 index 1bb7b9f2f96..00000000000 --- a/internal/flatmap/expand.go +++ /dev/null @@ -1,152 +0,0 @@ -package flatmap - -import ( - "fmt" - "sort" - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim" -) - -// 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 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 v, ok := m[key+".#"]; ok { - // If the count of the key is unknown, then just put the unknown - // value in the value itself. This will be detected by Terraform - // core later. - if v == hcl2shim.UnknownVariableValue { - return v - } - - 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) - } - - // If the number of elements in this array is 0, then return an - // empty slice as there is nothing to expand. Trying to expand it - // anyway could lead to crashes as any child maps, arrays or sets - // that no longer exist are still shown as empty with a count of 0. - if num == 0 { - return []interface{}{} - } - - // NOTE: "num" is not necessarily accurate, e.g. if a user tampers - // with state, so the following code should not crash when given a - // number of items more or less than what's given in num. The - // num key is mainly just a hint that this is a list or set. - - // The Schema "Set" type stores its values in an array format, but - // using numeric hash values instead of ordinal keys. Take the set - // of keys regardless of value, and expand them in numeric order. - // See GH-11042 for more details. - keySet := map[int]bool{} - computed := map[string]bool{} - for k := range m { - if !strings.HasPrefix(k, prefix+".") { - continue - } - - key := k[len(prefix)+1:] - idx := strings.Index(key, ".") - if idx != -1 { - key = key[:idx] - } - - // skip the count value - if key == "#" { - continue - } - - // strip the computed flag if there is one - if strings.HasPrefix(key, "~") { - key = key[1:] - computed[key] = true - } - - k, err := strconv.Atoi(key) - if err != nil { - panic(err) - } - keySet[int(k)] = true - } - - keysList := make([]int, 0, num) - for key := range keySet { - keysList = append(keysList, key) - } - sort.Ints(keysList) - - result := make([]interface{}, len(keysList)) - for i, key := range keysList { - keyString := strconv.Itoa(key) - if computed[keyString] { - keyString = "~" + keyString - } - result[i] = Expand(m, fmt.Sprintf("%s.%s", prefix, keyString)) - } - - return result -} - -func expandMap(m map[string]string, prefix string) map[string]interface{} { - // Submaps may not have a '%' key, so we can't count on this value being - // here. If we don't have a count, just proceed as if we have have a map. - if count, ok := m[prefix+"%"]; ok && count == "0" { - return 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 { - key = key[:idx] - } - if _, ok := result[key]; ok { - continue - } - - // skip the map count value - if key == "%" { - continue - } - - result[key] = Expand(m, k[:len(prefix)+len(key)]) - } - - return result -} diff --git a/internal/flatmap/expand_test.go b/internal/flatmap/expand_test.go deleted file mode 100644 index b9784423b5e..00000000000 --- a/internal/flatmap/expand_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package flatmap - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim" -) - -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{ - // # mismatches actual number of keys; actual number should - // "win" here, since the # is just a hint that this is a list. - "foo.#": "1", - "foo.0": "one", - "foo.1": "two", - "foo.2": "three", - }, - Key: "foo", - Output: []interface{}{ - "one", - "two", - "three", - }, - }, - - { - Map: map[string]string{ - // # mismatches actual number of keys; actual number should - // "win" here, since the # is just a hint that this is a list. - "foo.#": "5", - "foo.0": "one", - "foo.1": "two", - "foo.2": "three", - }, - Key: "foo", - Output: []interface{}{ - "one", - "two", - "three", - }, - }, - - { - 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, - }, - }, - }, - - { - Map: map[string]string{ - "foo.#": "1", - "foo.0.name": "bar", - "foo.0.ports.#": "2", - "foo.0.ports.0": "1", - "foo.0.ports.1": "2", - }, - Key: "foo", - Output: []interface{}{ - map[string]interface{}{ - "name": "bar", - "ports": []interface{}{ - "1", - "2", - }, - }, - }, - }, - - { - Map: map[string]string{ - "list_of_map.#": "2", - "list_of_map.0.%": "1", - "list_of_map.0.a": "1", - "list_of_map.1.%": "2", - "list_of_map.1.b": "2", - "list_of_map.1.c": "3", - }, - Key: "list_of_map", - Output: []interface{}{ - map[string]interface{}{ - "a": "1", - }, - map[string]interface{}{ - "b": "2", - "c": "3", - }, - }, - }, - - { - Map: map[string]string{ - "map_of_list.%": "2", - "map_of_list.list2.#": "1", - "map_of_list.list2.0": "c", - "map_of_list.list1.#": "2", - "map_of_list.list1.0": "a", - "map_of_list.list1.1": "b", - }, - Key: "map_of_list", - Output: map[string]interface{}{ - "list1": []interface{}{"a", "b"}, - "list2": []interface{}{"c"}, - }, - }, - - { - Map: map[string]string{ - "set.#": "3", - "set.1234": "a", - "set.1235": "b", - "set.1236": "c", - }, - Key: "set", - Output: []interface{}{"a", "b", "c"}, - }, - - { - Map: map[string]string{ - "computed_set.#": "1", - "computed_set.~1234.a": "a", - "computed_set.~1234.b": "b", - "computed_set.~1234.c": "c", - }, - Key: "computed_set", - Output: []interface{}{ - map[string]interface{}{"a": "a", "b": "b", "c": "c"}, - }, - }, - - { - Map: map[string]string{ - "struct.#": "1", - "struct.0.name": "hello", - "struct.0.rules.#": hcl2shim.UnknownVariableValue, - }, - Key: "struct", - Output: []interface{}{ - map[string]interface{}{ - "name": "hello", - "rules": hcl2shim.UnknownVariableValue, - }, - }, - }, - - { - Map: map[string]string{ - "struct.#": "1", - "struct.0.name": "hello", - "struct.0.set.#": "0", - "struct.0.set.0.key": "value", - }, - Key: "struct", - Output: []interface{}{ - map[string]interface{}{ - "name": "hello", - "set": []interface{}{}, - }, - }, - }, - - { - Map: map[string]string{ - "empty_map_of_sets.%": "0", - "empty_map_of_sets.set1.#": "0", - "empty_map_of_sets.set1.1234": "x", - }, - Key: "empty_map_of_sets", - Output: map[string]interface{}{}, - }, - } - - for _, tc := range cases { - t.Run(tc.Key, func(t *testing.T) { - actual := Expand(tc.Map, tc.Key) - if !reflect.DeepEqual(actual, tc.Output) { - t.Errorf( - "Key: %v\nMap:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n", - tc.Key, - tc.Map, - actual, - tc.Output) - } - }) - } -} diff --git a/internal/flatmap/flatten.go b/internal/flatmap/flatten.go deleted file mode 100644 index 9ff6e426526..00000000000 --- a/internal/flatmap/flatten.go +++ /dev/null @@ -1,71 +0,0 @@ -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 { - result := make(map[string]string) - - for k, raw := range thing { - flatten(result, k, reflect.ValueOf(raw)) - } - - return Map(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)) - } -} diff --git a/internal/flatmap/flatten_test.go b/internal/flatmap/flatten_test.go deleted file mode 100644 index 1aa4940f891..00000000000 --- a/internal/flatmap/flatten_test.go +++ /dev/null @@ -1,88 +0,0 @@ -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", - }, - }, - - { - Input: map[string]interface{}{ - "foo": []map[interface{}]interface{}{ - map[interface{}]interface{}{ - "name": "bar", - "ports": []string{ - "1", - "2", - }, - }, - }, - }, - Output: map[string]string{ - "foo.#": "1", - "foo.0.name": "bar", - "foo.0.ports.#": "2", - "foo.0.ports.0": "1", - "foo.0.ports.1": "2", - }, - }, - } - - for _, tc := range cases { - actual := Flatten(tc.Input) - if !reflect.DeepEqual(actual, Map(tc.Output)) { - t.Fatalf( - "Input:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n", - tc.Input, - actual, - tc.Output) - } - } -} diff --git a/internal/flatmap/map.go b/internal/flatmap/map.go deleted file mode 100644 index 46b72c4014a..00000000000 --- a/internal/flatmap/map.go +++ /dev/null @@ -1,82 +0,0 @@ -package flatmap - -import ( - "strings" -) - -// Map is a wrapper around map[string]string that provides some helpers -// above it that assume the map is in the format that flatmap expects -// (the result of Flatten). -// -// All modifying functions such as Delete are done in-place unless -// otherwise noted. -type Map map[string]string - -// Contains returns true if the map contains the given key. -func (m Map) Contains(key string) bool { - for _, k := range m.Keys() { - if k == key { - return true - } - } - - return false -} - -// Delete deletes a key out of the map with the given prefix. -func (m Map) Delete(prefix string) { - for k, _ := range m { - match := k == prefix - if !match { - if !strings.HasPrefix(k, prefix) { - continue - } - - if k[len(prefix):len(prefix)+1] != "." { - continue - } - } - - delete(m, k) - } -} - -// Keys returns all of the top-level keys in this map -func (m Map) Keys() []string { - ks := make(map[string]struct{}) - for k, _ := range m { - idx := strings.Index(k, ".") - if idx == -1 { - idx = len(k) - } - - ks[k[:idx]] = struct{}{} - } - - result := make([]string, 0, len(ks)) - for k, _ := range ks { - result = append(result, k) - } - - return result -} - -// Merge merges the contents of the other Map into this one. -// -// This merge is smarter than a simple map iteration because it -// will fully replace arrays and other complex structures that -// are present in this map with the other map's. For example, if -// this map has a 3 element "foo" list, and m2 has a 2 element "foo" -// list, then the result will be that m has a 2 element "foo" -// list. -func (m Map) Merge(m2 Map) { - for _, prefix := range m2.Keys() { - m.Delete(prefix) - - for k, v := range m2 { - if strings.HasPrefix(k, prefix) { - m[k] = v - } - } - } -} diff --git a/internal/flatmap/map_test.go b/internal/flatmap/map_test.go deleted file mode 100644 index e3b4cb1bd5c..00000000000 --- a/internal/flatmap/map_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package flatmap - -import ( - "reflect" - "sort" - "testing" -) - -func TestMapContains(t *testing.T) { - cases := []struct { - Input map[string]string - Key string - Result bool - }{ - { - Input: map[string]string{ - "foo": "bar", - "bar": "nope", - }, - Key: "foo", - Result: true, - }, - - { - Input: map[string]string{ - "foo": "bar", - "bar": "nope", - }, - Key: "baz", - Result: false, - }, - } - - for i, tc := range cases { - actual := Map(tc.Input).Contains(tc.Key) - if actual != tc.Result { - t.Fatalf("case %d bad: %#v", i, tc.Input) - } - } -} - -func TestMapDelete(t *testing.T) { - m := Flatten(map[string]interface{}{ - "foo": "bar", - "routes": []map[string]string{ - map[string]string{ - "foo": "bar", - }, - }, - }) - - m.Delete("routes") - - expected := Map(map[string]string{"foo": "bar"}) - if !reflect.DeepEqual(m, expected) { - t.Fatalf("bad: %#v", m) - } -} - -func TestMapKeys(t *testing.T) { - cases := []struct { - Input map[string]string - Output []string - }{ - { - Input: map[string]string{ - "foo": "bar", - "bar.#": "bar", - "bar.0.foo": "bar", - "bar.0.baz": "bar", - }, - Output: []string{ - "bar", - "foo", - }, - }, - } - - for _, tc := range cases { - actual := Map(tc.Input).Keys() - - // Sort so we have a consistent view of the output - sort.Strings(actual) - - if !reflect.DeepEqual(actual, tc.Output) { - t.Fatalf("input: %#v\n\nbad: %#v", tc.Input, actual) - } - } -} - -func TestMapMerge(t *testing.T) { - cases := []struct { - One map[string]string - Two map[string]string - Result map[string]string - }{ - { - One: map[string]string{ - "foo": "bar", - "bar": "nope", - }, - Two: map[string]string{ - "bar": "baz", - "baz": "buz", - }, - Result: map[string]string{ - "foo": "bar", - "bar": "baz", - "baz": "buz", - }, - }, - } - - for i, tc := range cases { - Map(tc.One).Merge(Map(tc.Two)) - if !reflect.DeepEqual(tc.One, tc.Result) { - t.Fatalf("case %d bad: %#v", i, tc.One) - } - } -} diff --git a/internal/helper/config/validator.go b/internal/helper/config/validator.go deleted file mode 100644 index 35a3e7a4989..00000000000 --- a/internal/helper/config/validator.go +++ /dev/null @@ -1,214 +0,0 @@ -package config - -import ( - "fmt" - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-sdk/internal/flatmap" - "github.com/hashicorp/terraform-plugin-sdk/terraform" -) - -// Validator is a helper that helps you validate the configuration -// of your resource, resource provider, etc. -// -// At the most basic level, set the Required and Optional lists to be -// specifiers of keys that are required or optional. If a key shows up -// that isn't in one of these two lists, then an error is generated. -// -// The "specifiers" allowed in this is a fairly rich syntax to help -// describe the format of your configuration: -// -// * Basic keys are just strings. For example: "foo" will match the -// "foo" key. -// -// * Nested structure keys can be matched by doing -// "listener.*.foo". This will verify that there is at least one -// listener element that has the "foo" key set. -// -// * The existence of a nested structure can be checked by simply -// doing "listener.*" which will verify that there is at least -// one element in the "listener" structure. This is NOT -// validating that "listener" is an array. It is validating -// that it is a nested structure in the configuration. -// -type Validator struct { - Required []string - Optional []string -} - -func (v *Validator) Validate( - c *terraform.ResourceConfig) (ws []string, es []error) { - // Flatten the configuration so it is easier to reason about - flat := flatmap.Flatten(c.Raw) - - keySet := make(map[string]validatorKey) - for i, vs := range [][]string{v.Required, v.Optional} { - req := i == 0 - for _, k := range vs { - vk, err := newValidatorKey(k, req) - if err != nil { - es = append(es, err) - continue - } - - keySet[k] = vk - } - } - - purged := make([]string, 0) - for _, kv := range keySet { - p, w, e := kv.Validate(flat) - if len(w) > 0 { - ws = append(ws, w...) - } - if len(e) > 0 { - es = append(es, e...) - } - - purged = append(purged, p...) - } - - // Delete all the keys we processed in order to find - // the unknown keys. - for _, p := range purged { - delete(flat, p) - } - - // The rest are unknown - for k, _ := range flat { - es = append(es, fmt.Errorf("Unknown configuration: %s", k)) - } - - return -} - -type validatorKey interface { - // Validate validates the given configuration and returns viewed keys, - // warnings, and errors. - Validate(map[string]string) ([]string, []string, []error) -} - -func newValidatorKey(k string, req bool) (validatorKey, error) { - var result validatorKey - - parts := strings.Split(k, ".") - if len(parts) > 1 && parts[1] == "*" { - result = &nestedValidatorKey{ - Parts: parts, - Required: req, - } - } else { - result = &basicValidatorKey{ - Key: k, - Required: req, - } - } - - return result, nil -} - -// basicValidatorKey validates keys that are basic such as "foo" -type basicValidatorKey struct { - Key string - Required bool -} - -func (v *basicValidatorKey) Validate( - m map[string]string) ([]string, []string, []error) { - for k, _ := range m { - // If we have the exact key its a match - if k == v.Key { - return []string{k}, nil, nil - } - } - - if !v.Required { - return nil, nil, nil - } - - return nil, nil, []error{fmt.Errorf( - "Key not found: %s", v.Key)} -} - -type nestedValidatorKey struct { - Parts []string - Required bool -} - -func (v *nestedValidatorKey) validate( - m map[string]string, - prefix string, - offset int) ([]string, []string, []error) { - if offset >= len(v.Parts) { - // We're at the end. Look for a specific key. - v2 := &basicValidatorKey{Key: prefix, Required: v.Required} - return v2.Validate(m) - } - - current := v.Parts[offset] - - // If we're at offset 0, special case to start at the next one. - if offset == 0 { - return v.validate(m, current, offset+1) - } - - // Determine if we're doing a "for all" or a specific key - if current != "*" { - // We're looking at a specific key, continue on. - return v.validate(m, prefix+"."+current, offset+1) - } - - // We're doing a "for all", so we loop over. - countStr, ok := m[prefix+".#"] - if !ok { - if !v.Required { - // It wasn't required, so its no problem. - return nil, nil, nil - } - - return nil, nil, []error{fmt.Errorf( - "Key not found: %s", prefix)} - } - - count, err := strconv.ParseInt(countStr, 0, 0) - if err != nil { - // This shouldn't happen if flatmap works properly - panic("invalid flatmap array") - } - - var e []error - var w []string - u := make([]string, 1, count+1) - u[0] = prefix + ".#" - for i := 0; i < int(count); i++ { - prefix := fmt.Sprintf("%s.%d", prefix, i) - - // Mark that we saw this specific key - u = append(u, prefix) - - // Mark all prefixes of this - for k, _ := range m { - if !strings.HasPrefix(k, prefix+".") { - continue - } - u = append(u, k) - } - - // If we have more parts, then validate deeper - if offset+1 < len(v.Parts) { - u2, w2, e2 := v.validate(m, prefix, offset+1) - - u = append(u, u2...) - w = append(w, w2...) - e = append(e, e2...) - } - } - - return u, w, e -} - -func (v *nestedValidatorKey) Validate( - m map[string]string) ([]string, []string, []error) { - return v.validate(m, "", 0) -} diff --git a/internal/helper/config/validator_test.go b/internal/helper/config/validator_test.go deleted file mode 100644 index 708b64907c1..00000000000 --- a/internal/helper/config/validator_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package config - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/terraform" -) - -func TestValidator(t *testing.T) { - v := &Validator{ - Required: []string{"foo"}, - Optional: []string{"bar"}, - } - - var c *terraform.ResourceConfig - - // Valid - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - }) - testValid(v, c) - - // Valid + optional - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }) - testValid(v, c) - - // Missing required - c = testConfig(t, map[string]interface{}{ - "bar": "baz", - }) - testInvalid(v, c) - - // Unknown key - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "what": "what", - }) - testInvalid(v, c) -} - -func TestValidator_array(t *testing.T) { - v := &Validator{ - Required: []string{ - "foo", - "nested.*", - }, - } - - var c *terraform.ResourceConfig - - // Valid - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "nested": []interface{}{"foo", "bar"}, - }) - testValid(v, c) - - // Not a nested structure - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "nested": "baa", - }) - testInvalid(v, c) -} - -func TestValidator_complex(t *testing.T) { - v := &Validator{ - Required: []string{ - "foo", - "nested.*", - }, - } - - var c *terraform.ResourceConfig - - // Valid - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "nested": []interface{}{ - map[string]interface{}{"foo": "bar"}, - }, - }) - testValid(v, c) - - // Not a nested structure - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "nested": "baa", - }) - testInvalid(v, c) -} - -func TestValidator_complexNested(t *testing.T) { - v := &Validator{ - Required: []string{ - "ingress.*", - "ingress.*.from_port", - }, - - Optional: []string{ - "ingress.*.cidr_blocks.*", - }, - } - - var c *terraform.ResourceConfig - - // Valid - c = testConfig(t, map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "from_port": "80", - }, - }, - }) - testValid(v, c) - - // Valid - c = testConfig(t, map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "from_port": "80", - "cidr_blocks": []interface{}{"foo"}, - }, - }, - }) - testValid(v, c) -} - -func TestValidator_complexDeepRequired(t *testing.T) { - v := &Validator{ - Required: []string{ - "foo", - "nested.*.foo", - }, - } - - var c *terraform.ResourceConfig - - // Valid - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "nested": []interface{}{ - map[string]interface{}{"foo": "bar"}, - }, - }) - testValid(v, c) - - // Valid - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - }) - testInvalid(v, c) - - // Not a nested structure - c = testConfig(t, map[string]interface{}{ - "foo": "bar", - "nested": "baa", - }) - testInvalid(v, c) -} - -func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { - return terraform.NewResourceConfigRaw(c) -} - -func testInvalid(v *Validator, c *terraform.ResourceConfig) { - ws, es := v.Validate(c) - if len(ws) > 0 { - panic(fmt.Sprintf("bad: %#v", ws)) - } - if len(es) == 0 { - panic(fmt.Sprintf("bad: %#v", es)) - } -} - -func testValid(v *Validator, c *terraform.ResourceConfig) { - ws, es := v.Validate(c) - if len(ws) > 0 { - panic(fmt.Sprintf("bad: %#v", ws)) - } - if len(es) > 0 { - estrs := make([]string, len(es)) - for i, e := range es { - estrs[i] = e.Error() - } - panic(fmt.Sprintf("bad: %#v", estrs)) - } -}