|
@@ -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"`
|
|
|
|
+}
|