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 }