Terraform Plugin Framework

repository·main·Indexed 18 days ago

https://github.com/hashicorp/terraform-plugin-framework

A Go module used to build Terraform providers, providing a higher-level abstraction over terraform-plugin-go to simplify provider development. It is compatible with Terraform v0.12 and above and requires Go 1.25 or later. The framework focuses on unit testability, compatibility, discoverability, Go-nativeness, and managing verbosity.

Tokens
28.8K
Snippets
63
Records
104
Agent score
63%

What's inside terraform-plugin-framework

  1. Overview of Terraform Plugin Framework

    main
    terraform-plugin-framework is a Go module designed for building Terraform providers. It is built on top of terraform-plugin-go and is intended to provide the same power and versatility while abstracting away implementation details and reducing repetitive, verbose tasks common in provider development.
  2. Understand the design choice between structs and interfaces in the framework

    main

    The Terraform Plugin Framework's design involves a fundamental choice in how it represents core concepts like Providers, Resources, and Data Sources: as structs (values) or interfaces (types).

    • Structs (Values): Represent resources as mutable, local instances. They are typically generated by helpers at startup and represent a single instance rather than an archetypal concept.
    • Interfaces (Types): Represent resources as immutable, global types. They are composed through type composition to share patterns and are not generated via helpers.

    This distinction affects how you implement and compose your provider logic, specifically regarding runtime mutability and how patterns are shared across your codebase.

  3. Compare tfprotov6.Schema vs framework.Schema

    main

    The framework provides two ways to think about schemas:

    1. tfprotov6.Schema: The underlying type used by the Terraform plugin-go ecosystem. It is highly verbose and requires manual management of Block fields and SchemaAttribute slices.
    2. framework.Schema (Proposed/Conceptual): A higher-level, less verbose wrapper. It uses Go maps where keys are the attribute or block names, reducing boilerplate. It handles defaults for fields like DescriptionKind and simplifies nested block definitions using a NestedBlockType.

    Key benefits of the framework-specific schema approach:

    • Reduced Verbosity: Uses map keys for names instead of explicit Name fields.
    • Discoverability: Types are documented within the framework rather than requiring external tftypes or tfprotov6 imports.
    • Abstraction: Hides the complexity of Terraform protocol version changes (e.g., moving from v5 to v6) from the provider developer.
  4. Understand the difference between Warnings and Errors in Terraform diagnostics

    main

    Terraform distinguishes between two levels of feedback to support human workflows:

    • Warnings: Signal an issue that the practitioner should be aware of, but they do not prevent the execution of the command.
    • Errors: Signal a critical issue that generally causes the Terraform CLI to return early and affects the exit status.

    In the terraform-plugin-framework, it is recommended to use diagnostics rather than simple Go error values or panic calls when you want to provide contextualized feedback (such as warnings) to the user.

  5. Differentiate between Concrete Values and Patterns of Values

    main

    When working with attribute paths, it is important to distinguish between two primary use cases:

    Concrete Values

    Concrete values point to a single, specific, and unique value (e.g., a specific index in a list or a specific key in a map). These are primarily used in diagnostics to tell a user exactly where an error occurred.

    Patterns of Values

    Patterns are used to describe a set or a rule that matches multiple values. This is primarily used in validation helpers. For example:

    • Ensuring that if an attribute is set on one element in a list, it cannot be set on any other elements in that list.
    • Requiring that every object element in a map contains a specific attribute.

    Patterns are a superset of concrete values; a pattern can be written so restrictively that it only matches a single attribute.

  6. Handle attribute paths in validation

    main

    Validation functions receive an *tftypes.AttributePath parameter. This allows you to:

    • Include the full path in error messages for better user feedback.
    • Use the path in logging for debugging.
    • Make logic decisions based on the specific attribute being validated.

    While the framework can automatically wrap errors with path information, passing the *tftypes.AttributePath directly is the most flexible approach for implementors.

  7. Design philosophy for Diagnostics in Terraform Plugin Framework

    main

    The Terraform Plugin Framework handles diagnostics (errors and warnings) using a dedicated interface-based approach rather than extending the standard Go error interface.

    This design choice is made to:

    1. Avoid semantic ambiguity: Extending the error interface would force warnings to be treated as errors by standard Go error handling, which is undesirable.
    2. Maintain framework ownership: By using a custom interface, the framework can own the abstraction and provide ergonomic methods (like Severity(), Summary(), and Detail()) without the constraints or verbosity of the standard error package requirements.
    3. Support extensibility: Using interfaces allows for specialized diagnostic types (e.g., ValidationErrorDiagnostic) that can implement additional capabilities, such as logging or path attribution, while remaining compatible with the core Diagnostic interface.
  8. Compare implementation patterns: Structs vs Interfaces vs Mixed

    main

    The framework's architecture can be conceptualized through three implementation patterns. While the actual framework implementation is specific, these patterns illustrate the different ways a developer might interact with the API surface:

    1. Pure Struct Pattern

    In this pattern, resources and providers are defined as structs containing function fields. This treats the resource as a collection of behaviors (functions) assigned to a value.

    2. Pure Interface Pattern

    In this pattern, resources and providers are defined as interfaces. This treats the resource as a type that must satisfy a specific contract of methods (e.g., Create, Read, Update, Delete).

    3. Mixed Pattern

    This pattern uses structs for state and configuration, but delegates core logic to an interface (e.g., a ResourceImplementation interface). This allows for a combination of structured data and polymorphic behavior.

    // Example of the Mixed Pattern approach
    type Resource struct {
    	Schema                *tfprotov5.Schema
    	Implementation        func(p ProviderState) ResourceImplementation
    }
    
    type ResourceImplementation interface {
    	Create(context.Context) []*tfprotov5.Diagnostic
    	Read(context.Context) []*tfprotov5.Diagnostic
    	Update(context.Context) []*tfprotov5.Diagnostic
    	Destroy(context.Context) []*tfprotov5.Diagnostic
    }
  9. Implement Data Source Configuration Validation

    main

    Data Source level validation can be implemented using two patterns:

    1. Declarative Validation: Implement the DataSourceWithConfigValidators interface by providing a ConfigValidators(context.Context) []DataSourceConfigValidator method. This is used for reusable, predefined validation rules (like ConflictingAttributes).
    2. Imperative Validation: Implement the DataSourceWithValidateConfig interface by providing a ValidateConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) method. This is used for complex, custom logic that cannot be expressed declaratively.

    Validation requests (ValidateDataSourceConfigRequest) provide access to the tfsdk.Config and the TypeName of the data source.

    // Declarative approach
    func (d *customDataSource) ConfigValidators(ctx context.Context) DataSourceConfigValidators {
        return DataSourceConfigValidators{
            ConflictingAttributes(
                tftypes.NewAttributePath().AttributeName("first_attribute"),
                tftypes.NewAttributePath().AttributeName("second_attribute"),
            ),
        }
    }
  10. Understand Schema implementation in Terraform Plugin Protocol v6

    main

    The Terraform Plugin Protocol version 6 introduced a significant change in how nested data is handled.

    Previously, complex nested structures could only be implemented using Blocks. Version 6 introduced the nested_type field within the Attribute message. This allows an Attribute to encode nested object schema information directly, effectively allowing attributes to act as containers for nested data (similar to how blocks work) while maintaining the properties of an attribute.

    This shift allows for more flexible schema definitions and better integration with modern Terraform features.

  11. Design patterns for validation in Terraform Plugin Framework

    main

    The Terraform Plugin Framework uses a Request and Response pattern for validation to ensure long-term compatibility and allow for context-specific data. Instead of passing raw, typed parameters (like attr.Value or path), validation functions receive a request object and a pointer to a response object. This allows the framework to evolve the underlying data structures without breaking existing provider implementations.

    For example, instead of: func(ctx, path, value) error

    The framework prefers: func(ctx, ValidateRequest, *ValidateResponse)

  12. How resource import works in Terraform

    main

    Resource import is the process of bringing existing, unmanaged resources under Terraform's management by creating their corresponding state in the Terraform statefile.

    When a user executes the terraform import command, the Terraform CLI forwards a request to the provider containing a resource address (parsed into a type_name) and an import identifier (id). The provider is responsible for responding with the relevant resource state(s).

    Terraform supports two main interaction patterns:

    1. 1:1 Import: A single resource address maps to a single resource state. This is the most common pattern.
    2. Multiple Resource Import: A single import command can trigger the import of multiple related resources (e.g., importing an S3 bucket also importing its bucket policy). These secondary resources are saved into the state using the same identifier or as indexed resources (e.g., resource.name[0]).
    # Example of a 1:1 import command
    terraform import aws_security_group.example sg-12345678