@@ -9,16 +9,17 @@ import (
9
9
"io"
10
10
"net/http"
11
11
"strconv"
12
+ "time"
12
13
13
14
"github.com/cloudflare/cloudflare-go/v5"
14
15
"github.com/cloudflare/cloudflare-go/v5/option"
16
+ "github.com/cloudflare/cloudflare-go/v5/packages/pagination"
15
17
"github.com/cloudflare/cloudflare-go/v5/rules"
16
18
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
17
19
"github.com/cloudflare/terraform-provider-cloudflare/internal/importpath"
18
20
"github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
19
21
"github.com/hashicorp/terraform-plugin-framework/resource"
20
22
"github.com/hashicorp/terraform-plugin-framework/types"
21
- "github.com/tidwall/gjson"
22
23
)
23
24
24
25
// Ensure provider defined types fully satisfy framework interfaces.
@@ -86,6 +87,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
86
87
option .WithRequestBody ("application/json" , wrappedBytes ),
87
88
option .WithResponseBodyInto (& res ),
88
89
option .WithMiddleware (logging .Middleware (ctx )),
90
+ option .WithRequestTimeout (time .Second * 3 ),
89
91
)
90
92
if err != nil {
91
93
resp .Diagnostics .AddError ("failed to make http request" , err .Error ())
@@ -99,25 +101,54 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
99
101
return
100
102
}
101
103
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
+
102
110
searchTerm := getSearchTerm (data )
103
111
findItemRes := new (http.Response )
104
- _ , err = r .client .Rules .Lists .Items .List (
112
+ listItems , err : = r .client .Rules .Lists .Items .List (
105
113
ctx ,
106
114
data .ListID .ValueString (),
107
115
rules.ListItemListParams {
108
116
AccountID : cloudflare .F (data .AccountID .ValueString ()),
109
117
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 ),
110
120
},
111
121
option .WithResponseBodyInto (& findItemRes ),
112
122
option .WithMiddleware (logging .Middleware (ctx )),
123
+ option .WithRequestTimeout (time .Second * 3 ),
113
124
)
114
125
if err != nil {
115
126
resp .Diagnostics .AddError ("failed to fetch individual list item" , err .Error ())
116
127
return
117
128
}
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 )
121
152
122
153
env := ListItemResultEnvelope {* data }
123
154
listItemRes := new (http.Response )
@@ -130,6 +161,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
130
161
},
131
162
option .WithResponseBodyInto (& listItemRes ),
132
163
option .WithMiddleware (logging .Middleware (ctx )),
164
+ option .WithRequestTimeout (time .Second * 3 ),
133
165
)
134
166
if err != nil {
135
167
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
148
180
}
149
181
150
182
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" , "" )
197
184
}
198
185
199
186
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
213
200
rules.ListItemGetParams {AccountID : cloudflare .F (data .AccountID .ValueString ())},
214
201
option .WithResponseBodyInto (& res ),
215
202
option .WithMiddleware (logging .Middleware (ctx )),
203
+ option .WithRequestTimeout (time .Second * 3 ),
216
204
)
217
205
if res != nil && res .StatusCode == 404 {
218
206
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
258
246
},
259
247
option .WithMiddleware (logging .Middleware (ctx )),
260
248
option .WithRequestBody ("application/json" , deleteBody ),
249
+ option .WithRequestTimeout (time .Second * 3 ),
261
250
)
262
251
if err != nil {
263
252
resp .Diagnostics .AddError ("failed to make http request" , err .Error ())
@@ -320,6 +309,37 @@ func (r *ListItemResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRe
320
309
321
310
}
322
311
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
+
323
343
type bodyDeletePayload struct {
324
344
Items []bodyDeleteItems `json:"items"`
325
345
}
@@ -353,3 +373,23 @@ func getSearchTerm(d *ListItemModel) string {
353
373
354
374
return ""
355
375
}
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