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 }