order_resource.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. package provider
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "time"
  7. "github.com/hashicorp-demoapp/hashicups-client-go"
  8. "github.com/hashicorp/terraform-plugin-framework/resource"
  9. "github.com/hashicorp/terraform-plugin-framework/resource/schema"
  10. "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
  11. "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
  12. "github.com/hashicorp/terraform-plugin-framework/types"
  13. )
  14. // Ensure the implementation satisfies the expected interfaces.
  15. var (
  16. _ resource.Resource = &orderResource{}
  17. _ resource.ResourceWithConfigure = &orderResource{}
  18. )
  19. // NewOrderResource is a helper function to simplify the provider implementation.
  20. func NewOrderResource() resource.Resource {
  21. return &orderResource{}
  22. }
  23. // orderResource is the resource implementation.
  24. type orderResource struct {
  25. client *hashicups.Client
  26. }
  27. // The *Model types describe the types known by the API.
  28. // They are basically DTOs for the TF provider to talk to the API, while
  29. // its native type is the Plan with its Schema.
  30. type orderResourceModel struct {
  31. ID types.String `tfsdk:"id"`
  32. Items []orderItemModel `tfsdk:"items"`
  33. LastUpdated types.String `tfsdk:"last_updated"`
  34. }
  35. // orderItemModel maps order item data.
  36. type orderItemModel struct {
  37. Coffee orderItemCoffeeModel `tfsdk:"coffee"`
  38. Quantity types.Int64 `tfsdk:"quantity"`
  39. }
  40. // orderItemCoffeeModel maps coffee order item data.
  41. type orderItemCoffeeModel struct {
  42. ID types.Int64 `tfsdk:"id"`
  43. Name types.String `tfsdk:"name"`
  44. Teaser types.String `tfsdk:"teaser"`
  45. Description types.String `tfsdk:"description"`
  46. Price types.Float64 `tfsdk:"price"`
  47. Image types.String `tfsdk:"image"`
  48. }
  49. // Metadata returns the resource type name.
  50. func (r *orderResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
  51. resp.TypeName = req.ProviderTypeName + "_order"
  52. }
  53. // Schema defines the schema for the resource.
  54. func (r *orderResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
  55. resp.Schema = schema.Schema{
  56. Attributes: map[string]schema.Attribute{
  57. "id": schema.StringAttribute{
  58. Computed: true,
  59. PlanModifiers: []planmodifier.String{
  60. stringplanmodifier.UseStateForUnknown(),
  61. },
  62. },
  63. "last_updated": schema.StringAttribute{
  64. Computed: true,
  65. },
  66. "items": schema.ListNestedAttribute{
  67. Required: true,
  68. NestedObject: schema.NestedAttributeObject{
  69. Attributes: map[string]schema.Attribute{
  70. "quantity": schema.Int64Attribute{
  71. Required: true,
  72. },
  73. "coffee": schema.SingleNestedAttribute{
  74. Required: true,
  75. Attributes: map[string]schema.Attribute{
  76. "id": schema.Int64Attribute{
  77. Required: true,
  78. },
  79. "name": schema.StringAttribute{
  80. Computed: true,
  81. },
  82. "teaser": schema.StringAttribute{
  83. Computed: true,
  84. },
  85. "description": schema.StringAttribute{
  86. Computed: true,
  87. },
  88. "price": schema.Float64Attribute{
  89. Computed: true,
  90. },
  91. "image": schema.StringAttribute{
  92. Computed: true,
  93. },
  94. },
  95. },
  96. },
  97. },
  98. },
  99. },
  100. }
  101. }
  102. // Configure adds the provider configured client to the resource.
  103. func (r *orderResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
  104. if req.ProviderData == nil {
  105. return
  106. }
  107. client, ok := req.ProviderData.(*hashicups.Client)
  108. if !ok {
  109. resp.Diagnostics.AddError(
  110. "Unexpected Resource Configure Type",
  111. fmt.Sprintf("Expected *hashicups.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
  112. )
  113. return
  114. }
  115. r.client = client
  116. }
  117. // Create a new resource.
  118. func (r *orderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
  119. var plan orderResourceModel
  120. // 1. Checks whether the API Client is configured.
  121. if r.client == nil {
  122. resp.Diagnostics.AddError(
  123. "cannot create order",
  124. "client is not configured in orderResource.Create",
  125. )
  126. return
  127. }
  128. // 2. Retrieves values from the plan.
  129. // The function will attempt to retrieve values from the plan and convert it to an orderResourceModel.
  130. // If not, the resource responds with an error.
  131. diags := req.Plan.Get(ctx, &plan)
  132. resp.Diagnostics.Append(diags...)
  133. if resp.Diagnostics.HasError() {
  134. return
  135. }
  136. // 3. Generates API request body from plan
  137. // The function loops through each plan item and maps it to a hashicups.OrderItem.
  138. // This is what the API client needs to create a new order.
  139. var items []hashicups.OrderItem
  140. for _, item := range plan.Items {
  141. items = append(items, hashicups.OrderItem{
  142. Coffee: hashicups.Coffee{
  143. ID: int(item.Coffee.ID.ValueInt64()),
  144. },
  145. Quantity: int(item.Quantity.ValueInt64()),
  146. })
  147. }
  148. // 4. Creates a new order.
  149. // The function invokes the API client's CreateOrder method.
  150. order, err := r.client.CreateOrder(items)
  151. if err != nil {
  152. resp.Diagnostics.AddError(
  153. "Error creating order",
  154. "Could not create order, unexpected error: "+err.Error(),
  155. )
  156. return
  157. }
  158. // 5. Maps response body to resource schema attributes and populate Computed attribute values
  159. // After the function creates an order, it maps the hashicups.Order response to []OrderItem so the provider can update the Terraform state.
  160. plan.ID = types.StringValue(strconv.Itoa(order.ID))
  161. for orderItemIndex, orderItem := range order.Items {
  162. plan.Items[orderItemIndex] = orderItemModel{
  163. Coffee: orderItemCoffeeModel{
  164. ID: types.Int64Value(int64(orderItem.Coffee.ID)),
  165. Name: types.StringValue(orderItem.Coffee.Name),
  166. Teaser: types.StringValue(orderItem.Coffee.Teaser),
  167. Description: types.StringValue(orderItem.Coffee.Description),
  168. Price: types.Float64Value(orderItem.Coffee.Price),
  169. Image: types.StringValue(orderItem.Coffee.Image),
  170. },
  171. Quantity: types.Int64Value(int64(orderItem.Quantity)),
  172. }
  173. }
  174. plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) // Why not RFC3339 ?
  175. // 6. Sets Terraform's state with the new order's details.
  176. diags = resp.State.Set(ctx, plan)
  177. resp.Diagnostics.Append(diags...)
  178. // Useless code in tutorial, why ?
  179. if resp.Diagnostics.HasError() {
  180. return
  181. }
  182. }
  183. // Read resource information.
  184. func (r *orderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
  185. var state orderResourceModel
  186. // 0. Checks whether the API Client is configured.
  187. if r.client == nil {
  188. resp.Diagnostics.AddError(
  189. "cannot read order",
  190. "client is not configured in orderResource.Read",
  191. )
  192. return
  193. }
  194. // 1. Gets the current state.
  195. // If it is unable to, the provider responds with an error.
  196. diags := req.State.Get(ctx, &state)
  197. resp.Diagnostics.Append(diags...)
  198. if resp.Diagnostics.HasError() {
  199. return
  200. }
  201. // 2. Retrieves the order ID from Terraform's state.
  202. id := state.ID.ValueString()
  203. // 3. Retrieves the order details from the client.
  204. // The function invokes the API client's GetOrder method with the order ID.
  205. order, err := r.client.GetOrder(id)
  206. if err != nil {
  207. resp.Diagnostics.AddError(
  208. "Error Reading HashiCups Order",
  209. "Could not read HashiCups order ID "+state.ID.ValueString()+": "+err.Error(),
  210. )
  211. return
  212. }
  213. // 4. Maps the response body to resource schema attributes.
  214. // == overwrite items with refreshed state
  215. // After the function retrieves the order, it maps the hashicups.Order response to []OrderItem so the provider can update the Terraform state.
  216. state.Items = []orderItemModel{}
  217. for _, item := range order.Items {
  218. state.Items = append(state.Items, orderItemModel{
  219. Coffee: orderItemCoffeeModel{
  220. ID: types.Int64Value(int64(item.Coffee.ID)),
  221. Name: types.StringValue(item.Coffee.Name),
  222. Teaser: types.StringValue(item.Coffee.Teaser),
  223. Description: types.StringValue(item.Coffee.Description),
  224. Price: types.Float64Value(item.Coffee.Price),
  225. Image: types.StringValue(item.Coffee.Image),
  226. },
  227. Quantity: types.Int64Value(int64(item.Quantity)),
  228. })
  229. }
  230. // 5. Set Terraform's state with the order's model.
  231. diags = resp.State.Set(ctx, state)
  232. resp.Diagnostics.Append(diags...)
  233. // Useless code in tutorial, why ?
  234. if resp.Diagnostics.HasError() {
  235. return
  236. }
  237. }
  238. // Update updates the resource and sets the updated Terraform state on success.
  239. func (r *orderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
  240. // 1. Retrieves values from the plan.
  241. // The method will attempt to retrieve values from the plan and convert it to an orderResourceModel.
  242. // The model includes the order's id attribute, which specifies which order to update.
  243. var plan orderResourceModel
  244. diags := req.Plan.Get(ctx, &plan)
  245. resp.Diagnostics.Append(diags...)
  246. if resp.Diagnostics.HasError() {
  247. return
  248. }
  249. // 2. Generates an API request body from the plan values.
  250. // The method loops through each plan item and maps it to a hashicups.OrderItem.
  251. // This is what the API client needs to update an existing order.
  252. var hashicupsItems []hashicups.OrderItem
  253. for _, item := range plan.Items {
  254. hashicupsItems = append(hashicupsItems, hashicups.OrderItem{
  255. Coffee: hashicups.Coffee{
  256. ID: int(item.Coffee.ID.ValueInt64()),
  257. },
  258. Quantity: int(item.Quantity.ValueInt64()),
  259. })
  260. }
  261. // 3. Updates the order.
  262. // 3.1 Apply changes.
  263. // The method invokes the API client's UpdateOrder method with the order's ID and OrderItems.
  264. _, err := r.client.UpdateOrder(plan.ID.ValueString(), hashicupsItems)
  265. if err != nil {
  266. resp.Diagnostics.AddError(
  267. "Error Reading HashiCups Order",
  268. "Could not read HashiCups order ID "+plan.ID.ValueString()+": "+err.Error(),
  269. )
  270. }
  271. // 3.2 Fetch updated items from GetOrder as UpdateOrder items are not populated.
  272. order, err := r.client.GetOrder(plan.ID.ValueString())
  273. if err != nil {
  274. resp.Diagnostics.AddError(
  275. "Error Reading HashiCups Order",
  276. "Could not read HashiCups order ID "+plan.ID.ValueString()+": "+err.Error(),
  277. )
  278. return
  279. }
  280. // 4. Maps the response body to resource schema attributes.
  281. // After the method updates the order, it maps the hashicups.Order response to []OrderItem
  282. // so the provider can update the Terraform state.
  283. plan.Items = []orderItemModel{}
  284. for _, orderItem := range order.Items {
  285. plan.Items = append(plan.Items, orderItemModel{
  286. Coffee: orderItemCoffeeModel{
  287. ID: types.Int64Value(int64(orderItem.Coffee.ID)),
  288. Name: types.StringValue(orderItem.Coffee.Name),
  289. Teaser: types.StringValue(orderItem.Coffee.Teaser),
  290. Description: types.StringValue(orderItem.Coffee.Description),
  291. Price: types.Float64Value(orderItem.Coffee.Price),
  292. Image: types.StringValue(orderItem.Coffee.Image),
  293. },
  294. Quantity: types.Int64Value(int64(orderItem.Quantity)),
  295. })
  296. }
  297. // 5. Sets the LastUpdated attribute.
  298. plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850))
  299. // 6. Sets Terraform's state with the updated order.
  300. diags = resp.State.Set(ctx, plan)
  301. resp.Diagnostics.Append(diags...)
  302. // Useless code in tutorial, why ?
  303. if resp.Diagnostics.HasError() {
  304. return
  305. }
  306. }
  307. // Delete deletes the resource and removes the Terraform state on success.
  308. func (r *orderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
  309. // 1. Retrieves values from the state.
  310. // 1.a The method will attempt to retrieve values from the state and convert it to an orderResourceModel struct
  311. var state orderResourceModel
  312. diags := req.State.Get(ctx, &state)
  313. resp.Diagnostics.Append(diags...)
  314. if resp.Diagnostics.HasError() {
  315. return
  316. }
  317. // 2. Deletes an existing order.
  318. // The method invokes the API client's DeleteOrder method.
  319. err := r.client.DeleteOrder(state.ID.ValueString())
  320. if err != nil {
  321. resp.Diagnostics.AddError(
  322. "Error Deleting HashiCups Order",
  323. "Could not delete order, unexpected error: "+err.Error(),
  324. )
  325. return
  326. }
  327. }