Skip to content

Commit

Permalink
provider: add example framework provider, resource and data source
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobbednarz committed Jan 16, 2023
1 parent 9e6b224 commit c15d540
Show file tree
Hide file tree
Showing 7 changed files with 580 additions and 0 deletions.
41 changes: 41 additions & 0 deletions internal/framework/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package framework

import (
"context"
"errors"
"fmt"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

type Config struct {
Email string
APIKey string
APIUserServiceKey string
APIToken string
Options []cloudflare.Option
}

func (c *Config) Client() (*cloudflare.API, error) {
var err error
var client *cloudflare.API
ctx := context.Background()

if c.APIUserServiceKey != "" {
client, err = cloudflare.NewWithUserServiceKey(c.APIUserServiceKey, c.Options...)
} else if c.APIToken != "" {
client, err = cloudflare.NewWithAPIToken(c.APIToken, c.Options...)
} else if c.APIKey != "" {
client, err = cloudflare.New(c.APIKey, c.Email, c.Options...)
} else {
return nil, errors.New("no credentials detected")
}

if err != nil {
return nil, fmt.Errorf("error creating new Cloudflare client: %w", err)
}

tflog.Info(ctx, fmt.Sprintf("cloudflare Client configured for user: %s", c.Email))
return client, nil
}
102 changes: 102 additions & 0 deletions internal/framework/data_source_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package framework

import (
"context"
"fmt"
"net/http"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// Ensure provider defined types fully satisfy framework interfaces
var _ datasource.DataSource = &ExampleDataSource{}

func NewExampleDataSource() datasource.DataSource {
return &ExampleDataSource{}
}

// ExampleDataSource defines the data source implementation.
type ExampleDataSource struct {
client *http.Client
}

// ExampleDataSourceModel describes the data source data model.
type ExampleDataSourceModel struct {
ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
Id types.String `tfsdk:"id"`
}

func (d *ExampleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_example"
}

func (d *ExampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Example data source",

Attributes: map[string]schema.Attribute{
"configurable_attribute": schema.StringAttribute{
MarkdownDescription: "Example configurable attribute",
Optional: true,
},
"id": schema.StringAttribute{
MarkdownDescription: "Example identifier",
Computed: true,
},
},
}
}

func (d *ExampleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*http.Client)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

d.client = client
}

func (d *ExampleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ExampleDataSourceModel

// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := d.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
// return
// }

// For the purposes of this example code, hardcoding a response value to
// save into the Terraform state.
data.Id = types.StringValue("example-id")

// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "read a data source")

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
29 changes: 29 additions & 0 deletions internal/framework/data_source_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package framework

import (
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccExampleDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccExampleDataSourceConfig,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.scaffolding_example.test", "id", "example-id"),
),
},
},
})
}

const testAccExampleDataSourceConfig = `
data "scaffolding_example" "test" {
configurable_attribute = "example"
}
`
158 changes: 158 additions & 0 deletions internal/framework/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package framework

