Stable API

tf’s API consists of:

As a library consumer and provider author, your primary interaction will be writing classes that satisfy Provider, Resource, and DataSource. If your provider has a large number of elements, you will likely want to implement your own higher-level abstractions to stamp out boilerplate class definitions. Protocols give you the flexibility to do this in a clean way.

However, it’s perfectly fine (if somewhat tedious) to implement each element against it’s protocol individually.

Execution Interface

The execution interface provides program entrypoints for OpenTofu to run your provider.

You provider should have a Console Script entrypoint (named terraform-provider-myprovider) pointing to your main function. Your main function should create an instance of your provider and call run_provider().

tf.runner.run_provider(provider: Provider, argv: list[str] | None = None)

Run the given provider with the given arguments.

Parameters:
  • provider – Provider instance to run

  • argv – Optional arguments to run the provider with

An installation utility is provided to install your provider into the plugins directory.

Warning

Running install_provider(). is only required if you want to install your provider in development mode into your plugin directory. There are easier ways to test your provider during development, such as setting the TF_CLI_CONFIG_FILE.

tf.runner.install_provider(host: str, namespace: str, project: str, version: str, plugin_dir: Path, provider_script: Path)

Installs the given (host, namespace, project, version) provider into the plugin directory. The provider_script should be the terraform-provider-<project> executable. If the plugin directory does not exist, it will be created.

Parameters:
  • host – Host of the provider

  • namespace – Namespace of the provider

  • project – Project of the provider

  • version – Version of the provider

  • plugin_dir – Directory to install the provider into

  • provider_script – Path to the provider executable (typically installed as a pip entrypoint)

Provider Interface

To implement a provider, you must subclass Provider and implement the methods. Essentially a Provider answers some questions about itself, allows configuration of itself, and provides a list of DataSource and Resource classes it supports.

class tf.iface.Provider(*args, **kwargs)
abstract configure_provider(diags: Diagnostics, config: dict)

Called when the provider is configured. This is a good place to set up any global state

abstract full_name() str

Get the full provider name eg terraform.example.com/ex/ex

abstract get_data_sources() list[Type[DataSource]]

Get all the data source types that this provider supports

get_functions() list[Type[Function]]

Get all the function types that this provider supports

abstract get_model_prefix() str

Get the model prefix for all loaded resources

abstract get_provider_schema(diags: Diagnostics) Schema

Get the schema for the provider

abstract get_resources() list[Type[Resource]]

Get all the resource types that this provider supports

abstract validate_config(diags: Diagnostics, config: dict)

Validate the provider configuration

The AbstractResource represents an element: a data source or a resource. Both types of elements must be named and have a schema.

class tf.iface.AbstractResource(*args, **kwargs)
abstract classmethod get_name() str

Get the type name for this resource type

abstract classmethod get_schema() Schema

Get the schema for this resource

Data Sources

A DataSource is the simplest element to implement – it’s stateless and only provides data. A DataSource must implement one method, read(), and may optionally implement validate_config().

class tf.iface.DataSource(*args, **kwargs)

Bases: AbstractResource, Protocol

abstract read(ctx: ReadDataContext, config: dict) dict | None

Read the data source

validate(diags: Diagnostics, type_name: str, config: dict)

Validate the data source configuration

Resources

A Resource is a more complex element to implement – it’s stateful and provides data and actions. You must implement: get_name() and, get_schema() as well as create(), read(), update(), and delete() to control the lifecycle of the resource.

You may also optionally implement import_() and plan(), upgrade(), and validate() to further hook into the lifecycle.

class tf.iface.Resource(*args, **kwargs)

Bases: AbstractResource, Protocol

abstract create(ctx: CreateContext, planned: dict) dict | None

Create the resource, returning the actual state after creation.

This is called when a user runs opentofu apply and the resource needs to be initially created.

Parameters:
  • ctx – CreateContext

  • planned – The planned state of the resource

abstract delete(ctx: DeleteContext, current: dict)

Delete the resource, returning None generally

import_(ctx: ImportContext, id: str) dict | None

Import a resource

This is called when a user runs opentofu import and provides a resource ID to import.

Parameters:
  • ctx – ImportContext

  • id – The resource ID to import

plan(ctx: PlanContext, current: dict | None, planned: dict) dict | None

Modify the resource change plan

abstract read(ctx: ReadContext, current: dict) dict | None

Read the current state of the resource

abstract update(ctx: UpdateContext, current: dict, planned: dict) dict | None

Update the resource to the planned state, returning the actual state after the update

upgrade(ctx: UpgradeContext, version: int, old: dict) dict | None

Upgrade an old resource state to the newest schema version

validate(diags: Diagnostics, type_name: str, config: dict)

Validate the resource configuration

This is called before any other operation to validate the configuration of the resource. You should run parameter validation here. Generate errors and warnings through the diags object.

