diff --git a/.changelog/4077.txt b/.changelog/4077.txt
new file mode 100644
index 0000000000..b88894c190
--- /dev/null
+++ b/.changelog/4077.txt
@@ -0,0 +1,7 @@
+```release-note:new-datasource
+cloudflare_infrastructure_access_targets
+```
+
+```release-note:new-resource
+cloudflare_infrastructure_access_target
+```
diff --git a/docs/data-sources/infrastructure_access_targets.md b/docs/data-sources/infrastructure_access_targets.md
new file mode 100644
index 0000000000..1e235ac608
--- /dev/null
+++ b/docs/data-sources/infrastructure_access_targets.md
@@ -0,0 +1,74 @@
+---
+page_title: "cloudflare_infrastructure_access_targets Data Source - Cloudflare"
+subcategory: ""
+description: |-
+ Use this data source to retrieve all Infrastructure Access Targets.
+---
+
+# cloudflare_infrastructure_access_targets (Data Source)
+
+Use this data source to retrieve all Infrastructure Access Targets.
+
+
+
+## Schema
+
+### Required
+
+- `account_id` (String) The account identifier to target for the resource.
+
+### Optional
+
+- `created_after` (String) A date and time after a target was created to filter on.
+- `hostname` (String) The name of the app type.
+- `hostname_contains` (String) The name of the app type.
+- `ipv4` (String) The name of the app type.
+- `ipv6` (String) The name of the app type.
+- `modified_after` (String) A date and time after a target was modified to filter on.
+- `virtual_network_id` (String) The name of the app type.
+
+### Read-Only
+
+- `targets` (Attributes List) (see [below for nested schema](#nestedatt--targets))
+
+
+### Nested Schema for `targets`
+
+Required:
+
+- `ip` (Attributes) The IPv4/IPv6 address that identifies where to reach a target. (see [below for nested schema](#nestedatt--targets--ip))
+
+Read-Only:
+
+- `account_id` (String) The account identifier to target for the resource.
+- `created_at` (String) The date and time at which the target was created.
+- `hostname` (String) A non-unique field that refers to a target.
+- `id` (String) The identifier of this resource. This is target's unique identifier.
+- `modified_at` (String) The date and time at which the target was last modified.
+
+
+### Nested Schema for `targets.ip`
+
+Optional:
+
+- `ipv4` (Attributes) The target's IPv4 address. (see [below for nested schema](#nestedatt--targets--ip--ipv4))
+- `ipv6` (Attributes) The target's IPv6 address. (see [below for nested schema](#nestedatt--targets--ip--ipv6))
+
+
+### Nested Schema for `targets.ip.ipv4`
+
+Required:
+
+- `ip_addr` (String) The IP address of the target.
+- `virtual_network_id` (String) The private virtual network identifier for the target.
+
+
+
+### Nested Schema for `targets.ip.ipv6`
+
+Required:
+
+- `ip_addr` (String) The IP address of the target.
+- `virtual_network_id` (String) The private virtual network identifier for the target.
+
+
diff --git a/docs/resources/infrastructure_access_target.md b/docs/resources/infrastructure_access_target.md
new file mode 100644
index 0000000000..3c51b6b291
--- /dev/null
+++ b/docs/resources/infrastructure_access_target.md
@@ -0,0 +1,53 @@
+---
+page_title: "cloudflare_infrastructure_access_target Resource - Cloudflare"
+subcategory: ""
+description: |-
+ The Infrastructure Access Target https://developers.cloudflare.com/cloudflare-one/insights/risk-score/ resource allows you to configure Cloudflare Risk Behaviors for an account.
+---
+
+# cloudflare_infrastructure_access_target (Resource)
+
+The [Infrastructure Access Target](https://developers.cloudflare.com/cloudflare-one/insights/risk-score/) resource allows you to configure Cloudflare Risk Behaviors for an account.
+
+
+
+## Schema
+
+### Required
+
+- `account_id` (String) The account identifier to target for the resource.
+- `hostname` (String) A non-unique field that refers to a target.
+- `ip` (Attributes) The IPv4/IPv6 address that identifies where to reach a target. (see [below for nested schema](#nestedatt--ip))
+
+### Read-Only
+
+- `created_at` (String) The date and time at which the target was created.
+- `id` (String) The identifier of this resource.
+- `modified_at` (String) The date and time at which the target was last modified.
+
+
+### Nested Schema for `ip`
+
+Optional:
+
+- `ipv4` (Attributes) The target's IPv4 address. (see [below for nested schema](#nestedatt--ip--ipv4))
+- `ipv6` (Attributes) The target's IPv6 address. (see [below for nested schema](#nestedatt--ip--ipv6))
+
+
+### Nested Schema for `ip.ipv4`
+
+Required:
+
+- `ip_addr` (String) The IP address of the target.
+- `virtual_network_id` (String) The private virtual network identifier for the target.
+
+
+
+### Nested Schema for `ip.ipv6`
+
+Required:
+
+- `ip_addr` (String) The IP address of the target.
+- `virtual_network_id` (String) The private virtual network identifier for the target.
+
+
diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go
index 6226c8a90e..e833efc078 100644
--- a/internal/framework/provider/provider.go
+++ b/internal/framework/provider/provider.go
@@ -26,6 +26,7 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/gateway_app_types"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/gateway_categories"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/hyperdrive_config"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/infrastructure_access_target"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/list_item"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/origin_ca_certificate"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/r2_bucket"
@@ -381,6 +382,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re
workers_for_platforms_dispatch_namespace_deprecated.NewResource,
workers_for_platforms_dispatch_namespace.NewResource,
zero_trust_risk_score_integration.NewResource,
+ infrastructure_access_target.NewResource,
}
}
@@ -393,6 +395,7 @@ func (p *CloudflareProvider) DataSources(ctx context.Context) []func() datasourc
gateway_categories.NewDataSource,
gateway_app_types.NewDataSource,
dcv_delegation.NewDataSource,
+ infrastructure_access_target.NewDataSource,
}
}
diff --git a/internal/framework/service/infrastructure_access_target/data_source.go b/internal/framework/service/infrastructure_access_target/data_source.go
new file mode 100644
index 0000000000..3f10cf84ff
--- /dev/null
+++ b/internal/framework/service/infrastructure_access_target/data_source.go
@@ -0,0 +1,90 @@
+package infrastructure_access_target
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/cloudflare/cloudflare-go"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &InfrastructureAccessTargetDataSource{}
+
+func NewDataSource() datasource.DataSource {
+ return &InfrastructureAccessTargetDataSource{}
+}
+
+type InfrastructureAccessTargetDataSource struct {
+ client *muxclient.Client
+}
+
+func (d *InfrastructureAccessTargetDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_infrastructure_access_targets"
+}
+
+func (d *InfrastructureAccessTargetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*muxclient.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected resource configure type",
+ fmt.Sprintf("Expected *muxclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = client
+}
+
+func (d *InfrastructureAccessTargetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data *InfrastructureAccessTargetsModel
+
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ accountId := data.AccountID.ValueString()
+ if accountId == "" {
+ resp.Diagnostics.AddError("failed to update infrastructure access target", "account id cannot be an empty string")
+ return
+ }
+ params := cloudflare.InfrastructureAccessTargetListParams{
+ Hostname: data.Hostname.ValueString(),
+ HostnameContains: data.HostnameContains.ValueString(),
+ IPV4: data.IPV4.ValueString(),
+ IPV6: data.IPV6.ValueString(),
+ CreatedAfter: data.CreatedAfter.ValueString(),
+ ModifedAfter: data.ModifiedAfter.ValueString(),
+ VirtualNetworkId: data.VirtualNetworkId.ValueString(),
+ }
+
+ allTargets, _, err := d.client.V1.ListInfrastructureAccessTargets(ctx, cloudflare.AccountIdentifier(accountId), params)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to fetch Infrastructure Access Targets: %w", err.Error())
+ return
+ }
+ if len(allTargets) == 0 {
+ resp.Diagnostics.AddError("failed to fetch Infrastructure Access Targets", "no Infrastructure Access Targets matching given query parameters")
+ }
+
+ var targets []InfrastructureAccessTargetModel
+ for _, target := range allTargets {
+ targets = append(targets, InfrastructureAccessTargetModel{
+ AccountID: types.StringValue(accountId),
+ Hostname: types.StringValue(target.Hostname),
+ ID: types.StringValue(target.ID),
+ IP: convertIPInfoToBaseTypeObject(target.IP),
+ CreatedAt: types.StringValue(target.CreatedAt),
+ ModifiedAt: types.StringValue(target.ModifiedAt),
+ })
+ }
+
+ data.Targets = targets
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/framework/service/infrastructure_access_target/data_source_test.go b/internal/framework/service/infrastructure_access_target/data_source_test.go
new file mode 100644
index 0000000000..878363bf81
--- /dev/null
+++ b/internal/framework/service/infrastructure_access_target/data_source_test.go
@@ -0,0 +1,54 @@
+package infrastructure_access_target_test
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccCloudflareInfrastructureAccessTarget_DataSource(t *testing.T) {
+ rnd1 := utils.GenerateRandomResourceName()
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testCloudflareInfrastructureTargetsMatchNoIpv6(rnd1),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("data.cloudflare_infrastructure_access_targets."+rnd1, "targets.#", "1"),
+ resource.TestCheckNoResourceAttr("data.cloudflare_infrastructure_access_targets."+rnd1, "ip.ipv6"),
+
+ resource.TestCheckResourceAttr("cloudflare_infrastructure_access_target."+rnd1, "hostname", rnd1),
+ resource.TestCheckResourceAttr("cloudflare_infrastructure_access_target."+rnd1, "ip.ipv4.ip_addr", "250.26.29.250"),
+ resource.TestCheckResourceAttr("cloudflare_infrastructure_access_target."+rnd1, "ip.ipv4.virtual_network_id", "b9c90134-52de-4903-81e8-004a3a06b435"),
+ ),
+ },
+ },
+ })
+}
+
+func testCloudflareInfrastructureTargetsMatchNoIpv6(hostname string) string {
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ return fmt.Sprintf(`
+resource "cloudflare_infrastructure_access_target" "%[2]s" {
+ account_id = "%[1]s"
+ hostname = "%[2]s"
+ ip = {
+ ipv4 = {
+ ip_addr = "250.26.29.250",
+ virtual_network_id = "b9c90134-52de-4903-81e8-004a3a06b435"
+ }
+ }
+}
+
+data "cloudflare_infrastructure_access_targets" "%[2]s" {
+ depends_on = [cloudflare_infrastructure_access_target.%[2]s]
+ account_id = "%[1]s"
+}
+`, accountID, hostname)
+}
diff --git a/internal/framework/service/infrastructure_access_target/model.go b/internal/framework/service/infrastructure_access_target/model.go
new file mode 100644
index 0000000000..f17f3964e6
--- /dev/null
+++ b/internal/framework/service/infrastructure_access_target/model.go
@@ -0,0 +1,36 @@
+package infrastructure_access_target
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type InfrastructureAccessTargetModel struct {
+ AccountID types.String `tfsdk:"account_id"`
+ Hostname types.String `tfsdk:"hostname"`
+ ID types.String `tfsdk:"id"`
+ IP types.Object `tfsdk:"ip"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ ModifiedAt types.String `tfsdk:"modified_at"`
+}
+
+type InfrastructureAccessTargetIPInfoModel struct {
+ IPV4 types.Object `tfsdk:"ipv4"`
+ IPV6 types.Object `tfsdk:"ipv6"`
+}
+
+type InfrastructureAccessTargetIPDetailsModel struct {
+ IPAddr types.String `tfsdk:"ip_addr"`
+ VirtualNetworkId types.String `tfsdk:"virtual_network_id"`
+}
+
+type InfrastructureAccessTargetsModel struct {
+ AccountID types.String `tfsdk:"account_id"`
+ Hostname types.String `tfsdk:"hostname"`
+ HostnameContains types.String `tfsdk:"hostname_contains"`
+ IPV4 types.String `tfsdk:"ipv4"`
+ IPV6 types.String `tfsdk:"ipv6"`
+ VirtualNetworkId types.String `tfsdk:"virtual_network_id"`
+ CreatedAfter types.String `tfsdk:"created_after"`
+ ModifiedAfter types.String `tfsdk:"modified_after"`
+ Targets []InfrastructureAccessTargetModel `tfsdk:"targets"`
+}
diff --git a/internal/framework/service/infrastructure_access_target/resource.go b/internal/framework/service/infrastructure_access_target/resource.go
new file mode 100644
index 0000000000..721726f794
--- /dev/null
+++ b/internal/framework/service/infrastructure_access_target/resource.go
@@ -0,0 +1,351 @@
+package infrastructure_access_target
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/cloudflare/cloudflare-go"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/flatteners"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ tftypes "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.Resource = &InfrastructureAccessTargetResource{}
+
+func NewResource() resource.Resource {
+ return &InfrastructureAccessTargetResource{}
+}
+
+// InfrastructureAccessTargetResource defines the resource implementation.
+type InfrastructureAccessTargetResource struct {
+ client *muxclient.Client
+}
+
+func (r *InfrastructureAccessTargetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_infrastructure_access_target"
+}
+
+func (r *InfrastructureAccessTargetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*muxclient.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "unexpected resource configure type",
+ fmt.Sprintf("Expected *muxclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = client
+}
+
+func (r *InfrastructureAccessTargetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data *InfrastructureAccessTargetModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ accountId := data.AccountID.ValueString()
+ if accountId == "" {
+ resp.Diagnostics.AddError("failed to create infrastructure access target", "account id cannot be an empty string")
+ return
+ }
+ ipInfo, err := buildCreateIPInfoFromDetails(ctx, data.IP, resp)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create infrastructure access target", "account id cannot be an empty string")
+ return
+ }
+ createTargetParams := cloudflare.CreateInfrastructureAccessTargetParams{
+ InfrastructureAccessTargetParams: cloudflare.InfrastructureAccessTargetParams{
+ Hostname: data.Hostname.ValueString(),
+ IP: ipInfo,
+ },
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("Creating Cloudflare Infrastructure Access Target from struct %+v", createTargetParams))
+ target, err := r.client.V1.CreateInfrastructureAccessTarget(ctx, cloudflare.AccountIdentifier(accountId), createTargetParams)
+ if err != nil {
+ resp.Diagnostics.AddError(fmt.Sprintf("error creating Infrastructure Access Target for account %q", accountId), err.Error())
+ return
+ }
+
+ data = buildTargetModelFromResponse(data.AccountID, target)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *InfrastructureAccessTargetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data *InfrastructureAccessTargetModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("Retrieving Cloudflare Infrastructure Access Target with ID %s", data.ID))
+ target, err := r.client.V1.GetInfrastructureAccessTarget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), data.ID.ValueString())
+ if err != nil {
+ var notFoundError *cloudflare.NotFoundError
+ if errors.As(err, ¬FoundError) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError(fmt.Sprintf("error finding Infrastructure Access Target with ID %s", data.ID), err.Error())
+ return
+ }
+
+ data = buildTargetModelFromResponse(data.AccountID, target)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *InfrastructureAccessTargetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data *InfrastructureAccessTargetModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ accountId := data.AccountID.ValueString()
+ if accountId == "" {
+ resp.Diagnostics.AddError("failed to update infrastructure access target", "account id cannot be an empty string")
+ return
+ }
+ ipInfo, err := buildUpdateIPInfoFromDetails(ctx, data.IP, resp)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create infrastructure access target", "account id cannot be an empty string")
+ return
+ }
+ updatedTargetParams := cloudflare.UpdateInfrastructureAccessTargetParams{
+ ID: data.ID.ValueString(),
+ ModifyParams: cloudflare.InfrastructureAccessTargetParams{
+ Hostname: data.Hostname.ValueString(),
+ IP: ipInfo,
+ },
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Infrastructure Access Target from struct: %+v", updatedTargetParams))
+ updatedTarget, err := r.client.V1.UpdateInfrastructureAccessTarget(ctx, cloudflare.AccountIdentifier(accountId), updatedTargetParams)
+ if err != nil {
+ resp.Diagnostics.AddError(fmt.Sprintf("error updating Infrastructure Access Target with ID %s for account %q", data.ID, accountId), err.Error())
+ return
+ }
+
+ data = buildTargetModelFromResponse(data.AccountID, updatedTarget)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *InfrastructureAccessTargetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data *InfrastructureAccessTargetModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("Deleting Cloudflare Infrastructure Access Target with ID: %s", data.ID))
+ err := r.client.V1.DeleteInfrastructureAccessTarget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), data.ID.ValueString())
+ var notFoundError *cloudflare.NotFoundError
+ if errors.As(err, ¬FoundError) {
+ // Return early without error if target is already deleted
+ return
+ }
+ if err != nil {
+ resp.Diagnostics.AddError(fmt.Sprintf("error deleting Infrastructure Access Target with ID %s for account %q", data.ID, data.AccountID.ValueString()), err.Error())
+ return
+ }
+}
+
+func buildCreateIPInfoFromDetails(ctx context.Context, ipInfoModel basetypes.ObjectValue, resp *resource.CreateResponse) (cloudflare.InfrastructureAccessTargetIPInfo, error) {
+ if ipInfoModel.IsNull() || ipInfoModel.IsUnknown() {
+ return cloudflare.InfrastructureAccessTargetIPInfo{}, fmt.Errorf("failed: ip info model is empty")
+ }
+ var ipInfo *InfrastructureAccessTargetIPInfoModel
+ resp.Diagnostics.Append(ipInfoModel.As(ctx, &ipInfo, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+
+ if (ipInfo.IPV4.IsNull() || ipInfo.IPV4.IsUnknown()) && (ipInfo.IPV6.IsNull() || ipInfo.IPV6.IsUnknown()) {
+ return cloudflare.InfrastructureAccessTargetIPInfo{}, fmt.Errorf("error creating target resource: one of ipv4 or ipv6 must be configured")
+ }
+
+ if !(ipInfo.IPV4.IsNull() || ipInfo.IPV4.IsUnknown()) && !(ipInfo.IPV6.IsNull() || ipInfo.IPV6.IsUnknown()) {
+ var ipv4Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV4.As(ctx, &ipv4Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ var ipv6Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV6.As(ctx, &ipv6Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ return buildIPInfoFromAttributes(ipv4Details.IPAddr.ValueString(), ipv6Details.IPAddr.ValueString(), ipv4Details.VirtualNetworkId.ValueString(), ipv6Details.VirtualNetworkId.ValueString()), nil
+ } else if !(ipInfo.IPV4.IsNull() || ipInfo.IPV4.IsUnknown()) {
+ var ipv4Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV4.As(ctx, &ipv4Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ return buildIPV4InfoFromAttributes(ipv4Details.IPAddr.ValueString(), ipv4Details.VirtualNetworkId.ValueString()), nil
+ } else {
+ var ipv6Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV6.As(ctx, &ipv6Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ return buildIPV6InfoFromAttributes(ipv6Details.IPAddr.ValueString(), ipv6Details.VirtualNetworkId.ValueString()), nil
+ }
+}
+
+func buildUpdateIPInfoFromDetails(ctx context.Context, ipInfoModel basetypes.ObjectValue, resp *resource.UpdateResponse) (cloudflare.InfrastructureAccessTargetIPInfo, error) {
+ if ipInfoModel.IsNull() || ipInfoModel.IsUnknown() {
+ return cloudflare.InfrastructureAccessTargetIPInfo{}, fmt.Errorf("failed: ip info model is empty")
+ }
+ var ipInfo *InfrastructureAccessTargetIPInfoModel
+ resp.Diagnostics.Append(ipInfoModel.As(ctx, &ipInfo, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+
+ if (ipInfo.IPV4.IsNull() || ipInfo.IPV4.IsUnknown()) && (ipInfo.IPV6.IsNull() || ipInfo.IPV6.IsUnknown()) {
+ return cloudflare.InfrastructureAccessTargetIPInfo{}, fmt.Errorf("error creating target resource: one of ipv4 or ipv6 must be configured")
+ }
+
+ if !(ipInfo.IPV4.IsNull() || ipInfo.IPV4.IsUnknown()) && !(ipInfo.IPV6.IsNull() || ipInfo.IPV6.IsUnknown()) {
+ var ipv4Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV4.As(ctx, &ipv4Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ var ipv6Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV6.As(ctx, &ipv6Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ return buildIPInfoFromAttributes(ipv4Details.IPAddr.ValueString(), ipv6Details.IPAddr.ValueString(), ipv4Details.VirtualNetworkId.ValueString(), ipv6Details.VirtualNetworkId.ValueString()), nil
+ } else if !(ipInfo.IPV4.IsNull() || ipInfo.IPV4.IsUnknown()) {
+ var ipv4Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV4.As(ctx, &ipv4Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ return buildIPV4InfoFromAttributes(ipv4Details.IPAddr.ValueString(), ipv4Details.VirtualNetworkId.ValueString()), nil
+ } else {
+ var ipv6Details *InfrastructureAccessTargetIPDetailsModel
+ resp.Diagnostics.Append(ipInfo.IPV6.As(ctx, &ipv6Details, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
+ return buildIPV6InfoFromAttributes(ipv6Details.IPAddr.ValueString(), ipv6Details.VirtualNetworkId.ValueString()), nil
+ }
+}
+
+func buildIPInfoFromAttributes(ipv4Addr string, ipv6Addr string, ipv4VirtualNetworkId string, ipv6VirtualNetworkId string) cloudflare.InfrastructureAccessTargetIPInfo {
+ return cloudflare.InfrastructureAccessTargetIPInfo{
+ IPV4: &cloudflare.InfrastructureAccessTargetIPDetails{
+ IPAddr: ipv4Addr,
+ VirtualNetworkId: ipv4VirtualNetworkId,
+ },
+ IPV6: &cloudflare.InfrastructureAccessTargetIPDetails{
+ IPAddr: ipv6Addr,
+ VirtualNetworkId: ipv6VirtualNetworkId,
+ },
+ }
+}
+
+func buildIPV4InfoFromAttributes(ipAddr string, virtualNetworkId string) cloudflare.InfrastructureAccessTargetIPInfo {
+ return cloudflare.InfrastructureAccessTargetIPInfo{
+ IPV4: &cloudflare.InfrastructureAccessTargetIPDetails{
+ IPAddr: ipAddr,
+ VirtualNetworkId: virtualNetworkId,
+ },
+ }
+}
+
+func buildIPV6InfoFromAttributes(ipAddr string, virtualNetworkId string) cloudflare.InfrastructureAccessTargetIPInfo {
+ return cloudflare.InfrastructureAccessTargetIPInfo{
+ IPV6: &cloudflare.InfrastructureAccessTargetIPDetails{
+ IPAddr: ipAddr,
+ VirtualNetworkId: virtualNetworkId,
+ },
+ }
+}
+
+func buildTargetModelFromResponse(accountID tftypes.String, target cloudflare.InfrastructureAccessTarget) *InfrastructureAccessTargetModel {
+ built := InfrastructureAccessTargetModel{
+ AccountID: accountID,
+ Hostname: flatteners.String(target.Hostname),
+ ID: flatteners.String(target.ID),
+ IP: convertIPInfoToBaseTypeObject(target.IP),
+ CreatedAt: flatteners.String(target.CreatedAt),
+ ModifiedAt: flatteners.String(target.ModifiedAt),
+ }
+ return &built
+}
+
+func convertIPInfoToBaseTypeObject(ipInfo cloudflare.InfrastructureAccessTargetIPInfo) basetypes.ObjectValue {
+ if ipInfo.IPV4 != nil && ipInfo.IPV6 != nil {
+ ipv4Object := buildObjectFromIpDetails(ipInfo.IPV4.IPAddr, ipInfo.IPV4.VirtualNetworkId)
+ ipv6Object := buildObjectFromIpDetails(ipInfo.IPV6.IPAddr, ipInfo.IPV6.VirtualNetworkId)
+ parentObjectMap := map[string]attr.Value{
+ "ipv4": ipv4Object,
+ "ipv6": ipv6Object,
+ }
+ parentObjectValue, _ := tftypes.ObjectValue(map[string]attr.Type{
+ "ipv4": tftypes.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ },
+ },
+ "ipv6": tftypes.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ },
+ },
+ }, parentObjectMap)
+ return parentObjectValue
+ } else if ipInfo.IPV4 != nil {
+ ipv4Object := buildObjectFromIpDetails(ipInfo.IPV4.IPAddr, ipInfo.IPV4.VirtualNetworkId)
+ return buildObjectFromIpInfoV4(ipv4Object)
+ } else {
+ ipv6Object := buildObjectFromIpDetails(ipInfo.IPV6.IPAddr, ipInfo.IPV6.VirtualNetworkId)
+ return buildObjectFromIpInfoV6(ipv6Object)
+ }
+}
+
+func buildObjectFromIpDetails(ipAddr string, virtualNetworkId string) basetypes.ObjectValue {
+ ipDetailsAttributes := map[string]attr.Value{
+ "ip_addr": flatteners.String(ipAddr),
+ "virtual_network_id": flatteners.String(virtualNetworkId),
+ }
+ ipDetailsObjectType, _ := tftypes.ObjectValue(map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ }, ipDetailsAttributes)
+
+ return ipDetailsObjectType
+}
+
+func buildObjectFromIpInfoV4(baseObjectMap basetypes.ObjectValue) basetypes.ObjectValue {
+ parentObjectMap := map[string]attr.Value{
+ "ipv4": baseObjectMap,
+ "ipv6": basetypes.NewObjectNull(map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ }),
+ }
+ return buildIPInfoObjectValue(parentObjectMap)
+}
+
+func buildObjectFromIpInfoV6(baseObjectMap basetypes.ObjectValue) basetypes.ObjectValue {
+ parentObjectMap := map[string]attr.Value{
+ "ipv4": basetypes.NewObjectNull(map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ }),
+ "ipv6": baseObjectMap,
+ }
+ return buildIPInfoObjectValue(parentObjectMap)
+}
+
+func buildIPInfoObjectValue(objectMap map[string]attr.Value) basetypes.ObjectValue {
+ ipInfoObjectValue, _ := tftypes.ObjectValue(map[string]attr.Type{
+ "ipv4": tftypes.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ },
+ },
+ "ipv6": tftypes.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "ip_addr": tftypes.StringType,
+ "virtual_network_id": tftypes.StringType,
+ },
+ },
+ }, objectMap)
+ return ipInfoObjectValue
+}
diff --git a/internal/framework/service/infrastructure_access_target/resource_test.go b/internal/framework/service/infrastructure_access_target/resource_test.go
new file mode 100644
index 0000000000..9904d366f9
--- /dev/null
+++ b/internal/framework/service/infrastructure_access_target/resource_test.go
@@ -0,0 +1,110 @@
+package infrastructure_access_target_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/cloudflare/cloudflare-go"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestMain(m *testing.M) {
+ resource.TestMain(m)
+}
+
+func init() {
+ resource.AddTestSweepers("cloudflare_infrastructure_access_target", &resource.Sweeper{
+ Name: "cloudflare_infrastructure_access_target",
+ F: func(region string) error {
+ client, err := acctest.SharedV1Client()
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+
+ if err != nil {
+ return fmt.Errorf("error establishing client: %w", err)
+ }
+
+ ctx := context.Background()
+ targets, _, err := client.ListInfrastructureAccessTargets(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.InfrastructureAccessTargetListParams{})
+ if err != nil {
+ return fmt.Errorf("failed to fetch rulesets: %w", err)
+ }
+
+ for _, target := range targets {
+ err := client.DeleteInfrastructureAccessTarget(ctx, cloudflare.AccountIdentifier(accountID), target.ID)
+ if err != nil {
+ return fmt.Errorf("failed to delete ruleset %q: %w", target.ID, err)
+ }
+ }
+
+ return nil
+ },
+ })
+}
+
+func TestAccCloudflareInfrastructureAccessTarget_Basic(t *testing.T) {
+ accID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ rnd := utils.GenerateRandomResourceName()
+ resourceName := fmt.Sprintf("cloudflare_infrastructure_access_target.%s", rnd)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ // Create resource configuration
+ Config: testAccCloudflareInfrastructureAccessTargetCreate(accID, rnd),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "hostname", rnd),
+ resource.TestCheckResourceAttr(resourceName, "ip.ipv4.ip_addr", "250.26.29.250"),
+ resource.TestCheckNoResourceAttr(resourceName, "ip.ipv6"),
+ ),
+ },
+ {
+ // Update resource configuration
+ Config: testAccCloudflareInfrastructureAccessTargetUpdate(accID, rnd),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "hostname", rnd+"-updated"),
+ resource.TestCheckResourceAttr(resourceName, "ip.ipv4.ip_addr", "250.26.29.250"),
+ resource.TestCheckResourceAttr(resourceName, "ip.ipv6.ip_addr", "64c0:64e8:f0b4:8dbf:7104:72b0:ec8f:f5e0"),
+ resource.TestCheckResourceAttr(resourceName, "ip.ipv6.virtual_network_id", "01920a8c-dc14-7bb2-b67b-14c858494a54"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCloudflareInfrastructureAccessTargetCreate(accID, hostname string) string {
+ return fmt.Sprintf(`
+resource "cloudflare_infrastructure_access_target" "%[2]s" {
+ account_id = "%[1]s"
+ hostname = "%[2]s"
+ ip = {
+ ipv4 = {
+ ip_addr = "250.26.29.250"
+ virtual_network_id = "01920a8c-dc14-7bb2-b67b-14c858494a54"
+ }
+ }
+}`, accID, hostname)
+}
+
+func testAccCloudflareInfrastructureAccessTargetUpdate(accID, hostname string) string {
+ return fmt.Sprintf(`
+resource "cloudflare_infrastructure_access_target" "%[2]s" {
+ account_id = "%[1]s"
+ hostname = "%[2]s-updated"
+ ip = {
+ ipv4 = {
+ ip_addr = "250.26.29.250"
+ virtual_network_id = "01920a8c-dc14-7bb2-b67b-14c858494a54"
+ },
+ ipv6 = {
+ ip_addr = "64c0:64e8:f0b4:8dbf:7104:72b0:ec8f:f5e0"
+ virtual_network_id = "01920a8c-dc14-7bb2-b67b-14c858494a54"
+ }
+ }
+}`, accID, hostname)
+}
diff --git a/internal/framework/service/infrastructure_access_target/schema.go b/internal/framework/service/infrastructure_access_target/schema.go
new file mode 100644
index 0000000000..6a281a4609
--- /dev/null
+++ b/internal/framework/service/infrastructure_access_target/schema.go
@@ -0,0 +1,192 @@
+package infrastructure_access_target
+
+import (
+ "context"
+
+ "github.com/MakeNowJust/heredoc/v2"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+)
+
+func (r *InfrastructureAccessTargetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: heredoc.Doc(`
+ The [Infrastructure Access Target](https://developers.cloudflare.com/cloudflare-one/insights/risk-score/) resource allows you to configure Cloudflare Risk Behaviors for an account.
+ `),
+ Attributes: map[string]schema.Attribute{
+ consts.AccountIDSchemaKey: schema.StringAttribute{
+ MarkdownDescription: consts.AccountIDSchemaDescription,
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ consts.IDSchemaKey: schema.StringAttribute{
+ Computed: true,
+ MarkdownDescription: consts.IDSchemaDescription,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "A non-unique field that refers to a target.",
+ Required: true,
+ },
+ "ip": schema.SingleNestedAttribute{
+ MarkdownDescription: "The IPv4/IPv6 address that identifies where to reach a target.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "ipv4": schema.SingleNestedAttribute{
+ MarkdownDescription: "The target's IPv4 address.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "ip_addr": schema.StringAttribute{
+ MarkdownDescription: "The IP address of the target.",
+ Required: true,
+ },
+ "virtual_network_id": schema.StringAttribute{
+ MarkdownDescription: "The private virtual network identifier for the target.",
+ Required: true,
+ },
+ },
+ },
+ "ipv6": schema.SingleNestedAttribute{
+ MarkdownDescription: "The target's IPv6 address.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "ip_addr": schema.StringAttribute{
+ MarkdownDescription: "The IP address of the target.",
+ Required: true,
+ },
+ "virtual_network_id": schema.StringAttribute{
+ MarkdownDescription: "The private virtual network identifier for the target.",
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The date and time at which the target was created.",
+ // Set value to read-only.
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "modified_at": schema.StringAttribute{
+ MarkdownDescription: "The date and time at which the target was last modified.",
+ // Set value to read-only.
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *InfrastructureAccessTargetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = dschema.Schema{
+ MarkdownDescription: "Use this data source to retrieve all Infrastructure Access Targets.",
+ Attributes: map[string]dschema.Attribute{
+ consts.AccountIDSchemaKey: dschema.StringAttribute{
+ MarkdownDescription: consts.AccountIDSchemaDescription,
+ Required: true,
+ },
+ "hostname": dschema.StringAttribute{
+ Optional: true,
+ Description: "The name of the app type.",
+ },
+ "hostname_contains": dschema.StringAttribute{
+ Optional: true,
+ Description: "The name of the app type.",
+ },
+ "ipv4": dschema.StringAttribute{
+ Optional: true,
+ Description: "The name of the app type.",
+ },
+ "ipv6": dschema.StringAttribute{
+ Optional: true,
+ Description: "The name of the app type.",
+ },
+ "virtual_network_id": dschema.StringAttribute{
+ Optional: true,
+ Description: "The name of the app type.",
+ },
+ "created_after": dschema.StringAttribute{
+ Optional: true,
+ Description: "A date and time after a target was created to filter on.",
+ },
+ "modified_after": dschema.StringAttribute{
+ Optional: true,
+ Description: "A date and time after a target was modified to filter on.",
+ },
+ // Schema for data source is separate from resource so attributes
+ // are re written here but modified to be computer aka read-only.
+ "targets": dschema.ListNestedAttribute{
+ Computed: true,
+ NestedObject: dschema.NestedAttributeObject{
+ Attributes: map[string]dschema.Attribute{
+ consts.AccountIDSchemaKey: dschema.StringAttribute{
+ MarkdownDescription: consts.AccountIDSchemaDescription,
+ Computed: true,
+ },
+ consts.IDSchemaKey: schema.StringAttribute{
+ MarkdownDescription: consts.IDSchemaDescription + " This is target's unique identifier.",
+ Computed: true,
+ },
+ "hostname": dschema.StringAttribute{
+ MarkdownDescription: "A non-unique field that refers to a target.",
+ Computed: true,
+ },
+ "ip": schema.SingleNestedAttribute{
+ MarkdownDescription: "The IPv4/IPv6 address that identifies where to reach a target.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "ipv4": schema.SingleNestedAttribute{
+ MarkdownDescription: "The target's IPv4 address.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "ip_addr": schema.StringAttribute{
+ MarkdownDescription: "The IP address of the target.",
+ Required: true,
+ },
+ "virtual_network_id": schema.StringAttribute{
+ MarkdownDescription: "The private virtual network identifier for the target.",
+ Required: true,
+ },
+ },
+ },
+ "ipv6": schema.SingleNestedAttribute{
+ MarkdownDescription: "The target's IPv6 address.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "ip_addr": schema.StringAttribute{
+ MarkdownDescription: "The IP address of the target.",
+ Required: true,
+ },
+ "virtual_network_id": schema.StringAttribute{
+ MarkdownDescription: "The private virtual network identifier for the target.",
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ "created_at": dschema.StringAttribute{
+ MarkdownDescription: "The date and time at which the target was created.",
+ Computed: true,
+ },
+ "modified_at": dschema.StringAttribute{
+ MarkdownDescription: "The date and time at which the target was last modified.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}