internal/legacy/helper/schema

moving helper/schema into the ineternal/legacy tree
This commit is contained in:
James Bardin 2020-11-17 18:02:15 -05:00
parent a49e7eee8b
commit e4edce22ca
46 changed files with 28920 additions and 0 deletions

View File

@ -0,0 +1,11 @@
# Terraform Helper Lib: schema
The `schema` package provides a high-level interface for writing resource
providers for Terraform.
If you're writing a resource provider, we recommend you use this package.
The interface exposed by this package is much friendlier than trying to
write to the Terraform API directly. The core Terraform API is low-level
and built for maximum flexibility and control, whereas this library is built
as a framework around that to more easily write common providers.

View File

@ -0,0 +1,200 @@
package schema
import (
"context"
"fmt"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/legacy/terraform"
ctyconvert "github.com/zclconf/go-cty/cty/convert"
)
// Backend represents a partial backend.Backend implementation and simplifies
// the creation of configuration loading and validation.
//
// Unlike other schema structs such as Provider, this struct is meant to be
// embedded within your actual implementation. It provides implementations
// only for Input and Configure and gives you a method for accessing the
// configuration in the form of a ResourceData that you're expected to call
// from the other implementation funcs.
type Backend struct {
// Schema is the schema for the configuration of this backend. If this
// Backend has no configuration this can be omitted.
Schema map[string]*Schema
// ConfigureFunc is called to configure the backend. Use the
// FromContext* methods to extract information from the context.
// This can be nil, in which case nothing will be called but the
// config will still be stored.
ConfigureFunc func(context.Context) error
config *ResourceData
}
var (
backendConfigKey = contextKey("backend config")
)
// FromContextBackendConfig extracts a ResourceData with the configuration
// from the context. This should only be called by Backend functions.
func FromContextBackendConfig(ctx context.Context) *ResourceData {
return ctx.Value(backendConfigKey).(*ResourceData)
}
func (b *Backend) ConfigSchema() *configschema.Block {
// This is an alias of CoreConfigSchema just to implement the
// backend.Backend interface.
return b.CoreConfigSchema()
}
func (b *Backend) PrepareConfig(configVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
if b == nil {
return configVal, nil
}
var diags tfdiags.Diagnostics
var err error
// In order to use Transform below, this needs to be filled out completely
// according the schema.
configVal, err = b.CoreConfigSchema().CoerceValue(configVal)
if err != nil {
return configVal, diags.Append(err)
}
// lookup any required, top-level attributes that are Null, and see if we
// have a Default value available.
configVal, err = cty.Transform(configVal, func(path cty.Path, val cty.Value) (cty.Value, error) {
// we're only looking for top-level attributes
if len(path) != 1 {
return val, nil
}
// nothing to do if we already have a value
if !val.IsNull() {
return val, nil
}
// get the Schema definition for this attribute
getAttr, ok := path[0].(cty.GetAttrStep)
// these should all exist, but just ignore anything strange
if !ok {
return val, nil
}
attrSchema := b.Schema[getAttr.Name]
// continue to ignore anything that doesn't match
if attrSchema == nil {
return val, nil
}
// this is deprecated, so don't set it
if attrSchema.Deprecated != "" || attrSchema.Removed != "" {
return val, nil
}
// find a default value if it exists
def, err := attrSchema.DefaultValue()
if err != nil {
diags = diags.Append(fmt.Errorf("error getting default for %q: %s", getAttr.Name, err))
return val, err
}
// no default
if def == nil {
return val, nil
}
// create a cty.Value and make sure it's the correct type
tmpVal := hcl2shim.HCL2ValueFromConfigValue(def)
// helper/schema used to allow setting "" to a bool
if val.Type() == cty.Bool && tmpVal.RawEquals(cty.StringVal("")) {
// return a warning about the conversion
diags = diags.Append("provider set empty string as default value for bool " + getAttr.Name)
tmpVal = cty.False
}
val, err = ctyconvert.Convert(tmpVal, val.Type())
if err != nil {
diags = diags.Append(fmt.Errorf("error setting default for %q: %s", getAttr.Name, err))
}
return val, err
})
if err != nil {
// any error here was already added to the diagnostics
return configVal, diags
}
shimRC := b.shimConfig(configVal)
warns, errs := schemaMap(b.Schema).Validate(shimRC)
for _, warn := range warns {
diags = diags.Append(tfdiags.SimpleWarning(warn))
}
for _, err := range errs {
diags = diags.Append(err)
}
return configVal, diags
}
func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
if b == nil {
return nil
}
var diags tfdiags.Diagnostics
sm := schemaMap(b.Schema)
shimRC := b.shimConfig(obj)
// Get a ResourceData for this configuration. To do this, we actually
// generate an intermediary "diff" although that is never exposed.
diff, err := sm.Diff(nil, shimRC, nil, nil, true)
if err != nil {
diags = diags.Append(err)
return diags
}
data, err := sm.Data(nil, diff)
if err != nil {
diags = diags.Append(err)
return diags
}
b.config = data
if b.ConfigureFunc != nil {
err = b.ConfigureFunc(context.WithValue(
context.Background(), backendConfigKey, data))
if err != nil {
diags = diags.Append(err)
return diags
}
}
return diags
}
// shimConfig turns a new-style cty.Value configuration (which must be of
// an object type) into a minimal old-style *terraform.ResourceConfig object
// that should be populated enough to appease the not-yet-updated functionality
// in this package. This should be removed once everything is updated.
func (b *Backend) shimConfig(obj cty.Value) *terraform.ResourceConfig {
shimMap, ok := hcl2shim.ConfigValueFromHCL2(obj).(map[string]interface{})
if !ok {
// If the configVal was nil, we still want a non-nil map here.
shimMap = map[string]interface{}{}
}
return &terraform.ResourceConfig{
Config: shimMap,
Raw: shimMap,
}
}
// Config returns the configuration. This is available after Configure is
// called.
func (b *Backend) Config() *ResourceData {
return b.config
}

View File

@ -0,0 +1,193 @@
package schema
import (
"context"
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestBackendPrepare(t *testing.T) {
cases := []struct {
Name string
B *Backend
Config map[string]cty.Value
Expect map[string]cty.Value
Err bool
}{
{
"Basic required field",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
},
map[string]cty.Value{},
map[string]cty.Value{},
true,
},
{
"Null config",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
},
nil,
map[string]cty.Value{},
true,
},
{
"Basic required field set",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
},
map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
false,
},
{
"unused default",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Optional: true,
Type: TypeString,
Default: "baz",
},
},
},
map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
false,
},
{
"default",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Default: "baz",
},
},
},
map[string]cty.Value{},
map[string]cty.Value{
"foo": cty.StringVal("baz"),
},
false,
},
{
"default func",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
DefaultFunc: func() (interface{}, error) {
return "baz", nil
},
},
},
},
map[string]cty.Value{},
map[string]cty.Value{
"foo": cty.StringVal("baz"),
},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
cfgVal := cty.NullVal(cty.Object(map[string]cty.Type{}))
if tc.Config != nil {
cfgVal = cty.ObjectVal(tc.Config)
}
configVal, diags := tc.B.PrepareConfig(cfgVal)
if diags.HasErrors() != tc.Err {
for _, d := range diags {
t.Error(d.Description())
}
}
if tc.Err {
return
}
expect := cty.ObjectVal(tc.Expect)
if !expect.RawEquals(configVal) {
t.Fatalf("\nexpected: %#v\ngot: %#v\n", expect, configVal)
}
})
}
}
func TestBackendConfigure(t *testing.T) {
cases := []struct {
Name string
B *Backend
Config map[string]cty.Value
Err bool
}{
{
"Basic config",
&Backend{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ConfigureFunc: func(ctx context.Context) error {
d := FromContextBackendConfig(ctx)
if d.Get("foo").(int) != 42 {
return fmt.Errorf("bad config data")
}
return nil
},
},
map[string]cty.Value{
"foo": cty.NumberIntVal(42),
},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
diags := tc.B.Configure(cty.ObjectVal(tc.Config))
if diags.HasErrors() != tc.Err {
t.Errorf("wrong number of diagnostics")
}
})
}
}

View File

@ -0,0 +1,309 @@
package schema
import (
"fmt"
"github.com/hashicorp/terraform/configs/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 {
// We return an actual (empty) object here, rather than a nil,
// because a nil result would mean that we don't have a schema at
// all, rather than that we have an empty one.
return &configschema.Block{}
}
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
}
if schema.Type == TypeMap {
// For TypeMap in particular, it isn't valid for Elem to be a
// *Resource (since that would be ambiguous in flatmap) and
// so Elem is treated as a TypeString schema if so. This matches
// how the field readers treat this situation, for compatibility
// with configurations targeting Terraform 0.11 and earlier.
if _, isResource := schema.Elem.(*Resource); isResource {
sch := *schema // shallow copy
sch.Elem = &Schema{
Type: TypeString,
}
ret.Attributes[name] = sch.coreConfigSchemaAttribute()
continue
}
}
switch schema.ConfigMode {
case SchemaConfigModeAttr:
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
case SchemaConfigModeBlock:
ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
default: // SchemaConfigModeAuto, or any other invalid value
if schema.Computed && !schema.Optional {
// Computed-only schemas are always handled as attributes,
// because they never appear in configuration.
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
continue
}
switch schema.Elem.(type) {
case *Schema, ValueType:
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 {
// The Schema.DefaultFunc capability adds some extra weirdness here since
// it can be combined with "Required: true" to create a situation where
// required-ness is conditional. Terraform Core doesn't share this concept,
// so we must sniff for this possibility here and conditionally turn
// off the "Required" flag if it looks like the DefaultFunc is going
// to provide a value.
// This is not 100% true to the original interface of DefaultFunc but
// works well enough for the EnvDefaultFunc and MultiEnvDefaultFunc
// situations, which are the main cases we care about.
//
// Note that this also has a consequence for commands that return schema
// information for documentation purposes: running those for certain
// providers will produce different results depending on which environment
// variables are set. We accept that weirdness in order to keep this
// interface to core otherwise simple.
reqd := s.Required
opt := s.Optional
if reqd && s.DefaultFunc != nil {
v, err := s.DefaultFunc()
// We can't report errors from here, so we'll instead just force
// "Required" to false and let the provider try calling its
// DefaultFunc again during the validate step, where it can then
// return the error.
if err != nil || (err == nil && v != nil) {
reqd = false
opt = true
}
}
return &configschema.Attribute{
Type: s.coreConfigSchemaType(),
Optional: opt,
Required: reqd,
Computed: s.Computed,
Sensitive: s.Sensitive,
Description: s.Description,
}
}
// 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
}
if s.Optional && s.MinItems > 0 {
// Historically helper/schema would ignore MinItems if Optional were
// set, so we must mimic this behavior here to ensure that providers
// relying on that undocumented behavior can continue to operate as
// they did before.
ret.MinItems = 0
}
if s.Computed && !s.Optional {
// MinItems/MaxItems are meaningless for computed nested blocks, since
// they are never set by the user anyway. This ensures that we'll never
// generate weird errors about them.
ret.MinItems = 0
ret.MaxItems = 0
}
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 ValueType:
// This represents a mistake in the provider code, but it's a
// common one so we'll just shim it.
elemType = (&Schema{Type: set}).coreConfigSchemaType()
case *Resource:
// By default we construct a NestedBlock in this case, but this
// behavior is selected either for computed-only schemas or
// when ConfigMode is explicitly SchemaConfigModeBlock.
// See schemaMap.CoreConfigSchema for the exact rules.
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. CoreConfigSchema adds the implicitly required "id"
// attribute for top level resources if it doesn't exist.
func (r *Resource) CoreConfigSchema() *configschema.Block {
block := r.coreConfigSchema()
if block.Attributes == nil {
block.Attributes = map[string]*configschema.Attribute{}
}
// Add the implicitly required "id" field if it doesn't exist
if block.Attributes["id"] == nil {
block.Attributes["id"] = &configschema.Attribute{
Type: cty.String,
Optional: true,
Computed: true,
}
}
_, timeoutsAttr := block.Attributes[TimeoutsConfigKey]
_, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey]
// Insert configured timeout values into the schema, as long as the schema
// didn't define anything else by that name.
if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock {
timeouts := configschema.Block{
Attributes: map[string]*configschema.Attribute{},
}
if r.Timeouts.Create != nil {
timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Read != nil {
timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Update != nil {
timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Delete != nil {
timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Default != nil {
timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
Nesting: configschema.NestingSingle,
Block: timeouts,
}
}
return block
}
func (r *Resource) coreConfigSchema() *configschema.Block {
return schemaMap(r.Schema).CoreConfigSchema()
}
// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema
// on the backends's schema.
func (r *Backend) CoreConfigSchema() *configschema.Block {
return schemaMap(r.Schema).CoreConfigSchema()
}

View File

@ -0,0 +1,458 @@
package schema
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
)
// add the implicit "id" attribute for test resources
func testResource(block *configschema.Block) *configschema.Block {
if block.Attributes == nil {
block.Attributes = make(map[string]*configschema.Attribute)
}
if block.BlockTypes == nil {
block.BlockTypes = make(map[string]*configschema.NestedBlock)
}
if block.Attributes["id"] == nil {
block.Attributes["id"] = &configschema.Attribute{
Type: cty.String,
Optional: true,
Computed: true,
}
}
return block
}
func TestSchemaMapCoreConfigSchema(t *testing.T) {
tests := map[string]struct {
Schema map[string]*Schema
Want *configschema.Block
}{
"empty": {
map[string]*Schema{},
testResource(&configschema.Block{}),
},
"primitives": {
map[string]*Schema{
"int": {
Type: TypeInt,
Required: true,
Description: "foo bar baz",
},
"float": {
Type: TypeFloat,
Optional: true,
},
"bool": {
Type: TypeBool,
Computed: true,
},
"string": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"int": {
Type: cty.Number,
Required: true,
Description: "foo bar baz",
},
"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.
},
},
testResource(&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{},
}),
},
"incorrectly-specified collections": {
// Historically we tolerated setting a type directly as the Elem
// attribute, rather than a Schema object. This is common enough
// in existing provider code that we must support it as an alias
// for a schema object with the given type.
map[string]*Schema{
"list": {
Type: TypeList,
Required: true,
Elem: TypeInt,
},
"set": {
Type: TypeSet,
Optional: true,
Elem: TypeString,
},
"map": {
Type: TypeMap,
Optional: true,
Elem: TypeBool,
},
},
testResource(&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,
},
},
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{},
},
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
// This one becomes a string attribute because helper/schema
// doesn't actually support maps of resource. The given
// "Elem" is just ignored entirely here, which is important
// because that is also true of the helper/schema logic and
// existing providers rely on this being ignored for
// correct operation.
"map": {
Type: cty.Map(cty.String),
Optional: true,
},
},
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
},
},
}),
},
"sub-resource collections minitems+optional": {
// This particular case is an odd one where the provider gives
// conflicting information about whether a sub-resource is required,
// by marking it as optional but also requiring one item.
// Historically the optional-ness "won" here, and so we must
// honor that for compatibility with providers that relied on this
// undocumented interaction.
map[string]*Schema{
"list": {
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{},
},
MinItems: 1,
MaxItems: 1,
},
"set": {
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{},
},
MinItems: 1,
MaxItems: 1,
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{},
BlockTypes: map[string]*configschema.NestedBlock{
"list": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
MinItems: 0,
MaxItems: 1,
},
"set": {
Nesting: configschema.NestingSet,
Block: configschema.Block{},
MinItems: 0,
MaxItems: 1,
},
},
}),
},
"sub-resource collections minitems+computed": {
map[string]*Schema{
"list": {
Type: TypeList,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{},
},
MinItems: 1,
MaxItems: 1,
},
"set": {
Type: TypeSet,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{},
},
MinItems: 1,
MaxItems: 1,
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"list": {
Type: cty.List(cty.EmptyObject),
Computed: true,
},
"set": {
Type: cty.Set(cty.EmptyObject),
Computed: true,
},
},
}),
},
"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{},
},
},
},
},
},
},
testResource(&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
},
},
}),
},
"sensitive": {
map[string]*Schema{
"string": {
Type: TypeString,
Optional: true,
Sensitive: true,
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"string": {
Type: cty.String,
Optional: true,
Sensitive: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
}),
},
"conditionally required on": {
map[string]*Schema{
"string": {
Type: TypeString,
Required: true,
DefaultFunc: func() (interface{}, error) {
return nil, nil
},
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"string": {
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
}),
},
"conditionally required off": {
map[string]*Schema{
"string": {
Type: TypeString,
Required: true,
DefaultFunc: func() (interface{}, error) {
// If we return a non-nil default then this overrides
// the "Required: true" for the purpose of building
// the core schema, so that core will ignore it not
// being set and let the provider handle it.
return "boop", nil
},
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"string": {
Type: cty.String,
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
}),
},
"conditionally required error": {
map[string]*Schema{
"string": {
Type: TypeString,
Required: true,
DefaultFunc: func() (interface{}, error) {
return nil, fmt.Errorf("placeholder error")
},
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"string": {
Type: cty.String,
Optional: true, // Just so we can progress to provider-driven validation and return the error there
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
}),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := (&Resource{Schema: test.Schema}).CoreConfigSchema()
if !cmp.Equal(got, test.Want, equateEmpty, typeComparer) {
t.Error(cmp.Diff(got, test.Want, equateEmpty, typeComparer))
}
})
}
}

View File

@ -0,0 +1,59 @@
package schema
import (
"fmt"
)
// DataSourceResourceShim takes a Resource instance describing a data source
// (with a Read implementation and a Schema, at least) and returns a new
// Resource instance with additional Create and Delete implementations that
// allow the data source to be used as a resource.
//
// This is a backward-compatibility layer for data sources that were formerly
// read-only resources before the data source concept was added. It should not
// be used for any *new* data sources.
//
// The Read function for the data source *must* call d.SetId with a non-empty
// id in order for this shim to function as expected.
//
// The provided Resource instance, and its schema, will be modified in-place
// to make it suitable for use as a full resource.
func DataSourceResourceShim(name string, dataSource *Resource) *Resource {
// Recursively, in-place adjust the schema so that it has ForceNew
// on any user-settable resource.
dataSourceResourceShimAdjustSchema(dataSource.Schema)
dataSource.Create = CreateFunc(dataSource.Read)
dataSource.Delete = func(d *ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
dataSource.Update = nil // should already be nil, but let's make sure
// FIXME: Link to some further docs either on the website or in the
// changelog, once such a thing exists.
dataSource.DeprecationMessage = fmt.Sprintf(
"using %s as a resource is deprecated; consider using the data source instead",
name,
)
return dataSource
}
func dataSourceResourceShimAdjustSchema(schema map[string]*Schema) {
for _, s := range schema {
// If the attribute is configurable then it must be ForceNew,
// since we have no Update implementation.
if s.Required || s.Optional {
s.ForceNew = true
}
// If the attribute is a nested resource, we need to recursively
// apply these same adjustments to it.
if s.Elem != nil {
if r, ok := s.Elem.(*Resource); ok {
dataSourceResourceShimAdjustSchema(r.Schema)
}
}
}
}

View File

@ -0,0 +1,6 @@
package schema
// Equal is an interface that checks for deep equality between two objects.
type Equal interface {
Equal(interface{}) bool
}

View File

@ -0,0 +1,343 @@
package schema
import (
"fmt"
"strconv"
"strings"
)
// FieldReaders are responsible for decoding fields out of data into
// the proper typed representation. ResourceData uses this to query data
// out of multiple sources: config, state, diffs, etc.
type FieldReader interface {
ReadField([]string) (FieldReadResult, error)
}
// FieldReadResult encapsulates all the resulting data from reading
// a field.
type FieldReadResult struct {
// Value is the actual read value. NegValue is the _negative_ value
// or the items that should be removed (if they existed). NegValue
// doesn't make sense for primitives but is important for any
// container types such as maps, sets, lists.
Value interface{}
ValueProcessed interface{}
// Exists is true if the field was found in the data. False means
// it wasn't found if there was no error.
Exists bool
// Computed is true if the field was found but the value
// is computed.
Computed bool
}
// ValueOrZero returns the value of this result or the zero value of the
// schema type, ensuring a consistent non-nil return value.
func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
if r.Value != nil {
return r.Value
}
return s.ZeroValue()
}
// SchemasForFlatmapPath tries its best to find a sequence of schemas that
// the given dot-delimited attribute path traverses through.
func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema {
parts := strings.Split(path, ".")
return addrToSchema(parts, schemaMap)
}
// addrToSchema finds the final element schema for the given address
// and the given schema. It returns all the schemas that led to the final
// schema. These are in order of the address (out to in).
func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
current := &Schema{
Type: typeObject,
Elem: schemaMap,
}
// If we aren't given an address, then the user is requesting the
// full object, so we return the special value which is the full object.
if len(addr) == 0 {
return []*Schema{current}
}
result := make([]*Schema, 0, len(addr))
for len(addr) > 0 {
k := addr[0]
addr = addr[1:]
REPEAT:
// We want to trim off the first "typeObject" since its not a
// real lookup that people do. i.e. []string{"foo"} in a structure
// isn't {typeObject, typeString}, its just a {typeString}.
if len(result) > 0 || current.Type != typeObject {
result = append(result, current)
}
switch t := current.Type; t {
case TypeBool, TypeInt, TypeFloat, TypeString:
if len(addr) > 0 {
return nil
}
case TypeList, TypeSet:
isIndex := len(addr) > 0 && addr[0] == "#"
switch v := current.Elem.(type) {
case *Resource:
current = &Schema{
Type: typeObject,
Elem: v.Schema,
}
case *Schema:
current = v
case ValueType:
current = &Schema{Type: v}
default:
// we may not know the Elem type and are just looking for the
// index
if isIndex {
break
}
if len(addr) == 0 {
// we've processed the address, so return what we've
// collected
return result
}
if len(addr) == 1 {
if _, err := strconv.Atoi(addr[0]); err == nil {
// we're indexing a value without a schema. This can
// happen if the list is nested in another schema type.
// Default to a TypeString like we do with a map
current = &Schema{Type: TypeString}
break
}
}
return nil
}
// If we only have one more thing and the next thing
// is a #, then we're accessing the index which is always
// an int.
if isIndex {
current = &Schema{Type: TypeInt}
break
}
case TypeMap:
if len(addr) > 0 {
switch v := current.Elem.(type) {
case ValueType:
current = &Schema{Type: v}
case *Schema:
current, _ = current.Elem.(*Schema)
default:
// maps default to string values. This is all we can have
// if this is nested in another list or map.
current = &Schema{Type: TypeString}
}
}
case typeObject:
// If we're already in the object, then we want to handle Sets
// and Lists specially. Basically, their next key is the lookup
// key (the set value or the list element). For these scenarios,
// we just want to skip it and move to the next element if there
// is one.
if len(result) > 0 {
lastType := result[len(result)-2].Type
if lastType == TypeSet || lastType == TypeList {
if len(addr) == 0 {
break
}
k = addr[0]
addr = addr[1:]
}
}
m := current.Elem.(map[string]*Schema)
val, ok := m[k]
if !ok {
return nil
}
current = val
goto REPEAT
}
}
return result
}
// readListField is a generic method for reading a list field out of a
// a FieldReader. It does this based on the assumption that there is a key
// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
// after that point.
func readListField(
r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) {
addrPadded := make([]string, len(addr)+1)
copy(addrPadded, addr)
addrPadded[len(addrPadded)-1] = "#"
// Get the number of elements in the list
countResult, err := r.ReadField(addrPadded)
if err != nil {
return FieldReadResult{}, err
}
if !countResult.Exists {
// No count, means we have no list
countResult.Value = 0
}
// If we have an empty list, then return an empty list
if countResult.Computed || countResult.Value.(int) == 0 {
return FieldReadResult{
Value: []interface{}{},
Exists: countResult.Exists,
Computed: countResult.Computed,
}, nil
}
// Go through each count, and get the item value out of it
result := make([]interface{}, countResult.Value.(int))
for i, _ := range result {
is := strconv.FormatInt(int64(i), 10)
addrPadded[len(addrPadded)-1] = is
rawResult, err := r.ReadField(addrPadded)
if err != nil {
return FieldReadResult{}, err
}
if !rawResult.Exists {
// This should never happen, because by the time the data
// gets to the FieldReaders, all the defaults should be set by
// Schema.
rawResult.Value = nil
}
result[i] = rawResult.Value
}
return FieldReadResult{
Value: result,
Exists: true,
}, nil
}
// readObjectField is a generic method for reading objects out of FieldReaders
// based on the assumption that building an address of []string{k, FIELD}
// will result in the proper field data.
func readObjectField(
r FieldReader,
addr []string,
schema map[string]*Schema) (FieldReadResult, error) {
result := make(map[string]interface{})
exists := false
for field, s := range schema {
addrRead := make([]string, len(addr), len(addr)+1)
copy(addrRead, addr)
addrRead = append(addrRead, field)
rawResult, err := r.ReadField(addrRead)
if err != nil {
return FieldReadResult{}, err
}
if rawResult.Exists {
exists = true
}
result[field] = rawResult.ValueOrZero(s)
}
return FieldReadResult{
Value: result,
Exists: exists,
}, nil
}
// convert map values to the proper primitive type based on schema.Elem
func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error {
elemType, err := getValueType(k, schema)
if err != nil {
return err
}
switch elemType {
case TypeInt, TypeFloat, TypeBool:
for k, v := range m {
vs, ok := v.(string)
if !ok {
continue
}
v, err := stringToPrimitive(vs, false, &Schema{Type: elemType})
if err != nil {
return err
}
m[k] = v
}
}
return nil
}
func stringToPrimitive(
value string, computed bool, schema *Schema) (interface{}, error) {
var returnVal interface{}
switch schema.Type {
case TypeBool:
if value == "" {
returnVal = false
break
}
if computed {
break
}
v, err := strconv.ParseBool(value)
if err != nil {
return nil, err
}
returnVal = v
case TypeFloat:
if value == "" {
returnVal = 0.0
break
}
if computed {
break
}
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, err
}
returnVal = v
case TypeInt:
if value == "" {
returnVal = 0
break
}
if computed {
break
}
v, err := strconv.ParseInt(value, 0, 0)
if err != nil {
return nil, err
}
returnVal = int(v)
case TypeString:
returnVal = value
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}
return returnVal, nil
}

View File

