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/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "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{} } // orderResource is the resource implementation. type orderResource struct { client *hashicups.Client } // 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"` } // orderItemModel maps order item data. type orderItemModel struct { Coffee orderItemCoffeeModel `tfsdk:"coffee"` Quantity types.Int64 `tfsdk:"quantity"` } // orderItemCoffeeModel maps coffee order item data. 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"` } // Metadata returns the resource type name. func (r *orderResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_order" } // Schema defines the schema for the resource. func (r *orderResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "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, }, }, }, }, }, }, }, } } // Configure adds the provider configured client to the resource. func (r *orderResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 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 } // Create a new resource. 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 API request body from plan // 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", "Could not create order, unexpected error: "+err.Error(), ) return } // 5. Maps response body to resource schema attributes and populate Computed attribute values // 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 } } // Read resource information. 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 ID "+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 { state.Items = append(state.Items, 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)), }) } // 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 } } // Update updates the resource and sets the updated Terraform state on success. func (r *orderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // 1. Retrieves values from the plan. // The method will attempt to retrieve values from the plan and convert it to an orderResourceModel. // The model includes the order's id attribute, which specifies which order to update. var plan orderResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // 2. Generates an API request body from the plan values. // The method loops through each plan item and maps it to a hashicups.OrderItem. // This is what the API client needs to update an existing order. var hashicupsItems []hashicups.OrderItem for _, item := range plan.Items { hashicupsItems = append(hashicupsItems, hashicups.OrderItem{ Coffee: hashicups.Coffee{ ID: int(item.Coffee.ID.ValueInt64()), }, Quantity: int(item.Quantity.ValueInt64()), }) } // 3. Updates the order. // 3.1 Apply changes. // The method invokes the API client's UpdateOrder method with the order's ID and OrderItems. _, err := r.client.UpdateOrder(plan.ID.ValueString(), hashicupsItems) if err != nil { resp.Diagnostics.AddError( "Error Reading HashiCups Order", "Could not read HashiCups order ID "+plan.ID.ValueString()+": "+err.Error(), ) } // 3.2 Fetch updated items from GetOrder as UpdateOrder items are not populated. order, err := r.client.GetOrder(plan.ID.ValueString()) if err != nil { resp.Diagnostics.AddError( "Error Reading HashiCups Order", "Could not read HashiCups order ID "+plan.ID.ValueString()+": "+err.Error(), ) return } // 4. Maps the response body to resource schema attributes. // After the method updates the order, it maps the hashicups.Order response to []OrderItem // so the provider can update the Terraform state. plan.Items = []orderItemModel{} for _, orderItem := range order.Items { plan.Items = append(plan.Items, 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)), }) } // 5. Sets the LastUpdated attribute. plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) // 6. Sets Terraform's state with the updated order. diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) // Useless code in tutorial, why ? if resp.Diagnostics.HasError() { return } } // Delete deletes the resource and removes the Terraform state on success. func (r *orderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { return }