in Go/gRPC • Terraform Plugin SDKv2 (Legacy) • Terraform Plugin Framework • Major public providers are still using SDKv2 • Google Cloud, Azure, GitHub, ... ; Contribution Chance!!!
to raw con fi gurations, plans, and states • Limited access to meaningless con fi gurations, plans, and states • e.g. no prior state on Create, no plan on Delete • Well-typed data models: panic-safe • Extensible type system, extensible validations • Terraform plugin protocol version6: support nested attributes
is also for data sources; some attrs are for resources, some are datasources return &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, // untyped in Go type system Required: true, }, }, CreateContext: resourceExampleCreate, // each functionaility is just attributes; cannot get error from compiler } } func resourceExampleCreate(_ context.Context, d *schema.ResourceData, meta any) error { client := meta.(*Client) // type assersion; panicable name := d.Get("name").(string) // type assersion; panicable example := client.CreateExample(name) d.Set("description", example.Description) // schema.ResourceData is merged plan or state value }
any) diag.Diagnostics { v := d.Get("...") // plan; no access to raw configuration. note that v is any old, new := d.GetChange("...") // old == nil, new == value with Get d.HasChange("...") // always true d.Set("...") // saved into new state } func ExampleRead(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { v := d.Get("...") // prior state old, new := d.GetChange("...") // no changes as only prior state is available d.HasChange("...") // always false d.Set("...") // saved into new state } func ExampleUpdate(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { v := d.Get("...") // plan; no access to raw configuration old, new := d.GetChange("...") // prior state and plan unless unknown d.HasChange("...") // comparison of prior state and plan d.Set("...") // saved into new state } func ExampleDelete(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { v := d.Get("...") // prior state old, new := d.GetChange("...") // no changes as only prior state is available d.HasChange("...") // always false d.Set("...") // extraneous, resource destroy leaves no state }
resource.CreateRequest, resp *resource.CreateResponse) { req.Config // raw configuration req.Plan // plan // No req.State as it is always nil resp.State // new state to save } func (_ ExampleResource) Read(_ context.Context, req resource.ReadRequest, resp *resource.CreateResponse) { // No req.Config as configuration cannot be read by provider during read // No req.Plan as there is no plan during read req.State // prior state resp.State // new state to save } func (_ ExampleResource) Update(_ context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { req.Config // raw configuration req.Plan // plan req.State // prior state resp.State // new state data to save } func (_ ExampleResource) Delete(_ context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // No req.Config as configuration cannot be read by provider during delete // No req.Plan as it is always null req.State // prior state resp.State // only available to explicitly remove on error }
{} type frameworkProviderModel struct { APIToken types.String `tfsdk:"api_token"` } func NewFrameworkProvider() provider.Provider { return &frameworkProvider{} } func (p *frameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ // Provider-level configuration schema: has to be the same with SDKv2's one. Attributes: map[string]schema.Attribute{ "api_token": schema.StringAttribute{ Description: "API token to call...", }, }, } } func (p *frameworkProvider) Configure(_ context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { var data frameworkProviderModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) // Do some initialization, e.g. create API client based on the provider-level configuration c := NewClient(data.APIToken) resp.ResourceData = c resp.DataSourceData = c }
(func() ProviderServer, error) { // SDKv2-based provider is v5. Need to upgrade to use new features in the Framework-based provider. sdkServer, _ := tf5to6server.UpgradeServer( ctx, NewSDKv2Provider().GRPCProvider, // The old SDKv2-based provider. ) // Mux muxServer, err := tf6muxserver.NewMuxServer( ctx, providerserver.NewProtocol6(NewFrameworkProvider()), // The new Framework-based provider. func() tfprotov6.ProviderServer{ return sdkServer }, ) return muxServer.ProviderServer, nil }
*testing.T) { resource.Test(t, resource.TestCase{ Steps: []resource.TestStep{ { // Step 1: Apply a configuration with the SDKv2-based provider. ProviderFactories: testSDKv2ProviderFactories(), ConfigDirectory: config.TestNameDirectory(), }, { // Step 2: Apply the same configuration with the Framework-based provider. ProviderFactories: testFrameworkProviderFactories(), ConfigDirectory: config.TestNameDirectory(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), // There should be no change if they're compatible. }, }, }, }, }) }
to achieve "nested attributes" in SDKv2 • The equivalent is Blocks but: not support Optional-Computed • Use Nested Attribute if breaking changes are acceptable • Unknown/empty value handling • You might need to add some workaround