Repository: ovh/terraform-provider-ovh

Issue:
[FEATURE] Private network region attributes should be a map
### Describe the bug

The private network `region_attributes` property is currently a set when it should likely be a map.

As most networking resources actually require the `openstackid` of a region-specific network, rather than the ID of the enclosing network resource, this is required quite frequently.

### Terraform Version

OpenTofu v1.9.0
on darwin_arm64

### OVH Terraform Provider Version

+ provider registry.opentofu.org/hashicorp/http v3.4.5
+ provider registry.opentofu.org/ovh/ovh v2.0.0
+ provider registry.opentofu.org/terraform-provider-openstack/openstack v3.0.0

### Affected Resource(s)

Please list the resources as a list, for example:
- ovh_cloud_project_network_private

### Expected Behavior

I would expect to be able to access region attributes by their region key:

```
locals {
  region_network_attributes = ovh_cloud_project_network_private.network.regions_attributes["GRA9"]
}
```

### Actual Behavior

To access a region's attributes I need to either filter:

```
locals {
  region_network_attributes = [for att in local.network.regions_attributes : att if att.region == "GRA9"][0]
}
```

Or create the required map conversion manually:
```
locals {
  region_networks = {
    for att in ovh_cloud_project_network_private.network.regions_attributes: 
      att.region => att
  }
  region_network_attributes = local. region_networks["GRA9"]
}
```

### Alternative 

An alternative would be to remove the `regions` property on `ovh_cloud_project_network_private` to make *all* networks cross-region by default.

Region specific network IDs could then be determined in the background from a `ovh_cloud_project_network_private` id and a region code.

Subnets could then be specified using the actual network id (rather than a region specific openstack net id) and a region code. Gateway's could then omit the network_id and use just the subnet_id, etc.

Discussion:
@amstuta: Hello @tcldr,

I agree that a map would be easier to use indeed. To avoid a breaking change, we will add a new field for this map.

I'm classifying this ticket as a feature request because it's not really a bug, and we'll work on it when possible.

Solution patch:
diff --git a/.gitignore b/.gitignore
index e818f088..b9a3c95d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,4 @@ terraform-provider-ovh
 examples/kube-nodepool-deployment/.terraform.lock.hcl
 examples/kube-nodepool-deployment/.terraform.tfstate.lock.info
 examples/kube-nodepool-deployment/logs
-examples/kube-nodepool-deployment/my-kube-cluster-*.yml
+examples/kube-nodepool-deployment/my-kube-cluster-*.yml
\ No newline at end of file
diff --git a/docs/data-sources/cloud_project_network_private.md b/docs/data-sources/cloud_project_network_private.md
index ea39717c..1d9b7340 100644
--- a/docs/data-sources/cloud_project_network_private.md
+++ b/docs/data-sources/cloud_project_network_private.md
@@ -40,3 +40,4 @@ The following attributes are exported:
   - `openstack_id`: Network ID on openstack region
   - `region`: Name of the region
   - `status`: Status of the network
+- `regions_openstack_ids` - A map with region name as key, and region-specific openstack id as value
diff --git a/docs/resources/cloud_project_network_private.md b/docs/resources/cloud_project_network_private.md
index 478303ab..bf8bee35 100644
--- a/docs/resources/cloud_project_network_private.md
+++ b/docs/resources/cloud_project_network_private.md
@@ -44,6 +44,7 @@ The following attributes are exported:
 * `regions_status` - (Deprecated) A map representing the status of the network per region.
 * `regions_status/region` - (Deprecated) The id of the region.
 * `regions_status/status` - (Deprecated) The status of the network in the region.
+* `regions_openstack_ids` - A map with region name as key, and region-specific openstack id as value
 * `status` - the status of the network. should be normally set to 'ACTIVE'.
 * `type` - the type of the network. Either 'private' or 'public'.
 
diff --git a/ovh/data_cloud_project_network_private.go b/ovh/data_cloud_project_network_private.go
index 09bf3cc4..c0135cd3 100644
--- a/ovh/data_cloud_project_network_private.go
+++ b/ovh/data_cloud_project_network_private.go
@@ -5,7 +5,9 @@ import (
 	"fmt"
 	"net/url"
 
+	"github.com/hashicorp/terraform-plugin-framework/attr"
 	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types"
 )
 
 var _ datasource.DataSourceWithConfigure = (*cloudProjectNetworkPrivateDataSource)(nil)
@@ -64,6 +66,17 @@ func (d *cloudProjectNetworkPrivateDataSource) Read(ctx context.Context, req dat
 		return
 	}
 
+	// Compute RegionsOpenstackIds map from Regions list
+	internal_openstack_ids := map[string]attr.Value{}
+	for _, region_attrs := range data.Regions.Elements() {
+		internal_openstack_ids[region_attrs.(RegionsValue).Region.ValueString()] = region_attrs.(RegionsValue).OpenstackId
+	}
+
+	regions_openstack_ids, diags := ovhtypes.NewTfMapNestedValue[ovhtypes.TfStringValue](ctx, internal_openstack_ids)
+	resp.Diagnostics.Append(diags...)
+
+	data.RegionsOpenstackIds = regions_openstack_ids
+
 	// Save data into Terraform state
 	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
 }