@ -0,0 +1,353 @@
package schema
import (
"fmt"
"log"
"strconv"
"strings"
"sync"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/mitchellh/mapstructure"
)
// ConfigFieldReader reads fields out of an untyped map[string]string to the
// best of its ability. It also applies defaults from the Schema. (The other
// field readers do not need default handling because they source fully
// populated data structures.)
type ConfigFieldReader struct {
Config *terraform.ResourceConfig
Schema map[string]*Schema
indexMaps map[string]map[string]int
once sync.Once
}
func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) {
r.once.Do(func() { r.indexMaps = make(map[string]map[string]int) })
return r.readField(address, false)
}
func (r *ConfigFieldReader) readField(
address []string, nested bool) (FieldReadResult, error) {
schemaList := addrToSchema(address, r.Schema)
if len(schemaList) == 0 {
return FieldReadResult{}, nil
}
if !nested {
// If we have a set anywhere in the address, then we need to
// read that set out in order and actually replace that part of
// the address with the real list index. i.e. set.50 might actually
// map to set.12 in the config, since it is in list order in the
// config, not indexed by set value.
for i, v := range schemaList {
// Sets are the only thing that cause this issue.
if v.Type != TypeSet {
continue
}
// If we're at the end of the list, then we don't have to worry
// about this because we're just requesting the whole set.
if i == len(schemaList)-1 {
continue
}
// If we're looking for the count, then ignore...
if address[i+1] == "#" {
continue
}
indexMap, ok := r.indexMaps[strings.Join(address[:i+1], ".")]
if !ok {
// Get the set so we can get the index map that tells us the
// mapping of the hash code to the list index
_, err := r.readSet(address[:i+1], v)
if err != nil {
return FieldReadResult{}, err
}
indexMap = r.indexMaps[strings.Join(address[:i+1], ".")]
}
index, ok := indexMap[address[i+1]]
if !ok {
return FieldReadResult{}, nil
}
address[i+1] = strconv.FormatInt(int64(index), 10)
}
}
k := strings.Join(address, ".")
schema := schemaList[len(schemaList)-1]
// If we're getting the single element of a promoted list, then
// check to see if we have a single element we need to promote.
if address[len(address)-1] == "0" && len(schemaList) > 1 {
lastSchema := schemaList[len(schemaList)-2]
if lastSchema.Type == TypeList && lastSchema.PromoteSingle {
k := strings.Join(address[:len(address)-1], ".")
result, err := r.readPrimitive(k, schema)
if err == nil {
return result, nil
}
}
}
if protoVersion5 {
switch schema.Type {
case TypeList, TypeSet, TypeMap, typeObject:
// Check if the value itself is unknown.
// The new protocol shims will add unknown values to this list of
// ComputedKeys. This is the only way we have to indicate that a
// collection is unknown in the config
for _, unknown := range r.Config.ComputedKeys {
if k == unknown {
log.Printf("[DEBUG] setting computed for %q from ComputedKeys", k)
return FieldReadResult{Computed: true, Exists: true}, nil
}
}
}
}
switch schema.Type {
case TypeBool, TypeFloat, TypeInt, TypeString:
return r.readPrimitive(k, schema)
case TypeList:
// If we support promotion then we first check if we have a lone
// value that we must promote.
// a value that is alone.
if schema.PromoteSingle {
result, err := r.readPrimitive(k, schema.Elem.(*Schema))
if err == nil && result.Exists {
result.Value = []interface{}{result.Value}
return result, nil
}
}
return readListField(&nestedConfigFieldReader{r}, address, schema)
case TypeMap:
return r.readMap(k, schema)
case TypeSet:
return r.readSet(address, schema)
case typeObject:
return readObjectField(
&nestedConfigFieldReader{r},
address, schema.Elem.(map[string]*Schema))
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}
}
func (r *ConfigFieldReader) readMap(k string, schema *Schema) (FieldReadResult, error) {
// We want both the raw value and the interpolated. We use the interpolated
// to store actual values and we use the raw one to check for
// computed keys. Actual values are obtained in the switch, depending on
// the type of the raw value.
mraw, ok := r.Config.GetRaw(k)
if !ok {
// check if this is from an interpolated field by seeing if it exists
// in the config
_, ok := r.Config.Get(k)
if !ok {
// this really doesn't exist
return FieldReadResult{}, nil
}
// We couldn't fetch the value from a nested data structure, so treat the
// raw value as an interpolation string. The mraw value is only used
// for the type switch below.
mraw = "${INTERPOLATED}"
}
result := make(map[string]interface{})
computed := false
switch m := mraw.(type) {
case string:
// This is a map which has come out of an interpolated variable, so we
// can just get the value directly from config. Values cannot be computed
// currently.
v, _ := r.Config.Get(k)
// If this isn't a map[string]interface, it must be computed.
mapV, ok := v.(map[string]interface{})
if !ok {
return FieldReadResult{
Exists: true,
Computed: true,
}, nil
}
// Otherwise we can proceed as usual.
for i, iv := range mapV {
result[i] = iv
}
case []interface{}:
for i, innerRaw := range m {
for ik := range innerRaw.(map[string]interface{}) {
key := fmt.Sprintf("%s.%d.%s", k, i, ik)
if r.Config.IsComputed(key) {
computed = true
break
}
v, _ := r.Config.Get(key)
result[ik] = v
}
}
case []map[string]interface{}:
for i, innerRaw := range m {
for ik := range innerRaw {
key := fmt.Sprintf("%s.%d.%s", k, i, ik)
if r.Config.IsComputed(key) {
computed = true
break
}
v, _ := r.Config.Get(key)
result[ik] = v
}
}
case map[string]interface{}:
for ik := range m {
key := fmt.Sprintf("%s.%s", k, ik)
if r.Config.IsComputed(key) {
computed = true
break
}
v, _ := r.Config.Get(key)
result[ik] = v
}
case nil:
// the map may have been empty on the configuration, so we leave the
// empty result
default:
panic(fmt.Sprintf("unknown type: %#v", mraw))
}
err := mapValuesToPrimitive(k, result, schema)
if err != nil {
return FieldReadResult{}, nil
}
var value interface{}
if !computed {
value = result
}
return FieldReadResult{
Value: value,
Exists: true,
Computed: computed,
}, nil
}
func (r *ConfigFieldReader) readPrimitive(
k string, schema *Schema) (FieldReadResult, error) {
raw, ok := r.Config.Get(k)
if !ok {
// Nothing in config, but we might still have a default from the schema
var err error
raw, err = schema.DefaultValue()
if err != nil {
return FieldReadResult{}, fmt.Errorf("%s, error loading default: %s", k, err)
}
if raw == nil {
return FieldReadResult{}, nil
}
}
var result string
if err := mapstructure.WeakDecode(raw, &result); err != nil {
return FieldReadResult{}, err
}
computed := r.Config.IsComputed(k)
returnVal, err := stringToPrimitive(result, computed, schema)
if err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: returnVal,
Exists: true,
Computed: computed,
}, nil
}
func (r *ConfigFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, error) {
indexMap := make(map[string]int)
// Create the set that will be our result
set := schema.ZeroValue().(*Set)
raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
if err != nil {
return FieldReadResult{}, err
}
if !raw.Exists {
return FieldReadResult{Value: set}, nil
}
// If the list is computed, the set is necessarilly computed
if raw.Computed {
return FieldReadResult{
Value: set,
Exists: true,
Computed: raw.Computed,
}, nil
}
// Build up the set from the list elements
for i, v := range raw.Value.([]interface{}) {
// Check if any of the keys in this item are computed
computed := r.hasComputedSubKeys(
fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema)
code := set.add(v, computed)
indexMap[code] = i
}
r.indexMaps[strings.Join(address, ".")] = indexMap
return FieldReadResult{
Value: set,
Exists: true,
}, nil
}
// hasComputedSubKeys walks through a schema and returns whether or not the
// given key contains any subkeys that are computed.
func (r *ConfigFieldReader) hasComputedSubKeys(key string, schema *Schema) bool {
prefix := key + "."
switch t := schema.Elem.(type) {
case *Resource:
for k, schema := range t.Schema {
if r.Config.IsComputed(prefix + k) {
return true
}
if r.hasComputedSubKeys(prefix+k, schema) {
return true
}
}
}
return false
}
// nestedConfigFieldReader is a funny little thing that just wraps a
// ConfigFieldReader to call readField when ReadField is called so that
// we don't recalculate the set rewrites in the address, which leads to
// an infinite loop.
type nestedConfigFieldReader struct {
Reader *ConfigFieldReader
}
func (r *nestedConfigFieldReader) ReadField(
address []string) (FieldReadResult, error) {
return r.Reader.readField(address, true)
}

View File

@ -0,0 +1,540 @@
package schema
import (
"bytes"
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func TestConfigFieldReader_impl(t *testing.T) {
var _ FieldReader = new(ConfigFieldReader)
}
func TestConfigFieldReader(t *testing.T) {
testFieldReader(t, func(s map[string]*Schema) FieldReader {
return &ConfigFieldReader{
Schema: s,
Config: testConfig(t, map[string]interface{}{
"bool": true,
"float": 3.1415,
"int": 42,
"string": "string",
"list": []interface{}{"foo", "bar"},
"listInt": []interface{}{21, 42},
"map": map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
"mapInt": map[string]interface{}{
"one": "1",
"two": "2",
},
"mapIntNestedSchema": map[string]interface{}{
"one": "1",
"two": "2",
},
"mapFloat": map[string]interface{}{
"oneDotTwo": "1.2",
},
"mapBool": map[string]interface{}{
"True": "true",
"False": "false",
},
"set": []interface{}{10, 50},
"setDeep": []interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
}),
}
})
}
// This contains custom table tests for our ConfigFieldReader
func TestConfigFieldReader_custom(t *testing.T) {
schema := map[string]*Schema{
"bool": &Schema{
Type: TypeBool,
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Config *terraform.ResourceConfig
Err bool
}{
"basic": {
[]string{"bool"},
FieldReadResult{
Value: true,
Exists: true,
},
testConfig(t, map[string]interface{}{
"bool": true,
}),
false,
},
"computed": {
[]string{"bool"},
FieldReadResult{
Exists: true,
Computed: true,
},
testConfig(t, map[string]interface{}{
"bool": hcl2shim.UnknownVariableValue,
}),
false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := &ConfigFieldReader{
Schema: schema,
Config: tc.Config,
}
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out.Value = s.List()
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
})
}
}
func TestConfigFieldReader_DefaultHandling(t *testing.T) {
schema := map[string]*Schema{
"strWithDefault": &Schema{
Type: TypeString,
Default: "ImADefault",
},
"strWithDefaultFunc": &Schema{
Type: TypeString,
DefaultFunc: func() (interface{}, error) {
return "FuncDefault", nil
},
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Config *terraform.ResourceConfig
Err bool
}{
"gets default value when no config set": {
[]string{"strWithDefault"},
FieldReadResult{
Value: "ImADefault",
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{}),
false,
},
"config overrides default value": {
[]string{"strWithDefault"},
FieldReadResult{
Value: "fromConfig",
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"strWithDefault": "fromConfig",
}),
false,
},
"gets default from function when no config set": {
[]string{"strWithDefaultFunc"},
FieldReadResult{
Value: "FuncDefault",
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{}),
false,
},
"config overrides default function": {
[]string{"strWithDefaultFunc"},
FieldReadResult{
Value: "fromConfig",
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"strWithDefaultFunc": "fromConfig",
}),
false,
},
}
for name, tc := range cases {
r := &ConfigFieldReader{
Schema: schema,
Config: tc.Config,
}
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out.Value = s.List()
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}
func TestConfigFieldReader_ComputedMap(t *testing.T) {
schema := map[string]*Schema{
"map": &Schema{
Type: TypeMap,
Computed: true,
},
"listmap": &Schema{
Type: TypeMap,
Computed: true,
Elem: TypeList,
},
"maplist": &Schema{
Type: TypeList,
Computed: true,
Elem: TypeMap,
},
}
cases := []struct {
Name string
Addr []string
Result FieldReadResult
Config *terraform.ResourceConfig
Err bool
}{
{
"set, normal",
[]string{"map"},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
},
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"map": map[string]interface{}{
"foo": "bar",
},
}),
false,
},
{
"computed element",
[]string{"map"},
FieldReadResult{
Exists: true,
Computed: true,
},
testConfig(t, map[string]interface{}{
"map": map[string]interface{}{
"foo": hcl2shim.UnknownVariableValue,
},
}),
false,
},
{
"native map",
[]string{"map"},
FieldReadResult{
Value: map[string]interface{}{
"bar": "baz",
"baz": "bar",
},
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"map": map[string]interface{}{
"bar": "baz",
"baz": "bar",
},
}),
false,
},
{
"map-from-list-of-maps",
[]string{"maplist", "0"},
FieldReadResult{
Value: map[string]interface{}{
"key": "bar",
},
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"maplist": []interface{}{
map[string]interface{}{
"key": "bar",
},
},
}),
false,
},
{
"value-from-list-of-maps",
[]string{"maplist", "0", "key"},
FieldReadResult{
Value: "bar",
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"maplist": []interface{}{
map[string]interface{}{
"key": "bar",
},
},
}),
false,
},
{
"list-from-map-of-lists",
[]string{"listmap", "key"},
FieldReadResult{
Value: []interface{}{"bar"},
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"listmap": map[string]interface{}{
"key": []interface{}{
"bar",
},
},
}),
false,
},
{
"value-from-map-of-lists",
[]string{"listmap", "key", "0"},
FieldReadResult{
Value: "bar",
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"listmap": map[string]interface{}{
"key": []interface{}{
"bar",
},
},
}),
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
r := &ConfigFieldReader{
Schema: schema,
Config: tc.Config,
}
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatal(err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to the raw map
out.Value = s.m
if len(s.m) == 0 {
out.Value = nil
}
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("\nexpected: %#v\ngot: %#v", tc.Result, out)
}
})
}
}
func TestConfigFieldReader_ComputedSet(t *testing.T) {
schema := map[string]*Schema{
"strSet": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeString},
Set: HashString,
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Config *terraform.ResourceConfig
Err bool
}{
"set, normal": {
[]string{"strSet"},
FieldReadResult{
Value: map[string]interface{}{
"2356372769": "foo",
},
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"strSet": []interface{}{"foo"},
}),
false,
},
"set, computed element": {
[]string{"strSet"},
FieldReadResult{
Value: nil,
Exists: true,
Computed: true,
},
testConfig(t, map[string]interface{}{
"strSet": []interface{}{hcl2shim.UnknownVariableValue},
}),
false,
},
}
for name, tc := range cases {
r := &ConfigFieldReader{
Schema: schema,
Config: tc.Config,
}
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to the raw map
out.Value = s.m
if len(s.m) == 0 {
out.Value = nil
}
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}
func TestConfigFieldReader_computedComplexSet(t *testing.T) {
hashfunc := func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["vhd_uri"].(string)))
return hashcode.String(buf.String())
}
schema := map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"name": {
Type: TypeString,
Required: true,
},
"vhd_uri": {
Type: TypeString,
Required: true,
},
},
},
Set: hashfunc,
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Config *terraform.ResourceConfig
Err bool
}{
"set, normal": {
[]string{"set"},
FieldReadResult{
Value: map[string]interface{}{
"532860136": map[string]interface{}{
"name": "myosdisk1",
"vhd_uri": "bar",
},
},
Exists: true,
Computed: false,
},
testConfig(t, map[string]interface{}{
"set": []interface{}{
map[string]interface{}{
"name": "myosdisk1",
"vhd_uri": "bar",
},
},
}),
false,
},
}
for name, tc := range cases {
r := &ConfigFieldReader{
Schema: schema,
Config: tc.Config,
}
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to the raw map
out.Value = s.m
if len(s.m) == 0 {
out.Value = nil
}
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}
func testConfig(t *testing.T, raw map[string]interface{}) *terraform.ResourceConfig {
return terraform.NewResourceConfigRaw(raw)
}

View File

@ -0,0 +1,244 @@
package schema
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/mitchellh/mapstructure"
)
// DiffFieldReader reads fields out of a diff structures.
//
// It also requires access to a Reader that reads fields from the structure
// that the diff was derived from. This is usually the state. This is required
// because a diff on its own doesn't have complete data about full objects
// such as maps.
//
// The Source MUST be the data that the diff was derived from. If it isn't,
// the behavior of this struct is undefined.
//
// Reading fields from a DiffFieldReader is identical to reading from
// Source except the diff will be applied to the end result.
//
// The "Exists" field on the result will be set to true if the complete
// field exists whether its from the source, diff, or a combination of both.
// It cannot be determined whether a retrieved value is composed of
// diff elements.
type DiffFieldReader struct {
Diff *terraform.InstanceDiff
Source FieldReader
Schema map[string]*Schema
// cache for memoizing ReadField calls.
cache map[string]cachedFieldReadResult
}
type cachedFieldReadResult struct {
val FieldReadResult
err error
}
func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) {
if r.cache == nil {
r.cache = make(map[string]cachedFieldReadResult)
}
// Create the cache key by joining around a value that isn't a valid part
// of an address. This assumes that the Source and Schema are not changed
// for the life of this DiffFieldReader.
cacheKey := strings.Join(address, "|")
if cached, ok := r.cache[cacheKey]; ok {
return cached.val, cached.err
}
schemaList := addrToSchema(address, r.Schema)
if len(schemaList) == 0 {
r.cache[cacheKey] = cachedFieldReadResult{}
return FieldReadResult{}, nil
}
var res FieldReadResult
var err error
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool, TypeInt, TypeFloat, TypeString:
res, err = r.readPrimitive(address, schema)
case TypeList:
res, err = readListField(r, address, schema)
case TypeMap:
res, err = r.readMap(address, schema)
case TypeSet:
res, err = r.readSet(address, schema)
case typeObject:
res, err = readObjectField(r, address, schema.Elem.(map[string]*Schema))
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
r.cache[cacheKey] = cachedFieldReadResult{
val: res,
err: err,
}
return res, err
}
func (r *DiffFieldReader) readMap(
address []string, schema *Schema) (FieldReadResult, error) {
result := make(map[string]interface{})
resultSet := false
// First read the map from the underlying source
source, err := r.Source.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
if source.Exists {
// readMap may return a nil value, or an unknown value placeholder in
// some cases, causing the type assertion to panic if we don't assign the ok value
result, _ = source.Value.(map[string]interface{})
resultSet = true
}
// Next, read all the elements we have in our diff, and apply
// the diff to our result.
prefix := strings.Join(address, ".") + "."
for k, v := range r.Diff.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
if strings.HasPrefix(k, prefix+"%") {
// Ignore the count field
continue
}
resultSet = true
k = k[len(prefix):]
if v.NewRemoved {
delete(result, k)
continue
}
result[k] = v.New
}
key := address[len(address)-1]
err = mapValuesToPrimitive(key, result, schema)
if err != nil {
return FieldReadResult{}, nil
}
var resultVal interface{}
if resultSet {
resultVal = result
}
return FieldReadResult{
Value: resultVal,
Exists: resultSet,
}, nil
}
func (r *DiffFieldReader) readPrimitive(
address []string, schema *Schema) (FieldReadResult, error) {
result, err := r.Source.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
attrD, ok := r.Diff.Attributes[strings.Join(address, ".")]
if !ok {
return result, nil
}
var resultVal string
if !attrD.NewComputed {
resultVal = attrD.New
if attrD.NewExtra != nil {
result.ValueProcessed = resultVal
if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil {
return FieldReadResult{}, err
}
}
}
result.Computed = attrD.NewComputed
result.Exists = true
result.Value, err = stringToPrimitive(resultVal, false, schema)
if err != nil {
return FieldReadResult{}, err
}
return result, nil
}
func (r *DiffFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, error) {
// copy address to ensure we don't modify the argument
address = append([]string(nil), address...)
prefix := strings.Join(address, ".") + "."
// Create the set that will be our result
set := schema.ZeroValue().(*Set)
// Go through the map and find all the set items
for k, d := range r.Diff.Attributes {
if d.NewRemoved {
// If the field is removed, we always ignore it
continue
}
if !strings.HasPrefix(k, prefix) {
continue
}
if strings.HasSuffix(k, "#") {
// Ignore any count field
continue
}
// Split the key, since it might be a sub-object like "idx.field"
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
raw, err := r.ReadField(append(address, idx))
if err != nil {
return FieldReadResult{}, err
}
if !raw.Exists {
// This shouldn't happen because we just verified it does exist
panic("missing field in set: " + k + "." + idx)
}
set.Add(raw.Value)
}
// Determine if the set "exists". It exists if there are items or if
// the diff explicitly wanted it empty.
exists := set.Len() > 0
if !exists {
// We could check if the diff value is "0" here but I think the
// existence of "#" on its own is enough to show it existed. This
// protects us in the future from the zero value changing from
// "0" to "" breaking us (if that were to happen).
if _, ok := r.Diff.Attributes[prefix+"#"]; ok {
exists = true
}
}
if !exists {
result, err := r.Source.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
if result.Exists {
return result, nil
}
}
return FieldReadResult{
Value: set,
Exists: exists,
}, nil
}

View File

@ -0,0 +1,524 @@
package schema
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func TestDiffFieldReader_impl(t *testing.T) {
var _ FieldReader = new(DiffFieldReader)
}
func TestDiffFieldReader_NestedSetUpdate(t *testing.T) {
hashFn := func(a interface{}) int {
m := a.(map[string]interface{})
return m["val"].(int)
}
schema := map[string]*Schema{
"list_of_sets_1": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"nested_set": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"val": &Schema{
Type: TypeInt,
},
},
},
Set: hashFn,
},
},
},
},
"list_of_sets_2": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"nested_set": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"val": &Schema{
Type: TypeInt,
},
},
},
Set: hashFn,
},
},
},
},
}
r := &DiffFieldReader{
Schema: schema,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"list_of_sets_1.0.nested_set.1.val": &terraform.ResourceAttrDiff{
Old: "1",
New: "0",
NewRemoved: true,
},
"list_of_sets_1.0.nested_set.2.val": &terraform.ResourceAttrDiff{
New: "2",
},
},
},
}
r.Source = &MultiLevelFieldReader{
Readers: map[string]FieldReader{
"diff": r,
"set": &MapFieldReader{Schema: schema},
"state": &MapFieldReader{
Map: &BasicMapReader{
"list_of_sets_1.#": "1",
"list_of_sets_1.0.nested_set.#": "1",
"list_of_sets_1.0.nested_set.1.val": "1",
"list_of_sets_2.#": "1",
"list_of_sets_2.0.nested_set.#": "1",
"list_of_sets_2.0.nested_set.1.val": "1",
},
Schema: schema,
},
},
Levels: []string{"state", "config"},
}
out, err := r.ReadField([]string{"list_of_sets_2"})
if err != nil {
t.Fatalf("err: %v", err)
}
s := &Set{F: hashFn}
s.Add(map[string]interface{}{"val": 1})
expected := s.List()
l := out.Value.([]interface{})
i := l[0].(map[string]interface{})
actual := i["nested_set"].(*Set).List()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: NestedSetUpdate\n\nexpected: %#v\n\ngot: %#v\n\n", expected, actual)
}
}
// https://github.com/hashicorp/terraform/issues/914
func TestDiffFieldReader_MapHandling(t *testing.T) {
schema := map[string]*Schema{
"tags": &Schema{
Type: TypeMap,
},
}
r := &DiffFieldReader{
Schema: schema,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"tags.%": &terraform.ResourceAttrDiff{
Old: "1",
New: "2",
},
"tags.baz": &terraform.ResourceAttrDiff{
Old: "",
New: "qux",
},
},
},
Source: &MapFieldReader{
Schema: schema,
Map: BasicMapReader(map[string]string{
"tags.%": "1",
"tags.foo": "bar",
}),
},
}
result, err := r.ReadField([]string{"tags"})
if err != nil {
t.Fatalf("ReadField failed: %#v", err)
}
expected := map[string]interface{}{
"foo": "bar",
"baz": "qux",
}
if !reflect.DeepEqual(expected, result.Value) {
t.Fatalf("bad: DiffHandling\n\nexpected: %#v\n\ngot: %#v\n\n", expected, result.Value)
}
}
func TestDiffFieldReader_extra(t *testing.T) {
schema := map[string]*Schema{
"stringComputed": &Schema{Type: TypeString},
"listMap": &Schema{
Type: TypeList,
Elem: &Schema{
Type: TypeMap,
},
},
"mapRemove": &Schema{Type: TypeMap},
"setChange": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"value": &Schema{
Type: TypeString,
Required: true,
},
},
},
Set: func(a interface{}) int {
m := a.(map[string]interface{})
return m["index"].(int)
},
},
"setEmpty": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"value": &Schema{
Type: TypeString,
Required: true,
},
},
},
Set: func(a interface{}) int {
m := a.(map[string]interface{})
return m["index"].(int)
},
},
}
r := &DiffFieldReader{
Schema: schema,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"stringComputed": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
NewComputed: true,
},
"listMap.0.bar": &terraform.ResourceAttrDiff{
NewRemoved: true,
},
"mapRemove.bar": &terraform.ResourceAttrDiff{
NewRemoved: true,
},
"setChange.10.value": &terraform.ResourceAttrDiff{
Old: "50",
New: "80",
},
"setEmpty.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "0",
},
},
},
Source: &MapFieldReader{
Schema: schema,
Map: BasicMapReader(map[string]string{
"listMap.#": "2",
"listMap.0.foo": "bar",
"listMap.0.bar": "baz",
"listMap.1.baz": "baz",
"mapRemove.foo": "bar",
"mapRemove.bar": "bar",
"setChange.#": "1",
"setChange.10.index": "10",
"setChange.10.value": "50",
"setEmpty.#": "2",
"setEmpty.10.index": "10",
"setEmpty.10.value": "50",
"setEmpty.20.index": "20",
"setEmpty.20.value": "50",
}),
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Err bool
}{
"stringComputed": {
[]string{"stringComputed"},
FieldReadResult{
Value: "",
Exists: true,
Computed: true,
},
false,
},
"listMapRemoval": {
[]string{"listMap"},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"baz": "baz",
},
},
Exists: true,
},
false,
},
"mapRemove": {
[]string{"mapRemove"},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
},
Exists: true,
Computed: false,
},
false,
},
"setChange": {
[]string{"setChange"},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"index": 10,
"value": "80",
},
},
Exists: true,
},
false,
},
"setEmpty": {
[]string{"setEmpty"},
FieldReadResult{
Value: []interface{}{},
Exists: true,
},
false,
},
}
for name, tc := range cases {
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out.Value = s.List()
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}
func TestDiffFieldReader(t *testing.T) {
testFieldReader(t, func(s map[string]*Schema) FieldReader {
return &DiffFieldReader{
Schema: s,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"bool": &terraform.ResourceAttrDiff{
Old: "",
New: "true",
},
"int": &terraform.ResourceAttrDiff{
Old: "",
New: "42",
},
"float": &terraform.ResourceAttrDiff{
Old: "",
New: "3.1415",
},
"string": &terraform.ResourceAttrDiff{
Old: "",
New: "string",
},
"stringComputed": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
NewComputed: true,
},
"list.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"list.0": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"list.1": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
"listInt.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"listInt.0": &terraform.ResourceAttrDiff{
Old: "",
New: "21",
},
"listInt.1": &terraform.ResourceAttrDiff{
Old: "",
New: "42",
},
"map.foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
"map.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
"mapInt.%": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapInt.one": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"mapInt.two": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapIntNestedSchema.%": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapIntNestedSchema.one": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"mapIntNestedSchema.two": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapFloat.%": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"mapFloat.oneDotTwo": &terraform.ResourceAttrDiff{
Old: "",
New: "1.2",
},
"mapBool.%": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"mapBool.True": &terraform.ResourceAttrDiff{
Old: "",
New: "true",
},
"mapBool.False": &terraform.ResourceAttrDiff{
Old: "",
New: "false",
},
"set.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"set.10": &terraform.ResourceAttrDiff{
Old: "",
New: "10",
},
"set.50": &terraform.ResourceAttrDiff{
Old: "",
New: "50",
},
"setDeep.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "2",
},
"setDeep.10.index": &terraform.ResourceAttrDiff{
Old: "",
New: "10",
},
"setDeep.10.value": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"setDeep.50.index": &terraform.ResourceAttrDiff{
Old: "",
New: "50",
},
"setDeep.50.value": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
Source: &MapFieldReader{
Schema: s,
Map: BasicMapReader(map[string]string{
"listMap.#": "2",
"listMap.0.foo": "bar",
"listMap.0.bar": "baz",
"listMap.1.baz": "baz",
}),
},
}
})
}

