github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/backend.go (about)

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