diff --git a/ovh/data_cloud_project_network_private_gen.go b/ovh/data_cloud_project_network_private_gen.go
index b1e9ebbc..08ce7301 100644
--- a/ovh/data_cloud_project_network_private_gen.go
+++ b/ovh/data_cloud_project_network_private_gen.go
@@ -71,6 +71,12 @@ func CloudProjectNetworkPrivateDataSourceSchema(ctx context.Context) schema.Sche
 			Description:         "Details about private network in region",
 			MarkdownDescription: "Details about private network in region",
 		},
+		"regions_openstack_ids": schema.MapAttribute{
+			CustomType:          ovhtypes.NewTfMapNestedType[ovhtypes.TfStringValue](ctx),
+			Computed:            true,
+			Description:         "Network openstack ids for each region",
+			MarkdownDescription: "Network openstack ids for each region",
+		},
 		"status": schema.StringAttribute{
 			CustomType:          ovhtypes.TfStringType{},
 			Computed:            true,
@@ -97,13 +103,14 @@ func CloudProjectNetworkPrivateDataSourceSchema(ctx context.Context) schema.Sche
 }
 
 type CloudProjectNetworkPrivateModel struct {
-	Name        ovhtypes.TfStringValue                   `tfsdk:"name" json:"name"`
-	NetworkId   ovhtypes.TfStringValue                   `tfsdk:"network_id" json:"id"`
-	Regions     ovhtypes.TfListNestedValue[RegionsValue] `tfsdk:"regions" json:"regions"`
-	ServiceName ovhtypes.TfStringValue                   `tfsdk:"service_name" json:"serviceName"`
-	Status      ovhtypes.TfStringValue                   `tfsdk:"status" json:"status"`
-	Type        ovhtypes.TfStringValue                   `tfsdk:"type" json:"type"`
-	VlanId      ovhtypes.TfInt64Value                    `tfsdk:"vlan_id" json:"vlanId"`
+	Name                ovhtypes.TfStringValue                            `tfsdk:"name" json:"name"`
+	NetworkId           ovhtypes.TfStringValue                            `tfsdk:"network_id" json:"id"`
+	Regions             ovhtypes.TfListNestedValue[RegionsValue]          `tfsdk:"regions" json:"regions"`
+	RegionsOpenstackIds ovhtypes.TfMapNestedValue[ovhtypes.TfStringValue] `tfsdk:"regions_openstack_ids" json:"regions_openstack_ids"`
+	ServiceName         ovhtypes.TfStringValue                            `tfsdk:"service_name" json:"serviceName"`
+	Status              ovhtypes.TfStringValue                            `tfsdk:"status" json:"status"`
+	Type                ovhtypes.TfStringValue                            `tfsdk:"type" json:"type"`
+	VlanId              ovhtypes.TfInt64Value                             `tfsdk:"vlan_id" json:"vlanId"`
 }
 
 func (v *CloudProjectNetworkPrivateModel) MergeWith(other *CloudProjectNetworkPrivateModel) {
@@ -136,6 +143,10 @@ func (v *CloudProjectNetworkPrivateModel) MergeWith(other *CloudProjectNetworkPr
 		v.VlanId = other.VlanId
 	}
 
+	if (v.RegionsOpenstackIds.IsUnknown() || v.RegionsOpenstackIds.IsNull()) && !other.RegionsOpenstackIds.IsUnknown() {
+		v.RegionsOpenstackIds = other.RegionsOpenstackIds
+	}
+
 }
 
 var _ basetypes.ObjectTypable = RegionsType{}
diff --git a/ovh/resource_cloud_project_network_private.go b/ovh/resource_cloud_project_network_private.go
index 8eb9b016..40fb8857 100644
--- a/ovh/resource_cloud_project_network_private.go
+++ b/ovh/resource_cloud_project_network_private.go
@@ -109,6 +109,11 @@ func resourceCloudProjectNetworkPrivate() *schema.Resource {
 				},
 				Set: RegionAttributesHash,
 			},