View File

@ -0,0 +1,235 @@
package schema
import (
"fmt"
"strings"
)
// MapFieldReader reads fields out of an untyped map[string]string to
// the best of its ability.
type MapFieldReader struct {
Map MapReader
Schema map[string]*Schema
}
func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) {
k := strings.Join(address, ".")
schemaList := addrToSchema(address, r.Schema)
if len(schemaList) == 0 {
return FieldReadResult{}, nil
}
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool, TypeInt, TypeFloat, TypeString:
return r.readPrimitive(address, schema)
case TypeList:
return readListField(r, address, schema)
case TypeMap:
return r.readMap(k, schema)
case TypeSet:
return r.readSet(address, schema)
case typeObject:
return readObjectField(r, address, schema.Elem.(map[string]*Schema))
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}
}
func (r *MapFieldReader) readMap(k string, schema *Schema) (FieldReadResult, error) {
result := make(map[string]interface{})
resultSet := false
// If the name of the map field is directly in the map with an
// empty string, it means that the map is being deleted, so mark
// that is is set.
if v, ok := r.Map.Access(k); ok && v == "" {
resultSet = true
}
prefix := k + "."
r.Map.Range(func(k, v string) bool {
if strings.HasPrefix(k, prefix) {
resultSet = true
key := k[len(prefix):]
if key != "%" && key != "#" {
result[key] = v
}
}
return true
})
err := mapValuesToPrimitive(k, result, schema)
if err != nil {
return FieldReadResult{}, nil
}
var resultVal interface{}
if resultSet {
resultVal = result
}
return FieldReadResult{
Value: resultVal,
Exists: resultSet,
}, nil
}
func (r *MapFieldReader) readPrimitive(
address []string, schema *Schema) (FieldReadResult, error) {
k := strings.Join(address, ".")
result, ok := r.Map.Access(k)
if !ok {
return FieldReadResult{}, nil
}
returnVal, err := stringToPrimitive(result, false, schema)
if err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: returnVal,
Exists: true,
}, nil
}
func (r *MapFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, error) {
// copy address to ensure we don't modify the argument
address = append([]string(nil), address...)
// Get the number of elements in the list
countRaw, err := r.readPrimitive(
append(address, "#"), &Schema{Type: TypeInt})
if err != nil {
return FieldReadResult{}, err
}
if !countRaw.Exists {
// No count, means we have no list
countRaw.Value = 0
}
// Create the set that will be our result
set := schema.ZeroValue().(*Set)
// If we have an empty list, then return an empty list
if countRaw.Computed || countRaw.Value.(int) == 0 {
return FieldReadResult{
Value: set,
Exists: countRaw.Exists,
Computed: countRaw.Computed,
}, nil
}
// Go through the map and find all the set items
prefix := strings.Join(address, ".") + "."
countExpected := countRaw.Value.(int)
countActual := make(map[string]struct{})
completed := r.Map.Range(func(k, _ string) bool {
if !strings.HasPrefix(k, prefix) {
return true
}
if strings.HasPrefix(k, prefix+"#") {
// Ignore the count field
return true
}
// Split the key, since it might be a sub-object like "idx.field"
parts := strings.Split(k[len(prefix):], ".")
idx := parts[0]
var raw FieldReadResult
raw, err = r.ReadField(append(address, idx))
if err != nil {
return false
}
if !raw.Exists {
// This shouldn't happen because we just verified it does exist
panic("missing field in set: " + k + "." + idx)
}
set.Add(raw.Value)
// Due to the way multimap readers work, if we've seen the number
// of fields we expect, then exit so that we don't read later values.
// For example: the "set" map might have "ports.#", "ports.0", and
// "ports.1", but the "state" map might have those plus "ports.2".
// We don't want "ports.2"
countActual[idx] = struct{}{}
if len(countActual) >= countExpected {
return false
}
return true
})
if !completed && err != nil {
return FieldReadResult{}, err
}
return FieldReadResult{
Value: set,
Exists: true,
}, nil
}
// MapReader is an interface that is given to MapFieldReader for accessing
// a "map". This can be used to have alternate implementations. For a basic
// map[string]string, use BasicMapReader.
type MapReader interface {
Access(string) (string, bool)
Range(func(string, string) bool) bool
}
// BasicMapReader implements MapReader for a single map.
type BasicMapReader map[string]string
func (r BasicMapReader) Access(k string) (string, bool) {
v, ok := r[k]
return v, ok
}
func (r BasicMapReader) Range(f func(string, string) bool) bool {
for k, v := range r {
if cont := f(k, v); !cont {
return false
}
}
return true
}
// MultiMapReader reads over multiple maps, preferring keys that are
// founder earlier (lower number index) vs. later (higher number index)
type MultiMapReader []map[string]string
func (r MultiMapReader) Access(k string) (string, bool) {
for _, m := range r {
if v, ok := m[k]; ok {
return v, ok
}
}
return "", false
}
func (r MultiMapReader) Range(f func(string, string) bool) bool {
done := make(map[string]struct{})
for _, m := range r {
for k, v := range m {
if _, ok := done[k]; ok {
continue
}
if cont := f(k, v); !cont {
return false
}
done[k] = struct{}{}
}
}
return true
}

View File

@ -0,0 +1,123 @@
package schema
import (
"reflect"
"testing"
)
func TestMapFieldReader_impl(t *testing.T) {
var _ FieldReader = new(MapFieldReader)
}
func TestMapFieldReader(t *testing.T) {
testFieldReader(t, func(s map[string]*Schema) FieldReader {
return &MapFieldReader{
Schema: s,
Map: BasicMapReader(map[string]string{
"bool": "true",
"int": "42",
"float": "3.1415",
"string": "string",
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
"listInt.#": "2",
"listInt.0": "21",
"listInt.1": "42",
"map.%": "2",
"map.foo": "bar",
"map.bar": "baz",
"set.#": "2",
"set.10": "10",
"set.50": "50",
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
"mapInt.%": "2",
"mapInt.one": "1",
"mapInt.two": "2",
"mapIntNestedSchema.%": "2",
"mapIntNestedSchema.one": "1",
"mapIntNestedSchema.two": "2",
"mapFloat.%": "1",
"mapFloat.oneDotTwo": "1.2",
"mapBool.%": "2",
"mapBool.True": "true",
"mapBool.False": "false",
}),
}
})
}
func TestMapFieldReader_extra(t *testing.T) {
r := &MapFieldReader{
Schema: map[string]*Schema{
"mapDel": &Schema{Type: TypeMap},
"mapEmpty": &Schema{Type: TypeMap},
},
Map: BasicMapReader(map[string]string{
"mapDel": "",
"mapEmpty.%": "0",
}),
}
cases := map[string]struct {
Addr []string
Out interface{}
OutOk bool
OutComputed bool
OutErr bool
}{
"mapDel": {
[]string{"mapDel"},
map[string]interface{}{},
true,
false,
false,
},
"mapEmpty": {
[]string{"mapEmpty"},
map[string]interface{}{},
true,
false,
false,
},
}
for name, tc := range cases {
out, err := r.ReadField(tc.Addr)
if err != nil != tc.OutErr {
t.Fatalf("%s: err: %s", name, err)
}
if out.Computed != tc.OutComputed {
t.Fatalf("%s: err: %#v", name, out.Computed)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out.Value = s.List()
}
if !reflect.DeepEqual(out.Value, tc.Out) {
t.Fatalf("%s: out: %#v", name, out.Value)
}
if out.Exists != tc.OutOk {
t.Fatalf("%s: outOk: %#v", name, out.Exists)
}
}
}

View File

@ -0,0 +1,63 @@
package schema
import (
"fmt"
)
// MultiLevelFieldReader reads from other field readers,
// merging their results along the way in a specific order. You can specify
// "levels" and name them in order to read only an exact level or up to
// a specific level.
//
// This is useful for saying things such as "read the field from the state
// and config and merge them" or "read the latest value of the field".
type MultiLevelFieldReader struct {
Readers map[string]FieldReader
Levels []string
}
func (r *MultiLevelFieldReader) ReadField(address []string) (FieldReadResult, error) {
return r.ReadFieldMerge(address, r.Levels[len(r.Levels)-1])
}
func (r *MultiLevelFieldReader) ReadFieldExact(
address []string, level string) (FieldReadResult, error) {
reader, ok := r.Readers[level]
if !ok {
return FieldReadResult{}, fmt.Errorf(
"Unknown reader level: %s", level)
}
result, err := reader.ReadField(address)
if err != nil {
return FieldReadResult{}, fmt.Errorf(
"Error reading level %s: %s", level, err)
}
return result, nil
}
func (r *MultiLevelFieldReader) ReadFieldMerge(
address []string, level string) (FieldReadResult, error) {
var result FieldReadResult
for _, l := range r.Levels {
if r, ok := r.Readers[l]; ok {
out, err := r.ReadField(address)
if err != nil {
return FieldReadResult{}, fmt.Errorf(
"Error reading level %s: %s", l, err)
}
// TODO: computed
if out.Exists {
result = out
}
}
if l == level {
break
}
}
return result, nil
}

View File

@ -0,0 +1,270 @@
package schema
import (
"reflect"
"strconv"
"testing"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) {
cases := map[string]struct {
Addr []string
Readers []FieldReader
Level string
Result FieldReadResult
}{
"specific": {
Addr: []string{"foo"},
Readers: []FieldReader{
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"foo": "bar",
}),
},
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"foo": "baz",
}),
},
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{}),
},
},
Level: "1",
Result: FieldReadResult{
Value: "baz",
Exists: true,
},
},
}
for name, tc := range cases {
readers := make(map[string]FieldReader)
levels := make([]string, len(tc.Readers))
for i, r := range tc.Readers {
is := strconv.FormatInt(int64(i), 10)
readers[is] = r
levels[i] = is
}
r := &MultiLevelFieldReader{
Readers: readers,
Levels: levels,
}
out, err := r.ReadFieldExact(tc.Addr, tc.Level)
if err != nil {
t.Fatalf("%s: err: %s", name, err)
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}
func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) {
cases := map[string]struct {
Addr []string
Readers []FieldReader
Result FieldReadResult
}{
"stringInDiff": {
Addr: []string{"availability_zone"},
Readers: []FieldReader{
&DiffFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Source: &MapFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"availability_zone": "foo",
}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
RequiresNew: true,
},
},
},
},
},
Result: FieldReadResult{
Value: "bar",
Exists: true,
},
},
"lastLevelComputed": {
Addr: []string{"availability_zone"},
Readers: []FieldReader{
&MapFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"availability_zone": "foo",
}),
},
&DiffFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Source: &MapFieldReader{
Schema: map[string]*Schema{
"availability_zone": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"availability_zone": "foo",
}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
NewComputed: true,
},
},
},
},
},
Result: FieldReadResult{
Value: "",
Exists: true,
Computed: true,
},
},
"list of maps with removal in diff": {
Addr: []string{"config_vars"},
Readers: []FieldReader{
&DiffFieldReader{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
Source: &MapFieldReader{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
Map: BasicMapReader(map[string]string{
"config_vars.#": "2",
"config_vars.0.foo": "bar",
"config_vars.0.bar": "bar",
"config_vars.1.bar": "baz",
}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"config_vars.0.bar": &terraform.ResourceAttrDiff{
NewRemoved: true,
},
},
},
},
},
Result: FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"foo": "bar",
},
map[string]interface{}{
"bar": "baz",
},
},
Exists: true,
},
},
"first level only": {
Addr: []string{"foo"},
Readers: []FieldReader{
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{
"foo": "bar",
}),
},
&MapFieldReader{
Schema: map[string]*Schema{
"foo": &Schema{Type: TypeString},
},
Map: BasicMapReader(map[string]string{}),
},
},
Result: FieldReadResult{
Value: "bar",
Exists: true,
},
},
}
for name, tc := range cases {
readers := make(map[string]FieldReader)
levels := make([]string, len(tc.Readers))
for i, r := range tc.Readers {
is := strconv.FormatInt(int64(i), 10)
readers[is] = r
levels[i] = is
}
r := &MultiLevelFieldReader{
Readers: readers,
Levels: levels,
}
out, err := r.ReadFieldMerge(tc.Addr, levels[len(levels)-1])
if err != nil {
t.Fatalf("%s: err: %s", name, err)
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}

View File

@ -0,0 +1,471 @@
package schema
import (
"reflect"
"testing"
)
func TestAddrToSchema(t *testing.T) {
cases := map[string]struct {
Addr []string
Schema map[string]*Schema
Result []ValueType
}{
"full object": {
[]string{},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{typeObject},
},
"list": {
[]string{"list"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{TypeList},
},
"list.#": {
[]string{"list", "#"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{TypeList, TypeInt},
},
"list.0": {
[]string{"list", "0"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
},
[]ValueType{TypeList, TypeInt},
},
"list.0 with resource": {
[]string{"list", "0"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"field": &Schema{Type: TypeString},
},
},
},
},
[]ValueType{TypeList, typeObject},
},
"list.0.field": {
[]string{"list", "0", "field"},
map[string]*Schema{
"list": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"field": &Schema{Type: TypeString},
},
},
},
},
[]ValueType{TypeList, typeObject, TypeString},
},
"set": {
[]string{"set"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
[]ValueType{TypeSet},
},
"set.#": {
[]string{"set", "#"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
[]ValueType{TypeSet, TypeInt},
},
"set.0": {
[]string{"set", "0"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
[]ValueType{TypeSet, TypeInt},
},
"set.0 with resource": {
[]string{"set", "0"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"field": &Schema{Type: TypeString},
},
},
},
},
[]ValueType{TypeSet, typeObject},
},
"mapElem": {
[]string{"map", "foo"},
map[string]*Schema{
"map": &Schema{Type: TypeMap},
},
[]ValueType{TypeMap, TypeString},
},
"setDeep": {
[]string{"set", "50", "index"},
map[string]*Schema{
"set": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{Type: TypeInt},
"value": &Schema{Type: TypeString},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["index"].(int)
},
},
},
[]ValueType{TypeSet, typeObject, TypeInt},
},
}
for name, tc := range cases {
result := addrToSchema(tc.Addr, tc.Schema)
types := make([]ValueType, len(result))
for i, v := range result {
types[i] = v.Type
}
if !reflect.DeepEqual(types, tc.Result) {
t.Fatalf("%s: %#v", name, types)
}
}
}
// testFieldReader is a helper that should be used to verify that
// a FieldReader behaves properly in all the common cases.
func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) {
schema := map[string]*Schema{
// Primitives
"bool": &Schema{Type: TypeBool},
"float": &Schema{Type: TypeFloat},
"int": &Schema{Type: TypeInt},
"string": &Schema{Type: TypeString},
// Lists
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeString},
},
"listInt": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
"listMap": &Schema{
Type: TypeList,
Elem: &Schema{
Type: TypeMap,
},
},
// Maps
"map": &Schema{Type: TypeMap},
"mapInt": &Schema{
Type: TypeMap,
Elem: TypeInt,
},
// This is used to verify that the type of a Map can be specified using the
// same syntax as for lists (as a nested *Schema passed to Elem)
"mapIntNestedSchema": &Schema{
Type: TypeMap,
Elem: &Schema{Type: TypeInt},
},
"mapFloat": &Schema{
Type: TypeMap,
Elem: TypeFloat,
},
"mapBool": &Schema{
Type: TypeMap,
Elem: TypeBool,
},
// Sets
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
"setDeep": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{Type: TypeInt},
"value": &Schema{Type: TypeString},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["index"].(int)
},
},
"setEmpty": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
}
cases := map[string]struct {
Addr []string
Result FieldReadResult
Err bool
}{
"noexist": {
[]string{"boolNOPE"},
FieldReadResult{
Value: nil,
Exists: false,
Computed: false,
},
false,
},
"bool": {
[]string{"bool"},
FieldReadResult{
Value: true,
Exists: true,
Computed: false,
},
false,
},
"float": {
[]string{"float"},
FieldReadResult{
Value: 3.1415,
Exists: true,
Computed: false,
},
false,
},
"int": {
[]string{"int"},
FieldReadResult{
Value: 42,
Exists: true,
Computed: false,
},
false,
},
"string": {
[]string{"string"},
FieldReadResult{
Value: "string",
Exists: true,
Computed: false,
},
false,
},
"list": {
[]string{"list"},
FieldReadResult{
Value: []interface{}{
"foo",
"bar",
},
Exists: true,
Computed: false,
},
false,
},
"listInt": {
[]string{"listInt"},
FieldReadResult{
Value: []interface{}{
21,
42,
},
Exists: true,
Computed: false,
},
false,
},
"map": {
[]string{"map"},
FieldReadResult{
Value: map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
Exists: true,
Computed: false,
},
false,
},
"mapInt": {
[]string{"mapInt"},
FieldReadResult{
Value: map[string]interface{}{
"one": 1,
"two": 2,
},
Exists: true,
Computed: false,
},
false,
},
"mapIntNestedSchema": {
[]string{"mapIntNestedSchema"},
FieldReadResult{
Value: map[string]interface{}{
"one": 1,
"two": 2,
},
Exists: true,
Computed: false,
},
false,
},
"mapFloat": {
[]string{"mapFloat"},
FieldReadResult{
Value: map[string]interface{}{
"oneDotTwo": 1.2,
},
Exists: true,
Computed: false,
},
false,
},
"mapBool": {
[]string{"mapBool"},
FieldReadResult{
Value: map[string]interface{}{
"True": true,
"False": false,
},
Exists: true,
Computed: false,
},
false,
},
"mapelem": {
[]string{"map", "foo"},
FieldReadResult{
Value: "bar",
Exists: true,
Computed: false,
},
false,
},
"set": {
[]string{"set"},
FieldReadResult{
Value: []interface{}{10, 50},
Exists: true,
Computed: false,
},
false,
},
"setDeep": {
[]string{"setDeep"},
FieldReadResult{
Value: []interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
Exists: true,
Computed: false,
},
false,
},
"setEmpty": {
[]string{"setEmpty"},
FieldReadResult{
Value: []interface{}{},
Exists: false,
},
false,
},
}
for name, tc := range cases {
r := f(schema)
out, err := r.ReadField(tc.Addr)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
if s, ok := out.Value.(*Set); ok {
// If it is a set, convert to a list so its more easily checked.
out.Value = s.List()
}
if !reflect.DeepEqual(tc.Result, out) {
t.Fatalf("%s: bad: %#v", name, out)
}
}
}

View File

@ -0,0 +1,8 @@
package schema
// FieldWriters are responsible for writing fields by address into
// a proper typed representation. ResourceData uses this to write new data
// into existing sources.
type FieldWriter interface {
WriteField([]string, interface{}) error
}

View File

