From 69650b0bbc8cbf7b266a13dfe5af32195ed15f08 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 4 Oct 2017 11:27:51 -0700 Subject: [PATCH] helper/schema: conversion of Schema to configschema.Block As part of moving to the next-generation HCL implementation, Terraform Core is getting its own representation of configuration schema that is tailored for configuration-processing use-cases. The capabilities of this are a subset of the helper/schema model primarily concerned with the configuration structure and value types, leaving detailed validation and defaults for helper/schema to still solve. These new methods allow mechanical creation of a schema in the new Core schema model from a schema expressed in the helper/schema model. This is not yet used as of this commit, but will be used later to implement some new ResourceProvider methods that will allow core to obtain the schema for provider, resource and data source configuration while remaining source-compatible with existing provider implementations. --- helper/schema/core_schema.go | 151 ++++++++++++++++++++ helper/schema/core_schema_test.go | 228 ++++++++++++++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 helper/schema/core_schema.go create mode 100644 helper/schema/core_schema_test.go diff --git a/helper/schema/core_schema.go b/helper/schema/core_schema.go new file mode 100644 index 000000000..b5f41dd22 --- /dev/null +++ b/helper/schema/core_schema.go @@ -0,0 +1,151 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform/config/configschema" + "github.com/zclconf/go-cty/cty" +) + +// The functions and methods in this file are concerned with the conversion +// of this package's schema model into the slightly-lower-level schema model +// used by Terraform core for configuration parsing. + +// CoreConfigSchema lowers the receiver to the schema model expected by +// Terraform core. +// +// This lower-level model has fewer features than the schema in this package, +// describing only the basic structure of configuration and state values we +// expect. The full schemaMap from this package is still required for full +// validation, handling of default values, etc. +// +// This method presumes a schema that passes InternalValidate, and so may +// panic or produce an invalid result if given an invalid schemaMap. +func (m schemaMap) CoreConfigSchema() *configschema.Block { + if len(m) == 0 { + return nil + } + + ret := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{}, + BlockTypes: map[string]*configschema.NestedBlock{}, + } + + for name, schema := range m { + if schema.Elem == nil { + ret.Attributes[name] = schema.coreConfigSchemaAttribute() + continue + } + switch schema.Elem.(type) { + case *Schema: + ret.Attributes[name] = schema.coreConfigSchemaAttribute() + case *Resource: + ret.BlockTypes[name] = schema.coreConfigSchemaBlock() + default: + // Should never happen for a valid schema + panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem)) + } + } + + return ret +} + +// coreConfigSchemaAttribute prepares a configschema.Attribute representation +// of a schema. This is appropriate only for primitives or collections whose +// Elem is an instance of Schema. Use coreConfigSchemaBlock for collections +// whose elem is a whole resource. +func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute { + return &configschema.Attribute{ + Type: s.coreConfigSchemaType(), + Optional: s.Optional, + Required: s.Required, + Computed: s.Computed, + } +} + +// coreConfigSchemaBlock prepares a configschema.NestedBlock representation of +// a schema. This is appropriate only for collections whose Elem is an instance +// of Resource, and will panic otherwise. +func (s *Schema) coreConfigSchemaBlock() *configschema.NestedBlock { + ret := &configschema.NestedBlock{} + if nested := s.Elem.(*Resource).CoreConfigSchema(); nested != nil { + ret.Block = *nested + } + switch s.Type { + case TypeList: + ret.Nesting = configschema.NestingList + case TypeSet: + ret.Nesting = configschema.NestingSet + case TypeMap: + ret.Nesting = configschema.NestingMap + default: + // Should never happen for a valid schema + panic(fmt.Errorf("invalid s.Type %s for s.Elem being resource", s.Type)) + } + + ret.MinItems = s.MinItems + ret.MaxItems = s.MaxItems + + if s.Required && s.MinItems == 0 { + // configschema doesn't have a "required" representation for nested + // blocks, but we can fake it by requiring at least one item. + ret.MinItems = 1 + } + + return ret +} + +// coreConfigSchemaType determines the core config schema type that corresponds +// to a particular schema's type. +func (s *Schema) coreConfigSchemaType() cty.Type { + switch s.Type { + case TypeString: + return cty.String + case TypeBool: + return cty.Bool + case TypeInt, TypeFloat: + // configschema doesn't distinguish int and float, so helper/schema + // will deal with this as an additional validation step after + // configuration has been parsed and decoded. + return cty.Number + case TypeList, TypeSet, TypeMap: + var elemType cty.Type + switch set := s.Elem.(type) { + case *Schema: + elemType = set.coreConfigSchemaType() + case *Resource: + // In practice we don't actually use this for normal schema + // construction because we construct a NestedBlock in that + // case instead. See schemaMap.CoreConfigSchema. + elemType = set.CoreConfigSchema().ImpliedType() + default: + if set != nil { + // Should never happen for a valid schema + panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", s.Elem)) + } + // Some pre-existing schemas assume string as default, so we need + // to be compatible with them. + elemType = cty.String + } + switch s.Type { + case TypeList: + return cty.List(elemType) + case TypeSet: + return cty.Set(elemType) + case TypeMap: + return cty.Map(elemType) + default: + // can never get here in practice, due to the case we're inside + panic("invalid collection type") + } + default: + // should never happen for a valid schema + panic(fmt.Errorf("invalid Schema.Type %s", s.Type)) + } +} + +// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema +// on the resource's schema. +func (r *Resource) CoreConfigSchema() *configschema.Block { + return schemaMap(r.Schema).CoreConfigSchema() +} diff --git a/helper/schema/core_schema_test.go b/helper/schema/core_schema_test.go new file mode 100644 index 000000000..097bab360 --- /dev/null +++ b/helper/schema/core_schema_test.go @@ -0,0 +1,228 @@ +package schema + +import ( + "reflect" + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/davecgh/go-spew/spew" + + "github.com/hashicorp/terraform/config/configschema" +) + +func TestSchemaMapCoreConfigSchema(t *testing.T) { + tests := map[string]struct { + Schema map[string]*Schema + Want *configschema.Block + }{ + "empty": { + map[string]*Schema{}, + nil, + }, + "primitives": { + map[string]*Schema{ + "int": { + Type: TypeInt, + Required: true, + }, + "float": { + Type: TypeFloat, + Optional: true, + }, + "bool": { + Type: TypeBool, + Computed: true, + }, + "string": { + Type: TypeString, + Optional: true, + Computed: true, + }, + }, + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "int": { + Type: cty.Number, + Required: true, + }, + "float": { + Type: cty.Number, + Optional: true, + }, + "bool": { + Type: cty.Bool, + Computed: true, + }, + "string": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{}, + }, + }, + "simple collections": { + map[string]*Schema{ + "list": { + Type: TypeList, + Required: true, + Elem: &Schema{ + Type: TypeInt, + }, + }, + "set": { + Type: TypeSet, + Optional: true, + Elem: &Schema{ + Type: TypeString, + }, + }, + "map": { + Type: TypeMap, + Optional: true, + Elem: &Schema{ + Type: TypeBool, + }, + }, + "map_default_type": { + Type: TypeMap, + Optional: true, + // Maps historically don't have elements because we + // assumed they would be strings, so this needs to work + // for pre-existing schemas. + }, + }, + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "list": { + Type: cty.List(cty.Number), + Required: true, + }, + "set": { + Type: cty.Set(cty.String), + Optional: true, + }, + "map": { + Type: cty.Map(cty.Bool), + Optional: true, + }, + "map_default_type": { + Type: cty.Map(cty.String), + Optional: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{}, + }, + }, + "sub-resource collections": { + map[string]*Schema{ + "list": { + Type: TypeList, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{}, + }, + MinItems: 1, + MaxItems: 2, + }, + "set": { + Type: TypeSet, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{}, + }, + }, + "map": { + Type: TypeMap, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{}, + }, + }, + }, + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{}, + BlockTypes: map[string]*configschema.NestedBlock{ + "list": { + Nesting: configschema.NestingList, + Block: configschema.Block{}, + MinItems: 1, + MaxItems: 2, + }, + "set": { + Nesting: configschema.NestingSet, + Block: configschema.Block{}, + MinItems: 1, // because schema is Required + }, + "map": { + Nesting: configschema.NestingMap, + Block: configschema.Block{}, + }, + }, + }, + }, + "nested attributes and blocks": { + map[string]*Schema{ + "foo": { + Type: TypeList, + Required: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "bar": { + Type: TypeList, + Required: true, + Elem: &Schema{ + Type: TypeList, + Elem: &Schema{ + Type: TypeString, + }, + }, + }, + "baz": { + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{}, + }, + }, + }, + }, + }, + }, + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{}, + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": &configschema.NestedBlock{ + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.List(cty.List(cty.String)), + Required: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "baz": { + Nesting: configschema.NestingSet, + Block: configschema.Block{}, + }, + }, + }, + MinItems: 1, // because schema is Required + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := schemaMap(test.Schema).CoreConfigSchema() + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(test.Want)) + } + }) + } +}