This guide explains the complete lifecycle of a Pyvider provider, from initialization through termination. Understanding the lifecycle helps you implement providers correctly and debug issues effectively.
🤖 AI-Generated Content
This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.
When: Terraform starts the provider plugin as a separate process
What Happens:
1. Terraform launches the provider binary (e.g., terraform-provider-pyvider)
2. Provider process starts and initializes gRPC server
3. Terraform and provider perform gRPC handshake
4. Plugin protocol version negotiation (v6)
When: Immediately after startup, before any Terraform operations
What Happens:
1. Framework scans for registered components using decorators
2. Resources discovered via @register_resource
3. Data sources discovered via @register_data_source
4. Functions discovered via @register_function
5. Provider discovered via @register_provider
6. Components registered in the hub
# Components are discovered automatically via decorators@register_provider("local")classLocalProvider(BaseProvider):"""Discovered during component discovery."""pass@register_resource("pyvider_file_content")classFileContentResource(BaseResource):"""Discovered during component discovery."""pass@register_data_source("pyvider_env_variables")classEnvVariablesDataSource(BaseDataSource):"""Discovered during component discovery."""pass
DEBUG - Discovering components...
INFO - Registered provider: local
INFO - Registered resource: pyvider_file_content
INFO - Registered data source: pyvider_env_variables
INFO - Registered function: format_string
DEBUG - Component discovery complete: 1 providers, 3 resources, 2 data sources, 5 functions
Key Points:
- Discovery happens once per provider process
- All components must be importable (in Python path)
- Registration decorators are evaluated at import time
- Hub maintains registry of all discovered components
When: After discovery, before serving any requests
What Happens:
1. Framework calls provider.setup() method
2. Provider can perform one-time initialization
3. Capabilities are integrated
4. Final schema is assembled
5. Provider marked as ready
@register_provider("mycloud")classMyCloudProvider(BaseProvider):asyncdefsetup(self)->None:""" Called once after discovery, before serving requests. Ideal for: - Assembling final schema - Integrating capabilities - One-time initialization - Setting up connection pools """logger.info("Provider setup starting")# Integrate capabilities into schemaawaitself._integrate_capabilities()# Set up connection poolself.http_client=httpx.AsyncClient(timeout=30.0,limits=httpx.Limits(max_connections=100))# Assemble final schemaself._final_schema=self._build_schema()logger.info("Provider setup complete")
INFO - Provider setup starting
DEBUG - Integrating capabilities...
DEBUG - Building provider schema
INFO - Provider setup complete
Important:
- setup() is called exactly once per provider instance
- Must set self._final_schema before serving requests
- Async operations are supported
- Exceptions here will prevent provider from starting
@register_provider("mycloud")classMyCloudProvider(BaseProvider):@propertydefschema(self)->PvsSchema:""" Return the provider configuration schema. Called by framework during GetProviderSchema RPC. """ifself._final_schemaisNone:raiseFrameworkConfigurationError("Provider schema requested before setup() hook was run.")returnself._final_schemadef_build_schema(self)->PvsSchema:"""Build the provider configuration schema."""returns_provider({"api_endpoint":a_str(required=True,description="API endpoint URL"),"api_key":a_str(required=True,sensitive=True,description="API authentication key"),"timeout":a_num(default=30,description="Request timeout in seconds"),})
DEBUG - GetProviderSchema called
DEBUG - Returning schema: 1 provider, 3 resources, 2 data sources, 5 functions
Schema Includes:
- Provider config schema: What the provider block requires
- Resource schemas: All resource types and their attributes
- Data source schemas: All data types and their attributes
- Function schemas: All function signatures and parameters
@register_provider("mycloud")classMyCloudProvider(BaseProvider):asyncdefconfigure(self,config:dict[str,CtyType])->None:""" Configure the provider with user-supplied configuration. Args: config: Configuration from provider block (in CTY format) Called when Terraform processes: provider "mycloud" { api_endpoint = "https://api.example.com" api_key = var.api_key timeout = 30 } """logger.info("Configuring provider",endpoint=config.get("api_endpoint"))# Store configurationself.api_endpoint=config["api_endpoint"]self.api_key=config["api_key"]self.timeout=config.get("timeout",30)# Validate configurationifnotself.api_endpoint.startswith("https://"):raiseProviderConfigurationError("API endpoint must use HTTPS")# Test authenticationtry:asyncwithhttpx.AsyncClient()asclient:response=awaitclient.get(f"{self.api_endpoint}/auth/test",headers={"Authorization":f"Bearer {self.api_key}"},timeout=self.timeout)response.raise_for_status()exceptExceptionase:raiseProviderConfigurationError(f"Failed to authenticate with API: {e}")# Mark as configuredself._configured=Truelogger.info("Provider configured successfully")
INFO - Configuring provider endpoint=https://api.example.com
DEBUG - Validating configuration...
DEBUG - Testing authentication...
INFO - Provider configured successfully
Key Points:
- configure() called once per provider block
- Configuration is validated by framework before calling
- Use this to authenticate, connect to APIs, validate credentials
- Exceptions here prevent all resource operations
- Thread-safe: uses async lock to prevent concurrent configuration
@register_resource("mycloud_server")classServerResource(BaseResource):asyncdefread(self,ctx:ResourceContext)->State|None:""" Read current state of the resource. Called during: refresh, before updates, during plan """frompyvider.hubimporthubprovider=hub.get_component("singleton","provider")logger.debug("Reading resource",resource_id=ctx.state.id)server=awaitprovider.api.get_server(ctx.state.id)ifnotserver:logger.debug("Resource not found",resource_id=ctx.state.id)returnNone# Resource was deleted outside TerraformreturnState(id=server.id,name=server.name,status=server.status,)asyncdef_create(self,ctx:ResourceContext,base_plan:dict):""" Create new resource. Called during: terraform apply (for new resources) """frompyvider.hubimporthubprovider=hub.get_component("singleton","provider")logger.info("Creating resource",name=base_plan["name"])server=awaitprovider.api.create_server(name=base_plan["name"],size=base_plan["size"])return{**base_plan,"id":server.id,"status":"running"},Noneasyncdef_update(self,ctx:ResourceContext,base_plan:dict):""" Update existing resource. Called during: terraform apply (for changed resources) """frompyvider.hubimporthubprovider=hub.get_component("singleton","provider")logger.info("Updating resource",resource_id=ctx.state.id)awaitprovider.api.update_server(ctx.state.id,name=base_plan["name"],size=base_plan["size"])returnbase_plan,Noneasyncdef_delete(self,ctx:ResourceContext):""" Delete resource. Called during: terraform destroy, terraform apply (for removed resources) """frompyvider.hubimporthubprovider=hub.get_component("singleton","provider")logger.info("Deleting resource",resource_id=ctx.state.id)awaitprovider.api.delete_server(ctx.state.id)
When: Terraform is done with all operations and exits
What Happens:
1. Terraform signals provider to shut down
2. Provider closes connections and cleans up resources
3. Provider process exits
4. gRPC server stops
@register_provider("mycloud")classMyCloudProvider(BaseProvider):asyncdefcleanup(self)->None:""" Called during provider shutdown. Use this to clean up resources: - Close HTTP connections - Disconnect from databases - Release file handles - Clean up temporary files """logger.info("Provider cleanup starting")# Close HTTP clientifhasattr(self,'http_client'):awaitself.http_client.aclose()logger.debug("HTTP client closed")# Close database connectionsifhasattr(self,'db_pool'):awaitself.db_pool.close()logger.debug("Database pool closed")logger.info("Provider cleanup complete")
classMyProvider(BaseProvider):def__init__(self):super().__init__()self._authenticated=Falseself._access_token=Noneasyncdefconfigure(self,config:dict[str,CtyType]):# Just store credentialsself.api_endpoint=config["api_endpoint"]self.api_key=config["api_key"]asyncdef_ensure_authenticated(self):"""Authenticate on first use."""ifnotself._authenticated:response=awaitself.http_client.post(f"{self.api_endpoint}/auth",json={"api_key":self.api_key})self._access_token=response.json()["access_token"]self._authenticated=Trueasyncdefget_server(self,server_id:str):awaitself._ensure_authenticated()# Lazy auth# ... use self._access_token
classMyProvider(BaseProvider):asyncdefsetup(self):"""Set up connection pool during initialization."""self.http_client=httpx.AsyncClient(timeout=30.0,limits=httpx.Limits(max_connections=100,max_keepalive_connections=20))asyncdefcleanup(self):"""Close pool during shutdown."""awaitself.http_client.aclose()
classMyProvider(BaseProvider):asyncdefsetup(self):"""Integrate capabilities into provider."""# Discover capability componentsauth_capability=self.hub.get_capability("authentication")cache_capability=self.hub.get_capability("caching")# Integrate into providerself.capabilities={"authentication":auth_capability,"caching":cache_capability,}# Build schema with capabilitiesself._final_schema=self._build_schema_with_capabilities()
asyncdef_create(self,ctx:ResourceContext,base_plan:dict):frompyvider.hubimporthubprovider=hub.get_component("singleton","provider")ifnotprovider._configured:raiseProviderError("Provider not configured")# Now safe to use provider configawaitprovider.api.create_resource(...)
Remember: The lifecycle is deterministic and predictable. Understanding the order of operations helps you implement providers correctly and debug issues when they arise.