1package schema
2
3import (
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.
24type 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
38var (
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.
44func FromContextBackendConfig(ctx context.Context) *ResourceData {
45	return ctx.Value(backendConfigKey).(*ResourceData)
46}
47
48func (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
54func (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
144func (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.
184func (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.
198func (b *Backend) Config() *ResourceData {
199	return b.config
200}
201