From 663be265dc90ebd76d45fc28c5838ce0554e4a78 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Jul 2014 11:14:07 -0700 Subject: [PATCH] helper/config: can validate nested structures /cc @pearkes - See docs --- helper/config/validator.go | 185 ++++++++++++++++++++++++++++---- helper/config/validator_test.go | 53 +++++++-- 2 files changed, 208 insertions(+), 30 deletions(-) diff --git a/helper/config/validator.go b/helper/config/validator.go index 095a0bbcb..2898c6faf 100644 --- a/helper/config/validator.go +++ b/helper/config/validator.go @@ -2,12 +2,36 @@ package config import ( "fmt" + "strconv" + "strings" + "github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/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 @@ -15,34 +39,153 @@ type Validator struct { func (v *Validator) Validate( c *terraform.ResourceConfig) (ws []string, es []error) { - keySet := make(map[string]bool) - reqSet := make(map[string]struct{}) - for _, k := range v.Required { - keySet[k] = true - reqSet[k] = struct{}{} - } - for _, k := range v.Optional { - keySet[k] = false + // 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 + } } - // Find any unknown keys used and mark off required keys that - // we have set. - for k, _ := range c.Raw { - _, ok := keySet[k] - if !ok { - es = append(es, fmt.Errorf( - "Unknown configuration key: %s", k)) - continue + 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...) } - delete(reqSet, k) + purged = append(purged, p...) } - // Check what keys are required that we didn't set - for k, _ := range reqSet { - es = append(es, fmt.Errorf( - "Required key is not set: %s", k)) + // 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(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] == "*" { + key := "" + if len(parts) >= 3 { + key = parts[2] + } + + result = &nestedValidatorKey{ + Prefix: parts[0], + Key: key, + 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 { + Prefix string + Key string + Required bool +} + +func (v *nestedValidatorKey) Validate( + m map[string]string) ([]string, []string, []error) { + countStr, ok := m[v.Prefix+".#"] + if !ok { + if !v.Required { + // Not present, that is okay + return nil, nil, nil + } else { + // Required and isn't present + return nil, nil, []error{fmt.Errorf( + "Key not found: %s", v.Prefix)} + } + } + + count, err := strconv.ParseInt(countStr, 0, 0) + if err != nil { + // This shouldn't happen if flatmap works properly + panic("invalid flatmap array") + } + + var errs []error + used := make([]string, 1, count+1) + used[0] = v.Prefix + ".#" + for i := 0; i < int(count); i++ { + prefix := fmt.Sprintf("%s.%d.", v.Prefix, i) + + if v.Key != "" { + key := prefix + v.Key + if _, ok := m[key]; !ok { + errs = append(errs, fmt.Errorf( + "%s[%d]: does not contain required key %s", + v.Prefix, + i, + v.Key)) + } + } + + for k, _ := range m { + if !strings.HasPrefix(k, prefix) { + continue + } + + used = append(used, k) + } + } + + return used, nil, errs +} diff --git a/helper/config/validator_test.go b/helper/config/validator_test.go index 8a42e8d2e..224c68896 100644 --- a/helper/config/validator_test.go +++ b/helper/config/validator_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "testing" "github.com/hashicorp/terraform/config" @@ -19,20 +20,54 @@ func TestValidator(t *testing.T) { c = testConfig(t, map[string]interface{}{ "foo": "bar", }) - testValid(t, v, c) + 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(t, v, c) + testInvalid(v, c) // Unknown key c = testConfig(t, map[string]interface{}{ "foo": "bar", "what": "what", }) - testInvalid(t, v, c) + 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": []map[string]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 testConfig( @@ -46,22 +81,22 @@ func testConfig( return terraform.NewResourceConfig(r) } -func testInvalid(t *testing.T, v *Validator, c *terraform.ResourceConfig) { +func testInvalid(v *Validator, c *terraform.ResourceConfig) { ws, es := v.Validate(c) if len(ws) > 0 { - t.Fatalf("bad: %#v", ws) + panic(fmt.Sprintf("bad: %#v", ws)) } if len(es) == 0 { - t.Fatalf("bad: %#v", es) + panic(fmt.Sprintf("bad: %#v", es)) } } -func testValid(t *testing.T, v *Validator, c *terraform.ResourceConfig) { +func testValid(v *Validator, c *terraform.ResourceConfig) { ws, es := v.Validate(c) if len(ws) > 0 { - t.Fatalf("bad: %#v", ws) + panic(fmt.Sprintf("bad: %#v", ws)) } if len(es) > 0 { - t.Fatalf("bad: %#v", es) + panic(fmt.Sprintf("bad: %#v", es)) } }