2020-11-18 00:02:15 +01:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
|
2021-05-17 19:11:06 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
2020-11-18 00:02:15 +01:00
|
|
|
"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
|
|
|
|
}
|