Terraform
Providers
Note: The Plugin Framework is in beta.
Providers are Terraform plugins that define resources and data sources for practitioners to use. Providers are wrapped by a provider server for interacting with Terraform.
This page describes the basic implementation details required for defining a provider. Further documentation is available for deeper provider concepts:
- Configure data sources with provider-level data types or clients.
- Configure resources with provider-level data types or clients.
- Validate practitioner configuration against acceptable values.
Define Provider Type
Implement the provider.Provider interface. Each of the methods described in more detail below.
In this example, a provider implementation is scaffolded:
// Ensure the implementation satisfies the provider.Provider interface.
var _ provider.Provider = &ExampleCloudProvider{}
type ExampleCloudProvider struct{
// Version is an example field that can be set with an actual provider
// version on release, "dev" when the provider is built and ran locally,
// and "test" when running acceptance testing.
Version string
}
// GetSchema satisfies the provider.Provider interface for exampleProvider.
func (p *ExampleCloudProvider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
// Provider specific implementation.
},
}, nil
}
// Configure satisfies the provider.Provider interface for ExampleCloudProvider.
func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Provider specific implementation.
}
// DataSources satisfies the provider.Provider interface for ExampleCloudProvider.
func (p *ExampleCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
// Provider specific implementation
}
}
// Resources satisfies the provider.Provider interface for ExampleCloudProvider.
func (p *ExampleCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
// Provider specific implementation
}
}
Conventionally, many providers also create a helper function named New
which can simplify provider server implementations.
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &ExampleCloudProvider{
Version: version,
}
}
}
GetSchema Method
The provider.Provider
interface GetSchema
method defines a schema describing what data is available in the provider's configuration. This configuration block is used to offer practitioners the opportunity to supply values to the provider and configure its behavior, rather than needing to include those values in every resource and data source. It is usually used to gather credentials, endpoints, and the other data used to authenticate with the API, but it is not limited to those uses.
In this example, a sample configuration and schema definition are provided:
// Example Terraform configuration:
//
// provider "examplecloud" {
// api_token = "v3rYs3cr3tt0k3n"
// endpoint = "https://example.com/"
// }
func (p *ExampleCloudProvider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"api_token": {
Optional: true,
Type: types.StringType,
},
"endpoint": {
Optional: true,
Type: types.StringType,
},
},
}, nil
}
If the provider does not accept practitioner Terraform configuration, it must return an empty schema (tfsdk.Schema{}
).
Configure Method
The provider.Provider
interface Configure
method handles the configuration of any provider-level data or clients. These configuration values may be from the practitioner Terraform configuration, environment variables, or other means such as reading vendor-specific configuration files.
This is the only chance the provider has to configure provider-level data or clients, so they need to be persisted if other data source or resource logic will need to reference them. Refer to the Configure Data Sources and Configure Resources pages for additional implementation details.
If the logic needs to return warning or error diagnostics, they can added into the provider.ConfigureResponse.Diagnostics
field.
In this example, the provider API token and endpoint are configured via environment variable or Terraform configuration:
type ExampleCloudProvider struct {}
type ExampleCloudProviderModel struct {
ApiToken types.String `tfsdk:"api_token"`
Endpoint types.String `tfsdk:"endpoint"`
}
func (p *ExampleCloudProvider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"api_token": {
Optional: true,
Type: types.StringType,
},
"endpoint": {
Optional: true,
Type: types.StringType,
},
},
}, nil
}
func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Check environment variables
apiToken := os.Getenv("EXAMPLECLOUD_API_TOKEN")
endpoint := os.Getenv("EXAMPLECLOUD_ENDPOINT")
var data ExampleCloudProviderModel
// Read configuration data into model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
// Check configuration data, which should take precedence over
// environment variable data, if found.
if data.ApiToken.Value != "" {
apiToken = data.ApiToken.Value
}
if data.Endpoint.Value != "" {
endpoint = data.Endpoint.Value
}
if apiToken == "" {
resp.Diagnostics.AddError(
"Missing API Token Configuration",
"While configuring the provider, the API token was not found in "+
"the EXAMPLECLOUD_API_TOKEN environment variable or provider "+
"configuration block api_token attribute.",
)
// Not returning early allows the logic to collect all errors.
}
if endpoint == "" {
resp.Diagnostics.AddError(
"Missing Endpoint Configuration",
"While configuring the provider, the endpoint was not found in "+
"the EXAMPLECLOUD_ENDPOINT environment variable or provider "+
"configuration block endpoint attribute.",
)
// Not returning early allows the logic to collect all errors.
}
// Create data/clients and persist to resp.DataSourceData and
// resp.ResourceData as appropriate.
}
Unknown Values
Not all values are guaranteed to be
known when Configure
is called.
For example, if a practitioner interpolates a resource's unknown value into the block,
that value may show up as unknown depending on how the graph executes:
resource "random_string" "example" {}
provider "examplecloud" {
api_token = random_string.example.result
endpoint = "https://example.com/"
}
In the example above, random_string.example.result
is a read-only field on
random_string.example
that won't be set until after random_string.example
has been
applied. So the Configure
method for the provider may report that the value
is unknown. You can choose how your provider handles this. If
some resources or data sources can be used without knowing that value, it may
be worthwhile to emit a warning and
check whether the value is set in resources and data sources before attempting
to use it. If resources and data sources can't provide any functionality
without knowing that value, it's often better to return an
error, which will halt the apply.
Resources
The provider.ProviderWithResources
interface Resources
method returns a slice of resources. Each element in the slice is a function to create a new resource.Resource
so data is not inadvertently shared across multiple, disjointed resource instance operations unless explicitly coded. Information such as the resource type name is managed by the resource.Resource
implementation.
In this example, the provider implements a single resource:
// With the provider.Provider implementation
func (p *ExampleCloudProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewThingResource,
}
}
// With the resource.Resource implementation
func NewThingResource() resource.Resource {
return &ThingResource{}
}
type ThingResource struct {}
Use Go slice techniques to include large numbers of resources outside the provider Resources
method code.
In this example, the provider codebase implements multiple "services" which group their own resources:
// With the provider.Provider implementation
func (p *ExampleCloudProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
servicex.Resources...,
servicey.Resources...,
}
}
// With the servicex implementation
package servicex
var Resources = []func() resource.Resource {
NewThingResource,
NewWidgetResource,
}
func NewThingResource() resource.Resource {
return &
}
type ThingResource struct {}
func NewWidgetResource() resource.Resource {
return &WidgetResource{}
}
type WidgetResource struct {}
DataSources
The provider.ProviderWithDataSources
interface DataSources
method returns a slice of data sources. Each element in the slice is a function to create a new datasource.DataSource
so data is not inadvertently shared across multiple, disjointed datasource instance operations unless explicitly coded. Information such as the datasource type name is managed by the datasource.DataSource
implementation.
In this example, the provider implements a single data source:
// With the provider.Provider implementation
func (p *ExampleCloudProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewThingDataSource,
}
}
// With the datasource.DataSource implementation
func NewThingDataSource() datasource.DataSource {
return &ThingDataSource{}
}
type ThingDataSource struct {}
Use Go slice techniques to include large numbers of data sources outside the provider DataSources
method code.
In this example, the provider codebase implements multiple "services" which group their own datasources:
// With the provider.Provider implementation
func (p *ExampleCloudProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
servicex.DataSources...,
servicey.DataSources...,
}
}
// With the servicex implementation
package servicex
var DataSources = []func() datasource.DataSource {
NewThingDataSource,
NewWidgetDataSource,
}
func NewThingDataSource() datasource.DataSource {
return &
}
type ThingDataSource struct {}
func NewWidgetDataSource() datasource.DataSource {
return &WidgetDataSource{}
}
type WidgetDataSource struct {}