@ -0,0 +1,357 @@
package schema
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/mitchellh/mapstructure"
)
// MapFieldWriter writes data into a single map[string]string structure.
type MapFieldWriter struct {
Schema map[string]*Schema
lock sync.Mutex
result map[string]string
}
// Map returns the underlying map that is being written to.
func (w *MapFieldWriter) Map() map[string]string {
w.lock.Lock()
defer w.lock.Unlock()
if w.result == nil {
w.result = make(map[string]string)
}
return w.result
}
func (w *MapFieldWriter) unsafeWriteField(addr string, value string) {
w.lock.Lock()
defer w.lock.Unlock()
if w.result == nil {
w.result = make(map[string]string)
}
w.result[addr] = value
}
// clearTree clears a field and any sub-fields of the given address out of the
// map. This should be used to reset some kind of complex structures (namely
// sets) before writing to make sure that any conflicting data is removed (for
// example, if the set was previously written to the writer's layer).
func (w *MapFieldWriter) clearTree(addr []string) {
prefix := strings.Join(addr, ".") + "."
for k := range w.result {
if strings.HasPrefix(k, prefix) {
delete(w.result, k)
}
}
}
func (w *MapFieldWriter) WriteField(addr []string, value interface{}) error {
w.lock.Lock()
defer w.lock.Unlock()
if w.result == nil {
w.result = make(map[string]string)
}
schemaList := addrToSchema(addr, w.Schema)
if len(schemaList) == 0 {
return fmt.Errorf("Invalid address to set: %#v", addr)
}
// If we're setting anything other than a list root or set root,
// then disallow it.
for _, schema := range schemaList[:len(schemaList)-1] {
if schema.Type == TypeList {
return fmt.Errorf(
"%s: can only set full list",
strings.Join(addr, "."))
}
if schema.Type == TypeMap {
return fmt.Errorf(
"%s: can only set full map",
strings.Join(addr, "."))
}
if schema.Type == TypeSet {
return fmt.Errorf(
"%s: can only set full set",
strings.Join(addr, "."))
}
}
return w.set(addr, value)
}
func (w *MapFieldWriter) set(addr []string, value interface{}) error {
schemaList := addrToSchema(addr, w.Schema)
if len(schemaList) == 0 {
return fmt.Errorf("Invalid address to set: %#v", addr)
}
schema := schemaList[len(schemaList)-1]
switch schema.Type {
case TypeBool, TypeInt, TypeFloat, TypeString:
return w.setPrimitive(addr, value, schema)
case TypeList:
return w.setList(addr, value, schema)
case TypeMap:
return w.setMap(addr, value, schema)
case TypeSet:
return w.setSet(addr, value, schema)
case typeObject:
return w.setObject(addr, value, schema)
default:
panic(fmt.Sprintf("Unknown type: %#v", schema.Type))
}
}
func (w *MapFieldWriter) setList(
addr []string,
v interface{},
schema *Schema) error {
k := strings.Join(addr, ".")
setElement := func(idx string, value interface{}) error {
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
return w.set(append(addrCopy, idx), value)
}
var vs []interface{}
if err := mapstructure.Decode(v, &vs); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
// Wipe the set from the current writer prior to writing if it exists.
// Multiple writes to the same layer is a lot safer for lists than sets due
// to the fact that indexes are always deterministic and the length will
// always be updated with the current length on the last write, but making
// sure we have a clean namespace removes any chance for edge cases to pop up
// and ensures that the last write to the set is the correct value.
w.clearTree(addr)
// Set the entire list.
var err error
for i, elem := range vs {
is := strconv.FormatInt(int64(i), 10)
err = setElement(is, elem)
if err != nil {
break
}
}
if err != nil {
for i, _ := range vs {
is := strconv.FormatInt(int64(i), 10)
setElement(is, nil)
}
return err
}
w.result[k+".#"] = strconv.FormatInt(int64(len(vs)), 10)
return nil
}
func (w *MapFieldWriter) setMap(
addr []string,
value interface{},
schema *Schema) error {
k := strings.Join(addr, ".")
v := reflect.ValueOf(value)
vs := make(map[string]interface{})
if value == nil {
// The empty string here means the map is removed.
w.result[k] = ""
return nil
}
if v.Kind() != reflect.Map {
return fmt.Errorf("%s: must be a map", k)
}
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("%s: keys must strings", k)
}
for _, mk := range v.MapKeys() {
mv := v.MapIndex(mk)
vs[mk.String()] = mv.Interface()
}
// Wipe this address tree. The contents of the map should always reflect the
// last write made to it.
w.clearTree(addr)
// Remove the pure key since we're setting the full map value
delete(w.result, k)
// Set each subkey
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
for subKey, v := range vs {
if err := w.set(append(addrCopy, subKey), v); err != nil {
return err
}
}
// Set the count
w.result[k+".%"] = strconv.Itoa(len(vs))
return nil
}
func (w *MapFieldWriter) setObject(
addr []string,
value interface{},
schema *Schema) error {
// Set the entire object. First decode into a proper structure
var v map[string]interface{}
if err := mapstructure.Decode(value, &v); err != nil {
return fmt.Errorf("%s: %s", strings.Join(addr, "."), err)
}
// Make space for additional elements in the address
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
// Set each element in turn
var err error
for k1, v1 := range v {
if err = w.set(append(addrCopy, k1), v1); err != nil {
break
}
}
if err != nil {
for k1, _ := range v {
w.set(append(addrCopy, k1), nil)
}
}
return err
}
func (w *MapFieldWriter) setPrimitive(
addr []string,
v interface{},
schema *Schema) error {
k := strings.Join(addr, ".")
if v == nil {
// The empty string here means the value is removed.
w.result[k] = ""
return nil
}
var set string
switch schema.Type {
case TypeBool:
var b bool
if err := mapstructure.Decode(v, &b); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
set = strconv.FormatBool(b)
case TypeString:
if err := mapstructure.Decode(v, &set); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
case TypeInt:
var n int
if err := mapstructure.Decode(v, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
set = strconv.FormatInt(int64(n), 10)
case TypeFloat:
var n float64
if err := mapstructure.Decode(v, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
set = strconv.FormatFloat(float64(n), 'G', -1, 64)
default:
return fmt.Errorf("Unknown type: %#v", schema.Type)
}
w.result[k] = set
return nil
}
func (w *MapFieldWriter) setSet(
addr []string,
value interface{},
schema *Schema) error {
addrCopy := make([]string, len(addr), len(addr)+1)
copy(addrCopy, addr)
k := strings.Join(addr, ".")
if value == nil {
w.result[k+".#"] = "0"
return nil
}
// If it is a slice, then we have to turn it into a *Set so that
// we get the proper order back based on the hash code.
if v := reflect.ValueOf(value); v.Kind() == reflect.Slice {
// Build a temp *ResourceData to use for the conversion
tempAddr := addr[len(addr)-1:]
tempSchema := *schema
tempSchema.Type = TypeList
tempSchemaMap := map[string]*Schema{tempAddr[0]: &tempSchema}
tempW := &MapFieldWriter{Schema: tempSchemaMap}
// Set the entire list, this lets us get values out of it
if err := tempW.WriteField(tempAddr, value); err != nil {
return err
}
// Build the set by going over the list items in order and
// hashing them into the set. The reason we go over the list and
// not the `value` directly is because this forces all types
// to become []interface{} (generic) instead of []string, which
// most hash functions are expecting.
s := schema.ZeroValue().(*Set)
tempR := &MapFieldReader{
Map: BasicMapReader(tempW.Map()),
Schema: tempSchemaMap,
}
for i := 0; i < v.Len(); i++ {
is := strconv.FormatInt(int64(i), 10)
result, err := tempR.ReadField(append(tempAddr, is))
if err != nil {
return err
}
if !result.Exists {
panic("set item just set doesn't exist")
}
s.Add(result.Value)
}
value = s
}
// Clear any keys that match the set address first. This is necessary because
// it's always possible and sometimes may be necessary to write to a certain
// writer layer more than once with different set data each time, which will
// lead to different keys being inserted, which can lead to determinism
// problems when the old data isn't wiped first.
w.clearTree(addr)
if value.(*Set) == nil {
w.result[k+".#"] = "0"
return nil
}
for code, elem := range value.(*Set).m {
if err := w.set(append(addrCopy, code), elem); err != nil {
return err
}
}
w.result[k+".#"] = strconv.Itoa(value.(*Set).Len())
return nil
}

View File

@ -0,0 +1,547 @@
package schema
import (
"reflect"
"testing"
)
func TestMapFieldWriter_impl(t *testing.T) {
var _ FieldWriter = new(MapFieldWriter)
}
func TestMapFieldWriter(t *testing.T) {
schema := map[string]*Schema{
"bool": &Schema{Type: TypeBool},
"int": &Schema{Type: TypeInt},
"string": &Schema{Type: TypeString},
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeString},
},
"listInt": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
"listResource": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"value": &Schema{
Type: TypeInt,
Optional: true,
},
},
},
},
"map": &Schema{Type: TypeMap},
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
"setDeep": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{Type: TypeInt},
"value": &Schema{Type: TypeString},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["index"].(int)
},
},
}
cases := map[string]struct {
Addr []string
Value interface{}
Err bool
Out map[string]string
}{
"noexist": {
[]string{"noexist"},
42,
true,
map[string]string{},
},
"bool": {
[]string{"bool"},
false,
false,
map[string]string{
"bool": "false",
},
},
"int": {
[]string{"int"},
42,
false,
map[string]string{
"int": "42",
},
},
"string": {
[]string{"string"},
"42",
false,
map[string]string{
"string": "42",
},
},
"string nil": {
[]string{"string"},
nil,
false,
map[string]string{
"string": "",
},
},
"list of resources": {
[]string{"listResource"},
[]interface{}{
map[string]interface{}{
"value": 80,
},
},
false,
map[string]string{
"listResource.#": "1",
"listResource.0.value": "80",
},
},
"list of resources empty": {
[]string{"listResource"},
[]interface{}{},
false,
map[string]string{
"listResource.#": "0",
},
},
"list of resources nil": {
[]string{"listResource"},
nil,
false,
map[string]string{
"listResource.#": "0",
},
},
"list of strings": {
[]string{"list"},
[]interface{}{"foo", "bar"},
false,
map[string]string{
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
},
},
"list element": {
[]string{"list", "0"},
"string",
true,
map[string]string{},
},
"map": {
[]string{"map"},
map[string]interface{}{"foo": "bar"},
false,
map[string]string{
"map.%": "1",
"map.foo": "bar",
},
},
"map delete": {
[]string{"map"},
nil,
false,
map[string]string{
"map": "",
},
},
"map element": {
[]string{"map", "foo"},
"bar",
true,
map[string]string{},
},
"set": {
[]string{"set"},
[]interface{}{1, 2, 5},
false,
map[string]string{
"set.#": "3",
"set.1": "1",
"set.2": "2",
"set.5": "5",
},
},
"set nil": {
[]string{"set"},
nil,
false,
map[string]string{
"set.#": "0",
},
},
"set typed nil": {
[]string{"set"},
func() *Set { return nil }(),
false,
map[string]string{
"set.#": "0",
},
},
"set resource": {
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
false,
map[string]string{
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
},
},
"set element": {
[]string{"set", "5"},
5,
true,
map[string]string{},
},
"full object": {
nil,
map[string]interface{}{
"string": "foo",
"list": []interface{}{"foo", "bar"},
},
false,
map[string]string{
"string": "foo",
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
},
},
}
for name, tc := range cases {
w := &MapFieldWriter{Schema: schema}
err := w.WriteField(tc.Addr, tc.Value)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%s: bad: %#v", name, actual)
}
}
}
func TestMapFieldWriterCleanSet(t *testing.T) {
schema := map[string]*Schema{
"setDeep": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{Type: TypeInt},
"value": &Schema{Type: TypeString},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["index"].(int)
},
},
}
values := []struct {
Addr []string
Value interface{}
Out map[string]string
}{
{
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
map[string]string{
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
},
},
{
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 20,
"value": "baz",
},
map[string]interface{}{
"index": 60,
"value": "qux",
},
},
map[string]string{
"setDeep.#": "2",
"setDeep.20.index": "20",
"setDeep.20.value": "baz",
"setDeep.60.index": "60",
"setDeep.60.value": "qux",
},
},
{
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 30,
"value": "one",
},
map[string]interface{}{
"index": 70,
"value": "two",
},
},
map[string]string{
"setDeep.#": "2",
"setDeep.30.index": "30",
"setDeep.30.value": "one",
"setDeep.70.index": "70",
"setDeep.70.value": "two",
},
},
}
w := &MapFieldWriter{Schema: schema}
for n, tc := range values {
err := w.WriteField(tc.Addr, tc.Value)
if err != nil {
t.Fatalf("%d: err: %s", n, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%d: bad: %#v", n, actual)
}
}
}
func TestMapFieldWriterCleanList(t *testing.T) {
schema := map[string]*Schema{
"listDeep": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"thing1": &Schema{Type: TypeString},
"thing2": &Schema{Type: TypeString},
},
},
},
}
values := []struct {
Addr []string
Value interface{}
Out map[string]string
}{
{
// Base list
[]string{"listDeep"},
[]interface{}{
map[string]interface{}{
"thing1": "a",
"thing2": "b",
},
map[string]interface{}{
"thing1": "c",
"thing2": "d",
},
map[string]interface{}{
"thing1": "e",
"thing2": "f",
},
map[string]interface{}{
"thing1": "g",
"thing2": "h",
},
},
map[string]string{
"listDeep.#": "4",
"listDeep.0.thing1": "a",
"listDeep.0.thing2": "b",
"listDeep.1.thing1": "c",
"listDeep.1.thing2": "d",
"listDeep.2.thing1": "e",
"listDeep.2.thing2": "f",
"listDeep.3.thing1": "g",
"listDeep.3.thing2": "h",
},
},
{
// Remove an element
[]string{"listDeep"},
[]interface{}{
map[string]interface{}{
"thing1": "a",
"thing2": "b",
},
map[string]interface{}{
"thing1": "c",
"thing2": "d",
},
map[string]interface{}{
"thing1": "e",
"thing2": "f",
},
},
map[string]string{
"listDeep.#": "3",
"listDeep.0.thing1": "a",
"listDeep.0.thing2": "b",
"listDeep.1.thing1": "c",
"listDeep.1.thing2": "d",
"listDeep.2.thing1": "e",
"listDeep.2.thing2": "f",
},
},
{
// Rewrite with missing keys. This should normally not be necessary, as
// hopefully the writers are writing zero values as necessary, but for
// brevity we want to make sure that what exists in the writer is exactly
// what the last write looked like coming from the provider.
[]string{"listDeep"},
[]interface{}{
map[string]interface{}{
"thing1": "a",
},
map[string]interface{}{
"thing1": "c",
},
map[string]interface{}{
"thing1": "e",
},
},
map[string]string{
"listDeep.#": "3",
"listDeep.0.thing1": "a",
"listDeep.1.thing1": "c",
"listDeep.2.thing1": "e",
},
},
}
w := &MapFieldWriter{Schema: schema}
for n, tc := range values {
err := w.WriteField(tc.Addr, tc.Value)
if err != nil {
t.Fatalf("%d: err: %s", n, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%d: bad: %#v", n, actual)
}
}
}
func TestMapFieldWriterCleanMap(t *testing.T) {
schema := map[string]*Schema{
"map": &Schema{
Type: TypeMap,
},
}
values := []struct {
Value interface{}
Out map[string]string
}{
{
// Base map
map[string]interface{}{
"thing1": "a",
"thing2": "b",
"thing3": "c",
"thing4": "d",
},
map[string]string{
"map.%": "4",
"map.thing1": "a",
"map.thing2": "b",
"map.thing3": "c",
"map.thing4": "d",
},
},
{
// Base map
map[string]interface{}{
"thing1": "a",
"thing2": "b",
"thing4": "d",
},
map[string]string{
"map.%": "3",
"map.thing1": "a",
"map.thing2": "b",
"map.thing4": "d",
},
},
}
w := &MapFieldWriter{Schema: schema}
for n, tc := range values {
err := w.WriteField([]string{"map"}, tc.Value)
if err != nil {
t.Fatalf("%d: err: %s", n, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%d: bad: %#v", n, actual)
}
}
}

View File

@ -0,0 +1,46 @@
// Code generated by "stringer -type=getSource resource_data_get_source.go"; DO NOT EDIT.
package schema
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[getSourceState-1]
_ = x[getSourceConfig-2]
_ = x[getSourceDiff-4]
_ = x[getSourceSet-8]
_ = x[getSourceExact-16]
_ = x[getSourceLevelMask-15]
}
const (
_getSource_name_0 = "getSourceStategetSourceConfig"
_getSource_name_1 = "getSourceDiff"
_getSource_name_2 = "getSourceSet"
_getSource_name_3 = "getSourceLevelMaskgetSourceExact"
)
var (
_getSource_index_0 = [...]uint8{0, 14, 29}
_getSource_index_3 = [...]uint8{0, 18, 32}
)
func (i getSource) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _getSource_name_0[_getSource_index_0[i]:_getSource_index_0[i+1]]
case i == 4:
return _getSource_name_1
case i == 8:
return _getSource_name_2
case 15 <= i && i <= 16:
i -= 15
return _getSource_name_3[_getSource_index_3[i]:_getSource_index_3[i+1]]
default:
return "getSource(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View File

@ -0,0 +1,477 @@
package schema
import (
"context"
"errors"
"fmt"
"sort"
"sync"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
var ReservedProviderFields = []string{
"alias",
"version",
}
// Provider represents a resource provider in Terraform, and properly
// implements all of the ResourceProvider API.
//
// By defining a schema for the configuration of the provider, the
// map of supporting resources, and a configuration function, the schema
// framework takes over and handles all the provider operations for you.
//
// After defining the provider structure, it is unlikely that you'll require any
// of the methods on Provider itself.
type Provider struct {
// Schema is the schema for the configuration of this provider. If this
// provider has no configuration, this can be omitted.
//
// The keys of this map are the configuration keys, and the value is
// the schema describing the value of the configuration.
Schema map[string]*Schema
// ResourcesMap is the list of available resources that this provider
// can manage, along with their Resource structure defining their
// own schemas and CRUD operations.
//
// Provider automatically handles routing operations such as Apply,
// Diff, etc. to the proper resource.
ResourcesMap map[string]*Resource
// DataSourcesMap is the collection of available data sources that
// this provider implements, with a Resource instance defining
// the schema and Read operation of each.
//
// Resource instances for data sources must have a Read function
// and must *not* implement Create, Update or Delete.
DataSourcesMap map[string]*Resource
// ProviderMetaSchema is the schema for the configuration of the meta
// information for this provider. If this provider has no meta info,
// this can be omitted. This functionality is currently experimental
// and subject to change or break without warning; it should only be
// used by providers that are collaborating on its use with the
// Terraform team.
ProviderMetaSchema map[string]*Schema
// ConfigureFunc is a function for configuring the provider. If the
// provider doesn't need to be configured, this can be omitted.
//
// See the ConfigureFunc documentation for more information.
ConfigureFunc ConfigureFunc
// MetaReset is called by TestReset to reset any state stored in the meta
// interface. This is especially important if the StopContext is stored by
// the provider.
MetaReset func() error
meta interface{}
// a mutex is required because TestReset can directly replace the stopCtx
stopMu sync.Mutex
stopCtx context.Context
stopCtxCancel context.CancelFunc
stopOnce sync.Once
TerraformVersion string
}
// ConfigureFunc is the function used to configure a Provider.
//
// The interface{} value returned by this function is stored and passed into
// the subsequent resources as the meta parameter. This return value is
// usually used to pass along a configured API client, a configuration
// structure, etc.
type ConfigureFunc func(*ResourceData) (interface{}, error)
// InternalValidate should be called to validate the structure
// of the provider.
//
// This should be called in a unit test for any provider to verify
// before release that a provider is properly configured for use with
// this library.
func (p *Provider) InternalValidate() error {
if p == nil {
return errors.New("provider is nil")
}
var validationErrors error
sm := schemaMap(p.Schema)
if err := sm.InternalValidate(sm); err != nil {
validationErrors = multierror.Append(validationErrors, err)
}
// Provider-specific checks
for k, _ := range sm {
if isReservedProviderFieldName(k) {
return fmt.Errorf("%s is a reserved field name for a provider", k)
}
}
for k, r := range p.ResourcesMap {
if err := r.InternalValidate(nil, true); err != nil {
validationErrors = multierror.Append(validationErrors, fmt.Errorf("resource %s: %s", k, err))
}
}
for k, r := range p.DataSourcesMap {
if err := r.InternalValidate(nil, false); err != nil {
validationErrors = multierror.Append(validationErrors, fmt.Errorf("data source %s: %s", k, err))
}
}
return validationErrors
}
func isReservedProviderFieldName(name string) bool {
for _, reservedName := range ReservedProviderFields {
if name == reservedName {
return true
}
}
return false
}
// Meta returns the metadata associated with this provider that was
// returned by the Configure call. It will be nil until Configure is called.
func (p *Provider) Meta() interface{} {
return p.meta
}
// SetMeta can be used to forcefully set the Meta object of the provider.
// Note that if Configure is called the return value will override anything
// set here.
func (p *Provider) SetMeta(v interface{}) {
p.meta = v
}
// Stopped reports whether the provider has been stopped or not.
func (p *Provider) Stopped() bool {
ctx := p.StopContext()
select {
case <-ctx.Done():
return true
default:
return false
}
}
// StopCh returns a channel that is closed once the provider is stopped.
func (p *Provider) StopContext() context.Context {
p.stopOnce.Do(p.stopInit)
p.stopMu.Lock()
defer p.stopMu.Unlock()
return p.stopCtx
}
func (p *Provider) stopInit() {
p.stopMu.Lock()
defer p.stopMu.Unlock()
p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
}
// Stop implementation of terraform.ResourceProvider interface.
func (p *Provider) Stop() error {
p.stopOnce.Do(p.stopInit)
p.stopMu.Lock()
defer p.stopMu.Unlock()
p.stopCtxCancel()
return nil
}
// TestReset resets any state stored in the Provider, and will call TestReset
// on Meta if it implements the TestProvider interface.
// This may be used to reset the schema.Provider at the start of a test, and is
// automatically called by resource.Test.
func (p *Provider) TestReset() error {
p.stopInit()
if p.MetaReset != nil {
return p.MetaReset()
}
return nil
}
// GetSchema implementation of terraform.ResourceProvider interface
func (p *Provider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) {
resourceTypes := map[string]*configschema.Block{}
dataSources := map[string]*configschema.Block{}
for _, name := range req.ResourceTypes {
if r, exists := p.ResourcesMap[name]; exists {
resourceTypes[name] = r.CoreConfigSchema()
}
}
for _, name := range req.DataSources {
if r, exists := p.DataSourcesMap[name]; exists {
dataSources[name] = r.CoreConfigSchema()
}
}
return &terraform.ProviderSchema{
Provider: schemaMap(p.Schema).CoreConfigSchema(),
ResourceTypes: resourceTypes,
DataSources: dataSources,
}, nil
}
// Input implementation of terraform.ResourceProvider interface.
func (p *Provider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return schemaMap(p.Schema).Input(input, c)
}
// Validate implementation of terraform.ResourceProvider interface.
func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
if err := p.InternalValidate(); err != nil {
return nil, []error{fmt.Errorf(
"Internal validation of the provider failed! This is always a bug\n"+
"with the provider itself, and not a user issue. Please report\n"+
"this bug:\n\n%s", err)}
}
return schemaMap(p.Schema).Validate(c)
}
// ValidateResource implementation of terraform.ResourceProvider interface.
func (p *Provider) ValidateResource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
r, ok := p.ResourcesMap[t]
if !ok {
return nil, []error{fmt.Errorf(
"Provider doesn't support resource: %s", t)}
}
return r.Validate(c)
}
// Configure implementation of terraform.ResourceProvider interface.
func (p *Provider) Configure(c *terraform.ResourceConfig) error {
// No configuration
if p.ConfigureFunc == nil {
return nil
}
sm := schemaMap(p.Schema)
// Get a ResourceData for this configuration. To do this, we actually
// generate an intermediary "diff" although that is never exposed.
diff, err := sm.Diff(nil, c, nil, p.meta, true)
if err != nil {
return err
}
data, err := sm.Data(nil, diff)
if err != nil {
return err
}
meta, err := p.ConfigureFunc(data)
if err != nil {
return err
}
p.meta = meta
return nil
}
// Apply implementation of terraform.ResourceProvider interface.
func (p *Provider) Apply(
info *terraform.InstanceInfo,
s *terraform.InstanceState,
d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
r, ok := p.ResourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
}
return r.Apply(s, d, p.meta)
}
// Diff implementation of terraform.ResourceProvider interface.
func (p *Provider) Diff(
info *terraform.InstanceInfo,
s *terraform.InstanceState,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
r, ok := p.ResourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
}
return r.Diff(s, c, p.meta)
}
// SimpleDiff is used by the new protocol wrappers to get a diff that doesn't
// attempt to calculate ignore_changes.
func (p *Provider) SimpleDiff(
info *terraform.InstanceInfo,
s *terraform.InstanceState,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
r, ok := p.ResourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
}
return r.simpleDiff(s, c, p.meta)
}
// Refresh implementation of terraform.ResourceProvider interface.
func (p *Provider) Refresh(
info *terraform.InstanceInfo,
s *terraform.InstanceState) (*terraform.InstanceState, error) {
r, ok := p.ResourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
}
return r.Refresh(s, p.meta)
}
// Resources implementation of terraform.ResourceProvider interface.
func (p *Provider) Resources() []terraform.ResourceType {
keys := make([]string, 0, len(p.ResourcesMap))
for k := range p.ResourcesMap {
keys = append(keys, k)
}
sort.Strings(keys)
result := make([]terraform.ResourceType, 0, len(keys))
for _, k := range keys {
resource := p.ResourcesMap[k]
// This isn't really possible (it'd fail InternalValidate), but
// we do it anyways to avoid a panic.
if resource == nil {
resource = &Resource{}
}
result = append(result, terraform.ResourceType{
Name: k,
Importable: resource.Importer != nil,
// Indicates that a provider is compiled against a new enough
// version of core to support the GetSchema method.
SchemaAvailable: true,
})
}
return result
}
func (p *Provider) ImportState(
info *terraform.InstanceInfo,
id string) ([]*terraform.InstanceState, error) {
// Find the resource
r, ok := p.ResourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
}
// If it doesn't support import, error
if r.Importer == nil {
return nil, fmt.Errorf("resource %s doesn't support import", info.Type)
}
// Create the data
data := r.Data(nil)
data.SetId(id)
data.SetType(info.Type)
// Call the import function
results := []*ResourceData{data}
if r.Importer.State != nil {
var err error
results, err = r.Importer.State(data, p.meta)
if err != nil {
return nil, err
}
}
// Convert the results to InstanceState values and return it
states := make([]*terraform.InstanceState, len(results))
for i, r := range results {
states[i] = r.State()
}
// Verify that all are non-nil. If there are any nil the error
// isn't obvious so we circumvent that with a friendlier error.
for _, s := range states {
if s == nil {
return nil, fmt.Errorf(
"nil entry in ImportState results. This is always a bug with\n" +
"the resource that is being imported. Please report this as\n" +
"a bug to Terraform.")
}
}
return states, nil
}
// ValidateDataSource implementation of terraform.ResourceProvider interface.
func (p *Provider) ValidateDataSource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
r, ok := p.DataSourcesMap[t]
if !ok {
return nil, []error{fmt.Errorf(
"Provider doesn't support data source: %s", t)}
}
return r.Validate(c)
}
// ReadDataDiff implementation of terraform.ResourceProvider interface.
func (p *Provider) ReadDataDiff(
info *terraform.InstanceInfo,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
r, ok := p.DataSourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown data source: %s", info.Type)
}
return r.Diff(nil, c, p.meta)
}
// RefreshData implementation of terraform.ResourceProvider interface.
func (p *Provider) ReadDataApply(
info *terraform.InstanceInfo,
d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
r, ok := p.DataSourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown data source: %s", info.Type)
}
return r.ReadDataApply(d, p.meta)
}
// DataSources implementation of terraform.ResourceProvider interface.
func (p *Provider) DataSources() []terraform.DataSource {
keys := make([]string, 0, len(p.DataSourcesMap))
for k, _ := range p.DataSourcesMap {
keys = append(keys, k)
}
sort.Strings(keys)
result := make([]terraform.DataSource, 0, len(keys))
for _, k := range keys {
result = append(result, terraform.DataSource{
Name: k,
// Indicates that a provider is compiled against a new enough
// version of core to support the GetSchema method.
SchemaAvailable: true,
})
}
return result
}

View File

