diff --git a/internal/services/list_item/model.go b/internal/services/list_item/model.go index 254eb9ce1e..c4013ea114 100644 --- a/internal/services/list_item/model.go +++ b/internal/services/list_item/model.go @@ -15,7 +15,7 @@ type ListItemResultEnvelope struct { type ListItemModel struct { ListID types.String `tfsdk:"list_id" path:"list_id,required"` AccountID types.String `tfsdk:"account_id" path:"account_id,required"` - ID types.String `tfsdk:"id" path:"item_id,computed"` + ID types.String `tfsdk:"id" json:"id,computed" path:"item_id,computed"` ASN types.Int64 `tfsdk:"asn" json:"asn,optional"` Comment types.String `tfsdk:"comment" json:"comment,optional"` IP types.String `tfsdk:"ip" json:"ip,optional"` diff --git a/internal/services/list_item/resource.go b/internal/services/list_item/resource.go index c6d1ab78d5..8f8aa32a84 100644 --- a/internal/services/list_item/resource.go +++ b/internal/services/list_item/resource.go @@ -9,16 +9,17 @@ import ( "io" "net/http" "strconv" + "time" "github.com/cloudflare/cloudflare-go/v5" "github.com/cloudflare/cloudflare-go/v5/option" + "github.com/cloudflare/cloudflare-go/v5/packages/pagination" "github.com/cloudflare/cloudflare-go/v5/rules" "github.com/cloudflare/terraform-provider-cloudflare/internal/apijson" "github.com/cloudflare/terraform-provider-cloudflare/internal/importpath" "github.com/cloudflare/terraform-provider-cloudflare/internal/logging" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/tidwall/gjson" ) // Ensure provider defined types fully satisfy framework interfaces. @@ -86,6 +87,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques option.WithRequestBody("application/json", wrappedBytes), option.WithResponseBodyInto(&res), option.WithMiddleware(logging.Middleware(ctx)), + option.WithRequestTimeout(time.Second*3), ) if err != nil { resp.Diagnostics.AddError("failed to make http request", err.Error()) @@ -99,25 +101,54 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques return } + err = pollBulkOperation(ctx, data.AccountID.ValueString(), createEnv.Result.OperationID.ValueString(), r.client) + if err != nil { + resp.Diagnostics.AddError("list item bulk operation failed", err.Error()) + return + } + searchTerm := getSearchTerm(data) findItemRes := new(http.Response) - _, err = r.client.Rules.Lists.Items.List( + listItems, err := r.client.Rules.Lists.Items.List( ctx, data.ListID.ValueString(), rules.ListItemListParams{ AccountID: cloudflare.F(data.AccountID.ValueString()), Search: cloudflare.F(searchTerm), + // 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) + PerPage: cloudflare.Int(500), }, option.WithResponseBodyInto(&findItemRes), option.WithMiddleware(logging.Middleware(ctx)), + option.WithRequestTimeout(time.Second*3), ) if err != nil { resp.Diagnostics.AddError("failed to fetch individual list item", err.Error()) return } - findListItem, _ := io.ReadAll(findItemRes.Body) - itemID := gjson.Get(string(findListItem), "result.0.id") - data.ID = types.StringValue(itemID.String()) + if listItems == nil { + resp.Diagnostics.AddWarning("failed to fetch individual list item", "list item pagination was nil") + } + + listItemsBytes, _ := io.ReadAll(findItemRes.Body) + + // TODO: when pagination is fixed in the API schema (and go sdk) this should paginate properly + var apiResult pagination.SinglePage[ListItemModel] + err = apijson.Unmarshal(listItemsBytes, &apiResult) + if err != nil { + resp.Diagnostics.AddError("failed to fetch individual list item", err.Error()) + } + + // find the actual list item, don't rely on the response to have the first entry be the correct one + var listItemID string + for _, item := range apiResult.Result { + if matchedItemID, ok := listItemMatchesOriginal(data, item); ok { + listItemID = matchedItemID + break + } + } + + data.ID = types.StringValue(listItemID) env := ListItemResultEnvelope{*data} listItemRes := new(http.Response) @@ -130,6 +161,7 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques }, option.WithResponseBodyInto(&listItemRes), option.WithMiddleware(logging.Middleware(ctx)), + option.WithRequestTimeout(time.Second*3), ) if err != nil { 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 } func (r *ListItemResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data *ListItemModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - - if resp.Diagnostics.HasError() { - return - } - - var state *ListItemModel - - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - if resp.Diagnostics.HasError() { - return - } - - dataBytes, err := data.MarshalJSONForUpdate(*state) - if err != nil { - resp.Diagnostics.AddError("failed to serialize http request", err.Error()) - return - } - res := new(http.Response) - env := ListItemResultEnvelope{*data} - _, err = r.client.Rules.Lists.Items.Update( - ctx, - data.ListID.ValueString(), - rules.ListItemUpdateParams{ - AccountID: cloudflare.F(data.AccountID.ValueString()), - }, - option.WithRequestBody("application/json", dataBytes), - option.WithResponseBodyInto(&res), - option.WithMiddleware(logging.Middleware(ctx)), - ) - if err != nil { - resp.Diagnostics.AddError("failed to make http request", err.Error()) - return - } - bytes, _ := io.ReadAll(res.Body) - err = apijson.UnmarshalComputed(bytes, &env) - if err != nil { - resp.Diagnostics.AddError("failed to deserialize http request", err.Error()) - return - } - data = &env.Result - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.AddError("update is not supported for list items", "") } 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 rules.ListItemGetParams{AccountID: cloudflare.F(data.AccountID.ValueString())}, option.WithResponseBodyInto(&res), option.WithMiddleware(logging.Middleware(ctx)), + option.WithRequestTimeout(time.Second*3), ) if res != nil && res.StatusCode == 404 { 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 }, option.WithMiddleware(logging.Middleware(ctx)), option.WithRequestBody("application/json", deleteBody), + option.WithRequestTimeout(time.Second*3), ) if err != nil { resp.Diagnostics.AddError("failed to make http request", err.Error()) @@ -320,6 +309,37 @@ func (r *ListItemResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRe } +func pollBulkOperation(ctx context.Context, accountID, operationID string, client *cloudflare.Client) error { + backoff := 1 * time.Second + maxBackoff := 30 * time.Second + + for { + bulkOperation, err := client.Rules.Lists.BulkOperations.Get( + ctx, + operationID, + rules.ListBulkOperationGetParams{ + AccountID: cloudflare.F(accountID), + }, + option.WithMiddleware(logging.Middleware(ctx)), + ) + if err != nil { + return err + } + switch bulkOperation.Status { + case rules.ListBulkOperationGetResponseStatusCompleted: + return nil + case rules.ListBulkOperationGetResponseStatusFailed: + return fmt.Errorf("failed to create list item: %s", bulkOperation.Error) + default: + time.Sleep(backoff) + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + } + } +} + type bodyDeletePayload struct { Items []bodyDeleteItems `json:"items"` } @@ -353,3 +373,23 @@ func getSearchTerm(d *ListItemModel) string { return "" } + +func listItemMatchesOriginal(original *ListItemModel, item ListItemModel) (string, bool) { + if original.IP != item.IP { + return "", false + } + + if original.ASN != item.ASN { + return "", false + } + + if !original.Hostname.IsNull() && !item.Hostname.IsNull() && !original.Hostname.Equal(item.Hostname) { + return "", false + } + + if !original.Redirect.IsNull() && !item.Redirect.IsNull() && !original.Redirect.Equal(item.Redirect) { + return "", false + } + + return item.ID.ValueString(), true +} diff --git a/internal/services/list_item/resource_test.go b/internal/services/list_item/resource_test.go index 95a6cc75a3..d56e02bf52 100644 --- a/internal/services/list_item/resource_test.go +++ b/internal/services/list_item/resource_test.go @@ -13,7 +13,6 @@ import ( ) func TestAccCloudflareListItem_Basic(t *testing.T) { - t.Skip("FIXME: Step 1/1 error: Error running apply: exit status 1. Getting rate limited, causing flaky tests.") rnd := utils.GenerateRandomResourceName() name := fmt.Sprintf("cloudflare_list_item.%s", rnd) accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") @@ -73,12 +72,29 @@ func TestAccCloudflareListItem_Import(t *testing.T) { return fmt.Sprintf("%s/%s/%s", accountID, listID, itemID), nil }, }, + { + ResourceName: itemName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[listName] + if !ok { + return "", fmt.Errorf("list resource not found: %s", listName) + } + listID := rs.Primary.ID + rs, ok = s.RootModule().Resources[itemName] + if !ok { + return "", fmt.Errorf("list_item resource not found: %s", itemName) + } + itemID := rs.Primary.ID + return fmt.Sprintf("%s/%s/%s", accountID, listID, itemID), nil + }, + }, }, }) } func TestAccCloudflareListItem_MultipleItems(t *testing.T) { - t.Skip("FIXME: Getting rate limited. Probably causing the cascading failures with the rest.") rnd := utils.GenerateRandomResourceName() name := fmt.Sprintf("cloudflare_list_item.%s", rnd) accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") @@ -102,12 +118,44 @@ func TestAccCloudflareListItem_MultipleItems(t *testing.T) { }) } +func TestAccCloudflareListItem_MultipleItemsHostname(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := fmt.Sprintf("cloudflare_list_item.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.TestAccPreCheck_AccountID(t) + }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareHostnameListItemMultipleEntries(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name+"_1", "hostname.url_hostname", "a.example.com"), + resource.TestCheckResourceAttr(name+"_2", "hostname.url_hostname", "example.com"), + ), + }, + { + Config: testAccCheckCloudflareHostnameListItemMultipleEntries(rnd, rnd, rnd+"-updated", accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name+"_1", "hostname.url_hostname", "a.example.com"), + resource.TestCheckResourceAttr(name+"_1", "comment", rnd+"-updated"), + resource.TestCheckResourceAttr(name+"_2", "hostname.url_hostname", "example.com"), + resource.TestCheckResourceAttr(name+"_2", "comment", rnd+"-updated"), + ), + }, + }, + }) +} + func TestAccCloudflareListItem_Update(t *testing.T) { - t.Skip("FIXME: Step 1/2 error: Error running apply: exit status 1. Getting rate limited, causing flaky tests.") rnd := utils.GenerateRandomResourceName() name := fmt.Sprintf("cloudflare_list_item.%s", rnd) accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + var listItemID string + resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.TestAccPreCheck_AccountID(t) @@ -118,12 +166,92 @@ func TestAccCloudflareListItem_Update(t *testing.T) { Config: testAccCheckCloudflareIPListItem(rnd, rnd, rnd, accountID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "ip", "192.0.2.0"), + func(s *terraform.State) error { + listItemID = s.RootModule().Resources[name].Primary.Attributes["id"] + return nil + }, ), }, { Config: testAccCheckCloudflareIPListItem(rnd, rnd, rnd+"-updated", accountID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "comment", rnd+"-updated"), + func(s *terraform.State) error { + newID := s.RootModule().Resources[name].Primary.Attributes["id"] + if newID == listItemID { + return fmt.Errorf("ID of list item did not change when updating comment") + } + return nil + }, + ), + }, + }, + }) +} + +func TestAccCloudflareListItem_UpdateHostname(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := fmt.Sprintf("cloudflare_list_item.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var listItemID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.TestAccPreCheck_AccountID(t) + }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareHostnameListItem(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "hostname.url_hostname", "example.com"), + resource.TestCheckResourceAttr(name, "comment", rnd), + func(s *terraform.State) error { + listItemID = s.RootModule().Resources[name].Primary.Attributes["id"] + return nil + }, + ), + }, + { + Config: testAccCheckCloudflareHostnameListItem(rnd, rnd, rnd+"-updated", accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "hostname.url_hostname", "example.com"), + resource.TestCheckResourceAttr(name, "comment", rnd+"-updated"), + func(s *terraform.State) error { + newID := s.RootModule().Resources[name].Primary.Attributes["id"] + if newID == listItemID { + return fmt.Errorf("ID of list item did not change when updating comment") + } + return nil + }, + ), + }, + }, + }) +} + +func TestAccCloudflareListItem_UpdateReplace(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := fmt.Sprintf("cloudflare_list_item.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.TestAccPreCheck_AccountID(t) + }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareIPListItemNewIp(rnd, rnd, rnd, accountID, "192.0.2.0"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "ip", "192.0.2.0"), + ), + }, + { + Config: testAccCheckCloudflareIPListItemNewIp(rnd, rnd, rnd, accountID, "192.0.2.1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "ip", "192.0.2.1"), ), }, }, @@ -131,7 +259,6 @@ func TestAccCloudflareListItem_Update(t *testing.T) { } func TestAccCloudflareListItem_ASN(t *testing.T) { - t.Skip("FIXME: Step 1/1 error: Error running apply: exit status 1. Getting rate limited, causing flaky tests.") rnd := utils.GenerateRandomResourceName() name := fmt.Sprintf("cloudflare_list_item.%s", rnd) accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") @@ -154,7 +281,6 @@ func TestAccCloudflareListItem_ASN(t *testing.T) { } func TestAccCloudflareListItem_Hostname(t *testing.T) { - t.Skip("FIXME: Getting rate limited, causing flaky tests.") rnd := utils.GenerateRandomResourceName() name := fmt.Sprintf("cloudflare_list_item.%s", rnd) accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") @@ -176,7 +302,6 @@ func TestAccCloudflareListItem_Hostname(t *testing.T) { } func TestAccCloudflareListItem_Redirect(t *testing.T) { - t.Skip("FIXME: Step 1/1 error: Error running apply: exit status 1. Getting rate limited, causing flaky tests.") rnd := utils.GenerateRandomResourceName() name := fmt.Sprintf("cloudflare_list_item.%s", rnd) accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") @@ -203,10 +328,18 @@ func testAccCheckCloudflareIPListItem(ID, name, comment, accountID string) strin return acctest.LoadTestCase("iplistitem.tf", ID, name, comment, accountID) } +func testAccCheckCloudflareIPListItemNewIp(ID, name, comment, accountID, ip string) string { + return acctest.LoadTestCase("iplistitem_newip.tf", ID, name, comment, accountID, ip) +} + func testAccCheckCloudflareIPListItemMultipleEntries(ID, name, comment, accountID string) string { return acctest.LoadTestCase("iplistitemmultipleentries.tf", ID, name, comment, accountID) } +func testAccCheckCloudflareHostnameListItemMultipleEntries(ID, name, comment, accountID string) string { + return acctest.LoadTestCase("hostnamelistitemmultipleentries.tf", ID, name, comment, accountID) +} + func testAccCheckCloudflareBadListItemType(ID, name, comment, accountID string) string { return acctest.LoadTestCase("badlistitemtype.tf", ID, name, comment, accountID) } @@ -224,7 +357,6 @@ func testAccCheckCloudflareHostnameRedirectItem(ID, name, comment, accountID str } func TestAccCloudflareListItem_RedirectWithOverlappingSourceURL(t *testing.T) { - t.Skip("Step 1/1 error: After applying this test step, the refresh plan was not empty. Getting rate limited, causing flaky tests.") rnd := utils.GenerateRandomResourceName() firstResource := fmt.Sprintf("cloudflare_list_item.%s_1", rnd) secondResource := fmt.Sprintf("cloudflare_list_item.%s_2", rnd) diff --git a/internal/services/list_item/schema.go b/internal/services/list_item/schema.go index c5375f0720..f44a774f8e 100644 --- a/internal/services/list_item/schema.go +++ b/internal/services/list_item/schema.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -34,15 +36,17 @@ func ResourceSchema(ctx context.Context) schema.Schema { "id": schema.StringAttribute{ Description: "The unique ID of the item in the List.", Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "asn": schema.Int64Attribute{ - Description: "A non-negative 32 bit integer", - Optional: true, + Description: "A non-negative 32 bit integer", + Optional: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, }, "comment": schema.StringAttribute{ - Description: "An informative summary of the list item.", - Optional: true, + Description: "An informative summary of the list item.", + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured()}, }, "created_on": schema.StringAttribute{ Description: "The RFC 3339 timestamp of when the item was created.", @@ -62,17 +66,20 @@ func ResourceSchema(ctx context.Context) schema.Schema { CustomType: customfield.NewNestedObjectType[ListItemHostnameModel](ctx), Attributes: map[string]schema.Attribute{ "url_hostname": schema.StringAttribute{ - Required: true, + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "exclude_exact_hostname": schema.BoolAttribute{ - Description: "Only applies to wildcard hostnames (e.g., *.example.com). When true (default), only subdomains are blocked. When false, both the root domain and subdomains are blocked.", - Optional: true, + Description: "Only applies to wildcard hostnames (e.g., *.example.com). When true (default), only subdomains are blocked. When false, both the root domain and subdomains are blocked.", + Optional: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplaceIfConfigured()}, }, }, }, "ip": schema.StringAttribute{ - Description: "An IPv4 address, an IPv4 CIDR, an IPv6 address, or an IPv6 CIDR.", - Optional: true, + Description: "An IPv4 address, an IPv4 CIDR, an IPv6 address, or an IPv6 CIDR.", + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "redirect": schema.SingleNestedAttribute{ Description: "The definition of the redirect.", @@ -80,25 +87,30 @@ func ResourceSchema(ctx context.Context) schema.Schema { CustomType: customfield.NewNestedObjectType[ListItemRedirectModel](ctx), Attributes: map[string]schema.Attribute{ "source_url": schema.StringAttribute{ - Required: true, + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "target_url": schema.StringAttribute{ - Required: true, + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "include_subdomains": schema.BoolAttribute{ - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplaceIfConfigured()}, }, "preserve_path_suffix": schema.BoolAttribute{ - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplaceIfConfigured()}, }, "preserve_query_string": schema.BoolAttribute{ - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplaceIfConfigured()}, }, "status_code": schema.Int64Attribute{ Description: "Available values: 301, 302, 307, 308.", @@ -112,12 +124,14 @@ func ResourceSchema(ctx context.Context) schema.Schema { 308, ), }, - Default: int64default.StaticInt64(301), + Default: int64default.StaticInt64(301), + PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplaceIfConfigured()}, }, "subpath_matching": schema.BoolAttribute{ - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplaceIfConfigured()}, }, }, }, diff --git a/internal/services/list_item/testdata/hostnamelistitemmultipleentries.tf b/internal/services/list_item/testdata/hostnamelistitemmultipleentries.tf new file mode 100644 index 0000000000..25feb9118b --- /dev/null +++ b/internal/services/list_item/testdata/hostnamelistitemmultipleentries.tf @@ -0,0 +1,25 @@ + + resource "cloudflare_list" "%[2]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "list named %[2]s" + kind = "hostname" + } + + resource "cloudflare_list_item" "%[1]s_1" { + account_id = "%[4]s" + list_id = cloudflare_list.%[2]s.id + hostname = { + url_hostname = "a.example.com" + } + comment = "%[3]s" + } + + resource "cloudflare_list_item" "%[1]s_2" { + account_id = "%[4]s" + list_id = cloudflare_list.%[2]s.id + hostname = { + url_hostname = "example.com" + } + comment = "%[3]s" + } diff --git a/internal/services/list_item/testdata/iplistitem_newip.tf b/internal/services/list_item/testdata/iplistitem_newip.tf new file mode 100644 index 0000000000..af7f9fb681 --- /dev/null +++ b/internal/services/list_item/testdata/iplistitem_newip.tf @@ -0,0 +1,14 @@ + + resource "cloudflare_list" "%[2]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "list named %[2]s" + kind = "ip" + } + + resource "cloudflare_list_item" "%[1]s" { + account_id = "%[4]s" + list_id = cloudflare_list.%[2]s.id + ip = "%[5]s" + comment = "%[3]s" + } \ No newline at end of file