Skip to content

Commit 87d9fe9

Browse files
Brad SwensonAndrew Jeffery
authored andcommitted
fix(cloudflare_list_item) fix bulk operation polling and update
1 parent c1101f8 commit 87d9fe9

File tree

6 files changed

+309
-84
lines changed

6 files changed

+309
-84
lines changed

internal/services/list_item/model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type ListItemResultEnvelope struct {
1515
type ListItemModel struct {
1616
ListID types.String `tfsdk:"list_id" path:"list_id,required"`
1717
AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
18-
ID types.String `tfsdk:"id" path:"item_id,computed"`
18+
ID types.String `tfsdk:"id" json:"id,computed" path:"item_id,computed"`
1919
ASN types.Int64 `tfsdk:"asn" json:"asn,optional"`
2020
Comment types.String `tfsdk:"comment" json:"comment,optional"`
2121
IP types.String `tfsdk:"ip" json:"ip,optional"`

internal/services/list_item/resource.go

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ import (
99
"io"
1010
"net/http"
1111
"strconv"
12+
"time"
1213

1314
"github.com/cloudflare/cloudflare-go/v5"
1415
"github.com/cloudflare/cloudflare-go/v5/option"
16+
"github.com/cloudflare/cloudflare-go/v5/packages/pagination"
1517
"github.com/cloudflare/cloudflare-go/v5/rules"
1618
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
1719
"github.com/cloudflare/terraform-provider-cloudflare/internal/importpath"
1820
"github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
1921
"github.com/hashicorp/terraform-plugin-framework/resource"
2022
"github.com/hashicorp/terraform-plugin-framework/types"
21-
"github.com/tidwall/gjson"
2223
)
2324

2425
// Ensure provider defined types fully satisfy framework interfaces.
@@ -86,6 +87,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
8687
option.WithRequestBody("application/json", wrappedBytes),
8788
option.WithResponseBodyInto(&res),
8889
option.WithMiddleware(logging.Middleware(ctx)),
90+
option.WithRequestTimeout(time.Second*3),
8991
)
9092
if err != nil {
9193
resp.Diagnostics.AddError("failed to make http request", err.Error())
@@ -99,25 +101,54 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
99101
return
100102
}
101103

104+
err = pollBulkOperation(ctx, data.AccountID.ValueString(), createEnv.Result.OperationID.ValueString(), r.client)
105+
if err != nil {
106+
resp.Diagnostics.AddError("list item bulk operation failed", err.Error())
107+
return
108+
}
109+
102110
searchTerm := getSearchTerm(data)
103111
findItemRes := new(http.Response)
104-
_, err = r.client.Rules.Lists.Items.List(
112+
listItems, err := r.client.Rules.Lists.Items.List(
105113
ctx,
106114
data.ListID.ValueString(),
107115
rules.ListItemListParams{
108116
AccountID: cloudflare.F(data.AccountID.ValueString()),
109117
Search: cloudflare.F(searchTerm),
118+
// TODO: when pagination is fixed in the API schema (and go sdk) we should not need to set this (items we are looking for are expected to be sorted near the top of the result list)
119+
PerPage: cloudflare.Int(500),
110120
},
111121
option.WithResponseBodyInto(&findItemRes),
112122
option.WithMiddleware(logging.Middleware(ctx)),
123+
option.WithRequestTimeout(time.Second*3),
113124
)
114125
if err != nil {
115126
resp.Diagnostics.AddError("failed to fetch individual list item", err.Error())
116127
return
117128
}
118-
findListItem, _ := io.ReadAll(findItemRes.Body)
119-
itemID := gjson.Get(string(findListItem), "result.0.id")
120-
data.ID = types.StringValue(itemID.String())
129+
if listItems == nil {
130+
resp.Diagnostics.AddWarning("failed to fetch individual list item", "list item pagination was nil")
131+
}
132+
133+
listItemsBytes, _ := io.ReadAll(findItemRes.Body)
134+
135+
// TODO: when pagination is fixed in the API schema (and go sdk) this should paginate properly
136+
var apiResult pagination.SinglePage[ListItemModel]
137+
err = apijson.Unmarshal(listItemsBytes, &apiResult)
138+
if err != nil {
139+
resp.Diagnostics.AddError("failed to fetch individual list item", err.Error())
140+
}
141+
142+
// find the actual list item, don't rely on the response to have the first entry be the correct one
143+
var listItemID string
144+
for _, item := range apiResult.Result {
145+
if matchedItemID, ok := listItemMatchesOriginal(data, item); ok {
146+
listItemID = matchedItemID
147+
break
148+
}
149+
}
150+
151+
data.ID = types.StringValue(listItemID)
121152

122153
env := ListItemResultEnvelope{*data}
123154
listItemRes := new(http.Response)
@@ -130,6 +161,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
130161
},
131162
option.WithResponseBodyInto(&listItemRes),
132163
option.WithMiddleware(logging.Middleware(ctx)),
164+
option.WithRequestTimeout(time.Second*3),
133165
)
134166
if err != nil {
135167
resp.Diagnostics.AddError("failed to fetch individual list item", err.Error())
@@ -148,52 +180,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
148180
}
149181