import (
"context"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// Ensure CloudflareProvider satisfies various provider interfaces.
var _ provider.Provider = &CloudflareProvider{}

// CloudflareProvider defines the provider implementation.
type CloudflareProvider struct {
// version is set to the provider version on release, "dev" when the
// provider is built and ran locally, and "test" when running acceptance
// testing.
version string
}

// CloudflareProviderModel describes the provider data model.
type CloudflareProviderModel struct {
APIKey types.String `tfsdk:"api_key"`
APIUserServiceKey types.String `tfsdk:"api_user_service_key"`
Email types.String `tfsdk:"email"`
MinBackOff types.Int64 `tfsdk:"min_backoff"`
RPS types.Int64 `tfsdk:"rps"`
AccountID types.String `tfsdk:"account_id"`
APIBasePath types.String `tfsdk:"api_base_path"`
APIToken types.String `tfsdk:"api_token"`
Retries types.Int64 `tfsdk:"retries"`
MaxBackoff types.Int64 `tfsdk:"max_backoff"`
APIClientLogging types.Bool `tfsdk:"api_client_logging"`
APIHostname types.String `tfsdk:"api_hostname"`
}

func (p *CloudflareProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "cloudflare"
resp.Version = p.version
}

func (p *CloudflareProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"email": schema.StringAttribute{
Optional: true,
MarkdownDescription: "A registered Cloudflare email address. Alternatively, can be configured using the `CLOUDFLARE_EMAIL` environment variable.",
},

"api_key": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The API key for operations. Alternatively, can be configured using the `CLOUDFLARE_API_KEY` environment variable. API keys are [now considered legacy by Cloudflare](https://developers.cloudflare.com/api/keys/#limitations), API tokens should be used instead.",
},

"api_token": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The API Token for operations. Alternatively, can be configured using the `CLOUDFLARE_API_TOKEN` environment variable.",
},

"api_user_service_key": schema.StringAttribute{
Optional: true,
MarkdownDescription: "A special Cloudflare API key good for a restricted set of endpoints. Alternatively, can be configured using the `CLOUDFLARE_API_USER_SERVICE_KEY` environment variable.",
},

"rps": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "RPS limit to apply when making calls to the API. Alternatively, can be configured using the `CLOUDFLARE_RPS` environment variable.",
},

"retries": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Maximum number of retries to perform when an API request fails. Alternatively, can be configured using the `CLOUDFLARE_RETRIES` environment variable.",
},

"min_backoff": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Minimum backoff period in seconds after failed API calls. Alternatively, can be configured using the `CLOUDFLARE_MIN_BACKOFF` environment variable.",
},

"max_backoff": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Maximum backoff period in seconds after failed API calls. Alternatively, can be configured using the `CLOUDFLARE_MAX_BACKOFF` environment variable.",
},

"api_client_logging": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Whether to print logs from the API client (using the default log library logger). Alternatively, can be configured using the `CLOUDFLARE_API_CLIENT_LOGGING` environment variable.",
},

"account_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Configure API client to always use a specific account. Alternatively, can be configured using the `CLOUDFLARE_ACCOUNT_ID` environment variable.",
DeprecationMessage: "Use resource specific `account_id` attributes instead.",
},

"api_hostname": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Configure the hostname used by the API client. Alternatively, can be configured using the `CLOUDFLARE_API_HOSTNAME` environment variable.",
},

"api_base_path": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Configure the base path used by the API client. Alternatively, can be configured using the `CLOUDFLARE_API_BASE_PATH` environment variable.",
},
},
}
}

func (p *CloudflareProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var data CloudflareProviderModel

resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// Configuration values are now available.
// if data.Endpoint.IsNull() { /* ... */ }

// Example client configuration for data sources and resources
options := []cloudflare.Option{}

options = append(options, cloudflare.BaseURL("https://api.cloudflare.com/client/v4"))
config := Config{Options: options}

client, _ := config.Client()
// if err != nil {
// resp.Diagnostics.Append(err...)
// return
// }
resp.DataSourceData = client
resp.ResourceData = client
}

func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewExampleResource,
}
}

func (p *CloudflareProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewExampleDataSource,
}
}

func New(version string) func() provider.Provider {
return func() provider.Provider {
return &CloudflareProvider{
version: version,
}
}
}
22 changes: 22 additions & 0 deletions internal/framework/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package framework

import (
"testing"

"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
)

// testAccProtoV6ProviderFactories are used to instantiate a provider during
// acceptance testing. The factory function will be invoked for every Terraform
// CLI command executed to create a provider server to which the CLI can
// reattach.
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"scaffolding": providerserver.NewProtocol6WithError(New("test")()),
}

func testAccPreCheck(t *testing.T) {
// You can add code here to run prior to any test case execution, for example assertions
// about the appropriate environment variables being set are common to see in a pre-check
// function.
}
Loading

0 comments on commit c15d540

Please sign in to comment.