@ -0,0 +1,620 @@
package schema
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(Provider)
}
func TestProviderGetSchema(t *testing.T) {
// This functionality is already broadly tested in core_schema_test.go,
// so this is just to ensure that the call passes through correctly.
p := &Provider{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Required: true,
},
},
ResourcesMap: map[string]*Resource{
"foo": &Resource{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Required: true,
},
},
},
},
DataSourcesMap: map[string]*Resource{
"baz": &Resource{
Schema: map[string]*Schema{
"bur": {
Type: TypeString,
Required: true,
},
},
},
},
}
want := &terraform.ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": &configschema.Attribute{
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
},
ResourceTypes: map[string]*configschema.Block{
"foo": testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": &configschema.Attribute{
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
}),
},
DataSources: map[string]*configschema.Block{
"baz": testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bur": &configschema.Attribute{
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{},
}),
},
}
got, err := p.GetSchema(&terraform.ProviderSchemaRequest{
ResourceTypes: []string{"foo", "bar"},
DataSources: []string{"baz", "bar"},
})
if err != nil {
t.Fatalf("unexpected error %s", err)
}
if !cmp.Equal(got, want, equateEmpty, typeComparer) {
t.Error("wrong result:\n", cmp.Diff(got, want, equateEmpty, typeComparer))
}
}
func TestProviderConfigure(t *testing.T) {
cases := []struct {
P *Provider
Config map[string]interface{}
Err bool
}{
{
P: &Provider{},
Config: nil,
Err: false,
},
{
P: &Provider{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ConfigureFunc: func(d *ResourceData) (interface{}, error) {
if d.Get("foo").(int) == 42 {
return nil, nil
}
return nil, fmt.Errorf("nope")
},
},
Config: map[string]interface{}{
"foo": 42,
},
Err: false,
},
{
P: &Provider{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ConfigureFunc: func(d *ResourceData) (interface{}, error) {
if d.Get("foo").(int) == 42 {
return nil, nil
}
return nil, fmt.Errorf("nope")
},
},
Config: map[string]interface{}{
"foo": 52,
},
Err: true,
},
}
for i, tc := range cases {
c := terraform.NewResourceConfigRaw(tc.Config)
err := tc.P.Configure(c)
if err != nil != tc.Err {
t.Fatalf("%d: %s", i, err)
}
}
}
func TestProviderResources(t *testing.T) {
cases := []struct {
P *Provider
Result []terraform.ResourceType
}{
{
P: &Provider{},
Result: []terraform.ResourceType{},
},
{
P: &Provider{
ResourcesMap: map[string]*Resource{
"foo": nil,
"bar": nil,
},
},
Result: []terraform.ResourceType{
terraform.ResourceType{Name: "bar", SchemaAvailable: true},
terraform.ResourceType{Name: "foo", SchemaAvailable: true},
},
},
{
P: &Provider{
ResourcesMap: map[string]*Resource{
"foo": nil,
"bar": &Resource{Importer: &ResourceImporter{}},
"baz": nil,
},
},
Result: []terraform.ResourceType{
terraform.ResourceType{Name: "bar", Importable: true, SchemaAvailable: true},
terraform.ResourceType{Name: "baz", SchemaAvailable: true},
terraform.ResourceType{Name: "foo", SchemaAvailable: true},
},
},
}
for i, tc := range cases {
actual := tc.P.Resources()
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d: %#v", i, actual)
}
}
}
func TestProviderDataSources(t *testing.T) {
cases := []struct {
P *Provider
Result []terraform.DataSource
}{
{
P: &Provider{},
Result: []terraform.DataSource{},
},
{
P: &Provider{
DataSourcesMap: map[string]*Resource{
"foo": nil,
"bar": nil,
},
},
Result: []terraform.DataSource{
terraform.DataSource{Name: "bar", SchemaAvailable: true},
terraform.DataSource{Name: "foo", SchemaAvailable: true},
},
},
}
for i, tc := range cases {
actual := tc.P.DataSources()
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d: got %#v; want %#v", i, actual, tc.Result)
}
}
}
func TestProviderValidate(t *testing.T) {
cases := []struct {
P *Provider
Config map[string]interface{}
Err bool
}{
{
P: &Provider{
Schema: map[string]*Schema{
"foo": &Schema{},
},
},
Config: nil,
Err: true,
},
}
for i, tc := range cases {
c := terraform.NewResourceConfigRaw(tc.Config)
_, es := tc.P.Validate(c)
if len(es) > 0 != tc.Err {
t.Fatalf("%d: %#v", i, es)
}
}
}
func TestProviderDiff_legacyTimeoutType(t *testing.T) {
p := &Provider{
ResourcesMap: map[string]*Resource{
"blah": &Resource{
Schema: map[string]*Schema{
"foo": {
Type: TypeInt,
Optional: true,
},
},
Timeouts: &ResourceTimeout{
Create: DefaultTimeout(10 * time.Minute),
},
},
},
}
invalidCfg := map[string]interface{}{
"foo": 42,
"timeouts": []interface{}{
map[string]interface{}{
"create": "40m",
},
},
}
ic := terraform.NewResourceConfigRaw(invalidCfg)
_, err := p.Diff(
&terraform.InstanceInfo{
Type: "blah",
},
nil,
ic,
)
if err != nil {
t.Fatal(err)
}
}
func TestProviderDiff_timeoutInvalidValue(t *testing.T) {
p := &Provider{
ResourcesMap: map[string]*Resource{
"blah": &Resource{
Schema: map[string]*Schema{
"foo": {
Type: TypeInt,
Optional: true,
},
},
Timeouts: &ResourceTimeout{
Create: DefaultTimeout(10 * time.Minute),
},
},
},
}
invalidCfg := map[string]interface{}{
"foo": 42,
"timeouts": map[string]interface{}{
"create": "invalid",
},
}
ic := terraform.NewResourceConfigRaw(invalidCfg)
_, err := p.Diff(
&terraform.InstanceInfo{
Type: "blah",
},
nil,
ic,
)
if err == nil {
t.Fatal("Expected provider.Diff to fail with invalid timeout value")
}
expectedErrMsg := `time: invalid duration "invalid"`
if !strings.Contains(err.Error(), expectedErrMsg) {
t.Fatalf("Unexpected error message: %q\nExpected message to contain %q",
err.Error(),
expectedErrMsg)
}
}
func TestProviderValidateResource(t *testing.T) {
cases := []struct {
P *Provider
Type string
Config map[string]interface{}
Err bool
}{
{
P: &Provider{},
Type: "foo",
Config: nil,
Err: true,
},
{
P: &Provider{
ResourcesMap: map[string]*Resource{
"foo": &Resource{},
},
},
Type: "foo",
Config: nil,
Err: false,
},
}
for i, tc := range cases {
c := terraform.NewResourceConfigRaw(tc.Config)
_, es := tc.P.ValidateResource(tc.Type, c)
if len(es) > 0 != tc.Err {
t.Fatalf("%d: %#v", i, es)
}
}
}
func TestProviderImportState_default(t *testing.T) {
p := &Provider{
ResourcesMap: map[string]*Resource{
"foo": &Resource{
Importer: &ResourceImporter{},
},
},
}
states, err := p.ImportState(&terraform.InstanceInfo{
Type: "foo",
}, "bar")
if err != nil {
t.Fatalf("err: %s", err)
}
if len(states) != 1 {
t.Fatalf("bad: %#v", states)
}
if states[0].ID != "bar" {
t.Fatalf("bad: %#v", states)
}
}
func TestProviderImportState_setsId(t *testing.T) {
var val string
stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) {
val = d.Id()
return []*ResourceData{d}, nil
}
p := &Provider{
ResourcesMap: map[string]*Resource{
"foo": &Resource{
Importer: &ResourceImporter{
State: stateFunc,
},
},
},
}
_, err := p.ImportState(&terraform.InstanceInfo{
Type: "foo",
}, "bar")
if err != nil {
t.Fatalf("err: %s", err)
}
if val != "bar" {
t.Fatal("should set id")
}
}
func TestProviderImportState_setsType(t *testing.T) {
var tVal string
stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) {
d.SetId("foo")
tVal = d.State().Ephemeral.Type
return []*ResourceData{d}, nil
}
p := &Provider{
ResourcesMap: map[string]*Resource{
"foo": &Resource{
Importer: &ResourceImporter{
State: stateFunc,
},
},
},
}
_, err := p.ImportState(&terraform.InstanceInfo{
Type: "foo",
}, "bar")
if err != nil {
t.Fatalf("err: %s", err)
}
if tVal != "foo" {
t.Fatal("should set type")
}
}
func TestProviderMeta(t *testing.T) {
p := new(Provider)
if v := p.Meta(); v != nil {
t.Fatalf("bad: %#v", v)
}
expected := 42
p.SetMeta(42)
if v := p.Meta(); !reflect.DeepEqual(v, expected) {
t.Fatalf("bad: %#v", v)
}
}
func TestProviderStop(t *testing.T) {
var p Provider
if p.Stopped() {
t.Fatal("should not be stopped")
}
// Verify stopch blocks
ch := p.StopContext().Done()
select {
case <-ch:
t.Fatal("should not be stopped")
case <-time.After(10 * time.Millisecond):
}
// Stop it
if err := p.Stop(); err != nil {
t.Fatalf("err: %s", err)
}
// Verify
if !p.Stopped() {
t.Fatal("should be stopped")
}
select {
case <-ch:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
func TestProviderStop_stopFirst(t *testing.T) {
var p Provider
// Stop it
if err := p.Stop(); err != nil {
t.Fatalf("err: %s", err)
}
// Verify
if !p.Stopped() {
t.Fatal("should be stopped")
}
select {
case <-p.StopContext().Done():
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
func TestProviderReset(t *testing.T) {
var p Provider
stopCtx := p.StopContext()
p.MetaReset = func() error {
stopCtx = p.StopContext()
return nil
}
// cancel the current context
p.Stop()
if err := p.TestReset(); err != nil {
t.Fatal(err)
}
// the first context should have been replaced
if err := stopCtx.Err(); err != nil {
t.Fatal(err)
}
// we should not get a canceled context here either
if err := p.StopContext().Err(); err != nil {
t.Fatal(err)
}
}
func TestProvider_InternalValidate(t *testing.T) {
cases := []struct {
P *Provider
ExpectedErr error
}{
{
P: &Provider{
Schema: map[string]*Schema{
"foo": {
Type: TypeBool,
Optional: true,
},
},
},
ExpectedErr: nil,
},
{ // Reserved resource fields should be allowed in provider block
P: &Provider{
Schema: map[string]*Schema{
"provisioner": {
Type: TypeString,
Optional: true,
},
"count": {
Type: TypeInt,
Optional: true,
},
},
},
ExpectedErr: nil,
},
{ // Reserved provider fields should not be allowed
P: &Provider{
Schema: map[string]*Schema{
"alias": {
Type: TypeString,
Optional: true,
},
},
},
ExpectedErr: fmt.Errorf("%s is a reserved field name for a provider", "alias"),
},
}
for i, tc := range cases {
err := tc.P.InternalValidate()
if tc.ExpectedErr == nil {
if err != nil {
t.Fatalf("%d: Error returned (expected no error): %s", i, err)
}
continue
}
if tc.ExpectedErr != nil && err == nil {
t.Fatalf("%d: Expected error (%s), but no error returned", i, tc.ExpectedErr)
}
if err.Error() != tc.ExpectedErr.Error() {
t.Fatalf("%d: Errors don't match. Expected: %#v Given: %#v", i, tc.ExpectedErr, err)
}
}
}

View File

@ -0,0 +1,205 @@
package schema
import (
"context"
"errors"
"fmt"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
// Provisioner represents a resource provisioner in Terraform and properly
// implements all of the ResourceProvisioner API.
//
// This higher level structure makes it much easier to implement a new or
// custom provisioner for Terraform.
//
// The function callbacks for this structure are all passed a context object.
// This context object has a number of pre-defined values that can be accessed
// via the global functions defined in context.go.
type Provisioner struct {
// ConnSchema is the schema for the connection settings for this
// provisioner.
//
// The keys of this map are the configuration keys, and the value is
// the schema describing the value of the configuration.
//
// NOTE: The value of connection keys can only be strings for now.
ConnSchema map[string]*Schema
// Schema is the schema for the usage of this provisioner.
//
// The keys of this map are the configuration keys, and the value is
// the schema describing the value of the configuration.
Schema map[string]*Schema
// ApplyFunc is the function for executing the provisioner. This is required.
// It is given a context. See the Provisioner struct docs for more
// information.
ApplyFunc func(ctx context.Context) error
// ValidateFunc is a function for extended validation. This is optional
// and should be used when individual field validation is not enough.
ValidateFunc func(*terraform.ResourceConfig) ([]string, []error)
stopCtx context.Context
stopCtxCancel context.CancelFunc
stopOnce sync.Once
}
// Keys that can be used to access data in the context parameters for
// Provisioners.
var (
connDataInvalid = contextKey("data invalid")
// This returns a *ResourceData for the connection information.
// Guaranteed to never be nil.
ProvConnDataKey = contextKey("provider conn data")
// This returns a *ResourceData for the config information.
// Guaranteed to never be nil.
ProvConfigDataKey = contextKey("provider config data")
// This returns a terraform.UIOutput. Guaranteed to never be nil.
ProvOutputKey = contextKey("provider output")
// This returns the raw InstanceState passed to Apply. Guaranteed to
// be set, but may be nil.
ProvRawStateKey = contextKey("provider raw state")
)
// InternalValidate should be called to validate the structure
// of the provisioner.
//
// This should be called in a unit test to verify before release that this
// structure is properly configured for use.
func (p *Provisioner) InternalValidate() error {
if p == nil {
return errors.New("provisioner is nil")
}
var validationErrors error
{
sm := schemaMap(p.ConnSchema)
if err := sm.InternalValidate(sm); err != nil {
validationErrors = multierror.Append(validationErrors, err)
}
}
{
sm := schemaMap(p.Schema)
if err := sm.InternalValidate(sm); err != nil {
validationErrors = multierror.Append(validationErrors, err)
}
}
if p.ApplyFunc == nil {
validationErrors = multierror.Append(validationErrors, fmt.Errorf(
"ApplyFunc must not be nil"))
}
return validationErrors
}
// StopContext returns a context that checks whether a provisioner is stopped.
func (p *Provisioner) StopContext() context.Context {
p.stopOnce.Do(p.stopInit)
return p.stopCtx
}
func (p *Provisioner) stopInit() {
p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
}
// Stop implementation of terraform.ResourceProvisioner interface.
func (p *Provisioner) Stop() error {
p.stopOnce.Do(p.stopInit)
p.stopCtxCancel()
return nil
}
// GetConfigSchema implementation of terraform.ResourceProvisioner interface.
func (p *Provisioner) GetConfigSchema() (*configschema.Block, error) {
return schemaMap(p.Schema).CoreConfigSchema(), nil
}
// Apply implementation of terraform.ResourceProvisioner interface.
func (p *Provisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
var connData, configData *ResourceData
{
// We first need to turn the connection information into a
// terraform.ResourceConfig so that we can use that type to more
// easily build a ResourceData structure. We do this by simply treating
// the conn info as configuration input.
raw := make(map[string]interface{})
if s != nil {
for k, v := range s.Ephemeral.ConnInfo {
raw[k] = v
}
}
c := terraform.NewResourceConfigRaw(raw)
sm := schemaMap(p.ConnSchema)
diff, err := sm.Diff(nil, c, nil, nil, true)
if err != nil {
return err
}
connData, err = sm.Data(nil, diff)
if err != nil {
return err
}
}
{
// Build the configuration data. Doing this requires making a "diff"
// even though that's never used. We use that just to get the correct types.
configMap := schemaMap(p.Schema)
diff, err := configMap.Diff(nil, c, nil, nil, true)
if err != nil {
return err
}
configData, err = configMap.Data(nil, diff)
if err != nil {
return err
}
}
// Build the context and call the function
ctx := p.StopContext()
ctx = context.WithValue(ctx, ProvConnDataKey, connData)
ctx = context.WithValue(ctx, ProvConfigDataKey, configData)
ctx = context.WithValue(ctx, ProvOutputKey, o)
ctx = context.WithValue(ctx, ProvRawStateKey, s)
return p.ApplyFunc(ctx)
}
// Validate implements the terraform.ResourceProvisioner interface.
func (p *Provisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
if err := p.InternalValidate(); err != nil {
return nil, []error{fmt.Errorf(
"Internal validation of the provisioner failed! This is always a bug\n"+
"with the provisioner itself, and not a user issue. Please report\n"+
"this bug:\n\n%s", err)}
}
if p.Schema != nil {
w, e := schemaMap(p.Schema).Validate(c)
ws = append(ws, w...)
es = append(es, e...)
}
if p.ValidateFunc != nil {
w, e := p.ValidateFunc(c)
ws = append(ws, w...)
es = append(es, e...)
}
return ws, es
}

View File

@ -0,0 +1,334 @@
package schema
import (
"context"
"fmt"
"reflect"
"testing"
"time"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func TestProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(Provisioner)
}
func noopApply(ctx context.Context) error {
return nil
}
func TestProvisionerValidate(t *testing.T) {
cases := []struct {
Name string
P *Provisioner
Config map[string]interface{}
Err bool
Warns []string
}{
{
Name: "No ApplyFunc",
P: &Provisioner{},
Config: nil,
Err: true,
},
{
Name: "Incorrect schema",
P: &Provisioner{
Schema: map[string]*Schema{
"foo": {},
},
ApplyFunc: noopApply,
},
Config: nil,
Err: true,
},
{
"Basic required field",
&Provisioner{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
ApplyFunc: noopApply,
},
nil,
true,
nil,
},
{
"Basic required field set",
&Provisioner{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
ApplyFunc: noopApply,
},
map[string]interface{}{
"foo": "bar",
},
false,
nil,
},
{
Name: "Warning from property validation",
P: &Provisioner{
Schema: map[string]*Schema{
"foo": {
Type: TypeString,
Optional: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
ws = append(ws, "Simple warning from property validation")
return
},
},
},
ApplyFunc: noopApply,
},
Config: map[string]interface{}{
"foo": "",
},
Err: false,
Warns: []string{"Simple warning from property validation"},
},
{
Name: "No schema",
P: &Provisioner{
Schema: nil,
ApplyFunc: noopApply,
},
Config: nil,
Err: false,
},
{
Name: "Warning from provisioner ValidateFunc",
P: &Provisioner{
Schema: nil,
ApplyFunc: noopApply,
ValidateFunc: func(*terraform.ResourceConfig) (ws []string, errors []error) {
ws = append(ws, "Simple warning from provisioner ValidateFunc")
return
},
},
Config: nil,
Err: false,
Warns: []string{"Simple warning from provisioner ValidateFunc"},
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c := terraform.NewResourceConfigRaw(tc.Config)
ws, es := tc.P.Validate(c)
if len(es) > 0 != tc.Err {
t.Fatalf("%d: %#v %s", i, es, es)
}
if (tc.Warns != nil || len(ws) != 0) && !reflect.DeepEqual(ws, tc.Warns) {
t.Fatalf("%d: warnings mismatch, actual: %#v", i, ws)
}
})
}
}
func TestProvisionerApply(t *testing.T) {
cases := []struct {
Name string
P *Provisioner
Conn map[string]string
Config map[string]interface{}
Err bool
}{
{
"Basic config",
&Provisioner{
ConnSchema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
cd := ctx.Value(ProvConnDataKey).(*ResourceData)
d := ctx.Value(ProvConfigDataKey).(*ResourceData)
if d.Get("foo").(int) != 42 {
return fmt.Errorf("bad config data")
}
if cd.Get("foo").(string) != "bar" {
return fmt.Errorf("bad conn data")
}
return nil
},
},
map[string]string{
"foo": "bar",
},
map[string]interface{}{
"foo": 42,
},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c := terraform.NewResourceConfigRaw(tc.Config)
state := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: tc.Conn,
},
}
err := tc.P.Apply(nil, state, c)
if err != nil != tc.Err {
t.Fatalf("%d: %s", i, err)
}
})
}
}
func TestProvisionerApply_nilState(t *testing.T) {
p := &Provisioner{
ConnSchema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
return nil
},
}
conf := map[string]interface{}{
"foo": 42,
}
c := terraform.NewResourceConfigRaw(conf)
err := p.Apply(nil, nil, c)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvisionerStop(t *testing.T) {
var p Provisioner
// Verify stopch blocks
ch := p.StopContext().Done()
select {
case <-ch:
t.Fatal("should not be stopped")
case <-time.After(10 * time.Millisecond):
}
// Stop it
if err := p.Stop(); err != nil {
t.Fatalf("err: %s", err)
}
select {
case <-ch:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
func TestProvisionerStop_apply(t *testing.T) {
p := &Provisioner{
ConnSchema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
<-ctx.Done()
return nil
},
}
conn := map[string]string{
"foo": "bar",
}
conf := map[string]interface{}{
"foo": 42,
}
c := terraform.NewResourceConfigRaw(conf)
state := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: conn,
},
}
// Run the apply in a goroutine
doneCh := make(chan struct{})
go func() {
p.Apply(nil, state, c)
close(doneCh)
}()
// Should block
select {
case <-doneCh:
t.Fatal("should not be done")
case <-time.After(10 * time.Millisecond):
}
// Stop!
p.Stop()
select {
case <-doneCh:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be done")
}
}
func TestProvisionerStop_stopFirst(t *testing.T) {
var p Provisioner
// Stop it
if err := p.Stop(); err != nil {
t.Fatalf("err: %s", err)
}
select {
case <-p.StopContext().Done():
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}

View File

@ -0,0 +1,842 @@
package schema
import (
"errors"
"fmt"
"log"
"strconv"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
)
var ReservedDataSourceFields = []string{
"connection",
"count",
"depends_on",
"lifecycle",
"provider",
"provisioner",
}
var ReservedResourceFields = []string{
"connection",
"count",
"depends_on",
"id",
"lifecycle",
"provider",
"provisioner",
}
// Resource represents a thing in Terraform that has a set of configurable
// attributes and a lifecycle (create, read, update, delete).
//
// The Resource schema is an abstraction that allows provider writers to
// worry only about CRUD operations while off-loading validation, diff
// generation, etc. to this higher level library.
//
// In spite of the name, this struct is not used only for terraform resources,
// but also for data sources. In the case of data sources, the Create,
// Update and Delete functions must not be provided.
type Resource struct {
// Schema is the schema for the configuration of this resource.
//
// The keys of this map are the configuration keys, and the values
// describe the schema of the configuration value.
//
// The schema is used to represent both configurable data as well
// as data that might be computed in the process of creating this
// resource.
Schema map[string]*Schema
// SchemaVersion is the version number for this resource's Schema
// definition. The current SchemaVersion stored in the state for each
// resource. Provider authors can increment this version number
// when Schema semantics change. If the State's SchemaVersion is less than
// the current SchemaVersion, the InstanceState is yielded to the
// MigrateState callback, where the provider can make whatever changes it
// needs to update the state to be compatible to the latest version of the
// Schema.
//
// When unset, SchemaVersion defaults to 0, so provider authors can start
// their Versioning at any integer >= 1
SchemaVersion int
// MigrateState is deprecated and any new changes to a resource's schema
// should be handled by StateUpgraders. Existing MigrateState implementations
// should remain for compatibility with existing state. MigrateState will
// still be called if the stored SchemaVersion is less than the
// first version of the StateUpgraders.
//
// MigrateState is responsible for updating an InstanceState with an old
// version to the format expected by the current version of the Schema.
//
// It is called during Refresh if the State's stored SchemaVersion is less
// than the current SchemaVersion of the Resource.
//
// The function is yielded the state's stored SchemaVersion and a pointer to
// the InstanceState that needs updating, as well as the configured
// provider's configured meta interface{}, in case the migration process
// needs to make any remote API calls.
MigrateState StateMigrateFunc
// StateUpgraders contains the functions responsible for upgrading an
// existing state with an old schema version to a newer schema. It is
// called specifically by Terraform when the stored schema version is less
// than the current SchemaVersion of the Resource.
//
// StateUpgraders map specific schema versions to a StateUpgrader
// function. The registered versions are expected to be ordered,
// consecutive values. The initial value may be greater than 0 to account
// for legacy schemas that weren't recorded and can be handled by
// MigrateState.
StateUpgraders []StateUpgrader
// The functions below are the CRUD operations for this resource.
//
// The only optional operation is Update. If Update is not implemented,
// then updates will not be supported for this resource.
//
// The ResourceData parameter in the functions below are used to
// query configuration and changes for the resource as well as to set
// the ID, computed data, etc.
//
// The interface{} parameter is the result of the ConfigureFunc in
// the provider for this resource. If the provider does not define
// a ConfigureFunc, this will be nil. This parameter should be used
// to store API clients, configuration structures, etc.
//
// If any errors occur during each of the operation, an error should be
// returned. If a resource was partially updated, be careful to enable
// partial state mode for ResourceData and use it accordingly.
//
// Exists is a function that is called to check if a resource still
// exists. If this returns false, then this will affect the diff
// accordingly. If this function isn't set, it will not be called. You
// can also signal existence in the Read method by calling d.SetId("")
// if the Resource is no longer present and should be removed from state.
// The *ResourceData passed to Exists should _not_ be modified.
Create CreateFunc
Read ReadFunc
Update UpdateFunc
Delete DeleteFunc
Exists ExistsFunc
// CustomizeDiff is a custom function for working with the diff that
// Terraform has created for this resource - it can be used to customize the
// diff that has been created, diff values not controlled by configuration,
// or even veto the diff altogether and abort the plan. It is passed a
// *ResourceDiff, a structure similar to ResourceData but lacking most write
// functions like Set, while introducing new functions that work with the
// diff such as SetNew, SetNewComputed, and ForceNew.
//
// The phases Terraform runs this in, and the state available via functions
// like Get and GetChange, are as follows:
//
// * New resource: One run with no state
// * Existing resource: One run with state
// * Existing resource, forced new: One run with state (before ForceNew),
// then one run without state (as if new resource)
// * Tainted resource: No runs (custom diff logic is skipped)
// * Destroy: No runs (standard diff logic is skipped on destroy diffs)
//
// This function needs to be resilient to support all scenarios.
//
// If this function needs to access external API resources, remember to flag
// the RequiresRefresh attribute mentioned below to ensure that
// -refresh=false is blocked when running plan or apply, as this means that
// this resource requires refresh-like behaviour to work effectively.
//
// For the most part, only computed fields can be customized by this
// function.
//
// This function is only allowed on regular resources (not data sources).
CustomizeDiff CustomizeDiffFunc
// Importer is the ResourceImporter implementation for this resource.
// If this is nil, then this resource does not support importing. If
// this is non-nil, then it supports importing and ResourceImporter
// must be validated. The validity of ResourceImporter is verified
// by InternalValidate on Resource.
Importer *ResourceImporter
// If non-empty, this string is emitted as a warning during Validate.
DeprecationMessage string
// Timeouts allow users to specify specific time durations in which an
// operation should time out, to allow them to extend an action to suit their
// usage. For example, a user may specify a large Creation timeout for their
// AWS RDS Instance due to it's size, or restoring from a snapshot.
// Resource implementors must enable Timeout support by adding the allowed
// actions (Create, Read, Update, Delete, Default) to the Resource struct, and
// accessing them in the matching methods.
Timeouts *ResourceTimeout
}
// ShimInstanceStateFromValue converts a cty.Value to a
// terraform.InstanceState.
func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) {
// Get the raw shimmed value. While this is correct, the set hashes don't
// match those from the Schema.
s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion)
// We now rebuild the state through the ResourceData, so that the set indexes
// match what helper/schema expects.
data, err := schemaMap(r.Schema).Data(s, nil)
if err != nil {
return nil, err
}
s = data.State()
if s == nil {
s = &terraform.InstanceState{}
}
return s, nil
}
// See Resource documentation.
type CreateFunc func(*ResourceData, interface{}) error
// See Resource documentation.
type ReadFunc func(*ResourceData, interface{}) error
// See Resource documentation.
type UpdateFunc func(*ResourceData, interface{}) error
// See Resource documentation.
type DeleteFunc func(*ResourceData, interface{}) error
// See Resource documentation.
type ExistsFunc func(*ResourceData, interface{}) (bool, error)
// See Resource documentation.
type StateMigrateFunc func(
int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
type StateUpgrader struct {
// Version is the version schema that this Upgrader will handle, converting
// it to Version+1.
Version int
// Type describes the schema that this function can upgrade. Type is
// required to decode the schema if the state was stored in a legacy
// flatmap format.
Type cty.Type
// Upgrade takes the JSON encoded state and the provider meta value, and
// upgrades the state one single schema version. The provided state is
// deocded into the default json types using a map[string]interface{}. It
// is up to the StateUpgradeFunc to ensure that the returned value can be
// encoded using the new schema.
Upgrade StateUpgradeFunc
}
// See StateUpgrader
type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error)
// See Resource documentation.
type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
// Apply creates, updates, and/or deletes a resource.
func (r *Resource) Apply(
s *terraform.InstanceState,
d *terraform.InstanceDiff,
meta interface{}) (*terraform.InstanceState, error) {
data, err := schemaMap(r.Schema).Data(s, d)
if err != nil {
return s, err
}
if s != nil && data != nil {
data.providerMeta = s.ProviderMeta
}
// Instance Diff shoould have the timeout info, need to copy it over to the
// ResourceData meta
rt := ResourceTimeout{}
if _, ok := d.Meta[TimeoutKey]; ok {
if err := rt.DiffDecode(d); err != nil {
log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
}
} else if s != nil {
if _, ok := s.Meta[TimeoutKey]; ok {
if err := rt.StateDecode(s); err != nil {
log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
}
}
} else {
log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
}
data.timeouts = &rt
if s == nil {
// The Terraform API dictates that this should never happen, but
// it doesn't hurt to be safe in this case.
s = new(terraform.InstanceState)
}
if d.Destroy || d.RequiresNew() {
if s.ID != "" {
// Destroy the resource since it is created
if err := r.Delete(data, meta); err != nil {
return r.recordCurrentSchemaVersion(data.State()), err
}
// Make sure the ID is gone.
data.SetId("")
}
// If we're only destroying, and not creating, then return
// now since we're done!
if !d.RequiresNew() {
return nil, nil
}
// Reset the data to be stateless since we just destroyed
data, err = schemaMap(r.Schema).Data(nil, d)
// data was reset, need to re-apply the parsed timeouts
data.timeouts = &rt
if err != nil {
return nil, err
}
}
err = nil
if data.Id() == "" {
// We're creating, it is a new resource.
data.MarkNewResource()
err = r.Create(data, meta)
} else {
if r.Update == nil {
return s, fmt.Errorf("doesn't support update")
}
err = r.Update(data, meta)
}
return r.recordCurrentSchemaVersion(data.State()), err
}
// Diff returns a diff of this resource.
func (r *Resource) Diff(
s *terraform.InstanceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.InstanceDiff, error) {
t := &ResourceTimeout{}
err := t.ConfigDecode(r, c)
if err != nil {
return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
}
instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true)
if err != nil {
return instanceDiff, err
}
if instanceDiff != nil {
if err := t.DiffEncode(instanceDiff); err != nil {
log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
}
} else {
log.Printf("[DEBUG] Instance Diff is nil in Diff()")
}
return instanceDiff, err
}
func (r *Resource) simpleDiff(
s *terraform.InstanceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.InstanceDiff, error) {
instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false)
if err != nil {
return instanceDiff, err
}
if instanceDiff == nil {
instanceDiff = terraform.NewInstanceDiff()
}
// Make sure the old value is set in each of the instance diffs.
// This was done by the RequiresNew logic in the full legacy Diff.
for k, attr := range instanceDiff.Attributes {
if attr == nil {
continue
}
if s != nil {
attr.Old = s.Attributes[k]
}
}
return instanceDiff, nil
}
// Validate validates the resource configuration against the schema.
func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
warns, errs := schemaMap(r.Schema).Validate(c)
if r.DeprecationMessage != "" {
warns = append(warns, r.DeprecationMessage)
}
return warns, errs
}
// ReadDataApply loads the data for a data source, given a diff that
// describes the configuration arguments and desired computed attributes.
func (r *Resource) ReadDataApply(
d *terraform.InstanceDiff,
meta interface{},
) (*terraform.InstanceState, error) {
// Data sources are always built completely from scratch
// on each read, so the source state is always nil.
data, err := schemaMap(r.Schema).Data(nil, d)
if err != nil {
return nil, err
}
err = r.Read(data, meta)
state := data.State()
if state != nil && state.ID == "" {
// Data sources can set an ID if they want, but they aren't
// required to; we'll provide a placeholder if they don't,
// to preserve the invariant that all resources have non-empty
// ids.
state.ID = "-"
}
return r.recordCurrentSchemaVersion(state), err
}
// RefreshWithoutUpgrade reads the instance state, but does not call
// MigrateState or the StateUpgraders, since those are now invoked in a
// separate API call.
// RefreshWithoutUpgrade is part of the new plugin shims.
func (r *Resource) RefreshWithoutUpgrade(
s *terraform.InstanceState,
meta interface{}) (*terraform.InstanceState, error) {
// If the ID is already somehow blank, it doesn't exist
if s.ID == "" {
return nil, nil
}
rt := ResourceTimeout{}
if _, ok := s.Meta[TimeoutKey]; ok {
if err := rt.StateDecode(s); err != nil {
log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
}
}
if r.Exists != nil {
// Make a copy of data so that if it is modified it doesn't
// affect our Read later.
data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil {
return s, err
}
if s != nil {
data.providerMeta = s.ProviderMeta
}
exists, err := r.Exists(data, meta)
if err != nil {
return s, err
}
if !exists {
return nil, nil
}
}
data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil {
return s, err
}
if s != nil {
data.providerMeta = s.ProviderMeta
}
err = r.Read(data, meta)
state := data.State()
if state != nil && state.ID == "" {
state = nil
}
return r.recordCurrentSchemaVersion(state), err
}
// Refresh refreshes the state of the resource.
func (r *Resource) Refresh(
s *terraform.InstanceState,
meta interface{}) (*terraform.InstanceState, error) {
// If the ID is already somehow blank, it doesn't exist
if s.ID == "" {
return nil, nil
}
rt := ResourceTimeout{}
if _, ok := s.Meta[TimeoutKey]; ok {
if err := rt.StateDecode(s); err != nil {
log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
}
}
if r.Exists != nil {
// Make a copy of data so that if it is modified it doesn't
// affect our Read later.
data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil {
return s, err
}
exists, err := r.Exists(data, meta)
if err != nil {
return s, err
}
if !exists {
return nil, nil
}
}
// there may be new StateUpgraders that need to be run
s, err := r.upgradeState(s, meta)
if err != nil {
return s, err
}
data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil {
return s, err
}
err = r.Read(data, meta)
state := data.State()
if state != nil && state.ID == "" {
state = nil
}
return r.recordCurrentSchemaVersion(state), err
}
func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
var err error
needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
migrate := needsMigration && r.MigrateState != nil
if migrate {
s, err = r.MigrateState(stateSchemaVersion, s, meta)
if err != nil {
return s, err
}
}
if len(r.StateUpgraders) == 0 {
return s, nil
}
// If we ran MigrateState, then the stateSchemaVersion value is no longer
// correct. We can expect the first upgrade function to be the correct
// schema type version.
if migrate {
stateSchemaVersion = r.StateUpgraders[0].Version
}
schemaType := r.CoreConfigSchema().ImpliedType()
// find the expected type to convert the state
for _, upgrader := range r.StateUpgraders {
if stateSchemaVersion == upgrader.Version {
schemaType = upgrader.Type
}
}
// StateUpgraders only operate on the new JSON format state, so the state
// need to be converted.
stateVal, err := StateValueFromInstanceState(s, schemaType)
if err != nil {
return nil, err
}
jsonState, err := StateValueToJSONMap(stateVal, schemaType)
if err != nil {
return nil, err
}
for _, upgrader := range r.StateUpgraders {
if stateSchemaVersion != upgrader.Version {
continue
}
jsonState, err = upgrader.Upgrade(jsonState, meta)
if err != nil {
return nil, err
}
stateSchemaVersion++
}
// now we need to re-flatmap the new state
stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema())
if err != nil {
return nil, err
}
return r.ShimInstanceStateFromValue(stateVal)
}
// InternalValidate should be called to validate the structure
// of the resource.
//
// This should be called in a unit test for any resource to verify
// before release that a resource is properly configured for use with
// this library.
//
// Provider.InternalValidate() will automatically call this for all of
// the resources it manages, so you don't need to call this manually if it
// is part of a Provider.
func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error {
if r == nil {
return errors.New("resource is nil")
}
if !writable {
if r.Create != nil || r.Update != nil || r.Delete != nil {
return fmt.Errorf("must not implement Create, Update or Delete")
}
// CustomizeDiff cannot be defined for read-only resources
if r.CustomizeDiff != nil {
return fmt.Errorf("cannot implement CustomizeDiff")
}
}
tsm := topSchemaMap
if r.isTopLevel() && writable {
// All non-Computed attributes must be ForceNew if Update is not defined
if r.Update == nil {
nonForceNewAttrs := make([]string, 0)
for k, v := range r.Schema {
if !v.ForceNew && !v.Computed {
nonForceNewAttrs = append(nonForceNewAttrs, k)
}
}
if len(nonForceNewAttrs) > 0 {
return fmt.Errorf(
"No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
}
} else {
nonUpdateableAttrs := make([]string, 0)
for k, v := range r.Schema {
if v.ForceNew || v.Computed && !v.Optional {
nonUpdateableAttrs = append(nonUpdateableAttrs, k)
}
}
updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
if updateableAttrs == 0 {
return fmt.Errorf(
"All fields are ForceNew or Computed w/out Optional, Update is superfluous")
}
}
tsm = schemaMap(r.Schema)
// Destroy, and Read are required
if r.Read == nil {
return fmt.Errorf("Read must be implemented")
}
if r.Delete == nil {
return fmt.Errorf("Delete must be implemented")
}
// If we have an importer, we need to verify the importer.
if r.Importer != nil {
if err := r.Importer.InternalValidate(); err != nil {
return err
}
}
for k, f := range tsm {
if isReservedResourceFieldName(k, f) {
return fmt.Errorf("%s is a reserved field name", k)
}
}
}
lastVersion := -1
for _, u := range r.StateUpgraders {
if lastVersion >= 0 && u.Version-lastVersion > 1 {
return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version)
}
if u.Version >= r.SchemaVersion {
return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion)
}
if !u.Type.IsObjectType() {
return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version)
}
if u.Upgrade == nil {
return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version)
}
lastVersion = u.Version
}
if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 {
return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion)
}
// Data source
if r.isTopLevel() && !writable {
tsm = schemaMap(r.Schema)
for k, _ := range tsm {
if isReservedDataSourceFieldName(k) {
return fmt.Errorf("%s is a reserved field name", k)
}
}
}
return schemaMap(r.Schema).InternalValidate(tsm)
}
func isReservedDataSourceFieldName(name string) bool {
for _, reservedName := range ReservedDataSourceFields {
if name == reservedName {
return true
}
}
return false
}
func isReservedResourceFieldName(name string, s *Schema) bool {
// Allow phasing out "id"
// See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415
if name == "id" && (s.Deprecated != "" || s.Removed != "") {
return false
}
for _, reservedName := range ReservedResourceFields {
if name == reservedName {
return true
}
}
return false
}
// Data returns a ResourceData struct for this Resource. Each return value
// is a separate copy and can be safely modified differently.
//
// The data returned from this function has no actual affect on the Resource
// itself (including the state given to this function).
//
// This function is useful for unit tests and ResourceImporter functions.
func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
result, err := schemaMap(r.Schema).Data(s, nil)
if err != nil {
// At the time of writing, this isn't possible (Data never returns
// non-nil errors). We panic to find this in the future if we have to.
// I don't see a reason for Data to ever return an error.
panic(err)
}
// load the Resource timeouts
result.timeouts = r.Timeouts
if result.timeouts == nil {
result.timeouts = &ResourceTimeout{}
}
// Set the schema version to latest by default
result.meta = map[string]interface{}{
"schema_version": strconv.Itoa(r.SchemaVersion),
}
return result
}
// TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
//
// TODO: May be able to be removed with the above ResourceData function.
func (r *Resource) TestResourceData() *ResourceData {
return &ResourceData{
schema: r.Schema,
}
}
// SchemasForFlatmapPath tries its best to find a sequence of schemas that
// the given dot-delimited attribute path traverses through in the schema
// of the receiving Resource.
func (r *Resource) SchemasForFlatmapPath(path string) []*Schema {
return SchemasForFlatmapPath(path, r.Schema)
}
// Returns true if the resource is "top level" i.e. not a sub-resource.
func (r *Resource) isTopLevel() bool {
// TODO: This is a heuristic; replace with a definitive attribute?
return (r.Create != nil || r.Read != nil)
}
// Determines if a given InstanceState needs to be migrated by checking the
// stored version number with the current SchemaVersion
func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
// Get the raw interface{} value for the schema version. If it doesn't
// exist or is nil then set it to zero.
raw := is.Meta["schema_version"]
if raw == nil {
raw = "0"
}
// Try to convert it to a string. If it isn't a string then we pretend
// that it isn't set at all. It should never not be a string unless it
// was manually tampered with.
rawString, ok := raw.(string)
if !ok {
rawString = "0"
}
stateSchemaVersion, _ := strconv.Atoi(rawString)
// Don't run MigrateState if the version is handled by a StateUpgrader,
// since StateMigrateFuncs are not required to handle unknown versions
maxVersion := r.SchemaVersion
if len(r.StateUpgraders) > 0 {
maxVersion = r.StateUpgraders[0].Version
}
return stateSchemaVersion < maxVersion, stateSchemaVersion
}
func (r *Resource) recordCurrentSchemaVersion(
state *terraform.InstanceState) *terraform.InstanceState {
if state != nil && r.SchemaVersion > 0 {
if state.Meta == nil {
state.Meta = make(map[string]interface{})
}
state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
}
return state
}
// Noop is a convenience implementation of resource function which takes
// no action and returns no error.
func Noop(*ResourceData, interface{}) error {
return nil
}
// RemoveFromState is a convenience implementation of a resource function
// which sets the resource ID to empty string (to remove it from state)
// and returns no error.
func RemoveFromState(d *ResourceData, _ interface{}) error {
d.SetId("")
return nil
}

