Skip to content

Latest commit

 

History

History
370 lines (272 loc) · 19.3 KB

File metadata and controls

370 lines (272 loc) · 19.3 KB
page_title description
Plugin Development - Framework: Resources
How to build resources in the provider development framework. Resources allow Terraform to manage infrastructure objects.

Resources

-> Note: The Plugin Framework is in beta.

Resources are an abstraction that allow Terraform to manage infrastructure objects, such as a compute instance, an access policy, or disk. Terraform assumes that every resource:

  • operates as a pure key/value store, with values getting returned exactly as they were written.
  • needs only one API call to update or return its state.
  • can be be created, read, updated, and deleted.

This page describes the basic implementation details required for supporting a resource within the provider. Further documentation is available for deeper resource concepts:

  • Configure resources with provider-level data types or clients.
  • Import state so practitioners can bring existing resources under Terraform lifecycle management.
  • Manage private state to store additional data in resource state that is not shown in plans.
  • Modify plans to enrich the output for expected resource behaviors during changes, such as including default values for missing configurations or marking a resource for replacement if an in-place update cannot occur.
  • Upgrade state to transparently update state data outside plans.
  • Validate practitioner configuration against acceptable values.
  • Timeouts in practitioner configuration for use in resource create, read, update and delete functions.

Define Resource Type

Implement the resource.Resource interface. Each of the methods is described in more detail below.

In this example, a resource named examplecloud_thing with sample lifecycle management behavior is defined:

// Ensure the implementation satisfies the desired interfaces.
var _ resource.Resource = &ThingResource{}

type ThingResource struct {}

type ThingResourceModel struct {
	ExampleAttribute types.String `tfsdk:"example_attribute"`
	ID               types.String `tfsdk:"id"`
}

func (r *ThingResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = "examplecloud_thing"
}

func (r *ThingResource) GetSchema(ctx context.Context) (fwschema.Schema, diag.Diagnostics) {
	return tfsdk.Schema{
		Attributes: map[string]tfsdk.Attribute{
			"example_attribute": {
				Required: true,
				Type:     types.StringType,
			},
			"id": {
				Computed: true,
				Type:     types.StringType,
			},
		},
	}, nil
}

func (r *ThingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data ThingResourceModel

	// Read Terraform plan data into the model
	resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

	// Typically resources will make external calls, however this example
	// hardcodes setting the id attribute to a specific value for brevity.
	data.ID = types.String{Value: "example-id"}

	// Save data into Terraform state
	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ThingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	var data ThingResourceModel

	// Read Terraform prior state data into the model
	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

	// Typically resources will make external calls, however this example
	// omits any refreshed data updates for brevity.

	// Save updated data into Terraform state
	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ThingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
	var data ThingResourceModel

	// Read Terraform plan data into the model
	resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

	// Typically resources will make external calls, however this example
	// omits any update calls for brevity.

	// Save updated data into Terraform state
	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ThingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	var data ThingResourceModel

	// Read Terraform prior state data into the model
	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

	// Typically resources will make external calls, however this example
	// omits any deletion calls for brevity.
}

Metadata Method

The resource.Resource interface Metadata method defines the resource name as it would appear in Terraform configurations. This name should include the provider type prefix, an underscore, then the resource specific name. For example, a provider named examplecloud and a resource that reads "thing" resources would be named examplecloud_thing. Ensure the Add Data Source To Provider documentation is followed so the resource becomes part of the provider implementation, and therefore available to practitioners.

In this example, the resource name in an examplecloud provider that reads "thing" resources is hardcoded to examplecloud_thing:

// With the resource.Resource implementation
func (r *ThingResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = "examplecloud_thing"
}

To simplify resource implementations, the provider.MetadataResponse.TypeName field from the provider.ProviderWithMetadata interface Metadata method can set the provider name so it is available in the resource.MetadataRequest.ProviderTypeName field.

In this example, the provider defines the examplecloud name for itself, and the data source is named examplecloud_thing:

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

// With the resource.Resource implementation
func (d *ThingDataSource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_thing"
}

GetSchema Method

The resource.Resource interface GetSchema method defines a schema describing what data is available in the resource's configuration, plan, and state.

Create

The resource.Resource interface Create method makes the necessary API calls to create the resource and then persist that resource's data into the Terraform state.

Implement the Create method by:

  1. Accessing the data from the resource.CreateRequest type. Most use cases should access the plan data in the resource.CreateRequest.Plan field.
  2. Performing logic or external calls to create and/or run the resource.
  3. Writing state data into the resource.CreateResponse.State field.

It is very important that every known value in the plan ends up in state as a byte-for-byte match, or Terraform will throw errors. The plan is the provider's contract with Terraform: the provider can only change values that are unknown in the plan. It's also very important that every unknown value in the plan gets a known, concrete value when it's set in the state; the state can never hold any unknown values.

If the logic needs to return warning or error diagnostics, they can added into the resource.CreateResponse.Diagnostics field. Any errors will trigger Terraform to mark the resource as tainted for recreation on the next Terraform plan.

Read

The resource.Resource interface Read method makes the necessary calls to retrieve the latest resource state and then persist that updated state into the Terraform state. There is no plan or configuration data in Read.

Implement the Read method by:

  1. Accessing prior state data from the resource.ReadRequest.State field.
  2. Retriving updated resource state, such as remote system information.
  3. Writing state data into the resource.ReadResponse.State field.

