helper/config: can validate nested structures
/cc @pearkes - See docs
This commit is contained in:
parent
6368526ac3
commit
663be265dc
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue