510 lines
16 KiB
Go
510 lines
16 KiB
Go
package schema
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
|
|
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
|
|
)
|
|
|
|
const uaEnvVar = "TF_APPEND_USER_AGENT"
|
|
|
|
var ReservedProviderFields = []string{
|
|
"alias",
|
|
"version",
|
|
}
|
|
|
|
// StopContext returns a context safe for global use that will cancel
|
|
// when Terraform requests a stop. This function should only be called
|
|
// within a ConfigureContextFunc, passing in the request scoped context
|
|
// received in that method.
|
|
//
|
|
// Deprecated: The use of a global context is discouraged. Please use the new
|
|
// context aware CRUD methods.
|
|
func StopContext(ctx context.Context) (context.Context, bool) {
|
|
stopContext, ok := ctx.Value(StopContextKey).(context.Context)
|
|
return stopContext, ok
|
|
}
|
|
|
|
// Provider represents a resource provider in Terraform, and properly
|
|
// implements all of the ResourceProvider API.
|
|
//
|
|
// By defining a schema for the configuration of the provider, the
|
|
// map of supporting resources, and a configuration function, the schema
|
|
// framework takes over and handles all the provider operations for you.
|
|
//
|
|
// After defining the provider structure, it is unlikely that you'll require any
|
|
// of the methods on Provider itself.
|
|
type Provider struct {
|
|
// Schema is the schema for the configuration of this provider. If this
|
|
// provider has no configuration, this can be omitted.
|
|
//
|
|
// The keys of this map are the configuration keys, and the value is
|
|
// the schema describing the value of the configuration.
|
|
Schema map[string]*Schema
|
|
|
|
// ResourcesMap is the list of available resources that this provider
|
|
// can manage, along with their Resource structure defining their
|
|
// own schemas and CRUD operations.
|
|
//
|
|
// Provider automatically handles routing operations such as Apply,
|
|
// Diff, etc. to the proper resource.
|
|
ResourcesMap map[string]*Resource
|
|
|
|
// DataSourcesMap is the collection of available data sources that
|
|
// this provider implements, with a Resource instance defining
|
|
// the schema and Read operation of each.
|
|
//
|
|
// Resource instances for data sources must have a Read function
|
|
// and must *not* implement Create, Update or Delete.
|
|
DataSourcesMap map[string]*Resource
|
|
|
|
// ProviderMetaSchema is the schema for the configuration of the meta
|
|
// information for this provider. If this provider has no meta info,
|
|
// this can be omitted. This functionality is currently experimental
|
|
// and subject to change or break without warning; it should only be
|
|
// used by providers that are collaborating on its use with the
|
|
// Terraform team.
|
|
ProviderMetaSchema map[string]*Schema
|
|
|
|
// ConfigureFunc is a function for configuring the provider. If the
|
|
// provider doesn't need to be configured, this can be omitted.
|
|
//
|
|
// Deprecated: Please use ConfigureContextFunc instead.
|
|
ConfigureFunc ConfigureFunc
|
|
|
|
// ConfigureContextFunc is a function for configuring the provider. If the
|
|
// provider doesn't need to be configured, this can be omitted. This function
|
|
// receives a context.Context that will cancel when Terraform sends a
|
|
// cancellation signal. This function can yield Diagnostics.
|
|
ConfigureContextFunc ConfigureContextFunc
|
|
|
|
// configured is enabled after a Configure() call
|
|
configured bool
|
|
|
|
meta interface{}
|
|
|
|
TerraformVersion string
|
|
}
|
|
|
|
// ConfigureFunc is the function used to configure a Provider.
|
|
//
|
|
// Deprecated: Please use ConfigureContextFunc
|
|
type ConfigureFunc func(*ResourceData) (interface{}, error)
|
|
|
|
// ConfigureContextFunc is the function used to configure a Provider.
|
|
//
|
|
// The interface{} value returned by this function is stored and passed into
|
|
// the subsequent resources as the meta parameter. This return value is
|
|
// usually used to pass along a configured API client, a configuration
|
|
// structure, etc.
|
|
type ConfigureContextFunc func(context.Context, *ResourceData) (interface{}, diag.Diagnostics)
|
|
|
|
// InternalValidate should be called to validate the structure
|
|
// of the provider.
|
|
//
|
|
// This should be called in a unit test for any provider to verify
|
|
// before release that a provider is properly configured for use with
|
|
// this library.
|
|
func (p *Provider) InternalValidate() error {
|
|
if p == nil {
|
|
return errors.New("provider is nil")
|
|
}
|
|
|
|
if p.ConfigureFunc != nil && p.ConfigureContextFunc != nil {
|
|
return errors.New("ConfigureFunc and ConfigureContextFunc must not both be set")
|
|
}
|
|
|
|
var validationErrors error
|
|
sm := schemaMap(p.Schema)
|
|
if err := sm.InternalValidate(sm); err != nil {
|
|
validationErrors = multierror.Append(validationErrors, err)
|
|
}
|
|
|
|
// Provider-specific checks
|
|
for k := range sm {
|
|
if isReservedProviderFieldName(k) {
|
|
return fmt.Errorf("%s is a reserved field name for a provider", k)
|
|
}
|
|
}
|
|
|
|
for k, r := range p.ResourcesMap {
|
|
if err := r.InternalValidate(nil, true); err != nil {
|
|
validationErrors = multierror.Append(validationErrors, fmt.Errorf("resource %s: %s", k, err))
|
|
}
|
|
}
|
|
|
|
for k, r := range p.DataSourcesMap {
|
|
if err := r.InternalValidate(nil, false); err != nil {
|
|
validationErrors = multierror.Append(validationErrors, fmt.Errorf("data source %s: %s", k, err))
|
|
}
|
|
}
|
|
|
|
return validationErrors
|
|
}
|
|
|
|
func isReservedProviderFieldName(name string) bool {
|
|
for _, reservedName := range ReservedProviderFields {
|
|
if name == reservedName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Meta returns the metadata associated with this provider that was
|
|
// returned by the Configure call. It will be nil until Configure is called.
|
|
func (p *Provider) Meta() interface{} {
|
|
return p.meta
|
|
}
|
|
|
|
// SetMeta can be used to forcefully set the Meta object of the provider.
|
|
// Note that if Configure is called the return value will override anything
|
|
// set here.
|
|
func (p *Provider) SetMeta(v interface{}) {
|
|
p.meta = v
|
|
}
|
|
|
|
// GetSchema returns the config schema for the main provider
|
|
// configuration, as would appear in a "provider" block in the
|
|
// configuration files.
|
|
//
|
|
// Currently not all providers support schema. Callers must therefore
|
|
// first call Resources and DataSources and ensure that at least one
|
|
// resource or data source has the SchemaAvailable flag set.
|
|
func (p *Provider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) {
|
|
resourceTypes := map[string]*configschema.Block{}
|
|
dataSources := map[string]*configschema.Block{}
|
|
|
|
for _, name := range req.ResourceTypes {
|
|
if r, exists := p.ResourcesMap[name]; exists {
|
|
resourceTypes[name] = r.CoreConfigSchema()
|
|
}
|
|
}
|
|
for _, name := range req.DataSources {
|
|
if r, exists := p.DataSourcesMap[name]; exists {
|
|
dataSources[name] = r.CoreConfigSchema()
|
|
}
|
|
}
|
|
|
|
return &terraform.ProviderSchema{
|
|
Provider: schemaMap(p.Schema).CoreConfigSchema(),
|
|
ResourceTypes: resourceTypes,
|
|
DataSources: dataSources,
|
|
}, nil
|
|
}
|
|
|
|
// Validate is called once at the beginning with the raw configuration
|
|
// (no interpolation done) and can return diagnostics
|
|
//
|
|
// This is called once with the provider configuration only. It may not
|
|
// be called at all if no provider configuration is given.
|
|
//
|
|
// This should not assume that any values of the configurations are valid.
|
|
// The primary use case of this call is to check that required keys are
|
|
// set.
|
|
func (p *Provider) Validate(c *terraform.ResourceConfig) diag.Diagnostics {
|
|
if err := p.InternalValidate(); err != nil {
|
|
return []diag.Diagnostic{
|
|
{
|
|
Severity: diag.Error,
|
|
Summary: "InternalValidate",
|
|
Detail: fmt.Sprintf("Internal validation of the provider failed! This is always a bug\n"+
|
|
"with the provider itself, and not a user issue. Please report\n"+
|
|
"this bug:\n\n%s", err),
|
|
},
|
|
}
|
|
}
|
|
|
|
return schemaMap(p.Schema).Validate(c)
|
|
}
|
|
|
|
// ValidateResource is called once at the beginning with the raw
|
|
// configuration (no interpolation done) and can return diagnostics.
|
|
//
|
|
// This is called once per resource.
|
|
//
|
|
// This should not assume any of the values in the resource configuration
|
|
// are valid since it is possible they have to be interpolated still.
|
|
// The primary use case of this call is to check that the required keys
|
|
// are set and that the general structure is correct.
|
|
func (p *Provider) ValidateResource(
|
|
t string, c *terraform.ResourceConfig) diag.Diagnostics {
|
|
r, ok := p.ResourcesMap[t]
|
|
if !ok {
|
|
return []diag.Diagnostic{
|
|
{
|
|
Severity: diag.Error,
|
|
Summary: fmt.Sprintf("Provider doesn't support resource: %s", t),
|
|
},
|
|
}
|
|
}
|
|
|
|
return r.Validate(c)
|
|
}
|
|
|
|
// Configure configures the provider itself with the configuration
|
|
// given. This is useful for setting things like access keys.
|
|
//
|
|
// This won't be called at all if no provider configuration is given.
|
|
func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) diag.Diagnostics {
|
|
// No configuration
|
|
if p.ConfigureFunc == nil && p.ConfigureContextFunc == nil {
|
|
return nil
|
|
}
|
|
|
|
if p.configured {
|
|
logging.HelperSchemaWarn(ctx, "Previously configured provider being re-configured. This can cause issues in concurrent testing if the configurations are not equal.")
|
|
}
|
|
|
|
sm := schemaMap(p.Schema)
|
|
|
|
// Get a ResourceData for this configuration. To do this, we actually
|
|
// generate an intermediary "diff" although that is never exposed.
|
|
diff, err := sm.Diff(ctx, nil, c, nil, p.meta, true)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
data, err := sm.Data(nil, diff)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
if p.ConfigureFunc != nil {
|
|
meta, err := p.ConfigureFunc(data)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
p.meta = meta
|
|
}
|
|
|
|
var diags diag.Diagnostics
|
|
|
|
if p.ConfigureContextFunc != nil {
|
|
meta, configureDiags := p.ConfigureContextFunc(ctx, data)
|
|
diags = append(diags, configureDiags...)
|
|
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
p.meta = meta
|
|
}
|
|
|
|
p.configured = true
|
|
|
|
return diags
|
|
}
|
|
|
|
// Resources returns all the available resource types that this provider
|
|
// knows how to manage.
|
|
func (p *Provider) Resources() []terraform.ResourceType {
|
|
keys := make([]string, 0, len(p.ResourcesMap))
|
|
for k := range p.ResourcesMap {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
result := make([]terraform.ResourceType, 0, len(keys))
|
|
for _, k := range keys {
|
|
resource := p.ResourcesMap[k]
|
|
|
|
// This isn't really possible (it'd fail InternalValidate), but
|
|
// we do it anyways to avoid a panic.
|
|
if resource == nil {
|
|
resource = &Resource{}
|
|
}
|
|
|
|
result = append(result, terraform.ResourceType{
|
|
Name: k,
|
|
Importable: resource.Importer != nil,
|
|
|
|
// Indicates that a provider is compiled against a new enough
|
|
// version of core to support the GetSchema method.
|
|
SchemaAvailable: true,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ImportState requests that the given resource be imported.
|
|
//
|
|
// The returned InstanceState only requires ID be set. Importing
|
|
// will always call Refresh after the state to complete it.
|
|
//
|
|
// IMPORTANT: InstanceState doesn't have the resource type attached
|
|
// to it. A type must be specified on the state via the Ephemeral
|
|
// field on the state.
|
|
//
|
|
// This function can return multiple states. Normally, an import
|
|
// will map 1:1 to a physical resource. However, some resources map
|
|
// to multiple. For example, an AWS security group may contain many rules.
|
|
// Each rule is represented by a separate resource in Terraform,
|
|
// therefore multiple states are returned.
|
|
func (p *Provider) ImportState(
|
|
ctx context.Context,
|
|
info *terraform.InstanceInfo,
|
|
id string) ([]*terraform.InstanceState, error) {
|
|
// Find the resource
|
|
r, ok := p.ResourcesMap[info.Type]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
|
|
}
|
|
|
|
// If it doesn't support import, error
|
|
if r.Importer == nil {
|
|
return nil, fmt.Errorf("resource %s doesn't support import", info.Type)
|
|
}
|
|
|
|
// Create the data
|
|
data := r.Data(nil)
|
|
data.SetId(id)
|
|
data.SetType(info.Type)
|
|
|
|
// Call the import function
|
|
results := []*ResourceData{data}
|
|
if r.Importer.State != nil || r.Importer.StateContext != nil {
|
|
var err error
|
|
logging.HelperSchemaTrace(ctx, "Calling downstream")
|
|
|
|
if r.Importer.StateContext != nil {
|
|
results, err = r.Importer.StateContext(ctx, data, p.meta)
|
|
} else {
|
|
results, err = r.Importer.State(data, p.meta)
|
|
}
|
|
logging.HelperSchemaTrace(ctx, "Called downstream")
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Convert the results to InstanceState values and return it
|
|
states := make([]*terraform.InstanceState, len(results))
|
|
for i, r := range results {
|
|
if r == nil {
|
|
return nil, fmt.Errorf("The provider returned a missing resource during ImportResourceState. " +
|
|
"This is generally a bug in the resource implementation for import. " +
|
|
"Resource import code should return an error for missing resources and skip returning a missing or empty ResourceData. " +
|
|
"Please report this to the provider developers.")
|
|
}
|
|
|
|
if r.Id() == "" {
|
|
return nil, fmt.Errorf("The provider returned a resource missing an identifier during ImportResourceState. " +
|
|
"This is generally a bug in the resource implementation for import. " +
|
|
"Resource import code should not call d.SetId(\"\") or create an empty ResourceData. " +
|
|
"If the resource is missing, instead return an error. " +
|
|
"Please report this to the provider developers.")
|
|
}
|
|
|
|
states[i] = r.State()
|
|
}
|
|
|
|
// Verify that all are non-nil. If there are any nil the error
|
|
// isn't obvious so we circumvent that with a friendlier error.
|
|
for _, s := range states {
|
|
if s == nil {
|
|
return nil, fmt.Errorf("The provider returned a missing resource during ImportResourceState. " +
|
|
"This is generally a bug in the resource implementation for import. " +
|
|
"Resource import code should return an error for missing resources. " +
|
|
"Please report this to the provider developers.")
|
|
}
|
|
}
|
|
|
|
return states, nil
|
|
}
|
|
|
|
// ValidateDataSource is called once at the beginning with the raw
|
|
// configuration (no interpolation done) and can return diagnostics.
|
|
//
|
|
// This is called once per data source instance.
|
|
//
|
|
// This should not assume any of the values in the resource configuration
|
|
// are valid since it is possible they have to be interpolated still.
|
|
// The primary use case of this call is to check that the required keys
|
|
// are set and that the general structure is correct.
|
|
func (p *Provider) ValidateDataSource(
|
|
t string, c *terraform.ResourceConfig) diag.Diagnostics {
|
|
r, ok := p.DataSourcesMap[t]
|
|
if !ok {
|
|
return []diag.Diagnostic{
|
|
{
|
|
Severity: diag.Error,
|
|
Summary: fmt.Sprintf("Provider doesn't support data source: %s", t),
|
|
},
|
|
}
|
|
}
|
|
|
|
return r.Validate(c)
|
|
}
|
|
|
|
// DataSources returns all of the available data sources that this
|
|
// provider implements.
|
|
func (p *Provider) DataSources() []terraform.DataSource {
|
|
keys := make([]string, 0, len(p.DataSourcesMap))
|
|
for k := range p.DataSourcesMap {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
result := make([]terraform.DataSource, 0, len(keys))
|
|
for _, k := range keys {
|
|
result = append(result, terraform.DataSource{
|
|
Name: k,
|
|
|
|
// Indicates that a provider is compiled against a new enough
|
|
// version of core to support the GetSchema method.
|
|
SchemaAvailable: true,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// UserAgent returns a string suitable for use in the User-Agent header of
|
|
// requests generated by the provider. The generated string contains the
|
|
// version of Terraform, the Plugin SDK, and the provider used to generate the
|
|
// request. `name` should be the hyphen-separated reporting name of the
|
|
// provider, and `version` should be the version of the provider.
|
|
//
|
|
// If TF_APPEND_USER_AGENT is set, its value will be appended to the returned
|
|
// string.
|
|
func (p *Provider) UserAgent(name, version string) string {
|
|
ua := fmt.Sprintf("Terraform/%s (+https://www.terraform.io) Terraform-Plugin-SDK/%s", p.TerraformVersion, meta.SDKVersionString())
|
|
if name != "" {
|
|
ua += " " + name
|
|
if version != "" {
|
|
ua += "/" + version
|
|
}
|
|
}
|
|
|
|
if add := os.Getenv(uaEnvVar); add != "" {
|
|
add = strings.TrimSpace(add)
|
|
if len(add) > 0 {
|
|
ua += " " + add
|
|
log.Printf("[DEBUG] Using modified User-Agent: %s", ua)
|
|
}
|
|
}
|
|
|
|
return ua
|
|
}
|
|
|
|
// GRPCProvider returns a gRPC server, for use with terraform-plugin-mux.
|
|
func (p *Provider) GRPCProvider() tfprotov5.ProviderServer {
|
|
return NewGRPCProviderServer(p)
|
|
}
|