| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package schema |
| |
| import ( |
| "context" |
| "fmt" |
| |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/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 |
| } |