View File

@ -0,0 +1,561 @@
package schema
import (
"log"
"reflect"
"strings"
"sync"
"time"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
// ResourceData is used to query and set the attributes of a resource.
//
// ResourceData is the primary argument received for CRUD operations on
// a resource as well as configuration of a provider. It is a powerful
// structure that can be used to not only query data, but check for changes,
// define partial state updates, etc.
//
// The most relevant methods to take a look at are Get, Set, and Partial.
type ResourceData struct {
// Settable (internally)
schema map[string]*Schema
config *terraform.ResourceConfig
state *terraform.InstanceState
diff *terraform.InstanceDiff
meta map[string]interface{}
timeouts *ResourceTimeout
providerMeta cty.Value
// Don't set
multiReader *MultiLevelFieldReader
setWriter *MapFieldWriter
newState *terraform.InstanceState
partial bool
partialMap map[string]struct{}
once sync.Once
isNew bool
panicOnError bool
}
// getResult is the internal structure that is generated when a Get
// is called that contains some extra data that might be used.
type getResult struct {
Value interface{}
ValueProcessed interface{}
Computed bool
Exists bool
Schema *Schema
}
// UnsafeSetFieldRaw allows setting arbitrary values in state to arbitrary
// values, bypassing schema. This MUST NOT be used in normal circumstances -
// it exists only to support the remote_state data source.
//
// Deprecated: Fully define schema attributes and use Set() instead.
func (d *ResourceData) UnsafeSetFieldRaw(key string, value string) {
d.once.Do(d.init)
d.setWriter.unsafeWriteField(key, value)
}
// Get returns the data for the given key, or nil if the key doesn't exist
// in the schema.
//
// If the key does exist in the schema but doesn't exist in the configuration,
// then the default value for that type will be returned. For strings, this is
// "", for numbers it is 0, etc.
//
// If you want to test if something is set at all in the configuration,
// use GetOk.
func (d *ResourceData) Get(key string) interface{} {
v, _ := d.GetOk(key)
return v
}
// GetChange returns the old and new value for a given key.
//
// HasChange should be used to check if a change exists. It is possible
// that both the old and new value are the same if the old value was not
// set and the new value is. This is common, for example, for boolean
// fields which have a zero value of false.
func (d *ResourceData) GetChange(key string) (interface{}, interface{}) {
o, n := d.getChange(key, getSourceState, getSourceDiff)
return o.Value, n.Value
}
// GetOk returns the data for the given key and whether or not the key
// has been set to a non-zero value at some point.
//
// The first result will not necessarilly be nil if the value doesn't exist.
// The second result should be checked to determine this information.
func (d *ResourceData) GetOk(key string) (interface{}, bool) {
r := d.getRaw(key, getSourceSet)
exists := r.Exists && !r.Computed
if exists {
// If it exists, we also want to verify it is not the zero-value.
value := r.Value
zero := r.Schema.Type.Zero()
if eq, ok := value.(Equal); ok {
exists = !eq.Equal(zero)
} else {
exists = !reflect.DeepEqual(value, zero)
}
}
return r.Value, exists
}
// GetOkExists returns the data for a given key and whether or not the key
// has been set to a non-zero value. This is only useful for determining
// if boolean attributes have been set, if they are Optional but do not
// have a Default value.
//
// This is nearly the same function as GetOk, yet it does not check
// for the zero value of the attribute's type. This allows for attributes
// without a default, to fully check for a literal assignment, regardless
// of the zero-value for that type.
// This should only be used if absolutely required/needed.
func (d *ResourceData) GetOkExists(key string) (interface{}, bool) {
r := d.getRaw(key, getSourceSet)
exists := r.Exists && !r.Computed
return r.Value, exists
}
func (d *ResourceData) getRaw(key string, level getSource) getResult {
var parts []string
if key != "" {
parts = strings.Split(key, ".")
}
return d.get(parts, level)
}
// HasChange returns whether or not the given key has been changed.
func (d *ResourceData) HasChange(key string) bool {
o, n := d.GetChange(key)
// If the type implements the Equal interface, then call that
// instead of just doing a reflect.DeepEqual. An example where this is
// needed is *Set
if eq, ok := o.(Equal); ok {
return !eq.Equal(n)
}
return !reflect.DeepEqual(o, n)
}
// Partial turns partial state mode on/off.
//
// When partial state mode is enabled, then only key prefixes specified
// by SetPartial will be in the final state. This allows providers to return
// partial states for partially applied resources (when errors occur).
func (d *ResourceData) Partial(on bool) {
d.partial = on
if on {
if d.partialMap == nil {
d.partialMap = make(map[string]struct{})
}
} else {
d.partialMap = nil
}
}
// Set sets the value for the given key.
//
// If the key is invalid or the value is not a correct type, an error
// will be returned.
func (d *ResourceData) Set(key string, value interface{}) error {
d.once.Do(d.init)
// If the value is a pointer to a non-struct, get its value and
// use that. This allows Set to take a pointer to primitives to
// simplify the interface.
reflectVal := reflect.ValueOf(value)
if reflectVal.Kind() == reflect.Ptr {
if reflectVal.IsNil() {
// If the pointer is nil, then the value is just nil
value = nil
} else {
// Otherwise, we dereference the pointer as long as its not
// a pointer to a struct, since struct pointers are allowed.
reflectVal = reflect.Indirect(reflectVal)
if reflectVal.Kind() != reflect.Struct {
value = reflectVal.Interface()
}
}
}
err := d.setWriter.WriteField(strings.Split(key, "."), value)
if err != nil && d.panicOnError {
panic(err)
}
return err
}
// SetPartial adds the key to the final state output while
// in partial state mode. The key must be a root key in the schema (i.e.
// it cannot be "list.0").
//
// If partial state mode is disabled, then this has no effect. Additionally,
// whenever partial state mode is toggled, the partial data is cleared.
func (d *ResourceData) SetPartial(k string) {
if d.partial {
d.partialMap[k] = struct{}{}
}
}
func (d *ResourceData) MarkNewResource() {
d.isNew = true
}
func (d *ResourceData) IsNewResource() bool {
return d.isNew
}
// Id returns the ID of the resource.
func (d *ResourceData) Id() string {
var result string
if d.state != nil {
result = d.state.ID
if result == "" {
result = d.state.Attributes["id"]
}
}
if d.newState != nil {
result = d.newState.ID
if result == "" {
result = d.newState.Attributes["id"]
}
}
return result
}
// ConnInfo returns the connection info for this resource.
func (d *ResourceData) ConnInfo() map[string]string {
if d.newState != nil {
return d.newState.Ephemeral.ConnInfo
}
if d.state != nil {
return d.state.Ephemeral.ConnInfo
}
return nil
}
// SetId sets the ID of the resource. If the value is blank, then the
// resource is destroyed.
func (d *ResourceData) SetId(v string) {
d.once.Do(d.init)
d.newState.ID = v
// once we transition away from the legacy state types, "id" will no longer
// be a special field, and will become a normal attribute.
// set the attribute normally
d.setWriter.unsafeWriteField("id", v)
// Make sure the newState is also set, otherwise the old value
// may get precedence.
if d.newState.Attributes == nil {
d.newState.Attributes = map[string]string{}
}
d.newState.Attributes["id"] = v
}
// SetConnInfo sets the connection info for a resource.
func (d *ResourceData) SetConnInfo(v map[string]string) {
d.once.Do(d.init)
d.newState.Ephemeral.ConnInfo = v
}
// SetType sets the ephemeral type for the data. This is only required
// for importing.
func (d *ResourceData) SetType(t string) {
d.once.Do(d.init)
d.newState.Ephemeral.Type = t
}
// State returns the new InstanceState after the diff and any Set
// calls.
func (d *ResourceData) State() *terraform.InstanceState {
var result terraform.InstanceState
result.ID = d.Id()
result.Meta = d.meta
// If we have no ID, then this resource doesn't exist and we just
// return nil.
if result.ID == "" {
return nil
}
if d.timeouts != nil {
if err := d.timeouts.StateEncode(&result); err != nil {
log.Printf("[ERR] Error encoding Timeout meta to Instance State: %s", err)
}
}
// Look for a magic key in the schema that determines we skip the
// integrity check of fields existing in the schema, allowing dynamic
// keys to be created.
hasDynamicAttributes := false
for k, _ := range d.schema {
if k == "__has_dynamic_attributes" {
hasDynamicAttributes = true
log.Printf("[INFO] Resource %s has dynamic attributes", result.ID)
}
}
// In order to build the final state attributes, we read the full
// attribute set as a map[string]interface{}, write it to a MapFieldWriter,
// and then use that map.
rawMap := make(map[string]interface{})
for k := range d.schema {
source := getSourceSet
if d.partial {
source = getSourceState
if _, ok := d.partialMap[k]; ok {
source = getSourceSet
}
}
raw := d.get([]string{k}, source)
if raw.Exists && !raw.Computed {
rawMap[k] = raw.Value
if raw.ValueProcessed != nil {
rawMap[k] = raw.ValueProcessed
}
}
}
mapW := &MapFieldWriter{Schema: d.schema}
if err := mapW.WriteField(nil, rawMap); err != nil {
log.Printf("[ERR] Error writing fields: %s", err)
return nil
}
result.Attributes = mapW.Map()
if hasDynamicAttributes {
// If we have dynamic attributes, just copy the attributes map
// one for one into the result attributes.
for k, v := range d.setWriter.Map() {
// Don't clobber schema values. This limits usage of dynamic
// attributes to names which _do not_ conflict with schema
// keys!
if _, ok := result.Attributes[k]; !ok {
result.Attributes[k] = v
}
}
}
if d.newState != nil {
result.Ephemeral = d.newState.Ephemeral
}
// TODO: This is hacky and we can remove this when we have a proper
// state writer. We should instead have a proper StateFieldWriter
// and use that.
for k, schema := range d.schema {
if schema.Type != TypeMap {
continue
}
if result.Attributes[k] == "" {
delete(result.Attributes, k)
}
}
if v := d.Id(); v != "" {
result.Attributes["id"] = d.Id()
}
if d.state != nil {
result.Tainted = d.state.Tainted
}
return &result
}
// Timeout returns the data for the given timeout key
// Returns a duration of 20 minutes for any key not found, or not found and no default.
func (d *ResourceData) Timeout(key string) time.Duration {
key = strings.ToLower(key)
// System default of 20 minutes
defaultTimeout := 20 * time.Minute
if d.timeouts == nil {
return defaultTimeout
}
var timeout *time.Duration
switch key {
case TimeoutCreate:
timeout = d.timeouts.Create
case TimeoutRead:
timeout = d.timeouts.Read
case TimeoutUpdate:
timeout = d.timeouts.Update
case TimeoutDelete:
timeout = d.timeouts.Delete
}
if timeout != nil {
return *timeout
}
if d.timeouts.Default != nil {
return *d.timeouts.Default
}
return defaultTimeout
}
func (d *ResourceData) init() {
// Initialize the field that will store our new state
var copyState terraform.InstanceState
if d.state != nil {
copyState = *d.state.DeepCopy()
}
d.newState = &copyState
// Initialize the map for storing set data
d.setWriter = &MapFieldWriter{Schema: d.schema}
// Initialize the reader for getting data from the
// underlying sources (config, diff, etc.)
readers := make(map[string]FieldReader)
var stateAttributes map[string]string
if d.state != nil {
stateAttributes = d.state.Attributes
readers["state"] = &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(stateAttributes),
}
}
if d.config != nil {
readers["config"] = &ConfigFieldReader{
Schema: d.schema,
Config: d.config,
}
}
if d.diff != nil {
readers["diff"] = &DiffFieldReader{
Schema: d.schema,
Diff: d.diff,
Source: &MultiLevelFieldReader{
Levels: []string{"state", "config"},
Readers: readers,
},
}
}
readers["set"] = &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(d.setWriter.Map()),
}
d.multiReader = &MultiLevelFieldReader{
Levels: []string{
"state",
"config",
"diff",
"set",
},
Readers: readers,
}
}
func (d *ResourceData) diffChange(
k string) (interface{}, interface{}, bool, bool, bool) {
// Get the change between the state and the config.
o, n := d.getChange(k, getSourceState, getSourceConfig|getSourceExact)
if !o.Exists {
o.Value = nil
}
if !n.Exists {
n.Value = nil
}
// Return the old, new, and whether there is a change
return o.Value, n.Value, !reflect.DeepEqual(o.Value, n.Value), n.Computed, false
}
func (d *ResourceData) getChange(
k string,
oldLevel getSource,
newLevel getSource) (getResult, getResult) {
var parts, parts2 []string
if k != "" {
parts = strings.Split(k, ".")
parts2 = strings.Split(k, ".")
}
o := d.get(parts, oldLevel)
n := d.get(parts2, newLevel)
return o, n
}
func (d *ResourceData) get(addr []string, source getSource) getResult {
d.once.Do(d.init)
level := "set"
flags := source & ^getSourceLevelMask
exact := flags&getSourceExact != 0
source = source & getSourceLevelMask
if source >= getSourceSet {
level = "set"
} else if source >= getSourceDiff {
level = "diff"
} else if source >= getSourceConfig {
level = "config"
} else {
level = "state"
}
var result FieldReadResult
var err error
if exact {
result, err = d.multiReader.ReadFieldExact(addr, level)
} else {
result, err = d.multiReader.ReadFieldMerge(addr, level)
}
if err != nil {
panic(err)
}
// If the result doesn't exist, then we set the value to the zero value
var schema *Schema
if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 {
schema = schemaL[len(schemaL)-1]
}
if result.Value == nil && schema != nil {
result.Value = result.ValueOrZero(schema)
}
// Transform the FieldReadResult into a getResult. It might be worth
// merging these two structures one day.
return getResult{
Value: result.Value,
ValueProcessed: result.ValueProcessed,
Computed: result.Computed,
Exists: result.Exists,
Schema: schema,
}
}
func (d *ResourceData) GetProviderMeta(dst interface{}) error {
if d.providerMeta.IsNull() {
return nil
}
return gocty.FromCtyValue(d.providerMeta, &dst)
}

View File

