]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package schema |
2 | ||
3 | import ( | |
4 | "context" | |
107c1cdb | 5 | "fmt" |
bae9f6d2 | 6 | |
107c1cdb ND |
7 | "github.com/hashicorp/terraform/tfdiags" |
8 | "github.com/zclconf/go-cty/cty" | |
9 | ||
10 | "github.com/hashicorp/terraform/config/hcl2shim" | |
11 | "github.com/hashicorp/terraform/configs/configschema" | |
bae9f6d2 | 12 | "github.com/hashicorp/terraform/terraform" |
107c1cdb | 13 | ctyconvert "github.com/zclconf/go-cty/cty/convert" |
bae9f6d2 JC |
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 | ||
107c1cdb ND |
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) { | |
bae9f6d2 | 55 | if b == nil { |
107c1cdb | 56 | return configVal, nil |
bae9f6d2 | 57 | } |
107c1cdb ND |
58 | var diags tfdiags.Diagnostics |
59 | var err error | |
bae9f6d2 | 60 | |
107c1cdb ND |
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 | } | |
bae9f6d2 | 67 | |
107c1cdb ND |
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 | |
bae9f6d2 JC |
131 | } |
132 | ||
107c1cdb ND |
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 | |
bae9f6d2 JC |
142 | } |
143 | ||
107c1cdb | 144 | func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { |
bae9f6d2 JC |
145 | if b == nil { |
146 | return nil | |
147 | } | |
148 | ||
107c1cdb | 149 | var diags tfdiags.Diagnostics |
bae9f6d2 | 150 | sm := schemaMap(b.Schema) |
107c1cdb | 151 | shimRC := b.shimConfig(obj) |
bae9f6d2 JC |
152 | |
153 | // Get a ResourceData for this configuration. To do this, we actually | |
154 | // generate an intermediary "diff" although that is never exposed. | |
107c1cdb | 155 | diff, err := sm.Diff(nil, shimRC, nil, nil, true) |
bae9f6d2 | 156 | if err != nil { |
107c1cdb ND |
157 | diags = diags.Append(err) |
158 | return diags | |
bae9f6d2 JC |
159 | } |
160 | ||
161 | data, err := sm.Data(nil, diff) | |
162 | if err != nil { | |
107c1cdb ND |
163 | diags = diags.Append(err) |
164 | return diags | |
bae9f6d2 JC |
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 { | |
107c1cdb ND |
172 | diags = diags.Append(err) |
173 | return diags | |
bae9f6d2 JC |
174 | } |
175 | } | |
176 | ||
107c1cdb ND |
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 | } | |
bae9f6d2 JC |
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 | } |