Parameters:
  • diags – Diagnostics

  • type_name – The type name of the resource

  • config – The configuration to validate

State

State is a snapshot of your resource at a point in time. It is a dictionary of field names to values.

tf.iface.State

State is the current state of a resource. It is a dictionary where field names are mapped to Python values (or None, or Unknown). Resource operations are mostly just pushing around, mutating, and returning State.

Config

Config is used instead of State during validation.

tf.iface.Config

Config is like State, except its used in configuration validation and the values are null when they are not bound to a value. This is because the configuration is not yet bound to a resource. This is merely for validating that set of input parameters or values are correct.

Schemas

Every resource must be specified with a schema. A schema consists of a version, a set of fields, and a set of block types.

class tf.schema.Schema(attributes: list[Attribute] | None = None, version: int | None = None, block_types: list[NestedBlock] | None = None, description: str | None = None, description_kind: TextFormat | None = None, deprecated: bool | None = None)

A schema is a description of the data model for a resource/data source/provider.

Parameters:
  • attributes – List of attributes

  • version – Version of the schema

  • block_types – List of nested block types

Example:

from tf.schema import Schema, Attribute
from tf.types import Number

schema = schema.Schema(
    version=2,
    attributes=[
        schema.Attribute("a", types.Number(), required=True),
        schema.Attribute("b", types.Number(), required=True, requires_replace=True),
        schema.Attribute("sum", types.Number(), computed=True),
    ],
)

Attributes

An attribute is a field in a schema. It ties a field name to a type and a set of behaviors.

class tf.schema.Attribute(name: str, type: TfType, description: str | None = None, required: bool | None = False, optional: bool | None = False, computed: bool | None = False, sensitive: bool | None = False, description_kind: TextFormat | None = None, deprecated: bool | None = None, requires_replace: bool | None = None, read_only: bool | None = False, default: Any = Unknown)

An attribute is a single field in a schema.

Parameters:
  • name – Name of the attribute

  • type – Type of the attribute

  • description – Description of the attribute

  • required – Required?

  • optional – Optional?

  • computed – Computed?

  • sensitive – Sensitive?

  • description_kind – Description kind (defaults to Markdown)

  • deprecated – Deprecated?

  • requires_replace – Should a change of this value require a replace of the resource?

  • default – If this value is computed but not set, this will be the default value in the change plan

Blocks

Blocks are a way to group fields together in a schema. They are akin to sub-resources. Blocks define their own attributes and may have nested blocks.

class tf.schema.Block(attributes: list[Attribute] | None = None, block_types: list[NestedBlock] | None = None, description: str | None = None, description_kind: TextFormat | None = None, deprecated: bool | None = None)

Types

All types must implement the TfType interface.

class tf.types.TfType(*args, **kwargs)
abstract decode(value: Any) Any

Decode the tf-serializable representation into the python representation

abstract encode(value: Any) Any

Encode the python representation into the tf-serializable

semantically_equal(a_decoded, b_decoded) bool

Check if two Python-types (represented by the implementing type) are semantically equal. For Integers, ints will be passed in, and so on.

abstract tf_type() bytes

Return the TF type pattern

A handful of types are provided for you:

class tf.types.Bool(*args, **kwargs)

True or False. Maps to Python bool.

class tf.types.Number(*args, **kwargs)

Numbers are numeric values. They can be integers or floats. Maps to Python int or float.

Usually this is fine, but if you need to distinguish between the two you must do it in your Resource CRUD implementation.

class tf.types.String(*args, **kwargs)

Strings are sequences of characters. Maps to Python str.

class tf.types.List(element_type: TfType)

Lists are ordered collections of homogeneously-typed values. Maps to Python list.

Parameters:

element_type – The type of the elements in the list.

class tf.types.Map(element_type: TfType)

Maps are collections of string-keyed, homogeneously-typed values. Maps to Python dict.

Parameters:

element_type – The type of the elements in the map.

class tf.types.Set(element_type: TfType)

Sets are collections of homogeneously-typed values. Sets are represented as lists in Python because TF Sets can have object values, which Python doesn’t like. Maps to Python list.

OK in TF, Bad in Python: set(({“a”: 123},))

Result: TypeError: unhashable type: ‘dict’

Parameters:

element_type – The type of the elements in the set.

Several utility types are also provided:

class tf.types.NormalizedJson(*args, **kwargs)

Bases: String

JSON type that doesn’t care about the order of keys.

Under the hood, this is just a string in the state file.

Unknown

Unknown is a special value that represents a value that is not known at plan time. Unknown is a fundamental part of TF’s design and is used extensively in the planning phase.

TF makes a distinction between null and unknown. tf represents them with None and tf.types.Unknown respectively.

tf.types.Unknown

Unknown is a sentinel value that represents a value that is not yet known. You will find these in a state plan.