@ -0,0 +1,17 @@
package schema
//go:generate go run golang.org/x/tools/cmd/stringer -type=getSource resource_data_get_source.go
// getSource represents the level we want to get for a value (internally).
// Any source less than or equal to the level will be loaded (whichever
// has a value first).
type getSource byte
const (
getSourceState getSource = 1 << iota
getSourceConfig
getSourceDiff
getSourceSet
getSourceExact // Only get from the _exact_ level
getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceDiff | getSourceSet
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,559 @@
package schema
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
// newValueWriter is a minor re-implementation of MapFieldWriter to include
// keys that should be marked as computed, to represent the new part of a
// pseudo-diff.
type newValueWriter struct {
*MapFieldWriter
// A list of keys that should be marked as computed.
computedKeys map[string]bool
// A lock to prevent races on writes. The underlying writer will have one as
// well - this is for computed keys.
lock sync.Mutex
// To be used with init.
once sync.Once
}
// init performs any initialization tasks for the newValueWriter.
func (w *newValueWriter) init() {
if w.computedKeys == nil {
w.computedKeys = make(map[string]bool)
}
}
// WriteField overrides MapValueWriter's WriteField, adding the ability to flag
// the address as computed.
func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error {
// Fail the write if we have a non-nil value and computed is true.
// NewComputed values should not have a value when written.
if value != nil && computed {
return errors.New("Non-nil value with computed set")
}
if err := w.MapFieldWriter.WriteField(address, value); err != nil {
return err
}
w.once.Do(w.init)
w.lock.Lock()
defer w.lock.Unlock()
if computed {
w.computedKeys[strings.Join(address, ".")] = true
}
return nil
}
// ComputedKeysMap returns the underlying computed keys map.
func (w *newValueWriter) ComputedKeysMap() map[string]bool {
w.once.Do(w.init)
return w.computedKeys
}
// newValueReader is a minor re-implementation of MapFieldReader and is the
// read counterpart to MapValueWriter, allowing the read of keys flagged as
// computed to accommodate the diff override logic in ResourceDiff.
type newValueReader struct {
*MapFieldReader
// The list of computed keys from a newValueWriter.
computedKeys map[string]bool
}
// ReadField reads the values from the underlying writer, returning the
// computed value if it is found as well.
func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) {
addrKey := strings.Join(address, ".")
v, err := r.MapFieldReader.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
for computedKey := range r.computedKeys {
if childAddrOf(addrKey, computedKey) {
if strings.HasSuffix(addrKey, ".#") {
// This is a count value for a list or set that has been marked as
// computed, or a sub-list/sub-set of a complex resource that has
// been marked as computed. We need to pass through to other readers
// so that an accurate previous count can be fetched for the diff.
v.Exists = false
}
v.Computed = true
}
}
return v, nil
}
// ResourceDiff is used to query and make custom changes to an in-flight diff.
// It can be used to veto particular changes in the diff, customize the diff
// that has been created, or diff values not controlled by config.
//
// The object functions similar to ResourceData, however most notably lacks
// Set, SetPartial, and Partial, as it should be used to change diff values
// only. Most other first-class ResourceData functions exist, namely Get,
// GetOk, HasChange, and GetChange exist.
//
// All functions in ResourceDiff, save for ForceNew, can only be used on
// computed fields.
type ResourceDiff struct {
// The schema for the resource being worked on.
schema map[string]*Schema
// The current config for this resource.
config *terraform.ResourceConfig
// The state for this resource as it exists post-refresh, after the initial
// diff.
state *terraform.InstanceState
// The diff created by Terraform. This diff is used, along with state,
// config, and custom-set diff data, to provide a multi-level reader
// experience similar to ResourceData.
diff *terraform.InstanceDiff
// The internal reader structure that contains the state, config, the default
// diff, and the new diff.
multiReader *MultiLevelFieldReader
// A writer that writes overridden new fields.
newWriter *newValueWriter
// Tracks which keys have been updated by ResourceDiff to ensure that the
// diff does not get re-run on keys that were not touched, or diffs that were
// just removed (re-running on the latter would just roll back the removal).
updatedKeys map[string]bool
// Tracks which keys were flagged as forceNew. These keys are not saved in
// newWriter, but we need to track them so that they can be re-diffed later.
forcedNewKeys map[string]bool
}
// newResourceDiff creates a new ResourceDiff instance.
func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff {
d := &ResourceDiff{
config: config,
state: state,
diff: diff,
schema: schema,
}
d.newWriter = &newValueWriter{
MapFieldWriter: &MapFieldWriter{Schema: d.schema},
}
readers := make(map[string]FieldReader)
var stateAttributes map[string]string
if d.state != nil {
stateAttributes = d.state.Attributes
readers["state"] = &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(stateAttributes),
}
}
if d.config != nil {
readers["config"] = &ConfigFieldReader{
Schema: d.schema,
Config: d.config,
}
}
if d.diff != nil {
readers["diff"] = &DiffFieldReader{
Schema: d.schema,
Diff: d.diff,
Source: &MultiLevelFieldReader{
Levels: []string{"state", "config"},
Readers: readers,
},
}
}
readers["newDiff"] = &newValueReader{
MapFieldReader: &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(d.newWriter.Map()),
},
computedKeys: d.newWriter.ComputedKeysMap(),
}
d.multiReader = &MultiLevelFieldReader{
Levels: []string{
"state",
"config",
"diff",
"newDiff",
},
Readers: readers,
}
d.updatedKeys = make(map[string]bool)
d.forcedNewKeys = make(map[string]bool)
return d
}
// UpdatedKeys returns the keys that were updated by this ResourceDiff run.
// These are the only keys that a diff should be re-calculated for.
//
// This is the combined result of both keys for which diff values were updated
// for or cleared, and also keys that were flagged to be re-diffed as a result
// of ForceNew.
func (d *ResourceDiff) UpdatedKeys() []string {
var s []string
for k := range d.updatedKeys {
s = append(s, k)
}
for k := range d.forcedNewKeys {
for _, l := range s {
if k == l {
break
}
}
s = append(s, k)
}
return s
}
// Clear wipes the diff for a particular key. It is called by ResourceDiff's
// functionality to remove any possibility of conflicts, but can be called on
// its own to just remove a specific key from the diff completely.
//
// Note that this does not wipe an override. This function is only allowed on
// computed keys.
func (d *ResourceDiff) Clear(key string) error {
if err := d.checkKey(key, "Clear", true); err != nil {
return err
}
return d.clear(key)
}
func (d *ResourceDiff) clear(key string) error {
// Check the schema to make sure that this key exists first.
schemaL := addrToSchema(strings.Split(key, "."), d.schema)
if len(schemaL) == 0 {
return fmt.Errorf("%s is not a valid key", key)
}
for k := range d.diff.Attributes {
if strings.HasPrefix(k, key) {
delete(d.diff.Attributes, k)
}
}
return nil
}
// GetChangedKeysPrefix helps to implement Resource.CustomizeDiff
// where we need to act on all nested fields
// without calling out each one separately
func (d *ResourceDiff) GetChangedKeysPrefix(prefix string) []string {
keys := make([]string, 0)
for k := range d.diff.Attributes {
if strings.HasPrefix(k, prefix) {
keys = append(keys, k)
}
}
return keys
}
// diffChange helps to implement resourceDiffer and derives its change values
// from ResourceDiff's own change data, in addition to existing diff, config, and state.
func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool, bool) {
old, new, customized := d.getChange(key)
if !old.Exists {
old.Value = nil
}
if !new.Exists || d.removed(key) {
new.Value = nil
}
return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed, customized
}
// SetNew is used to set a new diff value for the mentioned key. The value must
// be correct for the attribute's schema (mostly relevant for maps, lists, and
// sets). The original value from the state is used as the old value.
//
// This function is only allowed on computed attributes.
func (d *ResourceDiff) SetNew(key string, value interface{}) error {
if err := d.checkKey(key, "SetNew", false); err != nil {
return err
}
return d.setDiff(key, value, false)
}
// SetNewComputed functions like SetNew, except that it blanks out a new value
// and marks it as computed.
//
// This function is only allowed on computed attributes.
func (d *ResourceDiff) SetNewComputed(key string) error {
if err := d.checkKey(key, "SetNewComputed", false); err != nil {
return err
}
return d.setDiff(key, nil, true)
}
// setDiff performs common diff setting behaviour.
func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error {
if err := d.clear(key); err != nil {
return err
}
if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil {
return fmt.Errorf("Cannot set new diff value for key %s: %s", key, err)
}
d.updatedKeys[key] = true
return nil
}
// ForceNew force-flags ForceNew in the schema for a specific key, and
// re-calculates its diff, effectively causing this attribute to force a new
// resource.
//
// Keep in mind that forcing a new resource will force a second run of the
// resource's CustomizeDiff function (with a new ResourceDiff) once the current
// one has completed. This second run is performed without state. This behavior
// will be the same as if a new resource is being created and is performed to
// ensure that the diff looks like the diff for a new resource as much as
// possible. CustomizeDiff should expect such a scenario and act correctly.
//
// This function is a no-op/error if there is no diff.
//
// Note that the change to schema is permanent for the lifecycle of this
// specific ResourceDiff instance.
func (d *ResourceDiff) ForceNew(key string) error {
if !d.HasChange(key) {
return fmt.Errorf("ForceNew: No changes for %s", key)
}
keyParts := strings.Split(key, ".")
var schema *Schema
schemaL := addrToSchema(keyParts, d.schema)
if len(schemaL) > 0 {
schema = schemaL[len(schemaL)-1]
} else {
return fmt.Errorf("ForceNew: %s is not a valid key", key)
}
schema.ForceNew = true
// Flag this for a re-diff. Don't save any values to guarantee that existing
// diffs aren't messed with, as this gets messy when dealing with complex
// structures, zero values, etc.
d.forcedNewKeys[keyParts[0]] = true
return nil
}
// Get hands off to ResourceData.Get.
func (d *ResourceDiff) Get(key string) interface{} {
r, _ := d.GetOk(key)
return r
}
// GetChange gets the change between the state and diff, checking first to see
// if an overridden diff exists.
//
// This implementation differs from ResourceData's in the way that we first get
// results from the exact levels for the new diff, then from state and diff as
// per normal.
func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) {
old, new, _ := d.getChange(key)
return old.Value, new.Value
}
// GetOk functions the same way as ResourceData.GetOk, but it also checks the
// new diff levels to provide data consistent with the current state of the
// customized diff.
func (d *ResourceDiff) GetOk(key string) (interface{}, bool) {
r := d.get(strings.Split(key, "."), "newDiff")
exists := r.Exists && !r.Computed
if exists {
// If it exists, we also want to verify it is not the zero-value.
value := r.Value
zero := r.Schema.Type.Zero()
if eq, ok := value.(Equal); ok {
exists = !eq.Equal(zero)
} else {
exists = !reflect.DeepEqual(value, zero)
}
}
return r.Value, exists
}
// GetOkExists functions the same way as GetOkExists within ResourceData, but
// it also checks the new diff levels to provide data consistent with the
// current state of the customized diff.
//
// This is nearly the same function as GetOk, yet it does not check
// for the zero value of the attribute's type. This allows for attributes
// without a default, to fully check for a literal assignment, regardless
// of the zero-value for that type.
func (d *ResourceDiff) GetOkExists(key string) (interface{}, bool) {
r := d.get(strings.Split(key, "."), "newDiff")
exists := r.Exists && !r.Computed
return r.Value, exists
}
// NewValueKnown returns true if the new value for the given key is available
// as its final value at diff time. If the return value is false, this means
// either the value is based of interpolation that was unavailable at diff
// time, or that the value was explicitly marked as computed by SetNewComputed.
func (d *ResourceDiff) NewValueKnown(key string) bool {
r := d.get(strings.Split(key, "."), "newDiff")
return !r.Computed
}
// HasChange checks to see if there is a change between state and the diff, or
// in the overridden diff.
func (d *ResourceDiff) HasChange(key string) bool {
old, new := d.GetChange(key)
// If the type implements the Equal interface, then call that
// instead of just doing a reflect.DeepEqual. An example where this is
// needed is *Set
if eq, ok := old.(Equal); ok {
return !eq.Equal(new)
}
return !reflect.DeepEqual(old, new)
}
// Id returns the ID of this resource.
//
// Note that technically, ID does not change during diffs (it either has
// already changed in the refresh, or will change on update), hence we do not
// support updating the ID or fetching it from anything else other than state.
func (d *ResourceDiff) Id() string {
var result string
if d.state != nil {
result = d.state.ID
}
return result
}
// getChange gets values from two different levels, designed for use in
// diffChange, HasChange, and GetChange.
//
// This implementation differs from ResourceData's in the way that we first get
// results from the exact levels for the new diff, then from state and diff as
// per normal.
func (d *ResourceDiff) getChange(key string) (getResult, getResult, bool) {
old := d.get(strings.Split(key, "."), "state")
var new getResult
for p := range d.updatedKeys {
if childAddrOf(key, p) {
new = d.getExact(strings.Split(key, "."), "newDiff")
return old, new, true
}
}
new = d.get(strings.Split(key, "."), "newDiff")
return old, new, false
}
// removed checks to see if the key is present in the existing, pre-customized
// diff and if it was marked as NewRemoved.
func (d *ResourceDiff) removed(k string) bool {
diff, ok := d.diff.Attributes[k]
if !ok {
return false
}
return diff.NewRemoved
}
// get performs the appropriate multi-level reader logic for ResourceDiff,
// starting at source. Refer to newResourceDiff for the level order.
func (d *ResourceDiff) get(addr []string, source string) getResult {
result, err := d.multiReader.ReadFieldMerge(addr, source)
if err != nil {
panic(err)
}
return d.finalizeResult(addr, result)
}
// getExact gets an attribute from the exact level referenced by source.
func (d *ResourceDiff) getExact(addr []string, source string) getResult {
result, err := d.multiReader.ReadFieldExact(addr, source)
if err != nil {
panic(err)
}
return d.finalizeResult(addr, result)
}
// finalizeResult does some post-processing of the result produced by get and getExact.
func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult {
// If the result doesn't exist, then we set the value to the zero value
var schema *Schema
if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 {
schema = schemaL[len(schemaL)-1]
}
if result.Value == nil && schema != nil {
result.Value = result.ValueOrZero(schema)
}
// Transform the FieldReadResult into a getResult. It might be worth
// merging these two structures one day.
return getResult{
Value: result.Value,
ValueProcessed: result.ValueProcessed,
Computed: result.Computed,
Exists: result.Exists,
Schema: schema,
}
}
// childAddrOf does a comparison of two addresses to see if one is the child of
// the other.
func childAddrOf(child, parent string) bool {
cs := strings.Split(child, ".")
ps := strings.Split(parent, ".")
if len(ps) > len(cs) {
return false
}
return reflect.DeepEqual(ps, cs[:len(ps)])
}
// checkKey checks the key to make sure it exists and is computed.
func (d *ResourceDiff) checkKey(key, caller string, nested bool) error {
var schema *Schema
if nested {
keyParts := strings.Split(key, ".")
schemaL := addrToSchema(keyParts, d.schema)
if len(schemaL) > 0 {
schema = schemaL[len(schemaL)-1]
}
} else {
s, ok := d.schema[key]
if ok {
schema = s
}
}
if schema == nil {
return fmt.Errorf("%s: invalid key: %s", caller, key)
}
if !schema.Computed {
return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key)
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
package schema
// ResourceImporter defines how a resource is imported in Terraform. This
// can be set onto a Resource struct to make it Importable. Not all resources
// have to be importable; if a Resource doesn't have a ResourceImporter then
// it won't be importable.
//
// "Importing" in Terraform is the process of taking an already-created
// resource and bringing it under Terraform management. This can include
// updating Terraform state, generating Terraform configuration, etc.
type ResourceImporter struct {
// The functions below must all be implemented for importing to work.
// State is called to convert an ID to one or more InstanceState to
// insert into the Terraform state. If this isn't specified, then
// the ID is passed straight through.
State StateFunc
}
// StateFunc is the function called to import a resource into the
// Terraform state. It is given a ResourceData with only ID set. This
// ID is going to be an arbitrary value given by the user and may not map
// directly to the ID format that the resource expects, so that should
// be validated.
//
// This should return a slice of ResourceData that turn into the state
// that was imported. This might be as simple as returning only the argument
// that was given to the function. In other cases (such as AWS security groups),
// an import may fan out to multiple resources and this will have to return
// multiple.
//
// To create the ResourceData structures for other resource types (if
// you have to), instantiate your resource and call the Data function.
type StateFunc func(*ResourceData, interface{}) ([]*ResourceData, error)
// InternalValidate should be called to validate the structure of this
// importer. This should be called in a unit test.
//
// Resource.InternalValidate() will automatically call this, so this doesn't
// need to be called manually. Further, Resource.InternalValidate() is
// automatically called by Provider.InternalValidate(), so you only need
// to internal validate the provider.
func (r *ResourceImporter) InternalValidate() error {
return nil
}
// ImportStatePassthrough is an implementation of StateFunc that can be
// used to simply pass the ID directly through. This should be used only
// in the case that an ID-only refresh is possible.
func ImportStatePassthrough(d *ResourceData, m interface{}) ([]*ResourceData, error) {
return []*ResourceData{d}, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,263 @@
package schema
import (
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/mitchellh/copystructure"
)
const TimeoutKey = "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0"
const TimeoutsConfigKey = "timeouts"
const (
TimeoutCreate = "create"
TimeoutRead = "read"
TimeoutUpdate = "update"
TimeoutDelete = "delete"
TimeoutDefault = "default"
)
func timeoutKeys() []string {
return []string{
TimeoutCreate,
TimeoutRead,
TimeoutUpdate,
TimeoutDelete,
TimeoutDefault,
}
}
// could be time.Duration, int64 or float64
func DefaultTimeout(tx interface{}) *time.Duration {
var td time.Duration
switch raw := tx.(type) {
case time.Duration:
return &raw
case int64:
td = time.Duration(raw)
case float64:
td = time.Duration(int64(raw))
default:
log.Printf("[WARN] Unknown type in DefaultTimeout: %#v", tx)
}
return &td
}
type ResourceTimeout struct {
Create, Read, Update, Delete, Default *time.Duration
}
// ConfigDecode takes a schema and the configuration (available in Diff) and
// validates, parses the timeouts into `t`
func (t *ResourceTimeout) ConfigDecode(s *Resource, c *terraform.ResourceConfig) error {
if s.Timeouts != nil {
raw, err := copystructure.Copy(s.Timeouts)
if err != nil {
log.Printf("[DEBUG] Error with deep copy: %s", err)
}
*t = *raw.(*ResourceTimeout)
}
if raw, ok := c.Config[TimeoutsConfigKey]; ok {
var rawTimeouts []map[string]interface{}
switch raw := raw.(type) {
case map[string]interface{}:
rawTimeouts = append(rawTimeouts, raw)
case []map[string]interface{}:
rawTimeouts = raw
case string:
if raw == hcl2shim.UnknownVariableValue {
// Timeout is not defined in the config
// Defaults will be used instead
return nil
} else {
log.Printf("[ERROR] Invalid timeout value: %q", raw)
return fmt.Errorf("Invalid Timeout value found")
}
case []interface{}:
for _, r := range raw {
if rMap, ok := r.(map[string]interface{}); ok {
rawTimeouts = append(rawTimeouts, rMap)
} else {
// Go will not allow a fallthrough
log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
return fmt.Errorf("Invalid Timeout structure found")
}
}
default:
log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
return fmt.Errorf("Invalid Timeout structure found")
}
for _, timeoutValues := range rawTimeouts {
for timeKey, timeValue := range timeoutValues {
// validate that we're dealing with the normal CRUD actions
var found bool
for _, key := range timeoutKeys() {
if timeKey == key {
found = true
break
}
}
if !found {
return fmt.Errorf("Unsupported Timeout configuration key found (%s)", timeKey)
}
// Get timeout
rt, err := time.ParseDuration(timeValue.(string))
if err != nil {
return fmt.Errorf("Error parsing %q timeout: %s", timeKey, err)
}
var timeout *time.Duration
switch timeKey {
case TimeoutCreate:
timeout = t.Create
case TimeoutUpdate:
timeout = t.Update
case TimeoutRead:
timeout = t.Read
case TimeoutDelete:
timeout = t.Delete
case TimeoutDefault:
timeout = t.Default
}
// If the resource has not delcared this in the definition, then error
// with an unsupported message
if timeout == nil {
return unsupportedTimeoutKeyError(timeKey)
}
*timeout = rt
}
return nil
}
}
return nil
}
func unsupportedTimeoutKeyError(key string) error {
return fmt.Errorf("Timeout Key (%s) is not supported", key)
}
// DiffEncode, StateEncode, and MetaDecode are analogous to the Go stdlib JSONEncoder
// interface: they encode/decode a timeouts struct from an instance diff, which is
// where the timeout data is stored after a diff to pass into Apply.
//
// StateEncode encodes the timeout into the ResourceData's InstanceState for
// saving to state
//
func (t *ResourceTimeout) DiffEncode(id *terraform.InstanceDiff) error {
return t.metaEncode(id)
}
func (t *ResourceTimeout) StateEncode(is *terraform.InstanceState) error {
return t.metaEncode(is)
}
// metaEncode encodes the ResourceTimeout into a map[string]interface{} format
// and stores it in the Meta field of the interface it's given.
// Assumes the interface is either *terraform.InstanceState or
// *terraform.InstanceDiff, returns an error otherwise
func (t *ResourceTimeout) metaEncode(ids interface{}) error {
m := make(map[string]interface{})
if t.Create != nil {
m[TimeoutCreate] = t.Create.Nanoseconds()
}
if t.Read != nil {
m[TimeoutRead] = t.Read.Nanoseconds()
}
if t.Update != nil {
m[TimeoutUpdate] = t.Update.Nanoseconds()
}
if t.Delete != nil {
m[TimeoutDelete] = t.Delete.Nanoseconds()
}
if t.Default != nil {
m[TimeoutDefault] = t.Default.Nanoseconds()
// for any key above that is nil, if default is specified, we need to
// populate it with the default
for _, k := range timeoutKeys() {
if _, ok := m[k]; !ok {
m[k] = t.Default.Nanoseconds()
}
}
}
// only add the Timeout to the Meta if we have values
if len(m) > 0 {
switch instance := ids.(type) {
case *terraform.InstanceDiff:
if instance.Meta == nil {
instance.Meta = make(map[string]interface{})
}
instance.Meta[TimeoutKey] = m
case *terraform.InstanceState:
if instance.Meta == nil {
instance.Meta = make(map[string]interface{})
}
instance.Meta[TimeoutKey] = m
default:
return fmt.Errorf("Error matching type for Diff Encode")
}
}
return nil
}
func (t *ResourceTimeout) StateDecode(id *terraform.InstanceState) error {
return t.metaDecode(id)
}
func (t *ResourceTimeout) DiffDecode(is *terraform.InstanceDiff) error {
return t.metaDecode(is)
}
func (t *ResourceTimeout) metaDecode(ids interface{}) error {
var rawMeta interface{}
var ok bool
switch rawInstance := ids.(type) {
case *terraform.InstanceDiff:
rawMeta, ok = rawInstance.Meta[TimeoutKey]
if !ok {
return nil
}
case *terraform.InstanceState:
rawMeta, ok = rawInstance.Meta[TimeoutKey]
if !ok {
return nil
}
default:
return fmt.Errorf("Unknown or unsupported type in metaDecode: %#v", ids)
}
times := rawMeta.(map[string]interface{})
if len(times) == 0 {
return nil
}
if v, ok := times[TimeoutCreate]; ok {
t.Create = DefaultTimeout(v)
}
if v, ok := times[TimeoutRead]; ok {
t.Read = DefaultTimeout(v)
}
if v, ok := times[TimeoutUpdate]; ok {
t.Update = DefaultTimeout(v)
}
if v, ok := times[TimeoutDelete]; ok {
t.Delete = DefaultTimeout(v)
}
if v, ok := times[TimeoutDefault]; ok {
t.Default = DefaultTimeout(v)
}
return nil
}

View File

@ -0,0 +1,376 @@
package schema
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func TestResourceTimeout_ConfigDecode_badkey(t *testing.T) {
cases := []struct {
Name string
// what the resource has defined in source
ResourceDefaultTimeout *ResourceTimeout
// configuration provider by user in tf file
Config map[string]interface{}
// what we expect the parsed ResourceTimeout to be
Expected *ResourceTimeout
// Should we have an error (key not defined in source)
ShouldErr bool
}{
{
Name: "Source does not define 'delete' key",
ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
Config: expectedConfigForValues(2, 0, 0, 1, 0),
Expected: timeoutForValues(10, 0, 5, 0, 0),
ShouldErr: true,
},
{
Name: "Config overrides create",
ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
Config: expectedConfigForValues(2, 0, 7, 0, 0),
Expected: timeoutForValues(2, 0, 7, 0, 0),
ShouldErr: false,
},
{
Name: "Config overrides create, default provided. Should still have zero values",
ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
Config: expectedConfigForValues(2, 0, 7, 0, 0),
Expected: timeoutForValues(2, 0, 7, 0, 3),
ShouldErr: false,
},
{
Name: "Use something besides 'minutes'",
ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
Config: map[string]interface{}{
"create": "2h",
},
Expected: timeoutForValues(120, 0, 5, 0, 3),
ShouldErr: false,
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) {
r := &Resource{
Timeouts: c.ResourceDefaultTimeout,
}
conf := terraform.NewResourceConfigRaw(
map[string]interface{}{
"foo": "bar",
TimeoutsConfigKey: c.Config,
},
)
timeout := &ResourceTimeout{}
decodeErr := timeout.ConfigDecode(r, conf)
if c.ShouldErr {
if decodeErr == nil {
t.Fatalf("ConfigDecode case (%d): Expected bad timeout key: %s", i, decodeErr)
}
// should error, err was not nil, continue
return
} else {
if decodeErr != nil {
// should not error, error was not nil, fatal
t.Fatalf("decodeError was not nil: %s", decodeErr)
}
}
if !reflect.DeepEqual(c.Expected, timeout) {
t.Fatalf("ConfigDecode match error case (%d).\nExpected:\n%#v\nGot:\n%#v", i, c.Expected, timeout)
}
})
}
}
func TestResourceTimeout_ConfigDecode(t *testing.T) {
r := &Resource{
Timeouts: &ResourceTimeout{
Create: DefaultTimeout(10 * time.Minute),
Update: DefaultTimeout(5 * time.Minute),
},
}
c := terraform.NewResourceConfigRaw(
map[string]interface{}{
"foo": "bar",
TimeoutsConfigKey: map[string]interface{}{
"create": "2m",
"update": "1m",
},
},
)
timeout := &ResourceTimeout{}
err := timeout.ConfigDecode(r, c)
if err != nil {
t.Fatalf("Expected good timeout returned:, %s", err)
}
expected := &ResourceTimeout{
Create: DefaultTimeout(2 * time.Minute),
Update: DefaultTimeout(1 * time.Minute),
}
if !reflect.DeepEqual(timeout, expected) {
t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout)
}
}
func TestResourceTimeout_legacyConfigDecode(t *testing.T) {
r := &Resource{
Timeouts: &ResourceTimeout{
Create: DefaultTimeout(10 * time.Minute),
Update: DefaultTimeout(5 * time.Minute),
},
}
c := terraform.NewResourceConfigRaw(
map[string]interface{}{
"foo": "bar",
TimeoutsConfigKey: []interface{}{
map[string]interface{}{
"create": "2m",
"update": "1m",
},
},
},
)
timeout := &ResourceTimeout{}
err := timeout.ConfigDecode(r, c)
if err != nil {
t.Fatalf("Expected good timeout returned:, %s", err)
}
expected := &ResourceTimeout{
Create: DefaultTimeout(2 * time.Minute),
Update: DefaultTimeout(1 * time.Minute),
}
if !reflect.DeepEqual(timeout, expected) {
t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout)
}
}
func TestResourceTimeout_DiffEncode_basic(t *testing.T) {
cases := []struct {
Timeout *ResourceTimeout
Expected map[string]interface{}
// Not immediately clear when an error would hit
ShouldErr bool
}{
// Two fields
{
Timeout: timeoutForValues(10, 0, 5, 0, 0),
Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)},
ShouldErr: false,
},
// Two fields, one is Default
{
Timeout: timeoutForValues(10, 0, 0, 0, 7),
Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)},
ShouldErr: false,
},
// All fields
{
Timeout: timeoutForValues(10, 3, 4, 1, 7),
Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)},
ShouldErr: false,
},
// No fields
{
Timeout: &ResourceTimeout{},
Expected: nil,
ShouldErr: false,
},
}
for _, c := range cases {
state := &terraform.InstanceDiff{}
err := c.Timeout.DiffEncode(state)
if err != nil && !c.ShouldErr {
t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
}
// should maybe just compare [TimeoutKey] but for now we're assuming only
// that in Meta
if !reflect.DeepEqual(state.Meta, c.Expected) {
t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
}
}
// same test cases but for InstanceState
for _, c := range cases {
state := &terraform.InstanceState{}
err := c.Timeout.StateEncode(state)
if err != nil && !c.ShouldErr {
t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
}
// should maybe just compare [TimeoutKey] but for now we're assuming only
// that in Meta
if !reflect.DeepEqual(state.Meta, c.Expected) {
t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
}
}
}
func TestResourceTimeout_MetaDecode_basic(t *testing.T) {
cases := []struct {
State *terraform.InstanceDiff
Expected *ResourceTimeout
// Not immediately clear when an error would hit
ShouldErr bool
}{
// Two fields
{
State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)}},
Expected: timeoutForValues(10, 0, 5, 0, 0),
ShouldErr: false,
},
// Two fields, one is Default
{
State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)}},
Expected: timeoutForValues(10, 7, 7, 7, 7),
ShouldErr: false,
},
// All fields
{
State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)}},
Expected: timeoutForValues(10, 3, 4, 1, 7),
ShouldErr: false,
},
// No fields
{
State: &terraform.InstanceDiff{},
Expected: &ResourceTimeout{},
ShouldErr: false,
},
}
for _, c := range cases {
rt := &ResourceTimeout{}
err := rt.DiffDecode(c.State)
if err != nil && !c.ShouldErr {
t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, rt)
}
// should maybe just compare [TimeoutKey] but for now we're assuming only
// that in Meta
if !reflect.DeepEqual(rt, c.Expected) {
t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, rt)
}
}
}
func timeoutForValues(create, read, update, del, def int) *ResourceTimeout {
rt := ResourceTimeout{}
if create != 0 {
rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
}
if read != 0 {
rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
}
if update != 0 {
rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
}
if del != 0 {
rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
}
if def != 0 {
rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
}
return &rt
}
// Generates a ResourceTimeout struct that should reflect the
// d.Timeout("key") results
func expectedTimeoutForValues(create, read, update, del, def int) *ResourceTimeout {
rt := ResourceTimeout{}
defaultValues := []*int{&create, &read, &update, &del, &def}
for _, v := range defaultValues {
if *v == 0 {
*v = 20
}
}
if create != 0 {
rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
}
if read != 0 {
rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
}
if update != 0 {
rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
}
if del != 0 {
rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
}
if def != 0 {
rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
}
return &rt
}
func expectedForValues(create, read, update, del, def int) map[string]interface{} {
ex := make(map[string]interface{})
if create != 0 {
ex["create"] = DefaultTimeout(time.Duration(create) * time.Minute).Nanoseconds()
}
if read != 0 {
ex["read"] = DefaultTimeout(time.Duration(read) * time.Minute).Nanoseconds()
}
if update != 0 {
ex["update"] = DefaultTimeout(time.Duration(update) * time.Minute).Nanoseconds()
}
if del != 0 {
ex["delete"] = DefaultTimeout(time.Duration(del) * time.Minute).Nanoseconds()
}
if def != 0 {
defNano := DefaultTimeout(time.Duration(def) * time.Minute).Nanoseconds()
ex["default"] = defNano
for _, k := range timeoutKeys() {
if _, ok := ex[k]; !ok {
ex[k] = defNano
}
}
}
return ex
}
func expectedConfigForValues(create, read, update, delete, def int) map[string]interface{} {
ex := make(map[string]interface{}, 0)
if create != 0 {
ex["create"] = fmt.Sprintf("%dm", create)
}
if read != 0 {
ex["read"] = fmt.Sprintf("%dm", read)
}
if update != 0 {
ex["update"] = fmt.Sprintf("%dm", update)
}
if delete != 0 {
ex["delete"] = fmt.Sprintf("%dm", delete)
}
if def != 0 {
ex["default"] = fmt.Sprintf("%dm", def)
}
return ex
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,125 @@
package schema
import (
"bytes"
"fmt"
"sort"
"strconv"
)
func SerializeValueForHash(buf *bytes.Buffer, val interface{}, schema *Schema) {
if val == nil {
buf.WriteRune(';')
return
}
switch schema.Type {
case TypeBool:
if val.(bool) {
buf.WriteRune('1')
} else {
buf.WriteRune('0')
}
case TypeInt:
buf.WriteString(strconv.Itoa(val.(int)))
case TypeFloat:
buf.WriteString(strconv.FormatFloat(val.(float64), 'g', -1, 64))
case TypeString:
buf.WriteString(val.(string))
case TypeList:
buf.WriteRune('(')
l := val.([]interface{})
for _, innerVal := range l {
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
}
buf.WriteRune(')')
case TypeMap:
m := val.(map[string]interface{})
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
buf.WriteRune('[')
for _, k := range keys {
innerVal := m[k]
if innerVal == nil {
continue
}
buf.WriteString(k)
buf.WriteRune(':')
switch innerVal := innerVal.(type) {
case int:
buf.WriteString(strconv.Itoa(innerVal))
case float64:
buf.WriteString(strconv.FormatFloat(innerVal, 'g', -1, 64))
case string:
buf.WriteString(innerVal)
default:
panic(fmt.Sprintf("unknown value type in TypeMap %T", innerVal))
}
buf.WriteRune(';')
}
buf.WriteRune(']')
case TypeSet:
buf.WriteRune('{')
s := val.(*Set)
for _, innerVal := range s.List() {
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
}
buf.WriteRune('}')
default:
panic("unknown schema type to serialize")
}
buf.WriteRune(';')
}
// SerializeValueForHash appends a serialization of the given resource config
// to the given buffer, guaranteeing deterministic results given the same value
// and schema.
//
// Its primary purpose is as input into a hashing function in order
// to hash complex substructures when used in sets, and so the serialization
// is not reversible.
func SerializeResourceForHash(buf *bytes.Buffer, val interface{}, resource *Resource) {
if val == nil {
return
}
sm := resource.Schema
m := val.(map[string]interface{})
var keys []string
for k := range sm {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
innerSchema := sm[k]
// Skip attributes that are not user-provided. Computed attributes
// do not contribute to the hash since their ultimate value cannot
// be known at plan/diff time.
if !(innerSchema.Required || innerSchema.Optional) {
continue
}
buf.WriteString(k)
buf.WriteRune(':')
innerVal := m[k]
SerializeValueForHash(buf, innerVal, innerSchema)
}
}
func serializeCollectionMemberForHash(buf *bytes.Buffer, val interface{}, elem interface{}) {
switch tElem := elem.(type) {
case *Schema:
SerializeValueForHash(buf, val, tElem)
case *Resource:
buf.WriteRune('<')
SerializeResourceForHash(buf, val, tElem)
buf.WriteString(">;")
default:
panic(fmt.Sprintf("invalid element type: %T", tElem))
}
}

View File

@ -0,0 +1,238 @@
package schema
import (
"bytes"
"testing"
)
func TestSerializeForHash(t *testing.T) {
type testCase struct {
Schema interface{}
Value interface{}
Expected string
}
tests := []testCase{
testCase{
Schema: &Schema{
Type: TypeInt,
},
Value: 0,
Expected: "0;",
},
testCase{
Schema: &Schema{
Type: TypeInt,
},
Value: 200,
Expected: "200;",
},
testCase{
Schema: &Schema{
Type: TypeBool,
},
Value: true,
Expected: "1;",
},
testCase{
Schema: &Schema{
Type: TypeBool,
},
Value: false,
Expected: "0;",
},
testCase{
Schema: &Schema{
Type: TypeFloat,
},
Value: 1.0,
Expected: "1;",
},
testCase{
Schema: &Schema{
Type: TypeFloat,
},
Value: 1.54,
Expected: "1.54;",
},
testCase{
Schema: &Schema{
Type: TypeFloat,
},
Value: 0.1,
Expected: "0.1;",
},
testCase{
Schema: &Schema{
Type: TypeString,
},
Value: "hello",
Expected: "hello;",
},
testCase{
Schema: &Schema{
Type: TypeString,
},
Value: "1",
Expected: "1;",
},
testCase{
Schema: &Schema{
Type: TypeList,
Elem: &Schema{
Type: TypeString,
},
},
Value: []interface{}{},
Expected: "();",
},
testCase{
Schema: &Schema{
Type: TypeList,
Elem: &Schema{
Type: TypeString,
},
},
Value: []interface{}{"hello", "world"},
Expected: "(hello;world;);",
},
testCase{
Schema: &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"fo": &Schema{
Type: TypeString,
Required: true,
},
"fum": &Schema{
Type: TypeString,
Required: true,
},
},
},
},
Value: []interface{}{
map[string]interface{}{
"fo": "bar",
},
map[string]interface{}{
"fo": "baz",
"fum": "boz",
},
},
Expected: "(<fo:bar;fum:;>;<fo:baz;fum:boz;>;);",
},
testCase{
Schema: &Schema{
Type: TypeSet,
Elem: &Schema{
Type: TypeString,
},
},
Value: NewSet(func(i interface{}) int { return len(i.(string)) }, []interface{}{
"hello",
"woo",
}),
Expected: "{woo;hello;};",
},
testCase{
Schema: &Schema{
Type: TypeMap,
Elem: &Schema{
Type: TypeString,
},
},
Value: map[string]interface{}{
"foo": "bar",
"baz": "foo",
},
Expected: "[baz:foo;foo:bar;];",
},
testCase{
Schema: &Resource{
Schema: map[string]*Schema{
"name": &Schema{
Type: TypeString,
Required: true,
},
"size": &Schema{
Type: TypeInt,
Optional: true,
},
"green": &Schema{
Type: TypeBool,
Optional: true,
Computed: true,
},
"upside_down": &Schema{
Type: TypeBool,
Computed: true,
},
},
},
Value: map[string]interface{}{
"name": "my-fun-database",
"size": 12,
"green": true,
},
Expected: "green:1;name:my-fun-database;size:12;",
},
// test TypeMap nested in Schema: GH-7091
testCase{
Schema: &Resource{
Schema: map[string]*Schema{
"outer": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{
Type: TypeMap,
Optional: true,
},
},
},
},
Value: map[string]interface{}{
"outer": NewSet(func(i interface{}) int { return 42 }, []interface{}{
map[string]interface{}{
"foo": "bar",
"baz": "foo",
},
}),
},
Expected: "outer:{[baz:foo;foo:bar;];};",
},
}
for _, test := range tests {
var gotBuf bytes.Buffer
schema := test.Schema
switch s := schema.(type) {
case *Schema:
SerializeValueForHash(&gotBuf, test.Value, s)
case *Resource:
SerializeResourceForHash(&gotBuf, test.Value, s)
}
got := gotBuf.String()
if got != test.Expected {
t.Errorf("hash(%#v) got %#v, but want %#v", test.Value, got, test.Expected)
}
}
}

