Pyvider 0.5: Protocol 6.11
Terraform’s plugin protocol grew in 6.11. Providers can now serve the state backend itself, enumerate existing infrastructure without a data source per resource type, run imperative operations that aren’t part of a plan, and tell Terraform “ask me again later” instead of guessing at a value they cannot know yet.
Pyvider 0.5 implements all of it. If you have written a provider against 0.4, nothing you wrote stops working — these are additions, and the decorator style is the same one you already use.
State stores
A state store is a provider serving Terraform’s own state backend. Register one and Terraform can keep state in whatever your provider can reach:
from pyvider.state_stores import BaseStateStore, register_state_store
@register_state_store("mycloud_bucket")
class BucketStateStore(BaseStateStore):
async def read_state(self, type_name: str, state_id: str) -> bytes | None: ...
async def write_state(self, type_name: str, state_id: str, payload: bytes) -> None: ...
async def delete_state(self, type_name: str, state_id: str) -> None: ...
async def list_states(self, type_name: str) -> list[str]: ...
The interesting half is locking. lock_state / unlock_state / get_lock guard the read-modify-write that every concurrent apply performs, and the lease survives the process that took it — that is the point of an expiry. On POSIX the mutex underneath is a fcntl record lock, chosen over flock because record locks are the variant NFS implements, and state directories on a network share are an ordinary deployment.
Registration here is eager rather than marker-based, unlike ephemeral resources: ValidateStateStoreConfig can arrive before a full discovery sweep has run, and a store has to be resolvable the moment it does.
List resources
A list resource enumerates what already exists, and describes its results with the identity and state schemas of a managed resource you have already written:
from pyvider.list_resources import BaseListResource, ListResult, register_list_resource
@register_list_resource("mycloud_servers", resource_type="mycloud_server")
class Servers(BaseListResource):
async def list(self, ctx) -> AsyncIterator[ListResult]:
for server in await api.list_servers():
yield ListResult(identity={"id": server.id}, resource_object=server.as_dict())
list is an async iterator, so a provider streams results rather than assembling the whole inventory in memory first.
Actions
Actions are the imperative escape hatch — the operation that is not “make reality match this configuration”: rotate a credential, drain a node, trigger a run.
from pyvider.actions import ActionPlan, ActionProgress, BaseAction, register_action
@register_action("mycloud_drain_node")
class DrainNode(BaseAction):
async def plan(self, ctx) -> ActionPlan: ...
async def invoke(self, ctx) -> AsyncIterator[ActionProgress]: ...
invoke streams ActionProgress, so a long-running action reports as it goes instead of going quiet until it finishes.
Deferred responses
A resource handler can now answer “not yet”. When a value genuinely cannot be known during this plan — it depends on something not yet created — a provider says so, and Terraform defers rather than being handed a guess it will later have to reconcile.
Resource identity, carried across the import boundary
Identity now survives import, so a resource brought under management arrives with the identity Terraform will use for it afterwards, rather than one synthesised on the way in.
Capability advertisement
GetProviderSchema reports server and client capabilities, provider_meta included, so Terraform and the provider agree on what each side supports instead of inferring it.
Protocol fixes worth knowing
Several of these are the kind of bug you only find by running a real plan against a real Terraform:
StopProvideris answered before the server stops, rather than the connection dropping first.- An unknown data source is reported as a diagnostic instead of crashing the provider.
- The proposed new state may carry unknown values, which it must during a plan.
WriteStateBytesaccepts a multi-chunk stream — state larger than one message no longer truncates.- A create is no longer executed as a destroy. That one is exactly as bad as it sounds, and it is fixed.
Upgrading
Pyvider 0.5 requires pyvider-cty 0.5, which carries 61 breaking changes verified against real go-cty by 3,699 differential comparisons. Read its changelog before upgrading: behaviour a provider may depend on has moved, including arithmetic width, set ordering on the wire, mark propagation, regex argument order, and stricter csvdecode and jsondecode.
uv pip install --upgrade pyvider
1,846 tests pass, mypy is clean across 146 source files, and the provider is exercised against OpenTofu on Linux, macOS and Windows before release.