Skip to content

Latest commit

 

History

History
339 lines (261 loc) · 12.9 KB

File metadata and controls

339 lines (261 loc) · 12.9 KB
page_title description
Plugin Development - Framework: Providers
How to implement a provider in the provider development framework. Providers, wrapped by a provider server, are plugins that allow Terraform to interact with APIs.

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:

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
}

// Metadata satisfies the provider.Provider interface for ExampleCloudProvider
func (p *ExampleCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
	resp.TypeName = // provider specific implementation
}

// Schema satisfies the provider.Provider interface for ExampleCloudProvider.
func (p *ExampleCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			// Provider specific implementation.
		},
	}
}

// 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,
		}
	}
}

Metadata Method

The provider.Provider interface Metadata method defines information about the provider itself, such as its type name and version. This information is used to simplify creating data sources and resources.

In this example, the provider type name is set to examplecloud:

func (p *ExampleCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
	resp.TypeName = "examplecloud"
}

Schema Method

The provider.Provider interface Schema 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) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"api_token": schema.StringAttribute{
				Optional: true,
			},
			"endpoint": schema.StringAttribute{
				Optional: true,
			},
		},
	}
}

If the provider does not accept practitioner Terraform configuration, leave the method defined, but empty.

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) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"api_token": schema.StringAttribute{
				Optional: true,
			},
			"endpoint": schema.StringAttribute{
				Optional: true,
			},
		},
	}
}

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.ValueString() != "" {
		apiToken = data.ApiToken.ValueString()
	}

	if data.Endpoint.ValueString() != "" {
		endpoint = data.Endpoint.ValueString()
	}

	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 {}