View File

@ -0,0 +1,250 @@
package schema
import (
"bytes"
"fmt"
"reflect"
"sort"
"strconv"
"sync"
"github.com/hashicorp/terraform/helper/hashcode"
)
// HashString hashes strings. If you want a Set of strings, this is the
// SchemaSetFunc you want.
func HashString(v interface{}) int {
return hashcode.String(v.(string))
}
// HashInt hashes integers. If you want a Set of integers, this is the
// SchemaSetFunc you want.
func HashInt(v interface{}) int {
return hashcode.String(strconv.Itoa(v.(int)))
}
// HashResource hashes complex structures that are described using
// a *Resource. This is the default set implementation used when a set's
// element type is a full resource.
func HashResource(resource *Resource) SchemaSetFunc {
return func(v interface{}) int {
var buf bytes.Buffer
SerializeResourceForHash(&buf, v, resource)
return hashcode.String(buf.String())
}
}
// HashSchema hashes values that are described using a *Schema. This is the
// default set implementation used when a set's element type is a single
// schema.
func HashSchema(schema *Schema) SchemaSetFunc {
return func(v interface{}) int {
var buf bytes.Buffer
SerializeValueForHash(&buf, v, schema)
return hashcode.String(buf.String())
}
}
// Set is a set data structure that is returned for elements of type
// TypeSet.
type Set struct {
F SchemaSetFunc
m map[string]interface{}
once sync.Once
}
// NewSet is a convenience method for creating a new set with the given
// items.
func NewSet(f SchemaSetFunc, items []interface{}) *Set {
s := &Set{F: f}
for _, i := range items {
s.Add(i)
}
return s
}
// CopySet returns a copy of another set.
func CopySet(otherSet *Set) *Set {
return NewSet(otherSet.F, otherSet.List())
}
// Add adds an item to the set if it isn't already in the set.
func (s *Set) Add(item interface{}) {
s.add(item, false)
}
// Remove removes an item if it's already in the set. Idempotent.
func (s *Set) Remove(item interface{}) {
s.remove(item)
}
// Contains checks if the set has the given item.
func (s *Set) Contains(item interface{}) bool {
_, ok := s.m[s.hash(item)]
return ok
}
// Len returns the amount of items in the set.
func (s *Set) Len() int {
return len(s.m)
}
// List returns the elements of this set in slice format.
//
// The order of the returned elements is deterministic. Given the same
// set, the order of this will always be the same.
func (s *Set) List() []interface{} {
result := make([]interface{}, len(s.m))
for i, k := range s.listCode() {
result[i] = s.m[k]
}
return result
}
// Difference performs a set difference of the two sets, returning
// a new third set that has only the elements unique to this set.
func (s *Set) Difference(other *Set) *Set {
result := &Set{F: s.F}
result.once.Do(result.init)
for k, v := range s.m {
if _, ok := other.m[k]; !ok {
result.m[k] = v
}
}
return result
}
// Intersection performs the set intersection of the two sets
// and returns a new third set.
func (s *Set) Intersection(other *Set) *Set {
result := &Set{F: s.F}
result.once.Do(result.init)
for k, v := range s.m {
if _, ok := other.m[k]; ok {
result.m[k] = v
}
}
return result
}
// Union performs the set union of the two sets and returns a new third
// set.
func (s *Set) Union(other *Set) *Set {
result := &Set{F: s.F}
result.once.Do(result.init)
for k, v := range s.m {
result.m[k] = v
}
for k, v := range other.m {
result.m[k] = v
}
return result
}
func (s *Set) Equal(raw interface{}) bool {
other, ok := raw.(*Set)
if !ok {
return false
}
return reflect.DeepEqual(s.m, other.m)
}
// HashEqual simply checks to the keys the top-level map to the keys in the
// other set's top-level map to see if they are equal. This obviously assumes
// you have a properly working hash function - use HashResource if in doubt.
func (s *Set) HashEqual(raw interface{}) bool {
other, ok := raw.(*Set)
if !ok {
return false
}
ks1 := make([]string, 0)
ks2 := make([]string, 0)
for k := range s.m {
ks1 = append(ks1, k)
}
for k := range other.m {
ks2 = append(ks2, k)
}
sort.Strings(ks1)
sort.Strings(ks2)
return reflect.DeepEqual(ks1, ks2)
}
func (s *Set) GoString() string {
return fmt.Sprintf("*Set(%#v)", s.m)
}
func (s *Set) init() {
s.m = make(map[string]interface{})
}
func (s *Set) add(item interface{}, computed bool) string {
s.once.Do(s.init)
code := s.hash(item)
if computed {
code = "~" + code
if isProto5() {
tmpCode := code
count := 0
for _, exists := s.m[tmpCode]; exists; _, exists = s.m[tmpCode] {
count++
tmpCode = fmt.Sprintf("%s%d", code, count)
}
code = tmpCode
}
}
if _, ok := s.m[code]; !ok {
s.m[code] = item
}
return code
}
func (s *Set) hash(item interface{}) string {
code := s.F(item)
// Always return a nonnegative hashcode.
if code < 0 {
code = -code
}
return strconv.Itoa(code)
}
func (s *Set) remove(item interface{}) string {
s.once.Do(s.init)
code := s.hash(item)
delete(s.m, code)
return code
}
func (s *Set) index(item interface{}) int {
return sort.SearchStrings(s.listCode(), s.hash(item))
}
func (s *Set) listCode() []string {
// Sort the hash codes so the order of the list is deterministic
keys := make([]string, 0, len(s.m))
for k := range s.m {
keys = append(keys, k)
}
sort.Sort(sort.StringSlice(keys))
return keys
}

View File

@ -0,0 +1,217 @@
package schema
import (
"reflect"
"testing"
)
func TestSetAdd(t *testing.T) {
s := &Set{F: testSetInt}
s.Add(1)
s.Add(5)
s.Add(25)
expected := []interface{}{1, 25, 5}
actual := s.List()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestSetAdd_negative(t *testing.T) {
// Since we don't allow negative hashes, this should just hash to the
// same thing...
s := &Set{F: testSetInt}
s.Add(-1)
s.Add(1)
expected := []interface{}{-1}
actual := s.List()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestSetContains(t *testing.T) {
s := &Set{F: testSetInt}
s.Add(5)
s.Add(-5)
if s.Contains(2) {
t.Fatal("should not contain")
}
if !s.Contains(5) {
t.Fatal("should contain")
}
if !s.Contains(-5) {
t.Fatal("should contain")
}
}
func TestSetDifference(t *testing.T) {
s1 := &Set{F: testSetInt}
s2 := &Set{F: testSetInt}
s1.Add(1)
s1.Add(5)
s2.Add(5)
s2.Add(25)
difference := s1.Difference(s2)
difference.Add(2)
expected := []interface{}{1, 2}
actual := difference.List()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestSetIntersection(t *testing.T) {
s1 := &Set{F: testSetInt}
s2 := &Set{F: testSetInt}
s1.Add(1)
s1.Add(5)
s2.Add(5)
s2.Add(25)
intersection := s1.Intersection(s2)
intersection.Add(2)
expected := []interface{}{2, 5}
actual := intersection.List()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestSetUnion(t *testing.T) {
s1 := &Set{F: testSetInt}
s2 := &Set{F: testSetInt}
s1.Add(1)
s1.Add(5)
s2.Add(5)
s2.Add(25)
union := s1.Union(s2)
union.Add(2)
expected := []interface{}{1, 2, 25, 5}
actual := union.List()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func testSetInt(v interface{}) int {
return v.(int)
}
func TestHashResource_nil(t *testing.T) {
resource := &Resource{
Schema: map[string]*Schema{
"name": {
Type: TypeString,
Optional: true,
},
},
}
f := HashResource(resource)
idx := f(nil)
if idx != 0 {
t.Fatalf("Expected 0 when hashing nil, given: %d", idx)
}
}
func TestHashEqual(t *testing.T) {
nested := &Resource{
Schema: map[string]*Schema{
"foo": {
Type: TypeString,
Optional: true,
},
},
}
root := &Resource{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Optional: true,
},
"nested": {
Type: TypeSet,
Optional: true,
Elem: nested,
},
},
}
n1 := map[string]interface{}{"foo": "bar"}
n2 := map[string]interface{}{"foo": "baz"}
r1 := map[string]interface{}{
"bar": "baz",
"nested": NewSet(HashResource(nested), []interface{}{n1}),
}
r2 := map[string]interface{}{
"bar": "qux",
"nested": NewSet(HashResource(nested), []interface{}{n2}),
}
r3 := map[string]interface{}{
"bar": "baz",
"nested": NewSet(HashResource(nested), []interface{}{n2}),
}
r4 := map[string]interface{}{
"bar": "qux",
"nested": NewSet(HashResource(nested), []interface{}{n1}),
}
s1 := NewSet(HashResource(root), []interface{}{r1})
s2 := NewSet(HashResource(root), []interface{}{r2})
s3 := NewSet(HashResource(root), []interface{}{r3})
s4 := NewSet(HashResource(root), []interface{}{r4})
cases := []struct {
name string
set *Set
compare *Set
expected bool
}{
{
name: "equal",
set: s1,
compare: s1,
expected: true,
},
{
name: "not equal",
set: s1,
compare: s2,
expected: false,
},
{
name: "outer equal, should still not be equal",
set: s1,
compare: s3,
expected: false,
},
{
name: "inner equal, should still not be equal",
set: s1,
compare: s4,
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual := tc.set.HashEqual(tc.compare)
if tc.expected != actual {
t.Fatalf("expected %t, got %t", tc.expected, actual)
}
})
}
}

View File

@ -0,0 +1,115 @@
package schema
import (
"encoding/json"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
// DiffFromValues takes the current state and desired state as cty.Values and
// derives a terraform.InstanceDiff to give to the legacy providers. This is
// used to take the states provided by the new ApplyResourceChange method and
// convert them to a state+diff required for the legacy Apply method.
func DiffFromValues(prior, planned cty.Value, res *Resource) (*terraform.InstanceDiff, error) {
return diffFromValues(prior, planned, res, nil)
}
// diffFromValues takes an additional CustomizeDiffFunc, so we can generate our
// test fixtures from the legacy tests. In the new provider protocol the diff
// only needs to be created for the apply operation, and any customizations
// have already been done.
func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffFunc) (*terraform.InstanceDiff, error) {
instanceState, err := res.ShimInstanceStateFromValue(prior)
if err != nil {
return nil, err
}
configSchema := res.CoreConfigSchema()
cfg := terraform.NewResourceConfigShimmed(planned, configSchema)
removeConfigUnknowns(cfg.Config)
removeConfigUnknowns(cfg.Raw)
diff, err := schemaMap(res.Schema).Diff(instanceState, cfg, cust, nil, false)
if err != nil {
return nil, err
}
return diff, err
}
// During apply the only unknown values are those which are to be computed by
// the resource itself. These may have been marked as unknown config values, and
// need to be removed to prevent the UnknownVariableValue from appearing the diff.
func removeConfigUnknowns(cfg map[string]interface{}) {
for k, v := range cfg {
switch v := v.(type) {
case string:
if v == hcl2shim.UnknownVariableValue {
delete(cfg, k)
}
case []interface{}:
for _, i := range v {
if m, ok := i.(map[string]interface{}); ok {
removeConfigUnknowns(m)
}
}
case map[string]interface{}:
removeConfigUnknowns(v)
}
}
}
// ApplyDiff takes a cty.Value state and applies a terraform.InstanceDiff to
// get a new cty.Value state. This is used to convert the diff returned from
// the legacy provider Diff method to the state required for the new
// PlanResourceChange method.
func ApplyDiff(base cty.Value, d *terraform.InstanceDiff, schema *configschema.Block) (cty.Value, error) {
return d.ApplyToValue(base, schema)
}
// StateValueToJSONMap converts a cty.Value to generic JSON map via the cty JSON
// encoding.
func StateValueToJSONMap(val cty.Value, ty cty.Type) (map[string]interface{}, error) {
js, err := ctyjson.Marshal(val, ty)
if err != nil {
return nil, err
}
var m map[string]interface{}
if err := json.Unmarshal(js, &m); err != nil {
return nil, err
}
return m, nil
}
// JSONMapToStateValue takes a generic json map[string]interface{} and converts it
// to the specific type, ensuring that the values conform to the schema.
func JSONMapToStateValue(m map[string]interface{}, block *configschema.Block) (cty.Value, error) {
var val cty.Value
js, err := json.Marshal(m)
if err != nil {
return val, err
}
val, err = ctyjson.Unmarshal(js, block.ImpliedType())
if err != nil {
return val, err
}
return block.CoerceValue(val)
}
// StateValueFromInstanceState converts a terraform.InstanceState to a
// cty.Value as described by the provided cty.Type, and maintains the resource
// ID as the "id" attribute.
func StateValueFromInstanceState(is *terraform.InstanceState, ty cty.Type) (cty.Value, error) {
return is.AttrsAsObjectValue(ty)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
package schema
import (
"testing"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
// TestResourceDataRaw creates a ResourceData from a raw configuration map.
func TestResourceDataRaw(
t *testing.T, schema map[string]*Schema, raw map[string]interface{}) *ResourceData {
t.Helper()
c := terraform.NewResourceConfigRaw(raw)
sm := schemaMap(schema)
diff, err := sm.Diff(nil, c, nil, nil, true)
if err != nil {
t.Fatalf("err: %s", err)
}
result, err := sm.Data(nil, diff)
if err != nil {
t.Fatalf("err: %s", err)
}
return result
}

View File

@ -0,0 +1,21 @@
package schema
//go:generate go run golang.org/x/tools/cmd/stringer -type=ValueType valuetype.go
// ValueType is an enum of the type that can be represented by a schema.
type ValueType int
const (
TypeInvalid ValueType = iota
TypeBool
TypeInt
TypeFloat
TypeString
TypeList
TypeMap
TypeSet
typeObject
)
// NOTE: ValueType has more functions defined on it in schema.go. We can't
// put them here because we reference other files.

View File

@ -0,0 +1,31 @@
// Code generated by "stringer -type=ValueType valuetype.go"; DO NOT EDIT.
package schema
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TypeInvalid-0]
_ = x[TypeBool-1]
_ = x[TypeInt-2]
_ = x[TypeFloat-3]
_ = x[TypeString-4]
_ = x[TypeList-5]
_ = x[TypeMap-6]
_ = x[TypeSet-7]
_ = x[typeObject-8]
}
const _ValueType_name = "TypeInvalidTypeBoolTypeIntTypeFloatTypeStringTypeListTypeMapTypeSettypeObject"
var _ValueType_index = [...]uint8{0, 11, 19, 26, 35, 45, 53, 60, 67, 77}
func (i ValueType) String() string {
if i < 0 || i >= ValueType(len(_ValueType_index)-1) {
return "ValueType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ValueType_name[_ValueType_index[i]:_ValueType_index[i+1]]
}