blob: 24e5371c5c4ed21fba7f51aaa508b3ed855686de [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package terraform
import (
"fmt"
"log"
"sync"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
)
// contextPlugins represents a library of available plugins (providers and
// provisioners) which we assume will all be used with the same
// terraform.Context, and thus it'll be safe to cache certain information
// about the providers for performance reasons.
type contextPlugins struct {
providerFactories map[addrs.Provider]providers.Factory
provisionerFactories map[string]provisioners.Factory
// We memoize the schemas we've previously loaded in here, to avoid
// repeatedly paying the cost of activating the same plugins to access
// their schemas in various different spots. We use schemas for many
// purposes in Terraform, so there isn't a single choke point where
// it makes sense to preload all of them.
providerSchemas map[addrs.Provider]*ProviderSchema
provisionerSchemas map[string]*configschema.Block
schemasLock sync.Mutex
}
func newContextPlugins(providerFactories map[addrs.Provider]providers.Factory, provisionerFactories map[string]provisioners.Factory) *contextPlugins {
ret := &contextPlugins{
providerFactories: providerFactories,
provisionerFactories: provisionerFactories,
}
ret.init()
return ret
}
func (cp *contextPlugins) init() {
cp.providerSchemas = make(map[addrs.Provider]*ProviderSchema, len(cp.providerFactories))
cp.provisionerSchemas = make(map[string]*configschema.Block, len(cp.provisionerFactories))
}
func (cp *contextPlugins) HasProvider(addr addrs.Provider) bool {
_, ok := cp.providerFactories[addr]
return ok
}
func (cp *contextPlugins) NewProviderInstance(addr addrs.Provider) (providers.Interface, error) {
f, ok := cp.providerFactories[addr]
if !ok {
return nil, fmt.Errorf("unavailable provider %q", addr.String())
}
return f()
}
func (cp *contextPlugins) HasProvisioner(typ string) bool {
_, ok := cp.provisionerFactories[typ]
return ok
}
func (cp *contextPlugins) NewProvisionerInstance(typ string) (provisioners.Interface, error) {
f, ok := cp.provisionerFactories[typ]
if !ok {
return nil, fmt.Errorf("unavailable provisioner %q", typ)
}
return f()
}
// ProviderSchema uses a temporary instance of the provider with the given
// address to obtain the full schema for all aspects of that provider.
//
// ProviderSchema memoizes results by unique provider address, so it's fine
// to repeatedly call this method with the same address if various different
// parts of Terraform all need the same schema information.
func (cp *contextPlugins) ProviderSchema(addr addrs.Provider) (*ProviderSchema, error) {
cp.schemasLock.Lock()
defer cp.schemasLock.Unlock()
if schema, ok := cp.providerSchemas[addr]; ok {
return schema, nil
}
log.Printf("[TRACE] terraform.contextPlugins: Initializing provider %q to read its schema", addr)
provider, err := cp.NewProviderInstance(addr)
if err != nil {
return nil, fmt.Errorf("failed to instantiate provider %q to obtain schema: %s", addr, err)
}
defer provider.Close()
resp := provider.GetProviderSchema()
if resp.Diagnostics.HasErrors() {
return nil, fmt.Errorf("failed to retrieve schema from provider %q: %s", addr, resp.Diagnostics.Err())
}
s := &ProviderSchema{
Provider: resp.Provider.Block,
ResourceTypes: make(map[string]*configschema.Block),
DataSources: make(map[string]*configschema.Block),
ResourceTypeSchemaVersions: make(map[string]uint64),
}
if resp.Provider.Version < 0 {
// We're not using the version numbers here yet, but we'll check
// for validity anyway in case we start using them in future.
return nil, fmt.Errorf("provider %s has invalid negative schema version for its configuration blocks,which is a bug in the provider ", addr)
}
for t, r := range resp.ResourceTypes {
if err := r.Block.InternalValidate(); err != nil {
return nil, fmt.Errorf("provider %s has invalid schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err)
}
s.ResourceTypes[t] = r.Block
s.ResourceTypeSchemaVersions[t] = uint64(r.Version)
if r.Version < 0 {
return nil, fmt.Errorf("provider %s has invalid negative schema version for managed resource type %q, which is a bug in the provider", addr, t)
}
}
for t, d := range resp.DataSources {
if err := d.Block.InternalValidate(); err != nil {
return nil, fmt.Errorf("provider %s has invalid schema for data resource type %q, which is a bug in the provider: %q", addr, t, err)
}
s.DataSources[t] = d.Block
if d.Version < 0 {
// We're not using the version numbers here yet, but we'll check
// for validity anyway in case we start using them in future.
return nil, fmt.Errorf("provider %s has invalid negative schema version for data resource type %q, which is a bug in the provider", addr, t)
}
}
if resp.ProviderMeta.Block != nil {
s.ProviderMeta = resp.ProviderMeta.Block
}
cp.providerSchemas[addr] = s
return s, nil
}
// ProviderConfigSchema is a helper wrapper around ProviderSchema which first
// reads the full schema of the given provider and then extracts just the
// provider's configuration schema, which defines what's expected in a
// "provider" block in the configuration when configuring this provider.
func (cp *contextPlugins) ProviderConfigSchema(providerAddr addrs.Provider) (*configschema.Block, error) {
providerSchema, err := cp.ProviderSchema(providerAddr)
if err != nil {
return nil, err
}
return providerSchema.Provider, nil
}
// ResourceTypeSchema is a helper wrapper around ProviderSchema which first
// reads the schema of the given provider and then tries to find the schema
// for the resource type of the given resource mode in that provider.
//
// ResourceTypeSchema will return an error if the provider schema lookup
// fails, but will return nil if the provider schema lookup succeeds but then
// the provider doesn't have a resource of the requested type.
//
// Managed resource types have versioned schemas, so the second return value
// is the current schema version number for the requested resource. The version
// is irrelevant for other resource modes.
func (cp *contextPlugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (*configschema.Block, uint64, error) {
providerSchema, err := cp.ProviderSchema(providerAddr)
if err != nil {
return nil, 0, err
}
schema, version := providerSchema.SchemaForResourceType(resourceMode, resourceType)
return schema, version, nil
}
// ProvisionerSchema uses a temporary instance of the provisioner with the
// given type name to obtain the schema for that provisioner's configuration.
//
// ProvisionerSchema memoizes results by provisioner type name, so it's fine
// to repeatedly call this method with the same name if various different
// parts of Terraform all need the same schema information.
func (cp *contextPlugins) ProvisionerSchema(typ string) (*configschema.Block, error) {
cp.schemasLock.Lock()
defer cp.schemasLock.Unlock()
if schema, ok := cp.provisionerSchemas[typ]; ok {
return schema, nil
}
log.Printf("[TRACE] terraform.contextPlugins: Initializing provisioner %q to read its schema", typ)
provisioner, err := cp.NewProvisionerInstance(typ)
if err != nil {
return nil, fmt.Errorf("failed to instantiate provisioner %q to obtain schema: %s", typ, err)
}
defer provisioner.Close()
resp := provisioner.GetSchema()
if resp.Diagnostics.HasErrors() {
return nil, fmt.Errorf("failed to retrieve schema from provisioner %q: %s", typ, resp.Diagnostics.Err())
}
cp.provisionerSchemas[typ] = resp.Provisioner
return resp.Provisioner, nil
}