150182
func (r *ListItemResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
151-
var data *ListItemModel
152-
153-
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
154-
155-
if resp.Diagnostics.HasError() {
156-
return
157-
}
158-
159-
var state *ListItemModel
160-
161-
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
162-
163-
if resp.Diagnostics.HasError() {
164-
return
165-
}
166-
167-
dataBytes, err := data.MarshalJSONForUpdate(*state)
168-
if err != nil {
169-
resp.Diagnostics.AddError("failed to serialize http request", err.Error())
170-
return
171-
}
172-
res := new(http.Response)
173-
env := ListItemResultEnvelope{*data}
174-
_, err = r.client.Rules.Lists.Items.Update(
175-
ctx,
176-
data.ListID.ValueString(),
177-
rules.ListItemUpdateParams{
178-
AccountID: cloudflare.F(data.AccountID.ValueString()),
179-
},
180-
option.WithRequestBody("application/json", dataBytes),
181-
option.WithResponseBodyInto(&res),
182-
option.WithMiddleware(logging.Middleware(ctx)),
183-
)
184-
if err != nil {
185-
resp.Diagnostics.AddError("failed to make http request", err.Error())
186-
return
187-
}
188-
bytes, _ := io.ReadAll(res.Body)
189-
err = apijson.UnmarshalComputed(bytes, &env)
190-
if err != nil {
191-
resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
192-
return
193-
}
194-
data = &env.Result
195-
196-
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
183+
resp.Diagnostics.AddError("update is not supported for list items", "")
197184
}
198185

199186
func (r *ListItemResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
@@ -213,6 +200,7 @@ func (r *ListItemResource) Read(ctx context.Context, req resource.ReadRequest, r
213200
rules.ListItemGetParams{AccountID: cloudflare.F(data.AccountID.ValueString())},
214201
option.WithResponseBodyInto(&res),
215202
option.WithMiddleware(logging.Middleware(ctx)),
203+
option.WithRequestTimeout(time.Second*3),
216204
)
217205
if res != nil && res.StatusCode == 404 {
218206
resp.Diagnostics.AddWarning("Resource not found", "The resource was not found on the server and will be removed from state.")
@@ -258,6 +246,7 @@ func (r *ListItemResource) Delete(ctx context.Context, req resource.DeleteReques
258246
},
259247
option.WithMiddleware(logging.Middleware(ctx)),
260248
option.WithRequestBody("application/json", deleteBody),
249+
option.WithRequestTimeout(time.Second*3),
261250
)
262251
if err != nil {
263252
resp.Diagnostics.AddError("failed to make http request", err.Error())
@@ -320,6 +309,37 @@ func (r *ListItemResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRe
320309

321310
}
322311

312+
func pollBulkOperation(ctx context.Context, accountID, operationID string, client *cloudflare.Client) error {
313+
backoff := 1 * time.Second
314+
maxBackoff := 30 * time.Second
315+
316+
for {
317+
bulkOperation, err := client.Rules.Lists.BulkOperations.Get(
318+
ctx,
319+
operationID,
320+
rules.ListBulkOperationGetParams{
321+
AccountID: cloudflare.F(accountID),
322+
},
323+
option.WithMiddleware(logging.Middleware(ctx)),
324+
)
325+
if err != nil {
326+
return err
327+
}
328+
switch bulkOperation.Status {
329+
case rules.ListBulkOperationGetResponseStatusCompleted:
330+
return nil
331+
case rules.ListBulkOperationGetResponseStatusFailed:
332+
return fmt.Errorf("failed to create list item: %s", bulkOperation.Error)
333+
default:
334+
time.Sleep(backoff)
335+
backoff *= 2
336+
if backoff > maxBackoff {
337+
backoff = maxBackoff
338+
}
339+
}
340+
}
341+
}
342+
323343
type bodyDeletePayload struct {
324344
Items []bodyDeleteItems `json:"items"`
325345
}
@@ -353,3 +373,23 @@ func getSearchTerm(d *ListItemModel) string {
353373

354374
return ""
355375
}
376+
377+
func listItemMatchesOriginal(original *ListItemModel, item ListItemModel) (string, bool) {
378+
if original.IP != item.IP {
379+
return "", false
380+
}
381+
382+
if original.ASN != item.ASN {
383+
return "", false
384+
}
385+
386+
if !original.Hostname.IsNull() && !item.Hostname.IsNull() && !original.Hostname.Equal(item.Hostname) {
387+
return "", false
388+
}
389+
390+
if !original.Redirect.IsNull() && !item.Redirect.IsNull() && !original.Redirect.Equal(item.Redirect) {
391+
return "", false
392+
}
393+
394+
return item.ID.ValueString(), true
395+
}

0 commit comments

Comments
 (0)