From cc97de68b6253a247672d9a5b09465c9f5bb0804 Mon Sep 17 00:00:00 2001 From: angie pinilla Date: Sun, 10 Sep 2023 02:32:06 -0400 Subject: [PATCH] feat: configure `provider` and `quotes` data source (#1) * initial ds and provider setup * additional cleanup * check w.write * declare + assign * add test for 1.5 and add ID for tf1.0? * regenerate docs --- .github/workflows/test.yml | 2 + README.md | 19 +- docs/data-sources/quotes.md | 47 +++++ docs/data-sources/scaffolding_example.md | 30 --- docs/index.md | 14 +- docs/resources/scaffolding_example.md | 31 --- .../scaffolding_example/data-source.tf | 3 - .../theoffice_quotes/data-source.tf | 3 + examples/provider/provider.tf | 4 +- go.mod | 11 +- go.sum | 3 + internal/provider/example_data_source.go | 105 ---------- internal/provider/example_data_source_test.go | 32 --- internal/provider/example_resource.go | 187 ------------------ internal/provider/example_resource_test.go | 56 ------ internal/provider/provider.go | 76 ++++--- internal/provider/provider_test.go | 10 +- internal/provider/quotes_data_source.go | 166 ++++++++++++++++ internal/provider/quotes_data_source_test.go | 54 +++++ internal/theoffice/client.go | 114 +++++++++++ internal/theoffice/client_test.go | 91 +++++++++ main.go | 4 +- 22 files changed, 554 insertions(+), 508 deletions(-) create mode 100644 docs/data-sources/quotes.md delete mode 100644 docs/data-sources/scaffolding_example.md delete mode 100644 docs/resources/scaffolding_example.md delete mode 100644 examples/data-sources/scaffolding_example/data-source.tf create mode 100644 examples/data-sources/theoffice_quotes/data-source.tf delete mode 100644 internal/provider/example_data_source.go delete mode 100644 internal/provider/example_data_source_test.go delete mode 100644 internal/provider/example_resource.go delete mode 100644 internal/provider/example_resource_test.go create mode 100644 internal/provider/quotes_data_source.go create mode 100644 internal/provider/quotes_data_source_test.go create mode 100644 internal/theoffice/client.go create mode 100644 internal/theoffice/client_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88f22de..5ff0a04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,6 +64,8 @@ jobs: - '1.2.*' - '1.3.*' - '1.4.*' + - '1.5.*' + steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 diff --git a/README.md b/README.md index 557af02..0cf3521 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,4 @@ -# Terraform Provider Scaffolding (Terraform Plugin Framework) - -_This template repository is built on the [Terraform Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework). The template repository built on the [Terraform Plugin SDK](https://github.com/hashicorp/terraform-plugin-sdk) can be found at [terraform-provider-scaffolding](https://github.com/hashicorp/terraform-provider-scaffolding). See [Which SDK Should I Use?](https://developer.hashicorp.com/terraform/plugin/framework-benefits) in the Terraform documentation for additional information._ - -This repository is a *template* for a [Terraform](https://www.terraform.io) provider. It is intended as a starting point for creating Terraform providers, containing: - -- A resource and a data source (`internal/provider/`), -- Examples (`examples/`) and generated documentation (`docs/`), -- Miscellaneous meta files. - -These files contain boilerplate code that you will need to edit to create your own Terraform provider. Tutorials for creating Terraform providers can be found on the [HashiCorp Developer](https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework) platform. _Terraform Plugin Framework specific guides are titled accordingly._ - -Please see the [GitHub template repository documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) for how to create a new repository from this template on GitHub. - -Once you've written your provider, you'll want to [publish it on the Terraform Registry](https://developer.hashicorp.com/terraform/registry/providers/publishing) so that others can use it. +# Terraform Provider theOffice ## Requirements @@ -45,7 +31,8 @@ Then commit the changes to `go.mod` and `go.sum`. ## Using the provider -Fill this in for each provider +Refer to the documentation for provider configuration and +available data sources. ## Developing the Provider diff --git a/docs/data-sources/quotes.md b/docs/data-sources/quotes.md new file mode 100644 index 0000000..da06a20 --- /dev/null +++ b/docs/data-sources/quotes.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "theoffice_quotes Data Source - terraform-provider-theoffice" +subcategory: "" +description: |- + Fetches a list of quotes +--- + +# theoffice_quotes (Data Source) + +Fetches a list of quotes + +## Example Usage + +```terraform +data "theoffice_quotes" "example" { + season = 1 +} +``` + + +## Schema + +### Required + +- `season` (Number) Season number to filter results by + +### Optional + +- `episode` (Number) Episode number to filter results by + +### Read-Only + +- `id` (String) Placeholder identifier attribute. +- `quotes` (Attributes List) List of quotes (see [below for nested schema](#nestedatt--quotes)) + + +### Nested Schema for `quotes` + +Read-Only: + +- `character` (String) The character who said the quote. +- `episode` (Number) The episode the quote occurred in. +- `episode_name` (String) The name of the episode the quote occurred in. +- `quote` (String) The quote as a string +- `scene` (Number) The scene the quote occurred in. +- `season` (Number) The season the quote occurred in. diff --git a/docs/data-sources/scaffolding_example.md b/docs/data-sources/scaffolding_example.md deleted file mode 100644 index 9c9de1e..0000000 --- a/docs/data-sources/scaffolding_example.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "scaffolding_example Data Source - terraform-provider-scaffolding-framework" -subcategory: "" -description: |- - Example data source ---- - -# scaffolding_example (Data Source) - -Example data source - -## Example Usage - -```terraform -data "scaffolding_example" "example" { - configurable_attribute = "some-value" -} -``` - - -## Schema - -### Optional - -- `configurable_attribute` (String) Example configurable attribute - -### Read-Only - -- `id` (String) Example identifier diff --git a/docs/index.md b/docs/index.md index 8eecef4..dabba95 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,21 +1,19 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "scaffolding-framework Provider" +page_title: "theoffice Provider" subcategory: "" description: |- - + Interact with theOffice API --- -# scaffolding-framework Provider - +# theoffice Provider +Interact with theOffice API ## Example Usage ```terraform -provider "scaffolding" { - # example configuration here -} +provider "theoffice" {} ``` @@ -23,4 +21,4 @@ provider "scaffolding" { ### Optional -- `endpoint` (String) Example provider attribute +- `endpoint` (String) The REST API endpoint to use for reading data (default: http://theofficeapi-angelinepinilla.b4a.run) diff --git a/docs/resources/scaffolding_example.md b/docs/resources/scaffolding_example.md deleted file mode 100644 index 47e77ed..0000000 --- a/docs/resources/scaffolding_example.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "scaffolding_example Resource - terraform-provider-scaffolding-framework" -subcategory: "" -description: |- - Example resource ---- - -# scaffolding_example (Resource) - -Example resource - -## Example Usage - -```terraform -resource "scaffolding_example" "example" { - configurable_attribute = "some-value" -} -``` - - -## Schema - -### Optional - -- `configurable_attribute` (String) Example configurable attribute -- `defaulted` (String) Example configurable attribute with default value - -### Read-Only - -- `id` (String) Example identifier diff --git a/examples/data-sources/scaffolding_example/data-source.tf b/examples/data-sources/scaffolding_example/data-source.tf deleted file mode 100644 index a852489..0000000 --- a/examples/data-sources/scaffolding_example/data-source.tf +++ /dev/null @@ -1,3 +0,0 @@ -data "scaffolding_example" "example" { - configurable_attribute = "some-value" -} diff --git a/examples/data-sources/theoffice_quotes/data-source.tf b/examples/data-sources/theoffice_quotes/data-source.tf new file mode 100644 index 0000000..ab6ee2a --- /dev/null +++ b/examples/data-sources/theoffice_quotes/data-source.tf @@ -0,0 +1,3 @@ +data "theoffice_quotes" "example" { + season = 1 +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 942db45..1020527 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,3 +1 @@ -provider "scaffolding" { - # example configuration here -} +provider "theoffice" {} diff --git a/go.mod b/go.mod index 1de6997..1626ab7 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,16 @@ -module github.com/hashicorp/terraform-provider-scaffolding-framework +module github.com/anGie44/terraform-provider-theoffice -go 1.19 +go 1.20 require ( + github.com/hashicorp/go-hclog v1.5.0 + github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/terraform-plugin-docs v0.16.0 github.com/hashicorp/terraform-plugin-framework v1.4.0 github.com/hashicorp/terraform-plugin-go v0.19.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.5.1 + github.com/stretchr/testify v1.7.2 ) require ( @@ -20,6 +23,7 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -28,7 +32,6 @@ require ( github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.5.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -53,6 +56,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect github.com/russross/blackfriday v1.6.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect @@ -71,4 +75,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect google.golang.org/grpc v1.57.0 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index df6535d..29cc45b 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,7 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -67,6 +68,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= diff --git a/internal/provider/example_data_source.go b/internal/provider/example_data_source.go deleted file mode 100644 index 585b9d2..0000000 --- a/internal/provider/example_data_source.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -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)...) -} diff --git a/internal/provider/example_data_source_test.go b/internal/provider/example_data_source_test.go deleted file mode 100644 index 6f9aa7d..0000000 --- a/internal/provider/example_data_source_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/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" -} -` diff --git a/internal/provider/example_resource.go b/internal/provider/example_resource.go deleted file mode 100644 index 70e961a..0000000 --- a/internal/provider/example_resource.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// Ensure provider defined types fully satisfy framework interfaces. -var _ resource.Resource = &ExampleResource{} -var _ resource.ResourceWithImportState = &ExampleResource{} - -func NewExampleResource() resource.Resource { - return &ExampleResource{} -} - -// ExampleResource defines the resource implementation. -type ExampleResource struct { - client *http.Client -} - -// ExampleResourceModel describes the resource data model. -type ExampleResourceModel struct { - ConfigurableAttribute types.String `tfsdk:"configurable_attribute"` - Defaulted types.String `tfsdk:"defaulted"` - Id types.String `tfsdk:"id"` -} - -func (r *ExampleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_example" -} - -func (r *ExampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - // This description is used by the documentation generator and the language server. - MarkdownDescription: "Example resource", - - Attributes: map[string]schema.Attribute{ - "configurable_attribute": schema.StringAttribute{ - MarkdownDescription: "Example configurable attribute", - Optional: true, - }, - "defaulted": schema.StringAttribute{ - MarkdownDescription: "Example configurable attribute with default value", - Optional: true, - Computed: true, - Default: stringdefault.StaticString("example value when not configured"), - }, - "id": schema.StringAttribute{ - Computed: true, - MarkdownDescription: "Example identifier", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -func (r *ExampleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.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 Resource Configure Type", - fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), - ) - - return - } - - r.client = client -} - -func (r *ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data ExampleResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.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 := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create 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, "created a resource") - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r *ExampleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data ExampleResourceModel - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.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 := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err)) - // return - // } - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r *ExampleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data ExampleResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.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 := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err)) - // return - // } - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r *ExampleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data ExampleResourceModel - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.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 := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete example, got error: %s", err)) - // return - // } -} - -func (r *ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) -} diff --git a/internal/provider/example_resource_test.go b/internal/provider/example_resource_test.go deleted file mode 100644 index c5464d0..0000000 --- a/internal/provider/example_resource_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" -) - -func TestAccExampleResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Create and Read testing - { - Config: testAccExampleResourceConfig("one"), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("scaffolding_example.test", "configurable_attribute", "one"), - resource.TestCheckResourceAttr("scaffolding_example.test", "defaulted", "example value when not configured"), - resource.TestCheckResourceAttr("scaffolding_example.test", "id", "example-id"), - ), - }, - // ImportState testing - { - ResourceName: "scaffolding_example.test", - ImportState: true, - ImportStateVerify: true, - // This is not normally necessary, but is here because this - // example code does not have an actual upstream service. - // Once the Read method is able to refresh information from - // the upstream service, this can be removed. - ImportStateVerifyIgnore: []string{"configurable_attribute", "defaulted"}, - }, - // Update and Read testing - { - Config: testAccExampleResourceConfig("two"), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("scaffolding_example.test", "configurable_attribute", "two"), - ), - }, - // Delete testing automatically occurs in TestCase - }, - }) -} - -func testAccExampleResourceConfig(configurableAttribute string) string { - return fmt.Sprintf(` -resource "scaffolding_example" "test" { - configurable_attribute = %[1]q -} -`, configurableAttribute) -} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c326755..e64c10b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -5,49 +5,55 @@ package provider import ( "context" - "net/http" + "os" + + "github.com/anGie44/terraform-provider-theoffice/internal/theoffice" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" "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" + + "github.com/hashicorp/terraform-plugin-log/tflog" ) -// Ensure ScaffoldingProvider satisfies various provider interfaces. -var _ provider.Provider = &ScaffoldingProvider{} +// Ensure theOfficeProvider satisfies various provider interfaces. +var _ provider.Provider = &theOfficeProvider{} -// ScaffoldingProvider defines the provider implementation. -type ScaffoldingProvider struct { +// theOfficeProvider defines the provider implementation. +type theOfficeProvider 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 } -// ScaffoldingProviderModel describes the provider data model. -type ScaffoldingProviderModel struct { +// theOfficeProviderModel describes the provider data model. +type theOfficeProviderModel struct { Endpoint types.String `tfsdk:"endpoint"` } -func (p *ScaffoldingProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { - resp.TypeName = "scaffolding" +func (p *theOfficeProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "theoffice" resp.Version = p.version } -func (p *ScaffoldingProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { +func (p *theOfficeProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ + Description: "Interact with theOffice API", Attributes: map[string]schema.Attribute{ "endpoint": schema.StringAttribute{ - MarkdownDescription: "Example provider attribute", - Optional: true, + Description: "The REST API endpoint to use for reading data (default: http://theofficeapi-angelinepinilla.b4a.run)", + Optional: true, }, }, } } -func (p *ScaffoldingProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { - var data ScaffoldingProviderModel +func (p *theOfficeProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + var data theOfficeProviderModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) @@ -56,29 +62,53 @@ func (p *ScaffoldingProvider) Configure(ctx context.Context, req provider.Config } // Configuration values are now available. - // if data.Endpoint.IsNull() { /* ... */ } + if data.Endpoint.IsUnknown() { + resp.Diagnostics.AddAttributeError( + path.Root("endpoint"), + "Unknown theOffice API endpoint", + "The provider cannot create theOffice API client as there is an unknown configuration value for theOffice API endpoint. "+ + "Either target apply the source of the value first, set the value statically in the configuration, or use the THEOFFICE_ENDPOINT environment variable.", + ) + } + + if resp.Diagnostics.HasError() { + return + } + + endpoint := os.Getenv("THEOFFICE_ENDPOINT") + + if !data.Endpoint.IsNull() { + endpoint = data.Endpoint.ValueString() + } // Example client configuration for data sources and resources - client := http.DefaultClient + client, err := theoffice.NewClient(&theoffice.Config{ + Address: endpoint, + }) + if err != nil { + resp.Diagnostics.AddError("error configuring theOffice client", err.Error()) + return + } + resp.DataSourceData = client resp.ResourceData = client + + tflog.Info(ctx, "Configured theOffice client", map[string]any{"success": true}) } -func (p *ScaffoldingProvider) Resources(ctx context.Context) []func() resource.Resource { - return []func() resource.Resource{ - NewExampleResource, - } +func (p *theOfficeProvider) Resources(ctx context.Context) []func() resource.Resource { + return nil } -func (p *ScaffoldingProvider) DataSources(ctx context.Context) []func() datasource.DataSource { +func (p *theOfficeProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ - NewExampleDataSource, + NewQuotesDataSource, } } func New(version string) func() provider.Provider { return func() provider.Provider { - return &ScaffoldingProvider{ + return &theOfficeProvider{ version: version, } } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index ef6599b..667beb9 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -4,8 +4,6 @@ package provider import ( - "testing" - "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) @@ -15,11 +13,5 @@ import ( // 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. + "theoffice": providerserver.NewProtocol6WithError(New("test")()), } diff --git a/internal/provider/quotes_data_source.go b/internal/provider/quotes_data_source.go new file mode 100644 index 0000000..031ccda --- /dev/null +++ b/internal/provider/quotes_data_source.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/anGie44/terraform-provider-theoffice/internal/theoffice" + + "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 = &QuotesDataSource{} +) + +func NewQuotesDataSource() datasource.DataSource { + return &QuotesDataSource{} +} + +// QuotesDataSource defines the data source implementation. +type QuotesDataSource struct { + client *theoffice.Client +} + +// QuotesDataSourceModel describes the data source data model. +type QuotesDataSourceModel struct { + Episode types.Int64 `tfsdk:"episode"` + Season types.Int64 `tfsdk:"season"` + Quotes []quotesModel `tfsdk:"quotes"` + ID types.String `tfsdk:"id"` +} + +type quotesModel struct { + Season types.Int64 `tfsdk:"season"` + Episode types.Int64 `tfsdk:"episode"` + Scene types.Int64 `tfsdk:"scene"` + EpisodeName types.String `tfsdk:"episode_name"` + Character types.String `tfsdk:"character"` + Quote types.String `tfsdk:"quote"` +} + +func (d *QuotesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_quotes" +} + +func (d *QuotesDataSource) 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: "Fetches a list of quotes", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Placeholder identifier attribute.", + Computed: true, + }, + "episode": schema.Int64Attribute{ + Optional: true, + Description: "Episode number to filter results by", + }, + "season": schema.Int64Attribute{ + Required: true, + Description: "Season number to filter results by", + }, + "quotes": schema.ListNestedAttribute{ + Description: "List of quotes", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "season": schema.Int64Attribute{ + Description: "The season the quote occurred in.", + Computed: true, + }, + "episode": schema.Int64Attribute{ + Description: "The episode the quote occurred in.", + Computed: true, + }, + "scene": schema.Int64Attribute{ + Description: "The scene the quote occurred in.", + Computed: true, + }, + "episode_name": schema.StringAttribute{ + Description: "The name of the episode the quote occurred in.", + Computed: true, + }, + "character": schema.StringAttribute{ + Description: "The character who said the quote.", + Computed: true, + }, + "quote": schema.StringAttribute{ + Description: "The quote as a string", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (d *QuotesDataSource) 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.(*theoffice.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *theoffice.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *QuotesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data QuotesDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + // Read Terraform configuration data into the model + quotes, err := d.client.GetQuotes(ctx, int(data.Season.ValueInt64()), int(data.Episode.ValueInt64())) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read theOffice Quotes", + err.Error(), + ) + return + } + + for _, quote := range quotes.Quotes { + quoteState := quotesModel{ + Season: types.Int64Value(int64(quote.Season)), + Episode: types.Int64Value(int64(quote.Episode)), + Scene: types.Int64Value(int64(quote.Scene)), + EpisodeName: types.StringValue(quote.EpisodeName), + Character: types.StringValue(quote.Character), + Quote: types.StringValue(quote.Quote), + } + + data.Quotes = append(data.Quotes, quoteState) + } + + data.ID = types.StringValue("placeholder") + + // Save data into Terraform state + diags := resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "read quotes data source") +} diff --git a/internal/provider/quotes_data_source_test.go b/internal/provider/quotes_data_source_test.go new file mode 100644 index 0000000..8d05f6e --- /dev/null +++ b/internal/provider/quotes_data_source_test.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccQuotesDataSource(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Read testing + { + Config: testAccQuotesDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.theoffice_quotes.test", "quotes.#"), + ), + }, + }, + }) +} + +func TestAccQuotesDataSource_filterByEpisode(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Read testing + { + Config: testAccQuotesDataSourceConfig_filterByEpisode, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.theoffice_quotes.test", "quotes.#"), + resource.TestCheckResourceAttr("data.theoffice_quotes.test", "quotes.0.episode", "1"), + ), + }, + }, + }) +} + +const testAccQuotesDataSourceConfig = ` +data "theoffice_quotes" "test" { + season = 1 +} +` + +const testAccQuotesDataSourceConfig_filterByEpisode = ` +data "theoffice_quotes" "test" { + season = 1 + episode = 1 +} +` diff --git a/internal/theoffice/client.go b/internal/theoffice/client.go new file mode 100644 index 0000000..382d50d --- /dev/null +++ b/internal/theoffice/client.go @@ -0,0 +1,114 @@ +package theoffice + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/url" + "strings" + "time" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-retryablehttp" +) + +const ( + defaultAddress = "http://theofficeapi-angelinepinilla.b4a.run" + + defaultRetryWaitMax = 30 * time.Second +) + +type Config struct { + Address string +} + +type Client struct { + baseURL string + httpClient *retryablehttp.Client +} + +func NewClient(config *Config) (*Client, error) { + if config.Address == "" { + config.Address = defaultAddress + } + + if _, err := url.Parse(config.Address); err != nil { + return nil, fmt.Errorf("invalid address %q: %w", config.Address, err) + } + + client := retryablehttp.NewClient() + client.RetryWaitMax = defaultRetryWaitMax + client.ErrorHandler = retryablehttp.PassthroughErrorHandler + + return &Client{ + baseURL: config.Address, + httpClient: client, + }, nil +} + +type QuotesResponse struct { + Quotes []Quote +} + +type Quote struct { + Season int `json:"season,omitempty"` + Episode int `json:"episode,omitempty"` + Scene int `json:"scene,omitempty"` + EpisodeName string `json:"episode_name,omitempty"` + Character string `json:"character,omitempty"` + Quote string `json:"quote,omitempty"` +} + +func (c *Client) GetQuotes(ctx context.Context, season, episode int) (*QuotesResponse, error) { + path := fmt.Sprintf("/season/%d", season) + if episode > 0 { + path += fmt.Sprintf("/episode/%d", episode) + } else { + path += "/format/quotes" + } + resp := &QuotesResponse{} + err := c.do(ctx, "GET", path, nil, &resp.Quotes) + return resp, err +} + +func (c *Client) do(ctx context.Context, method, path string, rq, resp any) error { + f := func() error { + logger := hclog.FromContext(ctx).Named("theoffice_client") + ctx = hclog.WithContext(ctx, logger) + url := fmt.Sprintf("%s/%s", c.baseURL, strings.TrimPrefix(path, "/")) + var body io.Reader + if rq != nil { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(rq); err != nil { + return fmt.Errorf("encoding request: %w", err) + } + body = &buf + } + req, err := retryablehttp.NewRequest(method, url, body) + if err != nil { + return fmt.Errorf("constructing http request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + logger.Debug("making http request", "method", method, "url", url) + res, err := c.httpClient.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + ok := res.StatusCode >= 200 && res.StatusCode < 300 + if !ok { + resBody, err := io.ReadAll(res.Body) + if err != nil || string(resBody) == "" { + return fmt.Errorf("%s %s: bad status (%d)", method, url, res.StatusCode) + } + return fmt.Errorf("%s %s: bad status (%d)\n%s", method, url, res.StatusCode, string(resBody)) + } + + return json.NewDecoder(res.Body).Decode(resp) + } + return f() +} diff --git a/internal/theoffice/client_test.go b/internal/theoffice/client_test.go new file mode 100644 index 0000000..79d13c5 --- /dev/null +++ b/internal/theoffice/client_test.go @@ -0,0 +1,91 @@ +package theoffice + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientDefault(t *testing.T) { + _, err := NewClient(&Config{}) + assert.NoError(t, err) +} + +func TestClientQuotes_season(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/season/1/format/quotes", r.URL.Path) + assert.Equal(t, "GET", r.Method) + + _, err := w.Write([]byte(`[{"season": 1,"episode": 1,"scene": 1,"episode_name": "Diversity Day","character": "Jim","quote": "Really?"}]`)) + assert.NoError(t, err) + })) + defer srv.Close() + + c, err := NewClient(&Config{ + Address: srv.URL, + }) + assert.NoError(t, err) + + resp, err := c.GetQuotes(context.Background(), 1, 0) + assert.NoError(t, err) + + assert.Equal(t, 1, len(resp.Quotes)) + assert.Equal(t, 1, resp.Quotes[0].Season) + assert.Equal(t, 1, resp.Quotes[0].Episode) + assert.Equal(t, 1, resp.Quotes[0].Scene) + assert.Equal(t, "Diversity Day", resp.Quotes[0].EpisodeName) + assert.Equal(t, "Jim", resp.Quotes[0].Character) + assert.Equal(t, "Really?", resp.Quotes[0].Quote) +} + +func TestClientQuotes_episode(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/season/1/episode/1", r.URL.Path) + assert.Equal(t, "GET", r.Method) + + _, err := w.Write([]byte(`[{"season": 1,"episode": 1,"scene": 1,"episode_name": "Diversity Day","character": "Jim","quote": "Really?"}]`)) + assert.NoError(t, err) + })) + defer srv.Close() + + c, err := NewClient(&Config{ + Address: srv.URL, + }) + assert.NoError(t, err) + + resp, err := c.GetQuotes(context.Background(), 1, 1) + assert.NoError(t, err) + + assert.Equal(t, 1, len(resp.Quotes)) + assert.Equal(t, 1, len(resp.Quotes)) + assert.Equal(t, 1, resp.Quotes[0].Season) + assert.Equal(t, 1, resp.Quotes[0].Episode) + assert.Equal(t, 1, resp.Quotes[0].Scene) + assert.Equal(t, "Diversity Day", resp.Quotes[0].EpisodeName) + assert.Equal(t, "Jim", resp.Quotes[0].Character) + assert.Equal(t, "Really?", resp.Quotes[0].Quote) +} + +func TestClientQuotes_none(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/season/1/format/quotes", r.URL.Path) + assert.Equal(t, "GET", r.Method) + + _, err := w.Write([]byte(`[]`)) + assert.NoError(t, err) + })) + defer srv.Close() + + c, err := NewClient(&Config{ + Address: srv.URL, + }) + assert.NoError(t, err) + + resp, err := c.GetQuotes(context.Background(), 1, 0) + assert.NoError(t, err) + + assert.Equal(t, 0, len(resp.Quotes)) +} diff --git a/main.go b/main.go index e3d16b2..534be4e 100644 --- a/main.go +++ b/main.go @@ -8,8 +8,8 @@ import ( "flag" "log" + "github.com/anGie44/terraform-provider-theoffice/internal/provider" "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-provider-scaffolding-framework/internal/provider" ) // Run "go generate" to format example terraform files and generate the docs for the registry/website @@ -39,7 +39,7 @@ func main() { opts := providerserver.ServeOpts{ // TODO: Update this string with the published name of your provider. - Address: "registry.terraform.io/hashicorp/scaffolding", + Address: "registry.terraform.io/anGie44/theoffice", Debug: debug, }