helper/config: can validate nested structures

/cc @pearkes - See docs
This commit is contained in:
Mitchell Hashimoto 2014-07-08 11:14:07 -07:00
parent 6368526ac3
commit 663be265dc
2 changed files with 208 additions and 30 deletions

View File

@ -2,12 +2,36 @@ package config
import ( import (
"fmt" "fmt"
"strconv"
"strings"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// Validator is a helper that helps you validate the configuration // Validator is a helper that helps you validate the configuration
// of your resource, resource provider, etc. // 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 { type Validator struct {
Required []string Required []string
Optional []string Optional []string
@ -15,34 +39,153 @@ type Validator struct {
func (v *Validator) Validate( func (v *Validator) Validate(
c *terraform.ResourceConfig) (ws []string, es []error) { c *terraform.ResourceConfig) (ws []string, es []error) {
keySet := make(map[string]bool) // Flatten the configuration so it is easier to reason about
reqSet := make(map[string]struct{}) flat := flatmap.Flatten(c.Raw)
for _, k := range v.Required {
keySet[k] = true keySet := make(map[string]validatorKey)
reqSet[k] = struct{}{} for i, vs := range [][]string{v.Required, v.Optional} {
} req := i == 0
for _, k := range v.Optional { for _, k := range vs {
keySet[k] = false 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 purged := make([]string, 0)
// we have set. for _, kv := range keySet {
for k, _ := range c.Raw { p, w, e := kv.Validate(flat)
_, ok := keySet[k] if len(w) > 0 {
if !ok { ws = append(ws, w...)
es = append(es, fmt.Errorf( }
"Unknown configuration key: %s", k)) if len(e) > 0 {
continue es = append(es, e...)
} }
delete(reqSet, k) purged = append(purged, p...)
} }
// Check what keys are required that we didn't set // Delete all the keys we processed in order to find
for k, _ := range reqSet { // the unknown keys.
es = append(es, fmt.Errorf( for _, p := range purged {
"Required key is not set: %s", k)) delete(flat, p)
}
// The rest are unknown
for k, _ := range flat {
es = append(es, fmt.Errorf("Unknown configuration: %s", k))
} }
return 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
}

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"fmt"
"testing" "testing"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
@ -19,20 +20,54 @@ func TestValidator(t *testing.T) {
c = testConfig(t, map[string]interface{}{ c = testConfig(t, map[string]interface{}{
"foo": "bar", "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 // Missing required
c = testConfig(t, map[string]interface{}{ c = testConfig(t, map[string]interface{}{
"bar": "baz", "bar": "baz",
}) })
testInvalid(t, v, c) testInvalid(v, c)
// Unknown key // Unknown key
c = testConfig(t, map[string]interface{}{ c = testConfig(t, map[string]interface{}{
"foo": "bar", "foo": "bar",
"what": "what", "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( func testConfig(
@ -46,22 +81,22 @@ func testConfig(
return terraform.NewResourceConfig(r) 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) ws, es := v.Validate(c)
if len(ws) > 0 { if len(ws) > 0 {
t.Fatalf("bad: %#v", ws) panic(fmt.Sprintf("bad: %#v", ws))
} }
if len(es) == 0 { 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) ws, es := v.Validate(c)
if len(ws) > 0 { if len(ws) > 0 {
t.Fatalf("bad: %#v", ws) panic(fmt.Sprintf("bad: %#v", ws))
} }
if len(es) > 0 { if len(es) > 0 {
t.Fatalf("bad: %#v", es) panic(fmt.Sprintf("bad: %#v", es))
} }
} }