github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/backend.go (about)

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