diff --git a/docs/data-sources/vpc_network_acls.md b/docs/data-sources/vpc_network_acls.md
new file mode 100644
index 000000000..093d796e5
--- /dev/null
+++ b/docs/data-sources/vpc_network_acls.md
@@ -0,0 +1,106 @@
+---
+subcategory: "Virtual Private Cloud (VPC)"
+---
+
+# hcso_vpc_network_acls
+
+Use this data source to get the list of VPC network ACLs within Huawei Cloud Stack Online.
+
+## Example Usage
+
+```hcl
+variable "network_acl_name" {}
+variable "enterprise_project_id" {}
+
+data "hcso_vpc_network_acls" "basic" {
+ name = var.network_acl_name
+ enterprise_project_id = var.enterprise_project_id
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `region` - (Optional, String, ForceNew) Specifies the region in which to obtain the network ACLs.
+ If omitted, the provider-level region will be used.
+
+* `name` - (Optional, String) Specifies the network ACL name. The value can contain no more than 64 characters,
+ including letters, digits, underscores (_), hyphens (-), and periods (.).
+
+* `network_acl_id` - (Optional, String) Specifies the network ACL ID.
+
+* `enterprise_project_id` - (Optional, String) Specifies the enterprise project ID of the network ACL.
+
+* `enabled` - (Optional, String) Specifies whether the network ACL is enabled. The value can be **true** or **false**.
+
+* `status` - (Optional, String) Specifies the status of the network ACL.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `id` - The data source ID in uuid format.
+
+* `network_acls` - The list of VPC network ACLs.
+ The [network_acls](#network_acls) structure is documented below.
+
+
+The `network_acls` block supports:
+
+* `name` - The network ACL name.
+
+* `id` - The network ACL ID.
+
+* `enterprise_project_id` - The enterprise project ID of the network ACL.
+
+* `description` - The network ACL description.
+
+* `enabled` - Whether the network ACL is enabled.
+
+* `ingress_rules` - The ingress rules of the network ACL.
+ The [rules](#rules) structure is documented below.
+
+* `egress_rules` - The egress rules of the network ACL.
+ The [rules](#rules) structure is documented below.
+
+* `associated_subnets` - The associated subnets of the network ACL.
+ The [associated_subnets](#subnets) structure is documented below.
+
+* `status` - The status of the ACL.
+
+* `created_at` - The created time of the ACL.
+
+* `updated_at` - The updated time of the ACL.
+
+
+The `ingress_rules` and `egress_rules` block supports:
+
+* `rule_id` - The ID of the rule.
+
+* `action` - The rule action.
+
+* `protocol` - The rule protocol.
+
+* `ip_version` - The IP version of a network ACL rule.
+
+* `name` - The network ACL rule name.
+
+* `description` - The network ACL rule description.
+
+* `source_ip_address` - The source IP address or CIDR block of a network ACL rule.
+
+* `source_ip_address_group_id` - The source IP address group ID of a network ACL rule.
+
+* `source_port` - The source ports of a network ACL rule.
+
+* `destination_ip_address` - The destination IP address or CIDR block of a network ACL rule.
+
+* `destination_ip_address_group_id` - The destination IP address group ID of a network ACL rule.
+
+* `destination_port` - The destination ports of a network ACL rule.
+
+
+The `associated_subnets` block supports:
+
+* `subnet_id` - The ID of the subnet to associate with the network ACL.
diff --git a/docs/data-sources/vpc_route_table.md b/docs/data-sources/vpc_route_table.md
new file mode 100644
index 000000000..b3f2f7ff6
--- /dev/null
+++ b/docs/data-sources/vpc_route_table.md
@@ -0,0 +1,57 @@
+---
+subcategory: "Virtual Private Cloud (VPC)"
+---
+
+# hcso_vpc_route_table
+
+Provides details about a specific VPC route table within Huawei Cloud Stack Online.
+
+## Example Usage
+
+```hcl
+variable "vpc_id" {}
+
+# get the default route table
+data "hcso_vpc_route_table" "default" {
+ vpc_id = var.vpc_id
+}
+
+# get a custom route table
+data "hcso_vpc_route_table" "custom" {
+ vpc_id = var.vpc_id
+ name = "demo"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `region` - (Optional, String) The region in which to query the vpc route table.
+ If omitted, the provider-level region will be used.
+
+* `vpc_id` - (Required, String) Specifies the VPC ID where the route table resides.
+
+* `name` - (Optional, String) Specifies the name of the route table.
+
+* `id` - (Optional, String) Specifies the ID of the route table.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `default` - Whether the route table is default or not.
+
+* `description` - The supplementary information about the route table.
+
+* `subnets` - An array of one or more subnets associating with the route table.
+
+* `route` - The route object list. The [route object](#route_object) is documented below.
+
+
+The `route` block supports:
+
+* `type` - The route type.
+* `destination` - The destination address in the CIDR notation format
+* `nexthop` - The next hop.
+* `description` - The description about the route.
diff --git a/docs/data-sources/vpc_routes.md b/docs/data-sources/vpc_routes.md
new file mode 100644
index 000000000..bc2588cc2
--- /dev/null
+++ b/docs/data-sources/vpc_routes.md
@@ -0,0 +1,51 @@
+---
+subcategory: "Virtual Private Cloud (VPC)"
+---
+
+# hcso_vpc_routes
+
+Use this data source to get the list of VPC routes.
+
+## Example Usage
+
+```hcl
+data "hcso_vpc_routes" "test" {
+ type = "peering"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `region` - (Optional, String) Specifies the region in which to query the resource.
+ If omitted, the provider-level region will be used.
+
+* `type` - (Optional, String) Specifies the route type.
+
+* `vpc_id` - (Optional, String) Specifies the ID of the VPC to which the route belongs.
+
+* `destination` - (Optional, String) Specifies the route destination.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `id` - The data source ID.
+
+* `routes` - The list of routes.
+
+ The [routes](#routes_struct) structure is documented below.
+
+
+The `routes` block supports:
+
+* `id` - The route ID.
+
+* `type` - The route type.
+
+* `vpc_id` - The ID of the VPC to which the route belongs.
+
+* `destination` - The route destination.
+
+* `nexthop` - The next hop of the route.
diff --git a/docs/resources/vpc_network_acl.md b/docs/resources/vpc_network_acl.md
new file mode 100644
index 000000000..1712cc55e
--- /dev/null
+++ b/docs/resources/vpc_network_acl.md
@@ -0,0 +1,166 @@
+---
+subcategory: "Virtual Private Cloud (VPC)"
+---
+
+# hcso_vpc_network_acl
+
+Manages a VPC network ACL resource within Huawei Cloud Stack Online.
+
+## Example Usage
+
+```hcl
+variable "name" {}
+variable "subnet_id_1" {}
+variable "subnet_id_2" {}
+
+resource "hcso_vpc_network_acl" "test" {
+ name = var.name
+ description = "created by terraform"
+ enterprise_project_id = 0
+ enabled = true
+
+ ingress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "192.168.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ ingress_rules {
+ action = "deny"
+ ip_version = 4
+ protocol = "icmp"
+ source_ip_address = "192.168.0.0/24"
+ destination_ip_address = "0.0.0.0/0"
+ }
+
+ egress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "172.16.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ egress_rules {
+ action = "deny"
+ ip_version = 4
+ protocol = "icmp"
+ source_ip_address = "172.16.0.0/24"
+ destination_ip_address = "0.0.0.0/0"
+ }
+
+ associated_subnets {
+ subnet_id = var.subnet_id_1
+ }
+
+ associated_subnets {
+ subnet_id = var.subnet_id_2
+ }
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `region` - (Optional, String, ForceNew) Specifies the region in which to create the resource.
+ If omitted, the provider-level region will be used.
+ Changing this creates a new resource.
+
+* `name` - (Required, String) Specifies the network ACL name. The value can contain no more than 64 characters,
+ including letters, digits, underscores (_), hyphens (-), and periods (.).
+
+* `enterprise_project_id` - (Required, String) Specifies the enterprise project ID of the network ACL.
+
+* `description` - (Optional, String) Specifies the network ACL description. The value can contain no more
+ than 255 characters and cannot contain angle brackets (< or >).
+
+* `enabled` - (Optional, Bool) Specifies whether the network ACL is enabled. The default value is **true**.
+
+* `ingress_rules` - (Optional, List) Specifies the ingress rules of the network ACL.
+ The [rules](#rules) structure is documented below.
+
+* `egress_rules` - (Optional, List) Specifies the egress rules of the network ACL.
+ The [rules](#rules) structure is documented below.
+
+* `associated_subnets` - (Optional, List) Specifies the associated subnets of the network ACL.
+ The [associated_subnets](#subnets) structure is documented below.
+
+* `tags` - (Optional, Map) Specifies the key/value pairs to associate with the network ACL.
+
+
+The `ingress_rules` and `egress_rules` block supports:
+
+* `action` - (Required, String) Specifies the rule action. The value can be: **allow** and **deny**.
+
+* `protocol` - (Required, String) Specifies the rule protocol The value can be **tcp**, **udp**, **icmp**, **icmpv6**,
+ or an IP protocol number (0–255). The value **any** indicates all protocols.
+
+* `ip_version` - (Required, Int) Specifies the IP version of a network ACL rule.
+ The value can be **4** (IPv4) and **6** (IPv6).
+
+* `name` - (Optional, String) Specifies the network ACL rule name. The value can contain no more than 64 characters,
+ including letters, digits, underscores (_), hyphens (-), and periods (.).
+
+* `description` - (Optional, String) Specifies the network ACL rule description. The value can contain no more
+ than 255 characters. The value cannot contain angle brackets (< or >).
+
+* `source_ip_address` - (Optional, String) Specifies the source IP address or CIDR block of a network ACL rule.
+ The `source_ip_address` and `source_address_group_id` cannot be configured at the same time.
+
+* `source_ip_address_group_id` - (Optional, String) Specifies the source IP address group ID of a network ACL rule.
+ `source_ip_address` and `source_address_group_id` cannot be configured at the same time.
+
+* `source_port` - (Optional, String) Specifies the source ports of a network ACL rule.
+ You can specify a single port or a port range. Separate every two entries with a comma.
+
+* `destination_ip_address` - (Optional, String) Specifies the destination IP address or CIDR block of a network ACL rule.
+ The `destination_ip_address` and `destination_address_group_id` cannot be configured at the same time.
+
+* `destination_ip_address_group_id` - (Optional, String) Specifies the destination IP address group ID of a network ACL rule.
+ The `destination_ip_address` and `destination_address_group_id` cannot be configured at the same time.
+
+* `destination_port` - (Optional, String) Specifies the destination ports of a network ACL rule.
+ You can specify a single port or a port range. Separate every two entries with a comma.
+
+
+The `associated_subnets` block supports:
+
+* `subnet_id` - (Required, String) Specifies the ID of the subnet to associate with the network ACL.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `id` - The resource ID in uuid format.
+
+* `status` - The status of the ACL.
+
+* `created_at` - The created time of the ACL.
+
+* `updated_at` - The updated time of the ACL.
+
+* `ingress_rules` - The ingress rules of the network ACL.
+ The [rules](#rules_resp) structure is documented below.
+
+* `egress_rules` - The egress rules of the network ACL.
+ The [rules](#rules_resp) structure is documented below.
+
+
+The `ingress_rules` and `egress_rules` block supports:
+
+* `rule_id` - The ID of the rule.
+
+## Import
+
+The network ACL can be imported using `id`, e.g.
+
+```bash
+$ terraform import hcso_vpc_network_acl.test
+```
diff --git a/docs/resources/vpc_route.md b/docs/resources/vpc_route.md
new file mode 100644
index 000000000..40e289ed8
--- /dev/null
+++ b/docs/resources/vpc_route.md
@@ -0,0 +1,98 @@
+---
+subcategory: "Virtual Private Cloud (VPC)"
+---
+
+# hcso_vpc_route
+
+Manages a VPC route resource within Huawei Cloud Stack Online.
+
+## Example Usage
+
+### Add route to the default route table
+
+```hcl
+variable "vpc_id" {}
+variable "nexthop" {}
+
+resource "hcso_vpc_route" "vpc_route" {
+ vpc_id = var.vpc_id
+ destination = "192.168.0.0/16"
+ type = "peering"
+ nexthop = var.nexthop
+}
+```
+
+### Add route to a custom route table
+
+```hcl
+variable "vpc_id" {}
+variable "nexthop" {}
+
+data "hcso_vpc_route_table" "rtb" {
+ vpc_id = var.vpc_id
+ name = "demo"
+}
+
+resource "hcso_vpc_route" "vpc_route" {
+ vpc_id = var.vpc_id
+ route_table_id = data.hcso_vpc_route_table.rtb.id
+ destination = "172.16.8.0/24"
+ type = "ecs"
+ nexthop = var.nexthop
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `region` - (Optional, String, ForceNew) The region in which to create the VPC route. If omitted, the provider-level
+ region will be used. Changing this creates a new resource.
+
+* `vpc_id` - (Required, String, ForceNew) Specifies the VPC for which a route is to be added. Changing this creates a
+ new resource.
+
+* `destination` - (Required, String, ForceNew) Specifies the destination address in the CIDR notation format,
+ for example, 192.168.200.0/24. The destination of each route must be unique and cannot overlap with any
+ subnet in the VPC. Changing this creates a new resource.
+
+* `type` - (Required, String) Specifies the route type. Currently, the value can be:
+ **ecs**, **eni**, **vip**, **nat**, **peering**, **vpn** and **dc**.
+
+* `nexthop` - (Required, String) Specifies the next hop.
+ + If the route type is **ecs**, the value is an ECS instance ID in the VPC.
+ + If the route type is **eni**, the value is the extension NIC of an ECS in the VPC.
+ + If the route type is **vip**, the value is a virtual IP address.
+ + If the route type is **nat**, the value is a VPN gateway ID.
+ + If the route type is **peering**, the value is a VPC peering connection ID.
+ + If the route type is **vpn**, the value is a VPN gateway ID.
+ + If the route type is **dc**, the value is a Direct Connect gateway ID.
+
+* `description` - (Optional, String) Specifies the supplementary information about the route.
+ The value is a string of no more than 255 characters and cannot contain angle brackets (< or >).
+
+* `route_table_id` - (Optional, String, ForceNew) Specifies the route table ID for which a route is to be added.
+ If the value is not set, the route will be added to the *default* route table.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `id` - The route ID, the format is `/`
+
+* `route_table_name` - The name of route table.
+
+## Timeouts
+
+This resource provides the following timeouts configuration options:
+
+* `create` - Default is 10 minute.
+* `delete` - Default is 10 minute.
+
+## Import
+
+VPC routes can be imported using the route table ID and their `destination` separated by a slash, e.g.
+
+```
+$ terraform import hcso_vpc_route.test /
+```
diff --git a/docs/resources/vpc_route_table.md b/docs/resources/vpc_route_table.md
new file mode 100644
index 000000000..193739000
--- /dev/null
+++ b/docs/resources/vpc_route_table.md
@@ -0,0 +1,125 @@
+---
+subcategory: "Virtual Private Cloud (VPC)"
+---
+
+# hcso_vpc_route_table
+
+Manages a VPC custom route table resource within Huawei Cloud Stack Online.
+
+-> **NOTE:** To use a custom route table, you need to submit a service ticket to increase quota.
+
+## Example Usage
+
+### Basic Custom Route Table
+
+```hcl
+variable "vpc_id" {}
+variable "vpc_peering_id" {}
+
+resource "hcso_vpc_route_table" "demo" {
+ name = "demo"
+ vpc_id = var.vpc_id
+ description = "a custom route table demo"
+
+ route {
+ destination = "172.16.0.0/16"
+ type = "peering"
+ nexthop = var.vpc_peering_id
+ }
+}
+```
+
+### Associating Subnets with a Route Table
+
+```hcl
+variable "vpc_id" {}
+variable "vpc_peering_id" {}
+
+data "hcso_vpc_subnet_ids" "subnet_ids" {
+ vpc_id = var.vpc_id
+}
+
+resource "hcso_vpc_route_table" "demo" {
+ name = "demo"
+ vpc_id = var.vpc_id
+ subnets = data.hcso_vpc_subnet_ids.subnet_ids.ids
+
+ route {
+ destination = "172.16.0.0/16"
+ type = "peering"
+ nexthop = var.vpc_peering_id
+ }
+ route {
+ destination = "192.168.100.0/24"
+ type = "vip"
+ nexthop = "192.168.10.200"
+ }
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `region` - (Optional, String, ForceNew) The region in which to create the vpc route table.
+ If omitted, the provider-level region will be used. Changing this creates a new resource.
+
+* `vpc_id` - (Required, String, ForceNew) Specifies the VPC ID for which a route table is to be added.
+ Changing this creates a new resource.
+
+* `name` - (Required, String) Specifies the route table name. The value is a string of no more than
+ 64 characters that can contain letters, digits, underscores (_), hyphens (-), and periods (.).
+
+* `description` - (Optional, String) Specifies the supplementary information about the route table.
+ The value is a string of no more than 255 characters and cannot contain angle brackets (< or >).
+
+* `subnets` - (Optional, List) Specifies an array of one or more subnets associating with the route table.
+
+ -> **NOTE:** The custom route table associated with a subnet affects only the outbound traffic.
+ The default route table determines the inbound traffic.
+
+* `route` - (Optional, List) Specifies the route object list. The [route object](#route_object)
+ is documented below.
+
+
+The `route` block supports:
+
+* `destination` - (Required, String) Specifies the destination address in the CIDR notation format,
+ for example, 192.168.200.0/24. The destination of each route must be unique and cannot overlap
+ with any subnet in the VPC.
+
+* `type` - (Required, String) Specifies the route type. Currently, the value can be:
+ **ecs**, **eni**, **vip**, **nat**, **peering**, **vpn** and **dc**.
+
+* `nexthop` - (Required, String) Specifies the next hop.
+ + If the route type is **ecs**, the value is an ECS instance ID in the VPC.
+ + If the route type is **eni**, the value is the extension NIC of an ECS in the VPC.
+ + If the route type is **vip**, the value is a virtual IP address.
+ + If the route type is **nat**, the value is a VPN gateway ID.
+ + If the route type is **peering**, the value is a VPC peering connection ID.
+ + If the route type is **vpn**, the value is a VPN gateway ID.
+ + If the route type is **dc**, the value is a Direct Connect gateway ID.
+
+* `description` - (Optional, String) Specifies the supplementary information about the route.
+ The value is a string of no more than 255 characters and cannot contain angle brackets (< or >).
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `id` - The resource ID in UUID format.
+
+## Timeouts
+
+This resource provides the following timeouts configuration options:
+
+* `create` - Default is 10 minutes.
+* `delete` - Default is 10 minutes.
+
+## Import
+
+vpc route tables can be imported using the `id`, e.g.
+
+```
+$ terraform import hcso_vpc_route_table.demo e1b3208a-544b-42a7-84e6-5d70371dd982
+```
diff --git a/internal/provider.go b/internal/provider.go
index 29d722556..ad95c1543 100644
--- a/internal/provider.go
+++ b/internal/provider.go
@@ -38,6 +38,7 @@ import (
"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/vpc"
myconfig "github.com/huaweicloud/terraform-provider-hcso/internal/config"
+ myvpc "github.com/huaweicloud/terraform-provider-hcso/internal/services/vpc"
)
const (
@@ -381,6 +382,9 @@ func Provider() *schema.Provider {
"hcso_vpcs": vpc.DataSourceVpcs(),
"hcso_vpc_subnet_ids": vpc.DataSourceVpcSubnetIdsV1(),
"hcso_vpc_subnets": vpc.DataSourceVpcSubnets(),
+ "hcso_vpc_network_acls": myvpc.DataSourceNetworkAcls(),
+ "hcso_vpc_route_table": vpc.DataSourceVPCRouteTable(),
+ "hcso_vpc_routes": myvpc.DataSourceVpcRoutes(),
"hcso_networking_port": vpc.DataSourceNetworkingPortV2(),
"hcso_vpc_peering_connection": vpc.DataSourceVpcPeeringConnectionV2(),
"hcso_networking_secgroups": vpc.DataSourceNetworkingSecGroups(),
@@ -489,6 +493,9 @@ func Provider() *schema.Provider {
"hcso_vpc_subnet": vpc.ResourceVpcSubnetV1(),
"hcso_vpc_peering_connection": vpc.ResourceVpcPeeringConnectionV2(),
"hcso_vpc_peering_connection_accepter": vpc.ResourceVpcPeeringConnectionAccepterV2(),
+ "hcso_vpc_network_acl": myvpc.ResourceNetworkAcl(),
+ "hcso_vpc_route_table": vpc.ResourceVPCRouteTable(),
+ "hcso_vpc_route": vpc.ResourceVPCRouteTableRoute(),
"hcso_networking_secgroup": vpc.ResourceNetworkingSecGroup(),
"hcso_networking_secgroup_rule": vpc.ResourceNetworkingSecGroupRule(),
"hcso_networking_vip": vpc.ResourceNetworkingVip(),
diff --git a/internal/services/acceptance/vpc/data_source_huaweicloud_vpc_network_acls_test.go b/internal/services/acceptance/vpc/data_source_huaweicloud_vpc_network_acls_test.go
new file mode 100644
index 000000000..fbfe98333
--- /dev/null
+++ b/internal/services/acceptance/vpc/data_source_huaweicloud_vpc_network_acls_test.go
@@ -0,0 +1,127 @@
+package vpc
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+ "github.com/huaweicloud/terraform-provider-hcso/internal/services/acceptance"
+)
+
+func TestAccNetworkAclsDataSource_basic(t *testing.T) {
+ var (
+ name = acceptance.RandomAccResourceNameWithDash()
+ dataSource1 = "data.hcso_vpc_network_acls.basic"
+ dataSource2 = "data.hcso_vpc_network_acls.filter_by_name"
+ dataSource3 = "data.hcso_vpc_network_acls.filter_by_id"
+ dataSource4 = "data.hcso_vpc_network_acls.filter_by_eps_id"
+ dataSource5 = "data.hcso_vpc_network_acls.filter_by_enabled"
+ dataSource6 = "data.hcso_vpc_network_acls.filter_by_status"
+ dc1 = acceptance.InitDataSourceCheck(dataSource1)
+ dc2 = acceptance.InitDataSourceCheck(dataSource2)
+ dc3 = acceptance.InitDataSourceCheck(dataSource3)
+ dc4 = acceptance.InitDataSourceCheck(dataSource4)
+ dc5 = acceptance.InitDataSourceCheck(dataSource5)
+ dc6 = acceptance.InitDataSourceCheck(dataSource6)
+ )
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { acceptance.TestAccPreCheck(t) },
+ ProviderFactories: acceptance.TestAccProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccNetworkAclsDataSource_basic(name),
+ Check: resource.ComposeTestCheckFunc(
+ dc1.CheckResourceExists(),
+ dc2.CheckResourceExists(),
+ dc3.CheckResourceExists(),
+ dc4.CheckResourceExists(),
+ dc5.CheckResourceExists(),
+ dc6.CheckResourceExists(),
+ resource.TestCheckOutput("is_results_not_empty", "true"),
+ resource.TestCheckOutput("is_name_filter_useful", "true"),
+ resource.TestCheckOutput("is_id_filter_useful", "true"),
+ resource.TestCheckOutput("is_eps_id_filter_useful", "true"),
+ resource.TestCheckOutput("is_enabled_filter_useful", "true"),
+ resource.TestCheckOutput("is_status_filter_useful", "true"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccNetworkAclsDataSource_basic(name string) string {
+ return fmt.Sprintf(`
+%[1]s
+
+data "hcso_vpc_network_acls" "basic" {
+ depends_on = [hcso_vpc_network_acl.test]
+}
+
+data "hcso_vpc_network_acls" "filter_by_name" {
+ name = "%[2]s"
+
+ depends_on = [hcso_vpc_network_acl.test]
+}
+
+data "hcso_vpc_network_acls" "filter_by_id" {
+ network_acl_id = hcso_vpc_network_acl.test.id
+
+ depends_on = [hcso_vpc_network_acl.test]
+}
+
+data "hcso_vpc_network_acls" "filter_by_eps_id" {
+ enterprise_project_id = "0"
+
+ depends_on = [hcso_vpc_network_acl.test]
+}
+
+data "hcso_vpc_network_acls" "filter_by_enabled" {
+ enabled = "true"
+
+ depends_on = [hcso_vpc_network_acl.test]
+}
+
+data "hcso_vpc_network_acls" "filter_by_status" {
+ status = "INACTIVE"
+
+ depends_on = [hcso_vpc_network_acl.test]
+}
+
+locals {
+ name_filter_result = [for v in data.hcso_vpc_network_acls.filter_by_name.network_acls[*].name : v == "%[2]s"]
+ id_filter_result = [
+ for v in data.hcso_vpc_network_acls.filter_by_name.network_acls[*].id : v == hcso_vpc_network_acl.test.id
+ ]
+ eps_id_filter_result = [for v in data.hcso_vpc_network_acls.filter_by_eps_id.network_acls[*].enterprise_project_id : v == "0"]
+ enabled_filter_result = [for v in data.hcso_vpc_network_acls.filter_by_enabled.network_acls[*].enabled : v == true]
+ status_filter_result = [for v in data.hcso_vpc_network_acls.filter_by_status.network_acls[*].status : v == "INACTIVE"]
+
+}
+
+output "is_results_not_empty" {
+ value = length(data.hcso_vpc_network_acls.basic.network_acls) > 0
+}
+
+output "is_name_filter_useful" {
+ value = alltrue(local.name_filter_result) && length(local.name_filter_result) > 0
+}
+
+output "is_id_filter_useful" {
+ value = alltrue(local.id_filter_result) && length(local.id_filter_result) > 0
+}
+
+output "is_eps_id_filter_useful" {
+ value = alltrue(local.eps_id_filter_result) && length(local.eps_id_filter_result) > 0
+}
+
+output "is_enabled_filter_useful" {
+ value = alltrue(local.enabled_filter_result) && length(local.enabled_filter_result) > 0
+}
+
+output "is_status_filter_useful" {
+ value = alltrue(local.status_filter_result) && length(local.status_filter_result) > 0
+}
+`, testAccNetworkAcl_basic(name), name)
+}
diff --git a/internal/services/acceptance/vpc/data_source_huaweicloud_vpc_routes_test.go b/internal/services/acceptance/vpc/data_source_huaweicloud_vpc_routes_test.go
new file mode 100644
index 000000000..56b3e690d
--- /dev/null
+++ b/internal/services/acceptance/vpc/data_source_huaweicloud_vpc_routes_test.go
@@ -0,0 +1,98 @@
+package vpc
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+ "github.com/huaweicloud/terraform-provider-hcso/internal/services/acceptance"
+)
+
+func TestAccDataSourceVpcRoutes_basic(t *testing.T) {
+ rName := acceptance.RandomAccResourceName()
+ dataSource1 := "data.hcso_vpc_routes.basic"
+ dataSource2 := "data.hcso_vpc_routes.filter_by_type"
+ dataSource3 := "data.hcso_vpc_routes.filter_by_vpc_id"
+ dataSource4 := "data.hcso_vpc_routes.filter_by_destination"
+ dc1 := acceptance.InitDataSourceCheck(dataSource1)
+ dc2 := acceptance.InitDataSourceCheck(dataSource2)
+ dc3 := acceptance.InitDataSourceCheck(dataSource3)
+ dc4 := acceptance.InitDataSourceCheck(dataSource4)
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() {
+ acceptance.TestAccPreCheck(t)
+ },
+ ProviderFactories: acceptance.TestAccProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testDataSourceDataSourceVpcRoutes_basic(rName),
+ Check: resource.ComposeTestCheckFunc(
+ dc1.CheckResourceExists(),
+ dc2.CheckResourceExists(),
+ dc3.CheckResourceExists(),
+ dc4.CheckResourceExists(),
+ resource.TestCheckOutput("is_results_not_empty", "true"),
+ resource.TestCheckOutput("is_type_filter_useful", "true"),
+ resource.TestCheckOutput("is_vpc_id_filter_useful", "true"),
+ resource.TestCheckOutput("is_destination_filter_useful", "true"),
+ ),
+ },
+ },
+ })
+}
+
+func testDataSourceDataSourceVpcRoutes_basic(name string) string {
+ return fmt.Sprintf(`
+%s
+
+data "hcso_vpc_routes" "basic" {
+ depends_on = [hcso_vpc_route.test]
+}
+
+data "hcso_vpc_routes" "filter_by_type" {
+ type = "peering"
+
+ depends_on = [hcso_vpc_route.test]
+}
+
+data "hcso_vpc_routes" "filter_by_vpc_id" {
+ vpc_id = hcso_vpc.test1.id
+
+ depends_on = [hcso_vpc_route.test]
+}
+
+data "hcso_vpc_routes" "filter_by_destination" {
+ destination = hcso_vpc.test2.cidr
+
+ depends_on = [hcso_vpc_route.test]
+}
+
+locals {
+ type_filter_result = [for v in data.hcso_vpc_routes.filter_by_type.routes[*].type : v == "peering"]
+ vpc_id_filter_result = [
+ for v in data.hcso_vpc_routes.filter_by_vpc_id.routes[*].vpc_id : v == hcso_vpc.test1.id
+ ]
+ destination_filter_result = [
+ for v in data.hcso_vpc_routes.filter_by_destination.routes[*].destination : v == hcso_vpc.test2.cidr
+ ]
+}
+
+output "is_results_not_empty" {
+ value = length(data.hcso_vpc_routes.basic.routes) > 0
+}
+
+output "is_type_filter_useful" {
+ value = alltrue(local.type_filter_result) && length(local.type_filter_result) > 0
+}
+
+output "is_vpc_id_filter_useful" {
+ value = alltrue(local.vpc_id_filter_result) && length(local.vpc_id_filter_result) > 0
+}
+
+output "is_destination_filter_useful" {
+ value = alltrue(local.destination_filter_result) && length(local.destination_filter_result) > 0
+}
+`, testAccRouteDataSource_base(name))
+}
diff --git a/internal/services/acceptance/vpc/resource_huaweicloud_vpc_network_acl_test.go b/internal/services/acceptance/vpc/resource_huaweicloud_vpc_network_acl_test.go
new file mode 100644
index 000000000..7a50c49ab
--- /dev/null
+++ b/internal/services/acceptance/vpc/resource_huaweicloud_vpc_network_acl_test.go
@@ -0,0 +1,351 @@
+package vpc
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+ "github.com/chnsz/golangsdk"
+
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils"
+
+ "github.com/huaweicloud/terraform-provider-hcso/internal/services/acceptance"
+)
+
+func getNetworkAclResourceFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) {
+ region := acceptance.HCSO_REGION_NAME
+ client, err := cfg.NewServiceClient("vpcv3", region)
+ if err != nil {
+ return nil, fmt.Errorf("error creating VPC v3 client: %s", err)
+ }
+
+ getNetworkAclHttpUrl := "v3/{project_id}/vpc/firewalls/" + state.Primary.ID
+ getNetworkAclPath := client.Endpoint + getNetworkAclHttpUrl
+ getNetworkAclPath = strings.ReplaceAll(getNetworkAclPath, "{project_id}", client.ProjectID)
+
+ getNetworkAclOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ getNetworkAclResp, err := client.Request("GET", getNetworkAclPath, &getNetworkAclOpt)
+ if err != nil {
+ return nil, fmt.Errorf("error retrieving VPC network ACL: %s", err)
+ }
+
+ return utils.FlattenResponse(getNetworkAclResp)
+}
+
+func TestAccNetworkAcl_basic(t *testing.T) {
+ var (
+ networkAcl interface{}
+ name = acceptance.RandomAccResourceNameWithDash()
+ resourceName = "hcso_vpc_network_acl.test"
+
+ rc = acceptance.InitResourceCheck(
+ resourceName,
+ &networkAcl,
+ getNetworkAclResourceFunc,
+ )
+ )
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { acceptance.TestAccPreCheck(t) },
+ ProviderFactories: acceptance.TestAccProviderFactories,
+ CheckDestroy: rc.CheckResourceDestroy(),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccNetworkAcl_basic(name),
+ Check: resource.ComposeTestCheckFunc(
+ rc.CheckResourceExists(),
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttr(resourceName, "description", "created by terraform"),
+ resource.TestCheckResourceAttr(resourceName, "enabled", "true"),
+
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.action", "allow"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.ip_version", "4"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.protocol", "tcp"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.source_ip_address", "192.168.0.0/24"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.source_port", "22-30,33"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.destination_ip_address", "0.0.0.0/0"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.destination_port", "8001-8010"),
+
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.action", "allow"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.ip_version", "4"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.protocol", "tcp"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.source_ip_address", "172.16.0.0/24"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.source_port", "22-30,33"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.destination_ip_address", "0.0.0.0/0"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.destination_port", "8001-8010"),
+
+ resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar"),
+ resource.TestCheckResourceAttr(resourceName, "tags.key", "value"),
+
+ resource.TestCheckOutput("is_associated_subnets_different", "false"),
+
+ resource.TestCheckResourceAttrSet(resourceName, "created_at"),
+ resource.TestCheckResourceAttrSet(resourceName, "updated_at"),
+ ),
+ },
+ {
+ Config: testAccNetworkAcl_update(name),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "name", name+"-update"),
+ resource.TestCheckResourceAttr(resourceName, "description", "created by terraform update"),
+ resource.TestCheckResourceAttr(resourceName, "enabled", "false"),
+
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.action", "allow"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.ip_version", "4"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.protocol", "tcp"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.source_ip_address", "192.168.0.0/24"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.source_port", "22-30,33"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.destination_ip_address", "0.0.0.0/0"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.0.destination_port", "8001-8010"),
+
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.1.action", "deny"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.1.ip_version", "4"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.1.protocol", "icmp"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.1.source_ip_address", "192.168.0.0/24"),
+ resource.TestCheckResourceAttr(resourceName, "ingress_rules.1.destination_ip_address", "0.0.0.0/0"),
+
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.action", "allow"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.ip_version", "4"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.protocol", "tcp"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.source_ip_address", "172.16.0.0/24"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.source_port", "22-30,33"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.destination_ip_address", "0.0.0.0/0"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.0.destination_port", "8001-8010"),
+
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.1.action", "deny"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.1.ip_version", "4"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.1.protocol", "icmp"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.1.source_ip_address", "172.16.0.0/24"),
+ resource.TestCheckResourceAttr(resourceName, "egress_rules.1.destination_ip_address", "0.0.0.0/0"),
+
+ resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar_update"),
+ resource.TestCheckResourceAttr(resourceName, "tags.key_update", "value_update"),
+
+ resource.TestCheckOutput("is_associated_subnets_different", "false"),
+
+ resource.TestCheckResourceAttrSet(resourceName, "created_at"),
+ resource.TestCheckResourceAttrSet(resourceName, "updated_at"),
+ ),
+ },
+ {
+ Config: testAccNetworkAcl_import(name),
+ ResourceName: resourceName,
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ },
+ })
+}
+
+func testAccNetworkAcl_base(name string) string {
+ return fmt.Sprintf(`
+resource "hcso_vpc" "test" {
+ name = "%s"
+ cidr = "192.168.0.0/16"
+}
+`, name)
+}
+
+func testAccNetworkAcl_basic(name string) string {
+ return fmt.Sprintf(`
+%s
+
+resource "hcso_vpc_subnet" "test" {
+ count = 1
+ name = "%s-${count.index}"
+ cidr = cidrsubnet(hcso_vpc.test.cidr, 8, count.index)
+ gateway_ip = cidrhost(cidrsubnet(hcso_vpc.test.cidr, 8, count.index), 1)
+ vpc_id = hcso_vpc.test.id
+}
+
+resource "hcso_vpc_network_acl" "test" {
+ name = "%s"
+ description = "created by terraform"
+ enterprise_project_id = 0
+
+ ingress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "192.168.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ egress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "172.16.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ associated_subnets {
+ subnet_id = hcso_vpc_subnet.test[0].id
+ }
+
+ tags = {
+ foo = "bar"
+ key = "value"
+ }
+}
+
+output "is_associated_subnets_different" {
+ value = length(setsubtract(hcso_vpc_network_acl.test.associated_subnets[*].subnet_id,
+ hcso_vpc_subnet.test[*].id)) != 0
+}
+`, testAccNetworkAcl_base(name), name, name)
+}
+
+func testAccNetworkAcl_update(name string) string {
+ return fmt.Sprintf(`
+%s
+
+resource "hcso_vpc_subnet" "test" {
+ count = 2
+ name = "%s-${count.index}"
+ cidr = cidrsubnet(hcso_vpc.test.cidr, 8, count.index)
+ gateway_ip = cidrhost(cidrsubnet(hcso_vpc.test.cidr, 8, count.index), 1)
+ vpc_id = hcso_vpc.test.id
+}
+
+resource "hcso_vpc_network_acl" "test" {
+ name = "%s-update"
+ description = "created by terraform update"
+ enterprise_project_id = 0
+ enabled = false
+
+ ingress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "192.168.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ ingress_rules {
+ action = "deny"
+ ip_version = 4
+ protocol = "icmp"
+ source_ip_address = "192.168.0.0/24"
+ destination_ip_address = "0.0.0.0/0"
+ }
+
+ egress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "172.16.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ egress_rules {
+ action = "deny"
+ ip_version = 4
+ protocol = "icmp"
+ source_ip_address = "172.16.0.0/24"
+ destination_ip_address = "0.0.0.0/0"
+ }
+
+ associated_subnets {
+ subnet_id = hcso_vpc_subnet.test[0].id
+ }
+
+ associated_subnets {
+ subnet_id = hcso_vpc_subnet.test[1].id
+ }
+
+ tags = {
+ foo = "bar_update"
+ key_update = "value_update"
+ }
+}
+
+output "is_associated_subnets_different" {
+ value = length(setsubtract(hcso_vpc_network_acl.test.associated_subnets[*].subnet_id,
+ hcso_vpc_subnet.test[*].id)) != 0
+}
+`, testAccNetworkAcl_base(name), name, name)
+}
+
+func testAccNetworkAcl_import(name string) string {
+ return fmt.Sprintf(`
+%s
+
+resource "hcso_vpc_subnet" "test" {
+ count = 2
+ name = "%s-${count.index}"
+ cidr = cidrsubnet(hcso_vpc.test.cidr, 8, count.index)
+ gateway_ip = cidrhost(cidrsubnet(hcso_vpc.test.cidr, 8, count.index), 1)
+ vpc_id = hcso_vpc.test.id
+}
+
+resource "hcso_vpc_network_acl" "test" {
+ name = "%s-update"
+ description = "created by terraform update"
+ enterprise_project_id = 0
+ enabled = false
+
+ ingress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "192.168.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ ingress_rules {
+ action = "deny"
+ ip_version = 4
+ protocol = "icmp"
+ source_ip_address = "192.168.0.0/24"
+ destination_ip_address = "0.0.0.0/0"
+ }
+
+ egress_rules {
+ action = "allow"
+ ip_version = 4
+ protocol = "tcp"
+ source_ip_address = "172.16.0.0/24"
+ source_port = "22-30,33"
+ destination_ip_address = "0.0.0.0/0"
+ destination_port = "8001-8010"
+ }
+
+ egress_rules {
+ action = "deny"
+ ip_version = 4
+ protocol = "icmp"
+ source_ip_address = "172.16.0.0/24"
+ destination_ip_address = "0.0.0.0/0"
+ }
+
+ associated_subnets {
+ subnet_id = hcso_vpc_subnet.test[0].id
+ }
+
+ associated_subnets {
+ subnet_id = hcso_vpc_subnet.test[1].id
+ }
+}
+`, testAccNetworkAcl_base(name), name, name)
+}
diff --git a/internal/services/acceptance/vpc/resource_huaweicloud_vpc_route_table_test.go b/internal/services/acceptance/vpc/resource_huaweicloud_vpc_route_table_test.go
index c15118119..c2ebe58e9 100644
--- a/internal/services/acceptance/vpc/resource_huaweicloud_vpc_route_table_test.go
+++ b/internal/services/acceptance/vpc/resource_huaweicloud_vpc_route_table_test.go
@@ -8,7 +8,6 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/chnsz/golangsdk/openstack/networking/v1/routetables"
-
"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
"github.com/huaweicloud/terraform-provider-hcso/internal/services/acceptance"
diff --git a/internal/services/vpc/data_source_huaweicloud_vpc_network_acls.go b/internal/services/vpc/data_source_huaweicloud_vpc_network_acls.go
new file mode 100644
index 000000000..7e16d70c1
--- /dev/null
+++ b/internal/services/vpc/data_source_huaweicloud_vpc_network_acls.go
@@ -0,0 +1,289 @@
+package vpc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/hashicorp/go-uuid"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+ "github.com/chnsz/golangsdk"
+ "github.com/chnsz/golangsdk/pagination"
+
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils"
+)
+
+func DataSourceNetworkAcls() *schema.Resource {
+ return &schema.Resource{
+ ReadContext: dataSourceNetworkAclsRead,
+
+ Schema: map[string]*schema.Schema{
+ "region": {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ ForceNew: true,
+ },
+ "name": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "network_acl_id": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "enterprise_project_id": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "enabled": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "status": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "network_acls": {
+ Type: schema.TypeList,
+ Computed: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "name": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "enterprise_project_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "description": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "enabled": {
+ Type: schema.TypeBool,
+ Computed: true,
+ },
+ "ingress_rules": {
+ Type: schema.TypeList,
+ Elem: networkAclsRuleSchema(),
+ Computed: true,
+ },
+ "egress_rules": {
+ Type: schema.TypeList,
+ Elem: networkAclsRuleSchema(),
+ Computed: true,
+ },
+ "associated_subnets": {
+ Type: schema.TypeSet,
+ Elem: networkAclsSubnetSchema(),
+ Computed: true,
+ },
+ "status": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "created_at": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "updated_at": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func networkAclsRuleSchema() *schema.Resource {
+ sc := schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "action": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "protocol": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "ip_version": {
+ Type: schema.TypeInt,
+ Computed: true,
+ },
+ "name": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "description": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "source_ip_address": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "destination_ip_address": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "source_port": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "destination_port": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "source_ip_address_group_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "destination_ip_address_group_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "rule_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ }
+ return &sc
+}
+
+func networkAclsSubnetSchema() *schema.Resource {
+ sc := schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "subnet_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ }
+ return &sc
+}
+
+func dataSourceNetworkAclsRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ cfg := meta.(*config.Config)
+ region := cfg.GetRegion(d)
+ client, err := cfg.NewServiceClient("vpcv3", region)
+ if err != nil {
+ return diag.Errorf("error creating VPC v3 client: %s", err)
+ }
+
+ getNetworkAclsHttpUrl := "v3/{project_id}/vpc/firewalls"
+ getNetworkAclsPath := client.Endpoint + getNetworkAclsHttpUrl
+ getNetworkAclsPath = strings.ReplaceAll(getNetworkAclsPath, "{project_id}", client.ProjectID)
+
+ getNetworkAclsQueryParams := buildNetworkAclsQueryParams(d, cfg)
+ getNetworkAclsPath += getNetworkAclsQueryParams
+
+ getNetworkAclsResp, err := pagination.ListAllItems(
+ client,
+ "marker",
+ getNetworkAclsPath,
+ &pagination.QueryOpts{MarkerField: ""})
+
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ getNetworkAclsRespJson, err := json.Marshal(getNetworkAclsResp)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ var getNetworkAclsRespBody interface{}
+ err = json.Unmarshal(getNetworkAclsRespJson, &getNetworkAclsRespBody)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ uuid, err := uuid.GenerateUUID()
+ if err != nil {
+ return diag.Errorf("unable to generate ID: %s", err)
+ }
+ d.SetId(uuid)
+
+ ids := utils.PathSearch("firewalls[*].id", getNetworkAclsRespBody, []interface{}{})
+ networkAcls := make([]map[string]interface{}, len(ids.([]interface{})))
+
+ for i, id := range ids.([]interface{}) {
+ getNetworkAclHttpUrl := "v3/{project_id}/vpc/firewalls/" + id.(string)
+ getNetworkAclPath := client.Endpoint + getNetworkAclHttpUrl
+ getNetworkAclPath = strings.ReplaceAll(getNetworkAclPath, "{project_id}", client.ProjectID)
+
+ getNetworkAclOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ getNetworkAclResp, err := client.Request("GET", getNetworkAclPath, &getNetworkAclOpt)
+ if err != nil {
+ return diag.Errorf("error retrieving VPC network ACL(%s): %s", id, err)
+ }
+
+ getNetworkAclRespBody, err := utils.FlattenResponse(getNetworkAclResp)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ networkAcls[i] = flattenNetworkAcl(getNetworkAclRespBody)
+ }
+
+ mErr := multierror.Append(
+ nil,
+ d.Set("network_acls", networkAcls),
+ )
+
+ return diag.FromErr(mErr.ErrorOrNil())
+}
+
+func buildNetworkAclsQueryParams(d *schema.ResourceData, cfg *config.Config) string {
+ res := fmt.Sprintf("?enterprise_project_id=%v", cfg.DataGetEnterpriseProjectID(d))
+
+ if v, ok := d.GetOk("name"); ok {
+ res = fmt.Sprintf("%s&name=%v", res, v)
+ }
+ if v, ok := d.GetOk("network_acl_id"); ok {
+ res = fmt.Sprintf("%s&id=%v", res, v)
+ }
+ if v, ok := d.GetOk("status"); ok {
+ res = fmt.Sprintf("%s&status=%v", res, v)
+ }
+ if v, ok := d.GetOk("enabled"); ok {
+ res = fmt.Sprintf("%s&admin_state_up=%v", res, v)
+ }
+
+ return res
+}
+
+func flattenNetworkAcl(getNetworkAclRespBody interface{}) map[string]interface{} {
+ return map[string]interface{}{
+ "name": utils.PathSearch("firewall.name", getNetworkAclRespBody, nil),
+ "id": utils.PathSearch("firewall.id", getNetworkAclRespBody, nil),
+ "description": utils.PathSearch("firewall.description", getNetworkAclRespBody, nil),
+ "enterprise_project_id": utils.PathSearch("firewall.enterprise_project_id", getNetworkAclRespBody, nil),
+ "enabled": utils.PathSearch("firewall.admin_state_up", getNetworkAclRespBody, nil),
+ "ingress_rules": flattenRules(utils.PathSearch("firewall.ingress_rules", getNetworkAclRespBody, nil)),
+ "egress_rules": flattenRules(utils.PathSearch("firewall.egress_rules", getNetworkAclRespBody, nil)),
+ "associated_subnets": flattenSubnets(utils.PathSearch("firewall.associations", getNetworkAclRespBody, nil)),
+ "status": utils.PathSearch("firewall.status", getNetworkAclRespBody, nil),
+ "created_at": utils.PathSearch("firewall.created_at", getNetworkAclRespBody, nil),
+ "updated_at": utils.PathSearch("firewall.updated_at", getNetworkAclRespBody, nil),
+ }
+}
diff --git a/internal/services/vpc/data_source_huaweicloud_vpc_routes.go b/internal/services/vpc/data_source_huaweicloud_vpc_routes.go
new file mode 100644
index 000000000..59ac99b4c
--- /dev/null
+++ b/internal/services/vpc/data_source_huaweicloud_vpc_routes.go
@@ -0,0 +1,156 @@
+package vpc
+
+import (
+ "context"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/hashicorp/go-uuid"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/tidwall/gjson"
+
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils"
+
+ "github.com/huaweicloud/terraform-provider-hcso/internal/helper/httphelper"
+ "github.com/huaweicloud/terraform-provider-hcso/internal/helper/schemas"
+)
+
+func DataSourceVpcRoutes() *schema.Resource {
+ return &schema.Resource{
+ ReadContext: dataSourceVpcRoutesRead,
+
+ Schema: map[string]*schema.Schema{
+ "region": {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: `Specifies the region in which to query the resource. If omitted, the provider-level region will be used.`,
+ },
+ "type": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: `Specifies the route type.`,
+ },
+ "vpc_id": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: `Specifies the ID of the VPC to which the route belongs.`,
+ },
+ "destination": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: `Specifies the route destination.`,
+ },
+ "routes": {
+ Type: schema.TypeList,
+ Computed: true,
+ Description: `The list of routes.`,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: `The route ID.`,
+ },
+ "type": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: `The route type.`,
+ },
+ "vpc_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: `The ID of the VPC to which the route belongs.`,
+ },
+ "destination": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: `The route destination.`,
+ },
+ "nexthop": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: `The next hop of the route.`,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+type RoutesDSWrapper struct {
+ *schemas.ResourceDataWrapper
+ Config *config.Config
+}
+
+func newRoutesDSWrapper(d *schema.ResourceData, meta interface{}) *RoutesDSWrapper {
+ return &RoutesDSWrapper{
+ ResourceDataWrapper: schemas.NewSchemaWrapper(d),
+ Config: meta.(*config.Config),
+ }
+}
+
+func dataSourceVpcRoutesRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ wrapper := newRoutesDSWrapper(d, meta)
+ lisVpcRouRst, err := wrapper.ListVpcRoutes()
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ id, err := uuid.GenerateUUID()
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ d.SetId(id)
+
+ err = wrapper.listVpcRoutesToSchema(lisVpcRouRst)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ return nil
+}
+
+// @API VPC GET /v2.0/vpc/routes
+func (w *RoutesDSWrapper) ListVpcRoutes() (*gjson.Result, error) {
+ client, err := w.NewClient(w.Config, "vpc")
+ if err != nil {
+ return nil, err
+ }
+
+ uri := "/v2.0/vpc/routes"
+ params := map[string]any{
+ "type": w.Get("type"),
+ "vpc_id": w.Get("vpc_id"),
+ "destination": w.Get("destination"),
+ }
+ params = utils.RemoveNil(params)
+ return httphelper.New(client).
+ Method("GET").
+ URI(uri).
+ Query(params).
+ MarkerPager("routes", "routes[*].id | [-1]", "marker").
+ Request().
+ Result()
+}
+
+func (w *RoutesDSWrapper) listVpcRoutesToSchema(body *gjson.Result) error {
+ d := w.ResourceData
+ mErr := multierror.Append(nil,
+ d.Set("region", w.Config.GetRegion(w.ResourceData)),
+ d.Set("routes", schemas.SliceToList(body.Get("routes"),
+ func(route gjson.Result) any {
+ return map[string]any{
+ "id": route.Get("id").Value(),
+ "type": route.Get("type").Value(),
+ "vpc_id": route.Get("vpc_id").Value(),
+ "destination": route.Get("destination").Value(),
+ "nexthop": route.Get("nexthop").Value(),
+ }
+ },
+ )),
+ )
+ return mErr.ErrorOrNil()
+}
diff --git a/internal/services/vpc/resource_huaweicloud_vpc_network_acl.go b/internal/services/vpc/resource_huaweicloud_vpc_network_acl.go
new file mode 100644
index 000000000..e89091239
--- /dev/null
+++ b/internal/services/vpc/resource_huaweicloud_vpc_network_acl.go
@@ -0,0 +1,712 @@
+package vpc
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/jmespath/go-jmespath"
+
+ "github.com/chnsz/golangsdk"
+ "github.com/chnsz/golangsdk/openstack/common/tags"
+ "github.com/chnsz/golangsdk/openstack/eps/v1/enterpriseprojects"
+
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/common"
+ "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
+
+ "github.com/huaweicloud/terraform-provider-hcso/internal/utils"
+)
+
+func ResourceNetworkAcl() *schema.Resource {
+ return &schema.Resource{
+ CreateContext: resourceNetworkAclCreate,
+ ReadContext: resourceNetworkAclRead,
+ UpdateContext: resourceNetworkAclUpdate,
+ DeleteContext: resourceNetworkAclDelete,
+
+ Importer: &schema.ResourceImporter{
+ StateContext: schema.ImportStatePassthroughContext,
+ },
+
+ Schema: map[string]*schema.Schema{
+ "region": {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ ForceNew: true,
+ },
+ "name": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "enterprise_project_id": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "description": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "enabled": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Default: true,
+ },
+ "ingress_rules": {
+ Type: schema.TypeList,
+ Elem: networkAclRuleSchema(),
+ Optional: true,
+ },
+ "egress_rules": {
+ Type: schema.TypeList,
+ Elem: networkAclRuleSchema(),
+ Optional: true,
+ },
+ "associated_subnets": {
+ Type: schema.TypeSet,
+ Elem: networkAclSubnetSchema(),
+ Optional: true,
+ },
+ "tags": common.TagsSchema(),
+ "status": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "created_at": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "updated_at": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ }
+}
+
+func networkAclRuleSchema() *schema.Resource {
+ sc := schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "action": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "protocol": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "ip_version": {
+ Type: schema.TypeInt,
+ Required: true,
+ },
+ "name": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "description": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "source_ip_address": {
+ Type: schema.TypeString,
+ Optional: true,
+ ValidateFunc: utils.ValidateCIDR,
+ },
+ "destination_ip_address": {
+ Type: schema.TypeString,
+ Optional: true,
+ ValidateFunc: utils.ValidateCIDR,
+ },
+ "source_port": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "destination_port": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "source_ip_address_group_id": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "destination_ip_address_group_id": {
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "rule_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ }
+ return &sc
+}
+
+func networkAclSubnetSchema() *schema.Resource {
+ sc := schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "subnet_id": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ },
+ }
+ return &sc
+}
+
+func resourceNetworkAclCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ cfg := meta.(*config.Config)
+ region := cfg.GetRegion(d)
+ client, err := cfg.NewServiceClient("vpcv3", region)
+ if err != nil {
+ return diag.Errorf("error creating VPC v3 client: %s", err)
+ }
+
+ createNetworkAclHttpUrl := "v3/{project_id}/vpc/firewalls"
+ createNetworkAclPath := client.Endpoint + createNetworkAclHttpUrl
+ createNetworkAclPath = strings.ReplaceAll(createNetworkAclPath, "{project_id}", client.ProjectID)
+
+ createNetworkAclOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 201,
+ },
+ }
+
+ createNetworkAclOpt.JSONBody = utils.RemoveNil(buildCreateNetworkAclBodyParams(d))
+ createNetworkAclResp, err := client.Request("POST", createNetworkAclPath, &createNetworkAclOpt)
+ if err != nil {
+ return diag.Errorf("error creating network ACL: %s", err)
+ }
+
+ createNetworkAclRespBody, err := utils.FlattenResponse(createNetworkAclResp)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ id, err := jmespath.Search("firewall.id", createNetworkAclRespBody)
+ if err != nil || id == nil {
+ return diag.Errorf("unable to find ID in API response: %s", err)
+ }
+
+ d.SetId(id.(string))
+
+ if v, ok := d.GetOk("ingress_rules"); ok {
+ err = networkAclInsertRules(client, v.([]interface{}), "ingress_rules", id.(string))
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ if v, ok := d.GetOk("egress_rules"); ok {
+ err = networkAclInsertRules(client, v.([]interface{}), "egress_rules", id.(string))
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ if v, ok := d.GetOk("associated_subnets"); ok {
+ err = networkAclAssociatSubnets(client, v.(*schema.Set).List(), id.(string))
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ // set tags
+ tagRaw := d.Get("tags").(map[string]interface{})
+ if len(tagRaw) > 0 {
+ err := doTagsAction(client, tagRaw, id.(string), "create")
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ return resourceNetworkAclRead(ctx, d, meta)
+}
+
+func networkAclInsertRules(client *golangsdk.ServiceClient, rules []interface{}, ruleType, id string) error {
+ networkAclInsertRulesHttpUrl := "v3/{project_id}/vpc/firewalls/{firewall_id}/insert-rules"
+ networkAclInsertRulesPath := client.Endpoint + networkAclInsertRulesHttpUrl
+ networkAclInsertRulesPath = strings.ReplaceAll(networkAclInsertRulesPath, "{project_id}", client.ProjectID)
+ networkAclInsertRulesPath = strings.ReplaceAll(networkAclInsertRulesPath, "{firewall_id}", id)
+
+ networkAclInsertRulesOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ networkAclInsertRulesOpt.JSONBody = utils.RemoveNil(buildNetworkAclInsertRulesBodyParams(rules, ruleType))
+ _, err := client.Request("PUT", networkAclInsertRulesPath, &networkAclInsertRulesOpt)
+ if err != nil {
+ return fmt.Errorf("error inserting rules to network ACL(%s): %s", id, err)
+ }
+
+ return nil
+}
+
+func networkAclRemoveRules(client *golangsdk.ServiceClient, rules []interface{}, ruleType, id string) error {
+ networkAclRemoveRulesHttpUrl := "v3/{project_id}/vpc/firewalls/{firewall_id}/remove-rules"
+ networkAclRemoveRulesPath := client.Endpoint + networkAclRemoveRulesHttpUrl
+ networkAclRemoveRulesPath = strings.ReplaceAll(networkAclRemoveRulesPath, "{project_id}", client.ProjectID)
+ networkAclRemoveRulesPath = strings.ReplaceAll(networkAclRemoveRulesPath, "{firewall_id}", id)
+
+ networkAclRemoveRulesOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ networkAclRemoveRulesOpt.JSONBody = utils.RemoveNil(buildNetworkAclRemoveRulesBodyParams(rules, ruleType))
+ _, err := client.Request("PUT", networkAclRemoveRulesPath, &networkAclRemoveRulesOpt)
+ if err != nil {
+ return fmt.Errorf("error removing rules from network ACL(%s): %s", id, err)
+ }
+
+ return nil
+}
+
+func networkAclAssociatSubnets(client *golangsdk.ServiceClient, subnets []interface{}, id string) error {
+ networkAclAssociatSubnetsHttpUrl := "v3/{project_id}/vpc/firewalls/{firewall_id}/associate-subnets"
+ networkAclAssociatSubnetsPath := client.Endpoint + networkAclAssociatSubnetsHttpUrl
+ networkAclAssociatSubnetsPath = strings.ReplaceAll(networkAclAssociatSubnetsPath, "{project_id}", client.ProjectID)
+ networkAclAssociatSubnetsPath = strings.ReplaceAll(networkAclAssociatSubnetsPath, "{firewall_id}", id)
+
+ networkAclAssociatSubnetsOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ networkAclAssociatSubnetsOpt.JSONBody = utils.RemoveNil(buildNetworkAclSubnetsBodyParams(subnets))
+ _, err := client.Request("PUT", networkAclAssociatSubnetsPath, &networkAclAssociatSubnetsOpt)
+ if err != nil {
+ return fmt.Errorf("error associating subnets to network ACL(%s): %s", id, err)
+ }
+
+ return nil
+}
+
+func networkAclDisassociatSubnets(client *golangsdk.ServiceClient, subnets []interface{}, id string) error {
+ networkAclDisassociatSubnetsHttpUrl := "v3/{project_id}/vpc/firewalls/{firewall_id}/disassociate-subnets"
+ networkAclDisassociatSubnetsPath := client.Endpoint + networkAclDisassociatSubnetsHttpUrl
+ networkAclDisassociatSubnetsPath = strings.ReplaceAll(networkAclDisassociatSubnetsPath, "{project_id}", client.ProjectID)
+ networkAclDisassociatSubnetsPath = strings.ReplaceAll(networkAclDisassociatSubnetsPath, "{firewall_id}", id)
+
+ networkAclDisassociatSubnetsOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ networkAclDisassociatSubnetsOpt.JSONBody = utils.RemoveNil(buildNetworkAclSubnetsBodyParams(subnets))
+ _, err := client.Request("PUT", networkAclDisassociatSubnetsPath, &networkAclDisassociatSubnetsOpt)
+ if err != nil {
+ return fmt.Errorf("error disassociating subnets from network ACL(%s): %s", id, err)
+ }
+
+ return nil
+}
+
+func buildCreateNetworkAclBodyParams(d *schema.ResourceData) map[string]interface{} {
+ bodyParams := map[string]interface{}{
+ "firewall": map[string]interface{}{
+ "name": d.Get("name"),
+ "enterprise_project_id": d.Get("enterprise_project_id"),
+ "description": utils.ValueIgnoreEmpty(d.Get("description")),
+ "admin_state_up": d.Get("enabled"),
+ },
+ }
+ return bodyParams
+}
+
+func buildNetworkAclInsertRulesBodyParams(rules []interface{}, ruleType string) map[string]interface{} {
+ bodyParams := map[string]interface{}{
+ "firewall": map[string]interface{}{
+ ruleType: buildNetworkAclInsertRuleBodyParams(rules),
+ },
+ }
+
+ return bodyParams
+}
+
+func buildNetworkAclInsertRuleBodyParams(rules []interface{}) []map[string]interface{} {
+ bodyParams := make([]map[string]interface{}, len(rules))
+ for i, v := range rules {
+ rule := v.(map[string]interface{})
+ bodyParams[i] = map[string]interface{}{
+ "action": rule["action"],
+ "protocol": rule["protocol"],
+ "ip_version": rule["ip_version"],
+ "description": rule["description"],
+ "name": utils.ValueIgnoreEmpty(rule["name"]),
+ "source_ip_address": utils.ValueIgnoreEmpty(rule["source_ip_address"]),
+ "destination_ip_address": utils.ValueIgnoreEmpty(rule["destination_ip_address"]),
+ "source_port": utils.ValueIgnoreEmpty(rule["source_port"]),
+ "destination_port": utils.ValueIgnoreEmpty(rule["destination_port"]),
+ "source_address_group_id": utils.ValueIgnoreEmpty(rule["source_ip_address_group_id"]),
+ "destination_address_group_id": utils.ValueIgnoreEmpty(rule["destination_ip_address_group_id"]),
+ }
+ }
+
+ return bodyParams
+}
+
+func buildNetworkAclRemoveRulesBodyParams(rules []interface{}, ruleType string) map[string]interface{} {
+ bodyParams := map[string]interface{}{
+ "firewall": map[string]interface{}{
+ ruleType: buildNetworkAclRemoveRuleBodyParams(rules),
+ },
+ }
+
+ return bodyParams
+}
+
+func buildNetworkAclRemoveRuleBodyParams(rules []interface{}) []map[string]interface{} {
+ bodyParams := make([]map[string]interface{}, len(rules))
+ for i, v := range rules {
+ rule := v.(map[string]interface{})
+ bodyParams[i] = map[string]interface{}{
+ "id": rule["rule_id"],
+ }
+ }
+
+ return bodyParams
+}
+
+func buildNetworkAclSubnetsBodyParams(subnets []interface{}) map[string]interface{} {
+ bodyParams := make([]map[string]interface{}, len(subnets))
+ for i, v := range subnets {
+ subnet := v.(map[string]interface{})
+ bodyParams[i] = map[string]interface{}{
+ "virsubnet_id": subnet["subnet_id"],
+ }
+ }
+
+ return map[string]interface{}{
+ "subnets": bodyParams,
+ }
+}
+
+func resourceNetworkAclRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ cfg := meta.(*config.Config)
+ region := cfg.GetRegion(d)
+ client, err := cfg.NewServiceClient("vpcv3", region)
+ if err != nil {
+ return diag.Errorf("error creating VPC v3 client: %s", err)
+ }
+
+ getNetworkAclHttpUrl := "v3/{project_id}/vpc/firewalls/" + d.Id()
+ getNetworkAclPath := client.Endpoint + getNetworkAclHttpUrl
+ getNetworkAclPath = strings.ReplaceAll(getNetworkAclPath, "{project_id}", client.ProjectID)
+
+ getNetworkAclOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+
+ getNetworkAclResp, err := client.Request("GET", getNetworkAclPath, &getNetworkAclOpt)
+ if err != nil {
+ return common.CheckDeletedDiag(d, err, "VPC network ACL")
+ }
+
+ getNetworkAclRespBody, err := utils.FlattenResponse(getNetworkAclResp)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ mErr := multierror.Append(
+ nil,
+ d.Set("region", region),
+ d.Set("name", utils.PathSearch("firewall.name", getNetworkAclRespBody, nil)),
+ d.Set("description", utils.PathSearch("firewall.description", getNetworkAclRespBody, nil)),
+ d.Set("enterprise_project_id", utils.PathSearch("firewall.enterprise_project_id", getNetworkAclRespBody, nil)),
+ d.Set("enabled", utils.PathSearch("firewall.admin_state_up", getNetworkAclRespBody, nil)),
+ d.Set("ingress_rules", flattenRules(utils.PathSearch("firewall.ingress_rules", getNetworkAclRespBody, nil))),
+ d.Set("egress_rules", flattenRules(utils.PathSearch("firewall.egress_rules", getNetworkAclRespBody, nil))),
+ d.Set("associated_subnets", flattenSubnets(utils.PathSearch("firewall.associations", getNetworkAclRespBody, nil))),
+ d.Set("status", utils.PathSearch("firewall.status", getNetworkAclRespBody, nil)),
+ d.Set("created_at", utils.PathSearch("firewall.created_at", getNetworkAclRespBody, nil)),
+ d.Set("updated_at", utils.PathSearch("firewall.updated_at", getNetworkAclRespBody, nil)),
+ )
+
+ if resourceTags, err := tags.Get(client, "firewalls", d.Id()).Extract(); err == nil {
+ tagmap := utils.TagsToMap(resourceTags.Tags)
+ if err := d.Set("tags", tagmap); err != nil {
+ return diag.Errorf("error saving tags to state for network ACL (%s): %s", d.Id(), err)
+ }
+ } else {
+ log.Printf("[WARN] error fetching tags of network ACL (%s): %s", d.Id(), err)
+ }
+
+ return diag.FromErr(mErr.ErrorOrNil())
+}
+
+func flattenRules(rulesRaw interface{}) []map[string]interface{} {
+ if rulesRaw == nil {
+ return nil
+ }
+
+ rules := rulesRaw.([]interface{})
+ res := make([]map[string]interface{}, len(rules))
+ for i, v := range rules {
+ rule := v.(map[string]interface{})
+ res[i] = map[string]interface{}{
+ "rule_id": rule["id"],
+ "name": rule["name"],
+ "description": rule["description"],
+ "action": rule["action"],
+ "protocol": rule["protocol"],
+ "ip_version": rule["ip_version"],
+ "source_ip_address": rule["source_ip_address"],
+ "destination_ip_address": rule["destination_ip_address"],
+ "source_port": rule["source_port"],
+ "destination_port": rule["destination_port"],
+ "source_ip_address_group_id": rule["source_address_group_id"],
+ "destination_ip_address_group_id": rule["destination_address_group_id"],
+ }
+ }
+
+ return res
+}
+
+func flattenSubnets(subnetsRaw interface{}) []map[string]interface{} {
+ if subnetsRaw == nil {
+ return nil
+ }
+
+ subnets := subnetsRaw.([]interface{})
+ res := make([]map[string]interface{}, len(subnets))
+ for i, v := range subnets {
+ subnet := v.(map[string]interface{})
+ res[i] = map[string]interface{}{
+ "subnet_id": subnet["virsubnet_id"],
+ }
+ }
+
+ return res
+}
+
+func buildUpdateNetworkAclBodyParams(d *schema.ResourceData) map[string]interface{} {
+ bodyParams := map[string]interface{}{
+ "firewall": map[string]interface{}{
+ "name": d.Get("name"),
+ "description": d.Get("description"),
+ "admin_state_up": d.Get("enabled"),
+ },
+ }
+ return bodyParams
+}
+
+func resourceNetworkAclUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ cfg := meta.(*config.Config)
+ region := cfg.GetRegion(d)
+ id := d.Id()
+ client, err := cfg.NewServiceClient("vpcv3", region)
+ if err != nil {
+ return diag.Errorf("error creating VPC v3 Client: %s", err)
+ }
+
+ if d.HasChanges("name", "description", "enabled") {
+ updateNetworkAclHttpUrl := "v3/{project_id}/vpc/firewalls/" + id
+ updateNetworkAclPath := client.Endpoint + updateNetworkAclHttpUrl
+ updateNetworkAclPath = strings.ReplaceAll(updateNetworkAclPath, "{project_id}", client.ProjectID)
+
+ updateNetworkAclOpts := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 200,
+ },
+ }
+ updateNetworkAclOpts.JSONBody = utils.RemoveNil(buildUpdateNetworkAclBodyParams(d))
+ _, err = client.Request("PUT", updateNetworkAclPath, &updateNetworkAclOpts)
+ if err != nil {
+ return diag.Errorf("error updating VPC network ACL: %s", err)
+ }
+ }
+
+ if d.HasChange("ingress_rules") {
+ err = updateRules(client, d, "ingress_rules")
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ if d.HasChange("egress_rules") {
+ err = updateRules(client, d, "egress_rules")
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ if d.HasChange("associated_subnets") {
+ err = updateAssociatedSubnets(client, d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ if d.HasChange("enterprise_project_id") {
+ migrateOpts := enterpriseprojects.MigrateResourceOpts{
+ ResourceId: id,
+ ResourceType: "firewalls",
+ RegionId: region,
+ ProjectId: client.ProjectID,
+ }
+ if err := common.MigrateEnterpriseProject(ctx, cfg, d, migrateOpts); err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ // update tags
+ if d.HasChange("tags") {
+ tagErr := updateTags(client, d, id)
+ if tagErr != nil {
+ return diag.Errorf("error updating tags of network ACL %s: %s", d.Id(), tagErr)
+ }
+ }
+
+ return resourceNetworkAclRead(ctx, d, meta)
+}
+
+func updateTags(client *golangsdk.ServiceClient, d *schema.ResourceData, id string) error {
+ oRaw, nRaw := d.GetChange("tags")
+ oMap := oRaw.(map[string]interface{})
+ nMap := nRaw.(map[string]interface{})
+
+ // remove old tags
+ if len(oMap) > 0 {
+ err := doTagsAction(client, oMap, id, "delete")
+ if err != nil {
+ return err
+ }
+ }
+
+ // set new tags
+ if len(nMap) > 0 {
+ err := doTagsAction(client, nMap, id, "create")
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func doTagsAction(client *golangsdk.ServiceClient, tagsRaw map[string]interface{}, id, action string) error {
+ manageTagsHttpUrl := "v3/{project_id}/firewalls/{resource_id}/tags/{action}"
+ manageTagsPath := client.Endpoint + manageTagsHttpUrl
+ manageTagsPath = strings.ReplaceAll(manageTagsPath, "{project_id}", client.ProjectID)
+ manageTagsPath = strings.ReplaceAll(manageTagsPath, "{resource_id}", id)
+ manageTagsPath = strings.ReplaceAll(manageTagsPath, "{action}", action)
+ manageTagsOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 204,
+ },
+ }
+
+ manageTagsOpt.JSONBody = map[string]interface{}{
+ "tags": utils.ExpandResourceTags(tagsRaw),
+ }
+
+ _, err := client.Request("POST", manageTagsPath, &manageTagsOpt)
+ if err != nil {
+ return fmt.Errorf("unable to %s network ACL tags: %s", action, err)
+ }
+
+ return nil
+}
+
+func updateRules(client *golangsdk.ServiceClient, d *schema.ResourceData, ruleType string) error {
+ id := d.Id()
+ oldRules, newRules := d.GetChange(ruleType)
+ if len(oldRules.([]interface{})) > 0 {
+ err := networkAclRemoveRules(client, oldRules.([]interface{}), ruleType, id)
+ if err != nil {
+ return fmt.Errorf("error updating rules: %s", err)
+ }
+ }
+
+ if len(newRules.([]interface{})) > 0 {
+ err := networkAclInsertRules(client, newRules.([]interface{}), ruleType, id)
+ if err != nil {
+ // if failed to insert the new rules, insert the old rules back
+ if len(oldRules.([]interface{})) > 0 {
+ rollBackErr := networkAclInsertRules(client, oldRules.([]interface{}), ruleType, id)
+ if rollBackErr != nil {
+ return fmt.Errorf("error updating rules: %s, failed to roll back: %s", err, rollBackErr)
+ }
+ return fmt.Errorf("error updating rules: %s, it's rolled back", err)
+ }
+ return fmt.Errorf("error updating rules: %s", err)
+ }
+ }
+
+ return nil
+}
+
+func updateAssociatedSubnets(client *golangsdk.ServiceClient, d *schema.ResourceData) error {
+ id := d.Id()
+ oldSubnets, newSubnets := d.GetChange("associated_subnets")
+ if len(oldSubnets.(*schema.Set).List()) > 0 {
+ err := networkAclDisassociatSubnets(client, oldSubnets.(*schema.Set).List(), id)
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(newSubnets.(*schema.Set).List()) > 0 {
+ err := networkAclAssociatSubnets(client, newSubnets.(*schema.Set).List(), id)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func resourceNetworkAclDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ cfg := meta.(*config.Config)
+ region := cfg.GetRegion(d)
+ id := d.Id()
+ client, err := cfg.NewServiceClient("vpcv3", region)
+ if err != nil {
+ return diag.Errorf("error creating VPC v3 Client: %s", err)
+ }
+
+ // the associated subnets need to be removed before deleting the ACL
+ if d.Get("associated_subnets").(*schema.Set).Len() > 0 {
+ err = networkAclDisassociatSubnets(client, d.Get("associated_subnets").(*schema.Set).List(), id)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ }
+
+ deleteNetworkAclHttpUrl := "v3/{project_id}/vpc/firewalls/" + id
+ deleteNetworkAclPath := client.Endpoint + deleteNetworkAclHttpUrl
+ deleteNetworkAclPath = strings.ReplaceAll(deleteNetworkAclPath, "{project_id}", client.ProjectID)
+
+ deleteNetworkAclOpt := golangsdk.RequestOpts{
+ KeepResponseBody: true,
+ OkCodes: []int{
+ 204,
+ },
+ }
+ _, err = client.Request("DELETE", deleteNetworkAclPath, &deleteNetworkAclOpt)
+ if err != nil {
+ return diag.Errorf("error deleting network ACL: %s", err)
+ }
+
+ return nil
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index ef5ca3b09..5e57a8f73 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -542,3 +542,20 @@ func SchemaDesc(description string, schemaDescInput SchemaDescInput) string {
return description
}
+
+func ValueIgnoreEmpty(v interface{}) interface{} {
+ vl := reflect.ValueOf(v)
+ if !vl.IsValid() {
+ return v
+ }
+
+ if (vl.Kind() != reflect.Bool) && vl.IsZero() {
+ return nil
+ }
+
+ if (vl.Kind() == reflect.Array || vl.Kind() == reflect.Slice) && vl.Len() == 0 {
+ return nil
+ }
+
+ return v
+}