diff --git a/internal/framework/service/infrastructure_access_target/data_source.go b/internal/framework/service/infrastructure_access_target/data_source.go index 5d5a6d834d..166e0ea9c2 100644 --- a/internal/framework/service/infrastructure_access_target/data_source.go +++ b/internal/framework/service/infrastructure_access_target/data_source.go @@ -61,7 +61,7 @@ func (d *InfrastructureAccessTargetDataSource) Read(ctx context.Context, req dat return &s } } - params := cloudflare.TargetListParams{ + params := cloudflare.InfrastructureAccessTargetListParams{ Hostname: *checkSetNil(data.Hostname.String()), HostnameContains: *checkSetNil(data.HostnameContains.String()), IPV4: *checkSetNil(data.IPV4.String()), @@ -86,7 +86,7 @@ func (d *InfrastructureAccessTargetDataSource) Read(ctx context.Context, req dat AccountID: types.StringValue(accountId), Hostname: types.StringValue(target.Hostname), ID: types.StringValue(target.ID), - IP: target.IP, + IP: convertIPInfoToBaseTypeObject(target.IP), CreatedAt: types.StringValue(target.CreatedAt), ModifiedAt: types.StringValue(target.ModifiedAt), }) diff --git a/internal/framework/service/infrastructure_access_target/model.go b/internal/framework/service/infrastructure_access_target/model.go index 3e5d5665d7..273dabee6b 100644 --- a/internal/framework/service/infrastructure_access_target/model.go +++ b/internal/framework/service/infrastructure_access_target/model.go @@ -1,18 +1,27 @@ package infrastructure_access_target import ( - "github.com/cloudflare/cloudflare-go" "github.com/hashicorp/terraform-plugin-framework/types" ) // Resource model type InfrastructureAccessTargetModel struct { - AccountID types.String `tfsdk:"account_id"` - Hostname types.String `tfsdk:"hostname"` - ID types.String `tfsdk:"id"` - IP cloudflare.IPInfo `tfsdk:"ip"` - CreatedAt types.String `tfsdk:"hostname"` - ModifiedAt types.String `tfsdk:"hostname"` + 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 `json:"ipv4,omitempty"` + IPV6 types.Object `json:"ipv6,omitempty"` +} + +type InfrastructureAccessTargetIPDetailsModel struct { + IPAddr string `json:"ip_addr"` + VirtualNetworkId string `json:"virtual_network_id"` } // Data source model diff --git a/internal/framework/service/infrastructure_access_target/resource.go b/internal/framework/service/infrastructure_access_target/resource.go index d3235b8d15..9bb20a98b0 100644 --- a/internal/framework/service/infrastructure_access_target/resource.go +++ b/internal/framework/service/infrastructure_access_target/resource.go @@ -8,8 +8,10 @@ import ( "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" ) @@ -59,10 +61,15 @@ func (r *InfrastructureAccessTargetResource) Create(ctx context.Context, req res resp.Diagnostics.AddError("failed to create infrastructure access target", "account id cannot be an empty string") return } + ipInfo, err := validateParseIPInfoCreate(ctx, data, 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.String(), - IP: data.IP, + IP: ipInfo, }, } @@ -114,11 +121,16 @@ func (r *InfrastructureAccessTargetResource) Update(ctx context.Context, req res resp.Diagnostics.AddError("failed to update infrastructure access target", "account id cannot be an empty string") return } + ipInfo, err := validateParseIPInfoUpdate(ctx, data, 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.String(), ModifyParams: cloudflare.InfrastructureAccessTargetParams{ Hostname: data.Hostname.String(), - IP: data.IP, + IP: ipInfo, }, } @@ -149,14 +161,131 @@ func (r *InfrastructureAccessTargetResource) Delete(ctx context.Context, req res } } -func buildTargetModelFromResponse(accountID tftypes.String, target cloudflare.Target) *InfrastructureAccessTargetModel { +func validateParseIPInfoCreate(ctx context.Context, data *InfrastructureAccessTargetModel, resp *resource.CreateResponse) (cloudflare.InfrastructureAccessTargetIPInfo, error) { + var ipInfo *InfrastructureAccessTargetIPInfoModel + resp.Diagnostics.Append(data.IP.As(ctx, &ipInfo, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...) + + 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 validateParseIPInfo(ipv4Details, ipv6Details) +} + +func validateParseIPInfoUpdate(ctx context.Context, data *InfrastructureAccessTargetModel, resp *resource.UpdateResponse) (cloudflare.InfrastructureAccessTargetIPInfo, error) { + var ipInfo *InfrastructureAccessTargetIPInfoModel + resp.Diagnostics.Append(data.IP.As(ctx, &ipInfo, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...) + + 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 validateParseIPInfo(ipv4Details, ipv6Details) +} + +func validateParseIPInfo(ipv4Details *InfrastructureAccessTargetIPDetailsModel, ipv6Details *InfrastructureAccessTargetIPDetailsModel) (cloudflare.InfrastructureAccessTargetIPInfo, error) { + if ipv4Details == nil && ipv6Details == nil { + return cloudflare.InfrastructureAccessTargetIPInfo{}, fmt.Errorf("error creating target resource: one of ipv4 or ipv6 must be configured") + } + + if ipv4Details != nil && ipv6Details != nil { + return cloudflare.InfrastructureAccessTargetIPInfo{ + IPV4: &cloudflare.InfrastructureAccessTargetIPDetails{ + IPAddr: ipv4Details.IPAddr, + VirtualNetworkId: ipv4Details.VirtualNetworkId, + }, + IPV6: &cloudflare.InfrastructureAccessTargetIPDetails{ + IPAddr: ipv6Details.IPAddr, + VirtualNetworkId: ipv6Details.VirtualNetworkId, + }, + }, nil + } else if ipv4Details != nil { + return cloudflare.InfrastructureAccessTargetIPInfo{ + IPV4: &cloudflare.InfrastructureAccessTargetIPDetails{ + IPAddr: ipv4Details.IPAddr, + VirtualNetworkId: ipv4Details.VirtualNetworkId, + }, + }, nil + } else { + return cloudflare.InfrastructureAccessTargetIPInfo{ + IPV6: &cloudflare.InfrastructureAccessTargetIPDetails{ + IPAddr: ipv6Details.IPAddr, + VirtualNetworkId: ipv6Details.VirtualNetworkId, + }, + }, nil + } +} + +func buildTargetModelFromResponse(accountID tftypes.String, target cloudflare.InfrastructureAccessTarget) *InfrastructureAccessTargetModel { built := InfrastructureAccessTargetModel{ AccountID: accountID, Hostname: flatteners.String(target.Hostname), ID: flatteners.String(target.ID), - IP: target.IP, + 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 buildObjectFromIpInfo("ipv4", ipv4Object) + } else { + ipv6Object := buildObjectFromIpDetails(ipInfo.IPV6.IPAddr, ipInfo.IPV6.VirtualNetworkId) + return buildObjectFromIpInfo("ipv6", 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 buildObjectFromIpInfo(ipv string, baseObjectMap basetypes.ObjectValue) basetypes.ObjectValue { + parentObjectMap := map[string]attr.Value{ + ipv: baseObjectMap, + } + parentObjectValue, _ := tftypes.ObjectValue(map[string]attr.Type{ + ipv: tftypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "ip_addr": tftypes.StringType, + "virtual_network_id": tftypes.StringType, + }, + }, + }, parentObjectMap) + return parentObjectValue +} diff --git a/internal/framework/service/infrastructure_access_target/resource_test.go b/internal/framework/service/infrastructure_access_target/resource_test.go index 3195c682ad..d83db5a7d1 100644 --- a/internal/framework/service/infrastructure_access_target/resource_test.go +++ b/internal/framework/service/infrastructure_access_target/resource_test.go @@ -29,7 +29,7 @@ func init() { ctx := context.Background() // Retrieve all targets created under the current test account - targets, _, err := client.ListInfrastructureAccessTargets(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.TargetListParams{}) + targets, _, err := client.ListInfrastructureAccessTargets(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.InfrastructureAccessTargetListParams{}) if err != nil { return fmt.Errorf("failed to fetch rulesets: %w", err) } diff --git a/internal/framework/service/infrastructure_access_target/schema.go b/internal/framework/service/infrastructure_access_target/schema.go index 450c68da3a..e19367ccaa 100644 --- a/internal/framework/service/infrastructure_access_target/schema.go +++ b/internal/framework/service/infrastructure_access_target/schema.go @@ -21,7 +21,6 @@ func (r *InfrastructureAccessTargetResource) Schema(ctx context.Context, req res 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, @@ -32,7 +31,6 @@ func (r *InfrastructureAccessTargetResource) Schema(ctx context.Context, req res }, consts.IDSchemaKey: schema.StringAttribute{ Computed: true, - Optional: true, MarkdownDescription: consts.IDSchemaDescription + " This is target's unique identifier.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(),