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