From 4af387b986bcaa962b143e38cb55cef7aee76107 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 15 Aug 2014 22:00:16 -0700 Subject: [PATCH] helper/schema: validation --- helper/schema/resource.go | 5 + helper/schema/schema.go | 113 ++++++++++++++++++++++ helper/schema/schema_test.go | 175 +++++++++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+) diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 3140fbc75..e112b380f 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -37,6 +37,11 @@ func (r *Resource) Diff( return schemaMap(r.Schema).Diff(s, c) } +// Validate validates the resource configuration against the schema. +func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { + return schemaMap(r.Schema).Validate(c) +} + // InternalValidate should be called to validate the structure // of the resource. // diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 81cc61d51..a6e3a3f22 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -102,6 +102,24 @@ func (m schemaMap) Diff( return result, nil } +// Validate validates the configuration against this schema mapping. +func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { + var ws []string + var es []error + + for k, schema := range m { + ws2, es2 := m.validate(k, schema, c) + if len(ws2) > 0 { + ws = append(ws, ws2...) + } + if len(es2) > 0 { + es = append(es, es2...) + } + } + + return ws, es +} + func (m schemaMap) diff( k string, schema *Schema, @@ -264,3 +282,98 @@ func (m schemaMap) diffPrimitive( return nil } + +func (m schemaMap) validate( + k string, + schema *Schema, + c *terraform.ResourceConfig) ([]string, []error) { + raw, ok := c.Get(k) + if !ok { + if schema.Required { + return nil, []error{fmt.Errorf( + "%s: required field is not set")} + } + + return nil, nil + } + + return m.validatePrimitive(k, raw, schema, c) +} + +func (m schemaMap) validateList( + k string, + raw interface{}, + schema *Schema, + c *terraform.ResourceConfig) ([]string, []error) { + raws, ok := raw.([]interface{}) + if !ok { + return nil, []error{fmt.Errorf( + "%s: should be list", k)} + } + + var ws []string + var es []error + for i, raw := range raws { + key := fmt.Sprintf("%s.%d", k, i) + + var ws2 []string + var es2 []error + switch t := schema.Elem.(type) { + case *Resource: + // This is a sub-resource + ws2, es2 = m.validateObject(key, t.Schema, c) + case *Schema: + // This is some sort of primitive + ws2, es2 = m.validatePrimitive(key, raw, t, c) + } + + if len(ws2) > 0 { + ws = append(ws, ws2...) + } + if len(es2) > 0 { + es = append(es, es2...) + } + } + + return ws, es +} + +func (m schemaMap) validateObject( + k string, + schema map[string]*Schema, + c *terraform.ResourceConfig) ([]string, []error) { + var ws []string + var es []error + for subK, s := range schema { + key := fmt.Sprintf("%s.%s", k, subK) + ws2, es2 := m.validate(key, s, c) + + if len(ws2) > 0 { + ws = append(ws, ws2...) + } + if len(es2) > 0 { + es = append(es, es2...) + } + } + + return ws, es +} + +func (m schemaMap) validatePrimitive( + k string, + raw interface{}, + schema *Schema, + c *terraform.ResourceConfig) ([]string, []error) { + switch schema.Type { + case TypeList: + return m.validateList(k, raw, schema, c) + case TypeInt: + // Verify that we can parse this as an int + var n int + if err := mapstructure.WeakDecode(raw, &n); err != nil { + return nil, []error{err} + } + } + + return nil, nil +} diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index f1893d7d9..10f4f3d7e 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -455,3 +455,178 @@ func TestSchemaMap_Diff(t *testing.T) { } } } + +func TestSchemaMap_Validate(t *testing.T) { + cases := []struct { + Schema map[string]*Schema + Config map[string]interface{} + Warn bool + Err bool + }{ + // Good + { + Schema: map[string]*Schema{ + "availability_zone": &Schema{ + Type: TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + + Config: map[string]interface{}{ + "availability_zone": "foo", + }, + }, + + // Required field not set + { + Schema: map[string]*Schema{ + "availability_zone": &Schema{ + Type: TypeString, + Required: true, + }, + }, + + Config: map[string]interface{}{}, + + Err: true, + }, + + // Invalid type + { + Schema: map[string]*Schema{ + "port": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + + Config: map[string]interface{}{ + "port": "I am invalid", + }, + + Err: true, + }, + + // Optional sub-resource + { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "from": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + }, + + Config: map[string]interface{}{}, + + Err: false, + }, + + // Not a list + { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "from": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + }, + + Config: map[string]interface{}{ + "ingress": "foo", + }, + + Err: true, + }, + + // Required sub-resource field + { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "from": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + }, + + Config: map[string]interface{}{ + "ingress": []interface{}{ + map[string]interface{}{}, + }, + }, + + Err: true, + }, + + // Good sub-resource + { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Elem: &Resource{ + Schema: map[string]*Schema{ + "from": &Schema{ + Type: TypeInt, + Required: true, + }, + }, + }, + }, + }, + + Config: map[string]interface{}{ + "ingress": []interface{}{ + map[string]interface{}{ + "from": 80, + }, + }, + }, + + Err: false, + }, + } + + for i, tc := range cases { + c, err := config.NewRawConfig(tc.Config) + if err != nil { + t.Fatalf("err: %s", err) + } + + ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c)) + if (len(es) > 0) != tc.Err { + if len(es) == 0 { + t.Errorf("%d: no errors", i) + } + + for _, e := range es { + t.Errorf("%d: err: %s", i, e) + } + + t.FailNow() + } + + if (len(ws) > 0) != tc.Warn { + t.Fatalf("%d: ws: %#v", i, ws) + } + } +}