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 } 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) }