Kaynağa Gözat

Tutorial 5: create and read back.

Frederic G. MARAND 5 ay önce
ebeveyn
işleme
2653572c9c

+ 0 - 40
docs/data-sources/coffees.md

@@ -1,40 +0,0 @@
----
-# generated by https://github.com/hashicorp/terraform-plugin-docs
-page_title: "hashicups_coffees Data Source - hashicups"
-subcategory: ""
-description: |-
-  
----
-
-# hashicups_coffees (Data Source)
-
-
-
-
-
-<!-- schema generated by tfplugindocs -->
-## Schema
-
-### Read-Only
-
-- `coffees` (Attributes List) (see [below for nested schema](#nestedatt--coffees))
-
-<a id="nestedatt--coffees"></a>
-### Nested Schema for `coffees`
-
-Read-Only:
-
-- `description` (String)
-- `id` (Number)
-- `image` (String)
-- `ingredients` (Attributes List) (see [below for nested schema](#nestedatt--coffees--ingredients))
-- `name` (String)
-- `price` (Number)
-- `teaser` (String)
-
-<a id="nestedatt--coffees--ingredients"></a>
-### Nested Schema for `coffees.ingredients`
-
-Read-Only:
-
-- `id` (Number)

+ 0 - 28
docs/index.md

@@ -1,28 +0,0 @@
----
-# generated by https://github.com/hashicorp/terraform-plugin-docs
-page_title: "hashicups Provider"
-subcategory: ""
-description: |-
-  
----
-
-# hashicups Provider
-
-
-
-## Example Usage
-
-```terraform
-provider "scaffolding" {
-  # example configuration here
-}
-```
-
-<!-- schema generated by tfplugindocs -->
-## Schema
-
-### Optional
-
-- `host` (String)
-- `password` (String, Sensitive)
-- `username` (String)

+ 30 - 0
examples/order/main.tf

@@ -0,0 +1,30 @@
+terraform {
+  required_providers {
+    hashicups = {
+      source = "hashicorp.com/edu/hashicups"
+    }
+  }
+}
+
+provider "hashicups" {
+  host = "http://localhost:19090"
+  username = "education"
+  password = "test123"
+}
+
+resource "hashicups_order" "edu" {
+  items = [
+    {
+      coffee = { id = 3 }
+      quantity = 2
+    },
+    {
+      coffee = { id = 1 }
+      quantity = 2
+    },
+  ]
+}
+
+output "edu_order" {
+  value = hashicups_order.edu
+}

+ 15 - 3
internal/provider/coffees_data_source.go

@@ -10,6 +10,7 @@ import (
 	"github.com/hashicorp-demoapp/hashicups-client-go"
 	"github.com/hashicorp/terraform-plugin-framework/datasource"
 	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
 	"github.com/hashicorp/terraform-plugin-framework/types"
 )
 
@@ -91,6 +92,17 @@ func (d *coffeesDataSource) Schema(ctx context.Context, req datasource.SchemaReq
 
 // Read is used by the data source to refresh the Terraform state based on the schema data.
 func (d *coffeesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var diags diag.Diagnostics
+
+	// 0. Checks whether the API Client is configured.
+	if d.client == nil {
+		resp.Diagnostics.AddError(
+			"cannot read coffees",
+			"client is not configured in coffeesDataSource.Read",
+		)
+		return
+	}
+
 	// 1. Reads coffees list. The method invokes the API client's GetCoffees method.
 	var state coffeesDataSourceModel
 	coffees, err := d.client.GetCoffees()
@@ -123,11 +135,11 @@ func (d *coffeesDataSource) Read(ctx context.Context, req datasource.ReadRequest
 		state.Coffees = append(state.Coffees, coffeeState)
 	}
 
-	// 3. Sets state with coffees list.
-	diags := resp.State.Set(ctx, state)
+	// 3. Set Terraform's state with the coffees model.
+	diags = resp.State.Set(ctx, state)
 	resp.Diagnostics.Append(diags...)
 
-	// Useless code from tutorial, why ?
+	// Useless code in tutorial, why ?
 	if resp.Diagnostics.HasError() {
 		return
 	}

+ 261 - 0
internal/provider/order_resource.go

@@ -0,0 +1,261 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp-demoapp/hashicups-client-go"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource              = &orderResource{}
+	_ resource.ResourceWithConfigure = &orderResource{}
+)
+
+// NewOrderResource is a helper function to simplify the provider implementation.
+func NewOrderResource() resource.Resource {
+	return &orderResource{}
+}
+
+type orderResource struct {
+	client *hashicups.Client
+}
+
+func (r *orderResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	// Add a nil check when handling ProviderData because Terraform
+	// sets that data after it calls the ConfigureProvider RPC.
+	if req.ProviderData == nil {
+		return
+	}
+
+	client, ok := req.ProviderData.(*hashicups.Client)
+	if !ok {
+		resp.Diagnostics.AddError("Unexpected Resource Configure Type",
+			fmt.Sprintf("Expected *hashicups.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData))
+		return
+	}
+	r.client = client
+}
+
+func (r *orderResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_order"
+}
+
+func (r *orderResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Computed: true,
+			},
+			"last_updated": schema.StringAttribute{
+				Computed: true,
+			},
+			"items": schema.ListNestedAttribute{
+				Required: true,
+				NestedObject: schema.NestedAttributeObject{
+					Attributes: map[string]schema.Attribute{
+						"quantity": schema.Int64Attribute{
+							Required: true,
+						},
+						"coffee": schema.SingleNestedAttribute{
+							Required: true,
+							Attributes: map[string]schema.Attribute{
+								"id": schema.Int64Attribute{
+									Required: true,
+								},
+								"name": schema.StringAttribute{
+									Computed: true,
+								},
+								"teaser": schema.StringAttribute{
+									Computed: true,
+								},
+								"description": schema.StringAttribute{
+									Computed: true,
+								},
+								"price": schema.Float64Attribute{
+									Computed: true,
+								},
+								"image": schema.StringAttribute{
+									Computed: true,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func (r *orderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var plan orderResourceModel
+
+	// 1. Checks whether the API Client is configured.
+	if r.client == nil {
+		resp.Diagnostics.AddError(
+			"cannot create order",
+			"client is not configured in orderResource.Create",
+		)
+		return
+	}
+
+	// 2. Retrieves values from the plan.
+	// The function will attempt to retrieve values from the plan and convert it to an orderResourceModel.
+	// If not, the resource responds with an error.
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// 3. Generates an API request body from the plan values.
+	// The function loops through each plan item and maps it to a hashicups.OrderItem.
+	// This is what the API client needs to create a new order.
+	var items []hashicups.OrderItem
+	for _, item := range plan.Items {
+		items = append(items, hashicups.OrderItem{
+			Coffee: hashicups.Coffee{
+				ID: int(item.Coffee.ID.ValueInt64()),
+			},
+			Quantity: int(item.Quantity.ValueInt64()),
+		})
+	}
+
+	// 4. Creates a new order.
+	// The function invokes the API client's CreateOrder method.
+	order, err := r.client.CreateOrder(items)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error creating order",
+			err.Error(),
+		)
+		return
+	}
+
+	// 5. Maps response body to resource schema attributes.
+	// After the function creates an order, it maps the hashicups.Order response to []OrderItem so the provider can update the Terraform state.
+	plan.ID = types.StringValue(strconv.Itoa(order.ID))
+	for orderItemIndex, orderItem := range order.Items {
+		plan.Items[orderItemIndex] = orderItemModel{
+			Coffee: orderItemCoffeeModel{
+				ID:          types.Int64Value(int64(orderItem.Coffee.ID)),
+				Name:        types.StringValue(orderItem.Coffee.Name),
+				Teaser:      types.StringValue(orderItem.Coffee.Teaser),
+				Description: types.StringValue(orderItem.Coffee.Description),
+				Price:       types.Float64Value(orderItem.Coffee.Price),
+				Image:       types.StringValue(orderItem.Coffee.Image),
+			},
+			Quantity: types.Int64Value(int64(orderItem.Quantity)),
+		}
+	}
+	plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) // Why not RFC3339 ?
+
+	// 6. Sets Terraform's state with the new order's details.
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+
+	// Useless code in tutorial, why ?
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+func (r *orderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var state orderResourceModel
+
+	// 0. Checks whether the API Client is configured.
+	if r.client == nil {
+		resp.Diagnostics.AddError(
+			"cannot read order",
+			"client is not configured in orderResource.Read",
+		)
+		return
+	}
+
+	// 1. Gets the current state.
+	// If it is unable to, the provider responds with an error.
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// 2. Retrieves the order ID from Terraform's state.
+	id := state.ID.ValueString()
+
+	// 3. Retrieves the order details from the client.
+	// The function invokes the API client's GetOrder method with the order ID.
+	order, err := r.client.GetOrder(id)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading HashiCups order",
+			"Could not read HashiCups order "+state.ID.ValueString()+": "+err.Error(),
+		)
+		return
+	}
+
+	// 4. Maps the response body to resource schema attributes.
+	// == overwrite items with refreshed state
+	// After the function retrieves the order, it maps the hashicups.Order response to []OrderItem so the provider can update the Terraform state.
+	state.Items = []orderItemModel{}
+	for _, item := range order.Items {
+		itemState := orderItemModel{
+			Coffee: orderItemCoffeeModel{
+				ID:          types.Int64Value(int64(item.Coffee.ID)),
+				Name:        types.StringValue(item.Coffee.Name),
+				Teaser:      types.StringValue(item.Coffee.Teaser),
+				Description: types.StringValue(item.Coffee.Description),
+				Price:       types.Float64Value(item.Coffee.Price),
+				Image:       types.StringValue(item.Coffee.Image),
+			},
+			Quantity: types.Int64Value(int64(item.Quantity)),
+		}
+		state.Items = append(state.Items, itemState)
+	}
+
+	// 5. Set Terraform's state with the order's model.
+	diags = resp.State.Set(ctx, state)
+	resp.Diagnostics.Append(diags...)
+
+	// Useless code in tutorial, why ?
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+func (r *orderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	return
+}
+
+func (r *orderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	return
+}
+
+// The *Model types describe the types known by the API.
+// They are basically DTOs for the TF provider to talk to the API, while
+// its native type is the Plan with its Schema.
+type orderResourceModel struct {
+	ID          types.String     `tfsdk:"id"`
+	Items       []orderItemModel `tfsdk:"items"`
+	LastUpdated types.String     `tfsdk:"last_updated"`
+}
+
+type orderItemModel struct {
+	Quantity types.Int64          `tfsdk:"quantity"`
+	Coffee   orderItemCoffeeModel `tfsdk:"coffee"`
+}
+
+type orderItemCoffeeModel struct {
+	ID          types.Int64   `tfsdk:"id"`
+	Name        types.String  `tfsdk:"name"`
+	Teaser      types.String  `tfsdk:"teaser"`
+	Description types.String  `tfsdk:"description"`
+	Price       types.Float64 `tfsdk:"price"`
+	Image       types.String  `tfsdk:"image"`
+}

+ 17 - 1
internal/provider/provider.go

@@ -14,6 +14,7 @@ import (
 	"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 hashicupsProvider satisfies various provider interfaces.
@@ -59,6 +60,8 @@ func (p *hashicupsProvider) Schema(_ context.Context, _ provider.SchemaRequest,
 }
 
 func (p *hashicupsProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
+	tflog.Info(ctx, "Configuring HashiCups client")
+
 	// 1. Retrieves values from the configuration.
 	// The method will attempt to retrieve values from the provider configuration and convert it to an providerModel struct.
 	var config hashicupsProviderModel
@@ -158,6 +161,12 @@ func (p *hashicupsProvider) Configure(ctx context.Context, req provider.Configur
 		return
 	}
 
+	// All fields added to context will be included in subsequent log messages.
+	ctx = tflog.SetField(ctx, "hashicups_host", host)
+	ctx = tflog.SetField(ctx, "hashicups_username", username)
+	ctx = tflog.SetField(ctx, "hashicups_password", password)
+	tflog.Debug(ctx, "Creating HashiCups API Client")
+
 	// 4. Creates API client.
 	// The method invokes the HashiCups API client's NewClient function.
 	//
@@ -180,6 +189,11 @@ func (p *hashicupsProvider) Configure(ctx context.Context, req provider.Configur
 	// Make the HashiCups client available during DataSource and Resource type Configure methods.
 	resp.DataSourceData = client
 	resp.ResourceData = client
+
+	// Fields can also be added to only a single log message.
+	tflog.Info(ctx, "Successfully created HashiCups API Client", map[string]any{
+		"success": true,
+	})
 }
 
 func (p *hashicupsProvider) DataSources(_ context.Context) []func() datasource.DataSource {
@@ -190,7 +204,9 @@ func (p *hashicupsProvider) DataSources(_ context.Context) []func() datasource.D
 }
 
 func (p *hashicupsProvider) Resources(_ context.Context) []func() resource.Resource {
-	return nil
+	return []func() resource.Resource{
+		NewOrderResource,
+	}
 }
 
 // The Terraform Plugin Framework uses Go struct types with tfsdk struct field tags