github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/schema/backend.go (about)

     1  package schema
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
     8  	"github.com/zclconf/go-cty/cty"
     9  
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
    11  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim"
    12  	"github.com/hashicorp/terraform-plugin-sdk/terraform"
    13  	ctyconvert "github.com/zclconf/go-cty/cty/convert"
    14  )
    15  
    16  // Backend represents a partial backend.Backend implementation and simplifies
    17  // the creation of configuration loading and validation.
    18  //
    19  // Unlike other schema structs such as Provider, this struct is meant to be
    20  // embedded within your actual implementation. It provides implementations
    21  // only for Input and Configure and gives you a method for accessing the
    22  // configuration in the form of a ResourceData that you're expected to call
    23  // from the other implementation funcs.
    24  type Backend struct {
    25  	// Schema is the schema for the configuration of this backend. If this
    26  	// Backend has no configuration this can be omitted.
    27  	Schema map[string]*Schema
    28  
    29  	// ConfigureFunc is called to configure the backend. Use the
    30  	// FromContext* methods to extract information from the context.
    31  	// This can be nil, in which case nothing will be called but the
    32  	// config will still be stored.
    33  	ConfigureFunc func(context.Context) error
    34  
    35  	config *ResourceData
    36  }
    37  
    38  var (
    39  	backendConfigKey = contextKey("backend config")
    40  )
    41  
    42  // FromContextBackendConfig extracts a ResourceData with the configuration
    43  // from the context. This should only be called by Backend functions.
    44  func FromContextBackendConfig(ctx context.Context) *ResourceData {
    45  	return ctx.Value(backendConfigKey).(*ResourceData)
    46  }
    47  
    48  func (b *Backend) ConfigSchema() *configschema.Block {
    49  	// This is an alias of CoreConfigSchema just to implement the
    50  	// backend.Backend interface.
    51  	return b.CoreConfigSchema()
    52  }
    53  
    54  func (b *Backend) PrepareConfig(configVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
    55  	if b == nil {
    56  		return configVal, nil
    57  	}
    58  	var diags tfdiags.Diagnostics
    59  	var err error
    60  
    61  	// In order to use Transform below, this needs to be filled out completely
    62  	// according the schema.
    63  	configVal, err = b.CoreConfigSchema().CoerceValue(configVal)
    64  	if err != nil {
    65  		return configVal, diags.Append(err)
    66  	}
    67  
    68  	// lookup any required, top-level attributes that are Null, and see if we
    69  	// have a Default value available.
    70  	configVal, err = cty.Transform(configVal, func(path cty.Path, val cty.Value) (cty.Value, error) {
    71  		// we're only looking for top-level attributes
    72  		if len(path) != 1 {
    73  			return val, nil
    74  		}
    75  
    76  		// nothing to do if we already have a value
    77  		if !val.IsNull() {
    78  			return val, nil
    79  		}
    80  
    81  		// get the Schema definition for this attribute
    82  		getAttr, ok := path[0].(cty.GetAttrStep)
    83  		// these should all exist, but just ignore anything strange
    84  		if !ok {
    85  			return val, nil
    86  		}
    87  
    88  		attrSchema := b.Schema[getAttr.Name]
    89  		// continue to ignore anything that doesn't match
    90  		if attrSchema == nil {
    91  			return val, nil
    92  		}
    93  
    94  		// this is deprecated, so don't set it
    95  		if attrSchema.Deprecated != "" || attrSchema.Removed != "" {
    96  			return val, nil
    97  		}
    98  
    99  		// find a default value if it exists
   100  		def, err := attrSchema.DefaultValue()
   101  		if err != nil {
   102  			diags = diags.Append(fmt.Errorf("error getting default for %q: %s", getAttr.Name, err))
   103  			return val, err
   104  		}
   105  
   106  		// no default
   107  		if def == nil {
   108  			return val, nil
   109  		}
   110  
   111  		// create a cty.Value and make sure it's the correct type
   112  		tmpVal := hcl2shim.HCL2ValueFromConfigValue(def)
   113  
   114  		// helper/schema used to allow setting "" to a bool
   115  		if val.Type() == cty.Bool && tmpVal.RawEquals(cty.StringVal("")) {
   116  			// return a warning about the conversion
   117  			diags = diags.Append("provider set empty string as default value for bool " + getAttr.Name)
   118  			tmpVal = cty.False
   119  		}
   120  
   121  		val, err = ctyconvert.Convert(tmpVal, val.Type())
   122  		if err != nil {
   123  			diags = diags.Append(fmt.Errorf("error setting default for %q: %s", getAttr.Name, err))
   124  		}
   125  
   126  		return val, err
   127  	})
   128  	if err != nil {
   129  		// any error here was already added to the diagnostics
   130  		return configVal, diags
   131  	}
   132  
   133  	shimRC := b.shimConfig(configVal)
   134  	warns, errs := schemaMap(b.Schema).Validate(shimRC)
   135  	for _, warn := range warns {
   136  		diags = diags.Append(tfdiags.SimpleWarning(warn))
   137  	}
   138  	for _, err := range errs {
   139  		diags = diags.Append(err)
   140  	}
   141  	return configVal, diags
   142  }
   143  
   144  func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
   145  	if b == nil {
   146  		return nil
   147  	}
   148  
   149  	var diags tfdiags.Diagnostics
   150  	sm := schemaMap(b.Schema)
   151  	shimRC := b.shimConfig(obj)
   152  
   153  	// Get a ResourceData for this configuration. To do this, we actually
   154  	// generate an intermediary "diff" although that is never exposed.
   155  	diff, err := sm.Diff(nil, shimRC, nil, nil, true)
   156  	if err != nil {
   157  		diags = diags.Append(err)
   158  		return diags
   159  	}
   160  
   161  	data, err := sm.Data(nil, diff)
   162  	if err != nil {
   163  		diags = diags.Append(err)
   164  		return diags
   165  	}
   166  	b.config = data
   167  
   168  	if b.ConfigureFunc != nil {
   169  		err = b.ConfigureFunc(context.WithValue(
   170  			context.Background(), backendConfigKey, data))
   171  		if err != nil {
   172  			diags = diags.Append(err)
   173  			return diags
   174  		}
   175  	}
   176  
   177  	return diags
   178  }
   179  
   180  // shimConfig turns a new-style cty.Value configuration (which must be of
   181  // an object type) into a minimal old-style *terraform.ResourceConfig object
   182  // that should be populated enough to appease the not-yet-updated functionality
   183  // in this package. This should be removed once everything is updated.
   184  func (b *Backend) shimConfig(obj cty.Value) *terraform.ResourceConfig {
   185  	shimMap, ok := hcl2shim.ConfigValueFromHCL2(obj).(map[string]interface{})
   186  	if !ok {
   187  		// If the configVal was nil, we still want a non-nil map here.
   188  		shimMap = map[string]interface{}{}
   189  	}
   190  	return &terraform.ResourceConfig{
   191  		Config: shimMap,
   192  		Raw:    shimMap,
   193  	}
   194  }
   195  
   196  // Config returns the configuration. This is available after Configure is
   197  // called.
   198  func (b *Backend) Config() *ResourceData {
   199  	return b.config
   200  }