+			"regions_openstack_ids": {
+				Type:     schema.TypeMap,
+				Computed: true,
+				Elem:     schema.TypeString,
+			},
 			"status": {
 				Type:     schema.TypeString,
 				Computed: true,
@@ -189,6 +194,7 @@ func resourceCloudProjectNetworkPrivateRead(d *schema.ResourceData, meta interfa
 
 	regions_status := make([]map[string]interface{}, 0)
 	regions_attributes := make([]map[string]interface{}, 0)
+	regions_openstack_ids := map[string]string{}
 	regions := make([]string, 0)
 
 	for i := range r.Regions {
@@ -198,6 +204,8 @@ func resourceCloudProjectNetworkPrivateRead(d *schema.ResourceData, meta interfa
 		region_attributes["openstackid"] = r.Regions[i].OpenStackId
 		regions_attributes = append(regions_attributes, region_attributes)
 
+		regions_openstack_ids[r.Regions[i].Region] = r.Regions[i].OpenStackId
+
 		region_status := make(map[string]interface{})
 		region_status["region"] = r.Regions[i].Region
 		region_status["status"] = r.Regions[i].Status
@@ -206,6 +214,7 @@ func resourceCloudProjectNetworkPrivateRead(d *schema.ResourceData, meta interfa
 		regions = append(regions, fmt.Sprintf(r.Regions[i].Region))
 	}
 	d.Set("regions_attributes", regions_attributes)
+	d.Set("regions_openstack_ids", regions_openstack_ids)
 	d.Set("regions_status", regions_status)
 	d.Set("regions", regions)
 
diff --git a/templates/data-sources/cloud_project_network_private.md.tmpl b/templates/data-sources/cloud_project_network_private.md.tmpl
index d4c3f662..bf20eccd 100644
--- a/templates/data-sources/cloud_project_network_private.md.tmpl
+++ b/templates/data-sources/cloud_project_network_private.md.tmpl
@@ -36,3 +36,4 @@ The following attributes are exported:
   - `openstack_id`: Network ID on openstack region
   - `region`: Name of the region
   - `status`: Status of the network
+- `regions_openstack_ids` - A map with region name as key, and region-specific openstack id as value
diff --git a/templates/resources/cloud_project_network_private.md.tmpl b/templates/resources/cloud_project_network_private.md.tmpl
index 2448c743..b9f00019 100644
--- a/templates/resources/cloud_project_network_private.md.tmpl
+++ b/templates/resources/cloud_project_network_private.md.tmpl
@@ -42,6 +42,7 @@ The following attributes are exported:
 * `regions_status` - (Deprecated) A map representing the status of the network per region.
 * `regions_status/region` - (Deprecated) The id of the region.
 * `regions_status/status` - (Deprecated) The status of the network in the region.
+* `regions_openstack_ids` - A map with region name as key, and region-specific openstack id as value
 * `status` - the status of the network. should be normally set to 'ACTIVE'.
 * `type` - the type of the network. Either 'private' or 'public'.
 



Test patch:
diff --git a/ovh/data_cloud_project_network_private_test.go b/ovh/data_cloud_project_network_private_test.go
index 3d823484..52fc34ac 100644
--- a/ovh/data_cloud_project_network_private_test.go
+++ b/ovh/data_cloud_project_network_private_test.go
@@ -24,6 +24,8 @@ func TestAccDataSourceCloudProjectNetworkPrivate_basic(t *testing.T) {
 					resource.TestCheckResourceAttr("data.ovh_cloud_project_network_private.private", "service_name", os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST")),
 					resource.TestCheckResourceAttr("data.ovh_cloud_project_network_private.private", "network_id", os.Getenv("OVH_CLOUD_PROJECT_PRIVATE_NETWORK_TEST")),
 					resource.TestCheckResourceAttrSet("data.ovh_cloud_project_network_private.private", "name"),
+					resource.TestCheckResourceAttrSet("data.ovh_cloud_project_network_private.private", "regions.#"),
+					resource.TestCheckResourceAttrSet("data.ovh_cloud_project_network_private.private", "regions_openstack_ids.%"),
 				),
 			},
 		},
diff --git a/ovh/resource_cloud_project_network_private_test.go b/ovh/resource_cloud_project_network_private_test.go
index cfd9ae16..604291b9 100644
--- a/ovh/resource_cloud_project_network_private_test.go
+++ b/ovh/resource_cloud_project_network_private_test.go
@@ -14,7 +14,7 @@ func TestAccCloudProjectNetworkPrivate_basic(t *testing.T) {
       service_name = "%s"
 	  name         = "network_test"
 	  regions      = ["GRA11", "GRA9"]
-	  vlan_id      = 0
+	  vlan_id      = 11
 	}
 `, os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST"))
 
@@ -23,7 +23,7 @@ func TestAccCloudProjectNetworkPrivate_basic(t *testing.T) {
       service_name = "%s"
 	  name         = "network_test"
 	  regions      = ["GRA11"]
-	  vlan_id      = 0
+	  vlan_id      = 11
 	}
 `, os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST"))
 	resource.Test(t, resource.TestCase{
@@ -35,6 +35,8 @@ func TestAccCloudProjectNetworkPrivate_basic(t *testing.T) {
 				Check: resource.ComposeTestCheckFunc(
 					resource.TestCheckResourceAttr(
 						"ovh_cloud_project_network_private.testnetwork", "regions.#", "2"),
+					resource.TestCheckResourceAttr(
+						"ovh_cloud_project_network_private.testnetwork", "regions_openstack_ids.%", "2"),
 				),
 			},
 			{
@@ -42,6 +44,8 @@ func TestAccCloudProjectNetworkPrivate_basic(t *testing.T) {
 				Check: resource.ComposeTestCheckFunc(
 					resource.TestCheckResourceAttr(
 						"ovh_cloud_project_network_private.testnetwork", "regions.#", "1"),
+					resource.TestCheckResourceAttr(
+						"ovh_cloud_project_network_private.testnetwork", "regions_openstack_ids.%", "1"),
 				),
 			},
 		},