From 37f391f1f7f797677e9cafef3dad9260a1d205d1 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 25 Feb 2019 19:06:09 -0500 Subject: [PATCH] insert defaults during Backend.PrepareConfig Lookup any defaults and insert them into the config value before validation. --- helper/schema/backend.go | 85 ++++++++++++++++++++++++++++++++--- helper/schema/backend_test.go | 80 +++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 8 deletions(-) diff --git a/helper/schema/backend.go b/helper/schema/backend.go index d88757459..64c3b9f69 100644 --- a/helper/schema/backend.go +++ b/helper/schema/backend.go @@ -2,6 +2,7 @@ package schema import ( "context" + "fmt" "github.com/hashicorp/terraform/tfdiags" "github.com/zclconf/go-cty/cty" @@ -9,6 +10,7 @@ import ( "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/terraform" + ctyconvert "github.com/zclconf/go-cty/cty/convert" ) // Backend represents a partial backend.Backend implementation and simplifies @@ -49,13 +51,86 @@ func (b *Backend) ConfigSchema() *configschema.Block { return b.CoreConfigSchema() } -func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { +func (b *Backend) PrepareConfig(configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { if b == nil { - return obj, 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) } - var diags tfdiags.Diagnostics - shimRC := b.shimConfig(obj) + // 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)) @@ -63,7 +138,7 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) for _, err := range errs { diags = diags.Append(err) } - return obj, diags + return configVal, diags } func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { diff --git a/helper/schema/backend_test.go b/helper/schema/backend_test.go index 3517aee73..609ce5967 100644 --- a/helper/schema/backend_test.go +++ b/helper/schema/backend_test.go @@ -8,11 +8,12 @@ import ( "github.com/zclconf/go-cty/cty" ) -func TestBackendValidate(t *testing.T) { +func TestBackendPrepare(t *testing.T) { cases := []struct { Name string B *Backend Config map[string]cty.Value + Expect map[string]cty.Value Err bool }{ { @@ -26,6 +27,7 @@ func TestBackendValidate(t *testing.T) { }, }, map[string]cty.Value{}, + map[string]cty.Value{}, true, }, @@ -42,15 +44,87 @@ func TestBackendValidate(t *testing.T) { 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) { - _, diags := tc.B.PrepareConfig(cty.ObjectVal(tc.Config)) + configVal, diags := tc.B.PrepareConfig(cty.ObjectVal(tc.Config)) if diags.HasErrors() != tc.Err { - t.Errorf("wrong number of diagnostics") + 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) } }) }