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 +}