mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
e8eae17cc9
/cc @pearkes
215 lines
4.9 KiB
Go
215 lines
4.9 KiB
Go
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)
|
|
}
|