The provider can set any value in state, but you should be mindful of values that:

  • represent "drift," or instances when the API's state has deviated from the source of truth defined in the configuration file. This is usually (but not always) the result of someone or something other than Terraform modifying a resource Terraform "owns". When this happens, the value should always be updated in state to reflect the drifted value.
  • are semantically equivalent with values currently in state. Some values are semantically the same even if they are not a byte-for-byte match. JSON strings that change the order of keys or change the semantically-insignificant whitespace, for example, may not represent drift but are just different representations of the same value. When this happens, the existing value should always be maintained in state and should not be replaced with the new representation that the API is returning.

If the logic needs to return warning or error diagnostics, they can added into the resource.ReadResponse.Diagnostics field.

If the logic needs to signal that the resource no longer exists and should be recreated, call the RemoveResource method on the resource.ReadResponse.State field.

In this example, the Read function catches a HTTP 404 Not Found status and returns early to signal resource recreation:

type ThingResource struct {
	// client is configured via a Configure method, which is not shown in this
	// example for brevity. Refer to the Configure Resources documentation for
	// additional details for setting up resources with external clients.
	client *http.Client
}

func (r *ThingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	var data ThingResourceModel

	// Read Terraform prior state data into the model
	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

	httpReq := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		fmt.Sprintf("http://example.com/thing/%s", data.ID),
		nil,
	)

	httpResp, err := d.client.Do(httpReq)
	defer httpResp.Body.Close()

	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to Refresh Resource",
			"An unexpected error occurred while attempting to refresh resource state. "+
				"Please retry the operation or report this issue to the provider developers.\n\n"+
				"HTTP Error: "+err.Error(),
		)

		return
	}

	// Treat HTTP 404 Not Found status as a signal to recreate resource
	// and return early
	if httpResp.StatusCode == http.StatusNotFound {
		resp.State.RemoveResource(ctx)

		return
	}

	// Update data from httpResp

	// Save updated data into Terraform state
	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

Update

The resource.Resource interface Update method makes the necessary calls to modify the existing resource to match the configuration and then persist the updated state.

If the resource does not support modification and should always be recreated on configuration value updates, this method logic can be left empty and ensure all configurable schema attributes implement the resource.RequiresReplace plan modifier.

Implement the Update method by:

  1. Accessing the data from the resource.UpdateRequest type. Most use cases should access the plan data in the resource.UpdateRequest.Plan field.
  2. Performing logic or external calls to modify the resource.
  3. Writing state data into the resource.UpdateResponse.State field.

It is very important that every known value in the plan ends up in state as a byte-for-byte match, or Terraform will throw errors. The plan is the provider's contract with Terraform: the provider can only change values that are unknown in the plan. It's also very important that every unknown value in the plan gets a known, concrete value when it's set in the state; the state can never hold any unknown values.

If the logic needs to return warning or error diagnostics, they can added into the resource.UpdateResponse.Diagnostics field. Only successfully modified parts of the resource should be return updated data in the state response.

Delete

The resource.Resource interface Delete method makes the necessary API calls to destroy a resource and then to remove that resource from the Terraform state.

Implement the Delete method by:

  1. Accessing prior state data from the resource.DeleteRequest.State field.
  2. Performing logic or external calls to destroy the resource.

Terraform 1.3 and later enables deletion planning, which resources can implement to return warning and error diagnostics. For additional information, refer to the resource plan modification documentation.

If the logic needs to return warning or error diagnostics, they can added into the resource.DeleteResponse.Diagnostics field. Any errors will prevent the framework from automatically calling the RemoveResource method on the resource.DeleteResponse.State field. If the resource should still be removed from state, call the RemoveResource method manually.

If the logic needs to skip a condition, such as an API error, where the resource no longer exists then the logic can return early.

In this example, the Delete function allows HTTP 200 OK and 404 Not Found statuses to complete successfully without raising any errors:

type ThingResource struct {
	// client is configured via a Configure method, which is not shown in this
	// example for brevity. Refer to the Configure Resources documentation for
	// additional details for setting up resources with external clients.
	client *http.Client
}

func (r *ThingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	var data ThingResourceModel

	// Read Terraform prior state data into the model
	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

	httpReq := http.NewRequestWithContext(
		ctx,
		http.MethodDelete,
		fmt.Sprintf("http://example.com/thing/%s", data.ID),
		nil,
	)

	httpResp, err := d.client.Do(httpReq)
	defer httpResp.Body.Close()

	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to Delete Resource",
			"An unexpected error occurred while attempting to delete the resource. "+
				"Please retry the operation or report this issue to the provider developers.\n\n"+
				"HTTP Error: "+err.Error(),
		)

		return
	}

	// Treat HTTP 404 Not Found status as a signal to return early
	if httpResp.StatusCode != http.StatusNotFound && httpResp.StatusCode != http.StatusOK {
		resp.Diagnostics.AddError(
			"Unable to Delete Resource",
			"An unexpected error occurred while attempting to delete the resource. "+
				"Please retry the operation or report this issue to the provider developers.\n\n"+
				"HTTP Status: "+httpResp.Status,
		)

		return
	}

	// If the logic reaches here, it implicitly succeeded and will remove
	// the resource from state if there are no other errors.
}

Add Resource to Provider

Resources become available to practitioners when they are included in the provider implementation via the provider.ProviderWithResources interface Resources method.

In this example, the ThingResource type, which implements the resource.Resource interface, is added to the provider implementation:

// With the provider.Provider implementation
func (p *ExampleCloudProvider) Resources(_ context.Context) []func() resource.Resource {
	return []func() resource.Resource{
		func() resource.Resource {
			return &ThingResource{},
		},
	}
}

To simplify provider implementations, a named function can be created with the resource implementation.

In this example, the ThingResource code includes an additional NewThingResource function, which simplifies the provider implementation:

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