package schema import ( "context" "errors" "fmt" "sort" "sync" "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/terraform" ) // 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 // ConfigureFunc is a function for configuring the provider. If the // provider doesn't need to be configured, this can be omitted. // // See the ConfigureFunc documentation for more information. ConfigureFunc ConfigureFunc // MetaReset is called by TestReset to reset any state stored in the meta // interface. This is especially important if the StopContext is stored by // the provider. MetaReset func() error meta interface{} // a mutex is required because TestReset can directly replace the stopCtx stopMu sync.Mutex stopCtx context.Context stopCtxCancel context.CancelFunc stopOnce sync.Once } // ConfigureFunc 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 ConfigureFunc func(*ResourceData) (interface{}, error) // 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") } 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 config.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 } // Stopped reports whether the provider has been stopped or not. func (p *Provider) Stopped() bool { ctx := p.StopContext() select { case <-ctx.Done(): return true default: return false } } // StopCh returns a channel that is closed once the provider is stopped. func (p *Provider) StopContext() context.Context { p.stopOnce.Do(p.stopInit) p.stopMu.Lock() defer p.stopMu.Unlock() return p.stopCtx } func (p *Provider) stopInit() { p.stopMu.Lock() defer p.stopMu.Unlock() p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background()) } // Stop implementation of terraform.ResourceProvider interface. func (p *Provider) Stop() error { p.stopOnce.Do(p.stopInit) p.stopMu.Lock() defer p.stopMu.Unlock() p.stopCtxCancel() return nil } // TestReset resets any state stored in the Provider, and will call TestReset // on Meta if it implements the TestProvider interface. // This may be used to reset the schema.Provider at the start of a test, and is // automatically called by resource.Test. func (p *Provider) TestReset() error { p.stopInit() if p.MetaReset != nil { return p.MetaReset() } return nil } // GetSchema implementation of terraform.ResourceProvider interface 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 } // Input implementation of terraform.ResourceProvider interface. func (p *Provider) Input( input terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { return schemaMap(p.Schema).Input(input, c) } // Validate implementation of terraform.ResourceProvider interface. func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) { if err := p.InternalValidate(); err != nil { return nil, []error{fmt.Errorf( "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 implementation of terraform.ResourceProvider interface. func (p *Provider) ValidateResource( t string, c *terraform.ResourceConfig) ([]string, []error) { r, ok := p.ResourcesMap[t] if !ok { return nil, []error{fmt.Errorf( "Provider doesn't support resource: %s", t)} } return r.Validate(c) } // Configure implementation of terraform.ResourceProvider interface. func (p *Provider) Configure(c *terraform.ResourceConfig) error { // No configuration if p.ConfigureFunc == nil { return nil } 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(nil, c, nil, p.meta, true) if err != nil { return err } data, err := sm.Data(nil, diff) if err != nil { return err } meta, err := p.ConfigureFunc(data) if err != nil { return err } p.meta = meta return nil } // Apply implementation of terraform.ResourceProvider interface. func (p *Provider) Apply( info *terraform.InstanceInfo, s *terraform.InstanceState, d *terraform.InstanceDiff) (*terraform.InstanceState, error) { r, ok := p.ResourcesMap[info.Type] if !ok { return nil, fmt.Errorf("unknown resource type: %s", info.Type) } return r.Apply(s, d, p.meta) } // Diff implementation of terraform.ResourceProvider interface. func (p *Provider) Diff( info *terraform.InstanceInfo, s *terraform.InstanceState, c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { r, ok := p.ResourcesMap[info.Type] if !ok { return nil, fmt.Errorf("unknown resource type: %s", info.Type) } return r.Diff(s, c, p.meta) } // SimpleDiff is used by the new protocol wrappers to get a diff that doesn't // attempt to calculate ignore_changes. func (p *Provider) SimpleDiff( info *terraform.InstanceInfo, s *terraform.InstanceState, c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { r, ok := p.ResourcesMap[info.Type] if !ok { return nil, fmt.Errorf("unknown resource type: %s", info.Type) } return r.simpleDiff(s, c, p.meta) } // Refresh implementation of terraform.ResourceProvider interface. func (p *Provider) Refresh( info *terraform.InstanceInfo, s *terraform.InstanceState) (*terraform.InstanceState, error) { r, ok := p.ResourcesMap[info.Type] if !ok { return nil, fmt.Errorf("unknown resource type: %s", info.Type) } return r.Refresh(s, p.meta) } // Resources implementation of terraform.ResourceProvider interface. 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 } func (p *Provider) ImportState( 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 { var err error results, err = r.Importer.State(data, p.meta) 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 { 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( "nil entry in ImportState results. This is always a bug with\n" + "the resource that is being imported. Please report this as\n" + "a bug to Terraform.") } } return states, nil } // ValidateDataSource implementation of terraform.ResourceProvider interface. func (p *Provider) ValidateDataSource( t string, c *terraform.ResourceConfig) ([]string, []error) { r, ok := p.DataSourcesMap[t] if !ok { return nil, []error{fmt.Errorf( "Provider doesn't support data source: %s", t)} } return r.Validate(c) } // ReadDataDiff implementation of terraform.ResourceProvider interface. func (p *Provider) ReadDataDiff( info *terraform.InstanceInfo, c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { r, ok := p.DataSourcesMap[info.Type] if !ok { return nil, fmt.Errorf("unknown data source: %s", info.Type) } return r.Diff(nil, c, p.meta) } // RefreshData implementation of terraform.ResourceProvider interface. func (p *Provider) ReadDataApply( info *terraform.InstanceInfo, d *terraform.InstanceDiff) (*terraform.InstanceState, error) { r, ok := p.DataSourcesMap[info.Type] if !ok { return nil, fmt.Errorf("unknown data source: %s", info.Type) } return r.ReadDataApply(d, p.meta) } // DataSources implementation of terraform.ResourceProvider interface. 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 }