1package terraform
2
3import (
4	"fmt"
5	"log"
6
7	"github.com/hashicorp/terraform/internal/addrs"
8	"github.com/hashicorp/terraform/internal/configs"
9	"github.com/hashicorp/terraform/internal/configs/configschema"
10	"github.com/hashicorp/terraform/internal/providers"
11	"github.com/hashicorp/terraform/internal/states"
12	"github.com/hashicorp/terraform/internal/tfdiags"
13)
14
15// Schemas is a container for various kinds of schema that Terraform needs
16// during processing.
17type Schemas struct {
18	Providers    map[addrs.Provider]*ProviderSchema
19	Provisioners map[string]*configschema.Block
20}
21
22// ProviderSchema returns the entire ProviderSchema object that was produced
23// by the plugin for the given provider, or nil if no such schema is available.
24//
25// It's usually better to go use the more precise methods offered by type
26// Schemas to handle this detail automatically.
27func (ss *Schemas) ProviderSchema(provider addrs.Provider) *ProviderSchema {
28	if ss.Providers == nil {
29		return nil
30	}
31	return ss.Providers[provider]
32}
33
34// ProviderConfig returns the schema for the provider configuration of the
35// given provider type, or nil if no such schema is available.
36func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block {
37	ps := ss.ProviderSchema(provider)
38	if ps == nil {
39		return nil
40	}
41	return ps.Provider
42}
43
44// ResourceTypeConfig returns the schema for the configuration of a given
45// resource type belonging to a given provider type, or nil of no such
46// schema is available.
47//
48// In many cases the provider type is inferrable from the resource type name,
49// but this is not always true because users can override the provider for
50// a resource using the "provider" meta-argument. Therefore it's important to
51// always pass the correct provider name, even though it many cases it feels
52// redundant.
53func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (block *configschema.Block, schemaVersion uint64) {
54	ps := ss.ProviderSchema(provider)
55	if ps == nil || ps.ResourceTypes == nil {
56		return nil, 0
57	}
58	return ps.SchemaForResourceType(resourceMode, resourceType)
59}
60
61// ProvisionerConfig returns the schema for the configuration of a given
62// provisioner, or nil of no such schema is available.
63func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block {
64	return ss.Provisioners[name]
65}
66
67// LoadSchemas searches the given configuration, state  and plan (any of which
68// may be nil) for constructs that have an associated schema, requests the
69// necessary schemas from the given component factory (which must _not_ be nil),
70// and returns a single object representing all of the necessary schemas.
71//
72// If an error is returned, it may be a wrapped tfdiags.Diagnostics describing
73// errors across multiple separate objects. Errors here will usually indicate
74// either misbehavior on the part of one of the providers or of the provider
75// protocol itself. When returned with errors, the returned schemas object is
76// still valid but may be incomplete.
77func LoadSchemas(config *configs.Config, state *states.State, components contextComponentFactory) (*Schemas, error) {
78	schemas := &Schemas{
79		Providers:    map[addrs.Provider]*ProviderSchema{},
80		Provisioners: map[string]*configschema.Block{},
81	}
82	var diags tfdiags.Diagnostics
83
84	newDiags := loadProviderSchemas(schemas.Providers, config, state, components)
85	diags = diags.Append(newDiags)
86	newDiags = loadProvisionerSchemas(schemas.Provisioners, config, components)
87	diags = diags.Append(newDiags)
88
89	return schemas, diags.Err()
90}
91
92func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *configs.Config, state *states.State, components contextComponentFactory) tfdiags.Diagnostics {
93	var diags tfdiags.Diagnostics
94
95	ensure := func(fqn addrs.Provider) {
96		name := fqn.String()
97
98		if _, exists := schemas[fqn]; exists {
99			return
100		}
101
102		log.Printf("[TRACE] LoadSchemas: retrieving schema for provider type %q", name)
103		provider, err := components.ResourceProvider(fqn)
104		if err != nil {
105			// We'll put a stub in the map so we won't re-attempt this on
106			// future calls.
107			schemas[fqn] = &ProviderSchema{}
108			diags = diags.Append(
109				fmt.Errorf("failed to instantiate provider %q to obtain schema: %s", name, err),
110			)
111			return
112		}
113		defer func() {
114			provider.Close()
115		}()
116
117		resp := provider.GetProviderSchema()
118		if resp.Diagnostics.HasErrors() {
119			// We'll put a stub in the map so we won't re-attempt this on
120			// future calls.
121			schemas[fqn] = &ProviderSchema{}
122			diags = diags.Append(
123				fmt.Errorf("failed to retrieve schema from provider %q: %s", name, resp.Diagnostics.Err()),
124			)
125			return
126		}
127
128		s := &ProviderSchema{
129			Provider:      resp.Provider.Block,
130			ResourceTypes: make(map[string]*configschema.Block),
131			DataSources:   make(map[string]*configschema.Block),
132
133			ResourceTypeSchemaVersions: make(map[string]uint64),
134		}
135
136		if resp.Provider.Version < 0 {
137			// We're not using the version numbers here yet, but we'll check
138			// for validity anyway in case we start using them in future.
139			diags = diags.Append(
140				fmt.Errorf("invalid negative schema version provider configuration for provider %q", name),
141			)
142		}
143
144		for t, r := range resp.ResourceTypes {
145			s.ResourceTypes[t] = r.Block
146			s.ResourceTypeSchemaVersions[t] = uint64(r.Version)
147			if r.Version < 0 {
148				diags = diags.Append(
149					fmt.Errorf("invalid negative schema version for resource type %s in provider %q", t, name),
150				)
151			}
152		}
153
154		for t, d := range resp.DataSources {
155			s.DataSources[t] = d.Block
156			if d.Version < 0 {
157				// We're not using the version numbers here yet, but we'll check
158				// for validity anyway in case we start using them in future.
159				diags = diags.Append(
160					fmt.Errorf("invalid negative schema version for data source %s in provider %q", t, name),
161				)
162			}
163		}
164
165		schemas[fqn] = s
166
167		if resp.ProviderMeta.Block != nil {
168			s.ProviderMeta = resp.ProviderMeta.Block
169		}
170	}
171
172	if config != nil {
173		for _, fqn := range config.ProviderTypes() {
174			ensure(fqn)
175		}
176	}
177
178	if state != nil {
179		needed := providers.AddressedTypesAbs(state.ProviderAddrs())
180		for _, typeAddr := range needed {
181			ensure(typeAddr)
182		}
183	}
184
185	return diags
186}
187
188func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *configs.Config, components contextComponentFactory) tfdiags.Diagnostics {
189	var diags tfdiags.Diagnostics
190
191	ensure := func(name string) {
192		if _, exists := schemas[name]; exists {
193			return
194		}
195
196		log.Printf("[TRACE] LoadSchemas: retrieving schema for provisioner %q", name)
197		provisioner, err := components.ResourceProvisioner(name)
198		if err != nil {
199			// We'll put a stub in the map so we won't re-attempt this on
200			// future calls.
201			schemas[name] = &configschema.Block{}
202			diags = diags.Append(
203				fmt.Errorf("failed to instantiate provisioner %q to obtain schema: %s", name, err),
204			)
205			return
206		}
207		defer func() {
208			provisioner.Close()
209		}()
210
211		resp := provisioner.GetSchema()
212		if resp.Diagnostics.HasErrors() {
213			// We'll put a stub in the map so we won't re-attempt this on
214			// future calls.
215			schemas[name] = &configschema.Block{}
216			diags = diags.Append(
217				fmt.Errorf("failed to retrieve schema from provisioner %q: %s", name, resp.Diagnostics.Err()),
218			)
219			return
220		}
221
222		schemas[name] = resp.Provisioner
223	}
224
225	if config != nil {
226		for _, rc := range config.Module.ManagedResources {
227			for _, pc := range rc.Managed.Provisioners {
228				ensure(pc.Type)
229			}
230		}
231
232		// Must also visit our child modules, recursively.
233		for _, cc := range config.Children {
234			childDiags := loadProvisionerSchemas(schemas, cc, components)
235			diags = diags.Append(childDiags)
236		}
237	}
238
239	return diags
240}
241
242// ProviderSchema represents the schema for a provider's own configuration
243// and the configuration for some or all of its resources and data sources.
244//
245// The completeness of this structure depends on how it was constructed.
246// When constructed for a configuration, it will generally include only
247// resource types and data sources used by that configuration.
248type ProviderSchema struct {
249	Provider      *configschema.Block
250	ProviderMeta  *configschema.Block
251	ResourceTypes map[string]*configschema.Block
252	DataSources   map[string]*configschema.Block
253
254	ResourceTypeSchemaVersions map[string]uint64
255}
256
257// SchemaForResourceType attempts to find a schema for the given mode and type.
258// Returns nil if no such schema is available.
259func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) {
260	switch mode {
261	case addrs.ManagedResourceMode:
262		return ps.ResourceTypes[typeName], ps.ResourceTypeSchemaVersions[typeName]
263	case addrs.DataResourceMode:
264		// Data resources don't have schema versions right now, since state is discarded for each refresh
265		return ps.DataSources[typeName], 0
266	default:
267		// Shouldn't happen, because the above cases are comprehensive.
268		return nil, 0
269	}
270}
271
272// SchemaForResourceAddr attempts to find a schema for the mode and type from
273// the given resource address. Returns nil if no such schema is available.
274func (ps *ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) {
275	return ps.SchemaForResourceType(addr.Mode, addr.Type)
276}
277