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 }