From 5c3e604dc4ef8e65fa944f7984f83b4a2e7fd047 Mon Sep 17 00:00:00 2001 From: Mickael Stanislas Date: Fri, 7 Feb 2025 09:57:02 +0100 Subject: [PATCH] feat(elb): add ress/data `cloudavenue_elb_pool` --- .changelog/892.txt | 2 +- .changelog/901.txt | 2 +- .changelog/974.txt | 7 + docs/data-sources/elb_pool.md | 93 ++++ docs/resources/elb_pool.md | 236 ++++++++ .../cloudavenue_elb_pool/data-source.tf | 4 + .../resources/cloudavenue_elb_pool/import.sh | 1 + go.mod | 2 +- go.sum | 4 +- internal/provider/elb/pool_datasource.go | 116 ++++ internal/provider/elb/pool_resource.go | 489 +++++++++++++++++ internal/provider/elb/pool_schema.go | 441 +++++++++++++++ internal/provider/elb/pool_schema_test.go | 66 +++ internal/provider/elb/pool_types.go | 205 +++++++ internal/provider/provider_datasources.go | 11 +- internal/provider/provider_resources.go | 6 +- internal/testsacc/acctest_datasources_test.go | 3 + internal/testsacc/acctest_resources_test.go | 3 + .../edgegw_edgegateway_datasource_test.go | 13 + .../testsacc/edgegw_ip_set_resource_test.go | 22 + internal/testsacc/elb_pool_datasource_test.go | 71 +++ internal/testsacc/elb_pool_resource_test.go | 515 ++++++++++++++++++ templates/data-sources/elb_pool.md.tmpl | 25 + templates/resources/elb_pool.md.tmpl | 162 ++++++ 24 files changed, 2488 insertions(+), 11 deletions(-) create mode 100644 .changelog/974.txt create mode 100644 docs/data-sources/elb_pool.md create mode 100644 docs/resources/elb_pool.md create mode 100644 examples/data-sources/cloudavenue_elb_pool/data-source.tf create mode 100644 examples/resources/cloudavenue_elb_pool/import.sh create mode 100644 internal/provider/elb/pool_datasource.go create mode 100644 internal/provider/elb/pool_resource.go create mode 100644 internal/provider/elb/pool_schema.go create mode 100644 internal/provider/elb/pool_schema_test.go create mode 100644 internal/provider/elb/pool_types.go create mode 100644 internal/testsacc/elb_pool_datasource_test.go create mode 100644 internal/testsacc/elb_pool_resource_test.go create mode 100644 templates/data-sources/elb_pool.md.tmpl create mode 100644 templates/resources/elb_pool.md.tmpl diff --git a/.changelog/892.txt b/.changelog/892.txt index c8b19afe..fe8e3a16 100644 --- a/.changelog/892.txt +++ b/.changelog/892.txt @@ -1,3 +1,3 @@ ```release-note:new-data-source -`datasource/cloudavenue_alb_service_engine_group` - Added a new data source to retrieve information about a Service Engine Group. +`datasource/cloudavenue_elb_service_engine_group` - Added a new data source to retrieve information about a Service Engine Group. ``` \ No newline at end of file diff --git a/.changelog/901.txt b/.changelog/901.txt index a1aa7b97..f9f57f86 100644 --- a/.changelog/901.txt +++ b/.changelog/901.txt @@ -1,3 +1,3 @@ ```release-note:new-data-source -`datasource/cloudavenue_alb_service_engine_groups` - Added a new data source to list all Service Engine Group attached to an Edge Gateway. +`datasource/cloudavenue_elb_service_engine_groups` - Added a new data source to list all Service Engine Group attached to an Edge Gateway. ``` \ No newline at end of file diff --git a/.changelog/974.txt b/.changelog/974.txt new file mode 100644 index 00000000..144c9825 --- /dev/null +++ b/.changelog/974.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +`resource/cloudavenue_elb_pool` - Added new resource `cloudavenue_elb_pool` to manage edgegateway load balancer pools. Pools maintain the list of servers assigned to them and perform health monitoring, load balancing, persistence. +``` + +```release-note:new-data-source +`datasource/cloudavenue_elb_pool` - Added new datasource `cloudavenue_elb_pool` to read details of an existing edgegateway load balancer pool. +``` \ No newline at end of file diff --git a/docs/data-sources/elb_pool.md b/docs/data-sources/elb_pool.md new file mode 100644 index 00000000..0a67a441 --- /dev/null +++ b/docs/data-sources/elb_pool.md @@ -0,0 +1,93 @@ +--- +page_title: "cloudavenue_elb_pool Data Source - cloudavenue" +subcategory: "ELB (EdgeGateway Load Balancer)" +description: |- + The cloudavenue_elb_pool data source allows you to retrieve information about an existing edgegateway load balancer pool. +--- + +# cloudavenue_elb_pool (Data Source) + +The `cloudavenue_elb_pool` data source allows you to retrieve information about an existing edgegateway load balancer pool. + +## Example Usage + +```terraform +data "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = cloudavenue_edge_gateway.example.id +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the pool. + +### Optional + +- `edge_gateway_id` (String) The ID of the Edge Gateway. Ensure that one and only one attribute from this collection is set : `edge_gateway_name`, `edge_gateway_id`. +- `edge_gateway_name` (String) The name of the Edge Gateway. Ensure that one and only one attribute from this collection is set : `edge_gateway_name`, `edge_gateway_id`. + +### Read-Only + +- `algorithm` (String) The heart of a load balancer is its ability to effectively distribute traffic across healthy servers. If persistence is enabled, only the first connection from a client is load balanced. While the persistence remains in effect, subsequent connections or requests from a client are directed to the same server. +- `default_port` (Number) DefaultPort defines destination server port used by the traffic sent to the member. +- `description` (String) The name of the pool. +- `enabled` (Boolean) Enable or disable the pool. +- `health` (Attributes) . (see [below for nested schema](#nestedatt--health)) +- `id` (String) The ID of the pool. +- `members` (Attributes) . (see [below for nested schema](#nestedatt--members)) +- `persistence` (Attributes) . (see [below for nested schema](#nestedatt--persistence)) +- `tls` (Attributes) . (see [below for nested schema](#nestedatt--tls)) + + +### Nested Schema for `health` + +Read-Only: + +- `monitors` (List of String) The active health monitors. +- `passive_monitoring_enabled` (Boolean) PassiveMonitoringEnabled sets if client traffic should be used to check if pool member is up or down. + + + +### Nested Schema for `members` + +Read-Only: + +- `graceful_timeout_period` (String) Maximum time (in minutes) to gracefully disable a member. Virtual service waits for the specified time before terminating the existing connections to the members that are disabled. Special values: `0` represents `Immediate` and `-1` represents `Infinite`. The maximum value is `7200` minutes. +- `target_group` (String) The group contains reference to the Edge Firewall Group representing destination servers which are used by the Load Balancer Pool to direct load balanced traffic. This permit to reference `IP Set` or `Static Group` ID. +- `targets` (Attributes List) targets field defines list of destination servers which are used by the Load Balancer Pool to direct load balanced traffic. (see [below for nested schema](#nestedatt--members--targets)) + + +### Nested Schema for `members.targets` + +Read-Only: + +- `enabled` (Boolean) Enable or disable the member. +- `ip_address` (String) The IP address of the member. +- `port` (Number) The port of the member. +- `ratio` (Number) The ratio of the member. The ratio of each pool member denotes the traffic that goes to each server pool member. A server with a ratio of 2 gets twice as much traffic as a server with a ratio of 1. + + + + +### Nested Schema for `persistence` + +Read-Only: + +- `type` (String) The type of the persistence. +- `value` (String) The value of the persistence. + + + +### Nested Schema for `tls` + +Read-Only: + +- `ca_certificate_refs` (List of String) The CA certificate references point to root certificates to use when validating certificates presented by the pool members. +- `common_name_check_enabled` (Boolean) Enable common name check for server certificate. If enabled and no explicit domain name is specified, the incoming host header will be used to do the match. +- `domain_names` (List of String) The domain names of the TLS check. This attribute is taken into account if the `common_name_check_enabled` is set to `true`. +- `enabled` (Boolean) Enable or disable the TLS. + diff --git a/docs/resources/elb_pool.md b/docs/resources/elb_pool.md new file mode 100644 index 00000000..592a2ce2 --- /dev/null +++ b/docs/resources/elb_pool.md @@ -0,0 +1,236 @@ +--- +page_title: "cloudavenue_elb_pool Resource - cloudavenue" +subcategory: "ELB (EdgeGateway Load Balancer)" +description: |- + The cloudavenue_elb_pool resource allows you to manage edgegateway load balancer pools. Pools maintain the list of servers assigned to them and perform health monitoring, load balancing, persistence. A pool may only be used or referenced by only one virtual service at a time. +--- + +# cloudavenue_elb_pool (Resource) + +The `cloudavenue_elb_pool` resource allows you to manage edgegateway load balancer pools. Pools maintain the list of servers assigned to them and perform health monitoring, load balancing, persistence. A pool may only be used or referenced by only one virtual service at a time. + +## Example Usage + +Basic working example: + +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } +} +``` + + -> More examples can be found at the [Advanced Usage](#advanced-usage) section. + + + + +## Schema + +### Required + +- `default_port` (Number) DefaultPort defines destination server port used by the traffic sent to the member. +- `members` (Attributes) . (see [below for nested schema](#nestedatt--members)) +- `name` (String) The name of the pool. + +### Optional + +- `algorithm` (String) The heart of a load balancer is its ability to effectively distribute traffic across healthy servers. If persistence is enabled, only the first connection from a client is load balanced. While the persistence remains in effect, subsequent connections or requests from a client are directed to the same server. Value must be one of: `CONSISTENT_HASH` (New connections are distributed across the servers by using the IP address of the client to generate an IP hash.), `CORE_AFFINITY` (Each CPU core uses a subset of servers, and each server is used by a subset of cores. Essentially it provides a many-to-many mapping between servers and cores.), `FASTEST_RESPONSE` (New connections are sent to the server that is currently providing the fastest response to new connections or requests.), `FEWEST_SERVERS` (Instead of attempting to distribute all connections or requests across all servers, the fewest number of servers which are required to satisfy the current client load will be determined.), `FEWEST_TASKS` (Load is adaptively balanced, based on server feedback.), `LEAST_CONNECTIONS` (New connections are sent to the server that currently has the least number of outstanding concurrent connections.), `LEAST_LOAD` (New connections are sent to the server with the lightest load, regardless of the number of connections that server has.), `RANDOM` (Picks servers at random), `ROUND_ROBIN` (New connections are sent to the next eligible server in the pool in sequential order.). Value defaults to `LEAST_CONNECTIONS`. +- `description` (String) The name of the pool. +- `edge_gateway_id` (String) (ForceNew) The ID of the Edge Gateway. Ensure that one and only one attribute from this collection is set : `edge_gateway_name`, `edge_gateway_id`. +- `edge_gateway_name` (String) (ForceNew) The name of the Edge Gateway. Ensure that one and only one attribute from this collection is set : `edge_gateway_name`, `edge_gateway_id`. +- `enabled` (Boolean) Enable or disable the pool. Value defaults to `true`. +- `health` (Attributes) . Value defaults to `{"monitors":,"passive_monitoring_enabled":true}`. (see [below for nested schema](#nestedatt--health)) +- `persistence` (Attributes) . Value defaults to `{"type":"CLIENT_IP","value":}`. (see [below for nested schema](#nestedatt--persistence)) +- `tls` (Attributes) . Value defaults to `{"ca_certificate_refs":,"common_name_check_enabled":false,"domain_names":,"enabled":false}`. (see [below for nested schema](#nestedatt--tls)) + +### Read-Only + +- `id` (String) The ID of the pool. + + +### Nested Schema for `members` + +Optional: + +- `graceful_timeout_period` (String) Maximum time (in minutes) to gracefully disable a member. Virtual service waits for the specified time before terminating the existing connections to the members that are disabled. Special values: `0` represents `Immediate` and `-1` represents `Infinite`. The maximum value is `7200` minutes. Value defaults to `1`. +- `target_group` (String) The group contains reference to the Edge Firewall Group representing destination servers which are used by the Load Balancer Pool to direct load balanced traffic. This permit to reference `IP Set` or `Static Group` ID. Ensure that one and only one attribute from this collection is set : `targets`, `target_group`. +- `targets` (Attributes List) targets field defines list of destination servers which are used by the Load Balancer Pool to direct load balanced traffic. Ensure that one and only one attribute from this collection is set : `targets`, `target_group`. (see [below for nested schema](#nestedatt--members--targets)) + + +### Nested Schema for `members.targets` + +Required: + +- `ip_address` (String) The IP address of the member. The value must be a valid IPV4 address (`192.168.0.1`). +- `port` (Number) The port of the member. + +Optional: + +- `enabled` (Boolean) Enable or disable the member. Value defaults to `true`. +- `ratio` (Number) The ratio of the member. The ratio of each pool member denotes the traffic that goes to each server pool member. A server with a ratio of 2 gets twice as much traffic as a server with a ratio of 1. Value defaults to `1`. + + + + +### Nested Schema for `health` + +Optional: + +- `monitors` (List of String) The active health monitors. Element value must satisfy all validations: value must be one of: ["HTTP" "HTTPS" "PING" "TCP" "UDP"]. +- `passive_monitoring_enabled` (Boolean) PassiveMonitoringEnabled sets if client traffic should be used to check if pool member is up or down. Value defaults to `true`. + + + +### Nested Schema for `persistence` + +Optional: + +- `type` (String) The type of the persistence. Value must be one of: `APP_COOKIE` (Load Balancer reads existing server cookies or URI embedded data such as JSessionID. Cookie name must be provided as value.), `CLIENT_IP` (The clients IP is used as the identifier and mapped to the server.), `CUSTOM_HTTP_HEADER` (Custom, static mappings of header values to specific servers are used. Header name must be provided as value.), `HTTP_COOKIE` (Load Balancer inserts a cookie into HTTP responses. Cookie name must be provided as value.), `TLS` (Information is embedded in the client's SSL/TLS ticket ID. This will use default system profile System-Persistence-TLS.). Value defaults to `CLIENT_IP`. +- `value` (String) The value of the persistence. If the value of [`<.type`](#<.type) attribute is one of `HTTP_COOKIE`, `CUSTOM_HTTP_HEADER` or `APP_COOKIE` this attribute is **REQUIRED**. + + + +### Nested Schema for `tls` + +Optional: + +- `ca_certificate_refs` (List of String) The CA certificate references point to root certificates to use when validating certificates presented by the pool members. Use `cloudavenue_org_certificate` resource to create a certificate and get the ID. Element value must satisfy all validations: must start with "urn:vcloud:certificateLibraryItem:". +- `common_name_check_enabled` (Boolean) Enable common name check for server certificate. If enabled and no explicit domain name is specified, the incoming host header will be used to do the match. Value defaults to `false`. +- `domain_names` (List of String) The domain names of the TLS check. This attribute is taken into account if the `common_name_check_enabled` is set to `true`. List must contain at least 0 elements and at most 10 elements. +- `enabled` (Boolean) Enable or disable the TLS. Value defaults to `false`. + +## Advanced Usage + +### Multiple Members and health monitors +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + algorithm = "ROUND_ROBIN" + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + }, + { + ip_address = "192.168.0.2" + port = 80 + } + ] + } + health = { + monitors = ["HTTP", "TCP"] + } +} +``` + +### Setting TLS configuration +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + } +} +``` + +### Use IPSet for members +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + members = { + graceful_timeout_period = 2 + target_group = cloudavenue_edgegateway_ip_set.example.id + } +} +``` + +### Full configuration +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + algorithm = "ROUND_ROBIN" + members = { + graceful_timeout_period = 2 + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + enabled = true + ratio = 1 + }, + { + ip_address = "192.168.0.2" + port = 80 + enabled = true + ratio = 1 + }, + { + ip_address = "192.168.0.10" + port = 8080 + enabled = true + ratio = 10 + } + ] + } + + health = { + monitors = ["HTTP", "TCP"] + passive_monitoring_enabled = true + } + + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + common_name_check_enabled = true + } + + persistence = { + type = "CUSTOM_HTTP_HEADER" + value = "X-Custom" + } +} +``` + +## Import + +Import is supported using the following syntax: +```shell +terraform import cloudavenue_elb_pool.example edgeGatewayNameOrID.poolNameOrID +``` \ No newline at end of file diff --git a/examples/data-sources/cloudavenue_elb_pool/data-source.tf b/examples/data-sources/cloudavenue_elb_pool/data-source.tf new file mode 100644 index 00000000..38cbce11 --- /dev/null +++ b/examples/data-sources/cloudavenue_elb_pool/data-source.tf @@ -0,0 +1,4 @@ +data "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = cloudavenue_edge_gateway.example.id +} diff --git a/examples/resources/cloudavenue_elb_pool/import.sh b/examples/resources/cloudavenue_elb_pool/import.sh new file mode 100644 index 00000000..2ad303e0 --- /dev/null +++ b/examples/resources/cloudavenue_elb_pool/import.sh @@ -0,0 +1 @@ +terraform import cloudavenue_elb_pool.example edgeGatewayNameOrID.poolNameOrID \ No newline at end of file diff --git a/go.mod b/go.mod index ee1b6884..5e9dc234 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 github.com/iancoleman/strcase v0.3.0 github.com/madflojo/testcerts v1.4.0 - github.com/orange-cloudavenue/cloudavenue-sdk-go v0.21.4-0.20250205091902-6063db5d300f + github.com/orange-cloudavenue/cloudavenue-sdk-go v0.21.4-0.20250207103134-f809a9e41443 github.com/orange-cloudavenue/common-go/print v0.0.0-20250109171729-2be550d5d3ac github.com/orange-cloudavenue/common-go/utils v0.0.0-20240119163616-66b473d92339 github.com/orange-cloudavenue/terraform-plugin-framework-planmodifiers v1.4.0 diff --git a/go.sum b/go.sum index d8924e55..91930382 100644 --- a/go.sum +++ b/go.sum @@ -266,8 +266,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/orange-cloudavenue/cloudavenue-sdk-go v0.21.4-0.20250205091902-6063db5d300f h1:5G8037p+OPF9vKomSm2UMCDVwO6evq6rk3lzZ+HkbH4= -github.com/orange-cloudavenue/cloudavenue-sdk-go v0.21.4-0.20250205091902-6063db5d300f/go.mod h1:qijxgnnyB2JBkHWslCPD45NhJk5UVrVrm//Vq6qQ1c8= +github.com/orange-cloudavenue/cloudavenue-sdk-go v0.21.4-0.20250207103134-f809a9e41443 h1:BwXh2VkEiEEdwOo3+WyG4OLWY0WiZcxjYaA/RsnWnpg= +github.com/orange-cloudavenue/cloudavenue-sdk-go v0.21.4-0.20250207103134-f809a9e41443/go.mod h1:qijxgnnyB2JBkHWslCPD45NhJk5UVrVrm//Vq6qQ1c8= github.com/orange-cloudavenue/common-go/print v0.0.0-20250109171729-2be550d5d3ac h1:f1Fd70+PMDTK6FE4gHdNfoHSQHLn5pfJMTjZPzOWZtc= github.com/orange-cloudavenue/common-go/print v0.0.0-20250109171729-2be550d5d3ac/go.mod h1:IYtCusqpEGS0dhC6F8X9GHrrt1gp1zHaNhSKGYV59Xg= github.com/orange-cloudavenue/common-go/utils v0.0.0-20240119163616-66b473d92339 h1:DEKcWLGbEhu/I6kn9NAXhVCFrbPhR+Ef7oLmpLVnnPM= diff --git a/internal/provider/elb/pool_datasource.go b/internal/provider/elb/pool_datasource.go new file mode 100644 index 00000000..5b21be76 --- /dev/null +++ b/internal/provider/elb/pool_datasource.go @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +// Package elb provides a Terraform datasource. +package elb + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + + "github.com/orange-cloudavenue/cloudavenue-sdk-go/v1/edgeloadbalancer" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/client" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/metrics" +) + +var ( + _ datasource.DataSource = &PoolDataSource{} + _ datasource.DataSourceWithConfigure = &PoolDataSource{} +) + +func NewPoolDataSource() datasource.DataSource { + return &PoolDataSource{} +} + +type PoolDataSource struct { + client *client.CloudAvenue + elb edgeloadbalancer.Client +} + +// Init Initializes the data source. +func (d *PoolDataSource) Init(ctx context.Context, dm *PoolModel) (diags diag.Diagnostics) { + var err error + + d.elb, err = edgeloadbalancer.NewClient() + if err != nil { + diags.AddError("Error creating elb client", err.Error()) + } + + return +} + +func (d *PoolDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + categoryName + "_pool" +} + +func (d *PoolDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = poolSchema(ctx).GetDataSource(ctx) +} + +func (d *PoolDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.CloudAvenue) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.CloudAvenue, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + d.client = client +} + +func (d *PoolDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + defer metrics.New("data.cloudavenue_elb_pool", d.client.GetOrgName(), metrics.Read)() + + config := &PoolModel{} + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, config)...) + if resp.Diagnostics.HasError() { + return + } + + // Init the resource + resp.Diagnostics.Append(d.Init(ctx, config)...) + if resp.Diagnostics.HasError() { + return + } + + /* + Implement the data source read logic here. + */ + + s := &PoolResource{ + client: d.client, + elb: d.elb, + } + + // Read data from the API + data, found, diags := s.read(ctx, config) + if !found { + resp.Diagnostics.AddError("Resource not found", "The resource was not found") + return + } + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/elb/pool_resource.go b/internal/provider/elb/pool_resource.go new file mode 100644 index 00000000..716b08f2 --- /dev/null +++ b/internal/provider/elb/pool_resource.go @@ -0,0 +1,489 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +package elb + +import ( + "context" + "fmt" + "strings" + + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/vmware/go-vcloud-director/v2/govcd" + + "github.com/hashicorp/terraform-plugin-framework/diag" + + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/orange-cloudavenue/cloudavenue-sdk-go/pkg/urn" + v1 "github.com/orange-cloudavenue/cloudavenue-sdk-go/v1" + "github.com/orange-cloudavenue/cloudavenue-sdk-go/v1/edgeloadbalancer" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/client" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/metrics" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/common/mutex" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &PoolResource{} + _ resource.ResourceWithConfigure = &PoolResource{} + _ resource.ResourceWithImportState = &PoolResource{} +) + +// NewPoolResource is a helper function to simplify the provider implementation. +func NewPoolResource() resource.Resource { + return &PoolResource{} +} + +// PoolResource is the resource implementation. +type PoolResource struct { + client *client.CloudAvenue + elb edgeloadbalancer.Client + edge *v1.EdgeClient +} + +// Init Initializes the resource. +func (r *PoolResource) Init(ctx context.Context, rm *PoolModel) (diags diag.Diagnostics) { + var err error + + r.elb, err = edgeloadbalancer.NewClient() + if err != nil { + diags.AddError("Error creating elb client", err.Error()) + } + + eIDOrName := rm.EdgeGatewayID.Get() + if eIDOrName == "" { + eIDOrName = rm.EdgeGatewayName.Get() + } + r.edge, err = r.client.CAVSDK.V1.EdgeGateway.Get(eIDOrName) + if err != nil { + diags.AddError("Error creating edge client", err.Error()) + } + + rm.EdgeGatewayID.Set(urn.Normalize(urn.Gateway, r.edge.GetID()).String()) + rm.EdgeGatewayName.Set(r.edge.GetName()) + + return +} + +// Metadata returns the resource type name. +func (r *PoolResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + categoryName + "_pool" +} + +// Schema defines the schema for the resource. +func (r *PoolResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = poolSchema(ctx).GetResource(ctx) +} + +func (r *PoolResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.CloudAvenue) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.CloudAvenue, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.client = client +} + +// Create creates the resource and sets the initial Terraform state. +func (r *PoolResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + defer metrics.New("cloudavenue_elb_pool", r.client.GetOrgName(), metrics.Create)() + + plan := &PoolModel{} + + // Retrieve values from plan + resp.Diagnostics.Append(req.Plan.Get(ctx, plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Init the resource + resp.Diagnostics.Append(r.Init(ctx, plan)...) + if resp.Diagnostics.HasError() { + return + } + + /* + Implement the resource creation logic here. + */ + + mutex.GlobalMutex.KvLock(ctx, plan.EdgeGatewayID.Get()) + defer mutex.GlobalMutex.KvUnlock(ctx, plan.EdgeGatewayID.Get()) + + model, d := plan.ToSDKPoolModelRequest(ctx, r.client) + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + poolCreated, err := r.elb.CreatePool(ctx, *model) + if err != nil { + resp.Diagnostics.AddError("Error creating pool", err.Error()) + return + } + + plan.ID.Set(poolCreated.ID) + + // Use generic read function to refresh the state + state, found, d := r.read(ctx, plan) + if !found { + resp.Diagnostics.AddError("Resource not found", "The resource was not found after creation") + return + } + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +// Read refreshes the Terraform state with the latest data. +func (r *PoolResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + defer metrics.New("cloudavenue_elb_pool", r.client.GetOrgName(), metrics.Read)() + + state := &PoolModel{} + + // Get current state + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + if resp.Diagnostics.HasError() { + return + } + + // Init the resource + resp.Diagnostics.Append(r.Init(ctx, state)...) + if resp.Diagnostics.HasError() { + return + } + + // Refresh the state + stateRefreshed, found, d := r.read(ctx, state) + if !found { + resp.Diagnostics.AddError("Resource not found", "The resource was not found after refresh") + resp.State.RemoveResource(ctx) + return + } + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, stateRefreshed)...) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *PoolResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + defer metrics.New("cloudavenue_elb_pool", r.client.GetOrgName(), metrics.Update)() + + var ( + plan = &PoolModel{} + state = &PoolModel{} + ) + + // Get current plan and state + resp.Diagnostics.Append(req.Plan.Get(ctx, plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + if resp.Diagnostics.HasError() { + return + } + + // Init the resource + resp.Diagnostics.Append(r.Init(ctx, state)...) + if resp.Diagnostics.HasError() { + return + } + + /* + Implement the resource update here + */ + + mutex.GlobalMutex.KvLock(ctx, plan.EdgeGatewayID.Get()) + defer mutex.GlobalMutex.KvUnlock(ctx, plan.EdgeGatewayID.Get()) + + model, d := plan.ToSDKPoolModelRequest(ctx, r.client) + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + _, err := r.elb.UpdatePool(ctx, state.ID.Get(), *model) + if err != nil { + resp.Diagnostics.AddError("Error updating pool", err.Error()) + return + } + + // Use generic read function to refresh the state + stateRefreshed, found, d := r.read(ctx, plan) + if !found { + resp.Diagnostics.AddError("Resource not found", "The resource was not found after update") + return + } + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, stateRefreshed)...) +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *PoolResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + defer metrics.New("cloudavenue_elb_pool", r.client.GetOrgName(), metrics.Delete)() + + state := &PoolModel{} + + // Get current state + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + if resp.Diagnostics.HasError() { + return + } + + // Init the resource + resp.Diagnostics.Append(r.Init(ctx, state)...) + if resp.Diagnostics.HasError() { + return + } + + /* + Implement the resource deletion here + */ + + mutex.GlobalMutex.KvLock(ctx, state.EdgeGatewayID.Get()) + defer mutex.GlobalMutex.KvUnlock(ctx, state.EdgeGatewayID.Get()) + + if err := r.elb.DeletePool(ctx, state.ID.Get()); err != nil { + resp.Diagnostics.AddError("Error deleting pool", err.Error()) + return + } +} + +func (r *PoolResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + defer metrics.New("cloudavenue_elb_pool", r.client.GetOrgName(), metrics.Import)() + + // Import format is edgeGatewayIDOrName.poolIDOrName + + // * Import with custom logic + idParts := strings.Split(req.ID, ".") + + if len(idParts) != 2 { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: edgeGatewayIDOrName.poolIDOrName. Got: %q", req.ID), + ) + return + } + + x := &PoolModel{ + ID: supertypes.NewStringNull(), + Name: supertypes.NewStringNull(), + EdgeGatewayID: supertypes.NewStringNull(), + EdgeGatewayName: supertypes.NewStringNull(), + } + + if urn.IsEdgeGateway(idParts[0]) { + x.EdgeGatewayID.Set(idParts[0]) + } else { + edge, err := r.client.CAVSDK.V1.EdgeGateway.Get(idParts[0]) + if err != nil { + resp.Diagnostics.AddError("Error retrieving Edge Gateway", err.Error()) + return + } + x.EdgeGatewayName.Set(idParts[0]) + x.EdgeGatewayID.Set(urn.Normalize(urn.Gateway, edge.GetID()).String()) + } + + if urn.IsLoadBalancerPool(idParts[1]) { + x.ID.Set(idParts[1]) + } else { + x.Name.Set(idParts[1]) + } + + resp.Diagnostics.Append(r.Init(ctx, x)...) + if resp.Diagnostics.HasError() { + return + } + + stateRefreshed, found, d := r.read(ctx, x) + if !found { + resp.State.RemoveResource(ctx) + return + } + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, stateRefreshed)...) +} + +// * CustomFuncs + +// read is a generic read function that can be used by the resource Create, Read and Update functions. +func (r *PoolResource) read(ctx context.Context, planOrState *PoolModel) (stateRefreshed *PoolModel, found bool, diags diag.Diagnostics) { + stateRefreshed = planOrState.Copy() + + /* + Implement the resource read here + */ + + idOrName := planOrState.ID.Get() + if idOrName == "" { + idOrName = planOrState.Name.Get() + } + + data, err := r.elb.GetPool(ctx, planOrState.EdgeGatewayID.Get(), idOrName) + if err != nil { + if govcd.ContainsNotFound(err) { + return nil, false, nil + } + diags.AddError("Error retrieving pool", err.Error()) + return nil, true, diags + } + + stateRefreshed.ID.Set(data.ID) + stateRefreshed.Name.Set(data.Name) + stateRefreshed.Description.Set(data.Description) + stateRefreshed.EdgeGatewayID.Set(data.GatewayRef.ID) + stateRefreshed.EdgeGatewayName.Set(data.GatewayRef.Name) + stateRefreshed.Enabled.SetPtr(data.Enabled) + stateRefreshed.Algorithm.Set(string(data.Algorithm)) + stateRefreshed.DefaultPort.SetIntPtr(data.DefaultPort) + + // * Members + members := &PoolModelMembers{ + GracefulTimeoutPeriod: supertypes.NewStringNull(), + TargetGroup: supertypes.NewStringNull(), + Targets: supertypes.NewListNestedObjectValueOfNull[PoolModelMembersIPAddress](ctx), + } + + if data.MemberGroupRef != nil { + members.TargetGroup.Set(data.MemberGroupRef.ID) + } + if data.GracefulTimeoutPeriod != nil { + members.GracefulTimeoutPeriod.Set(fmt.Sprintf("%d", *data.GracefulTimeoutPeriod)) + } + + if len(data.Members) != 0 { + ipAddress := make([]*PoolModelMembersIPAddress, 0) + for _, m := range data.Members { + ipa := &PoolModelMembersIPAddress{ + Enabled: supertypes.NewBoolNull(), + IPAddress: supertypes.NewStringNull(), + Port: supertypes.NewInt64Null(), + Ratio: supertypes.NewInt64Null(), + } + + ipa.Enabled.Set(m.Enabled) + ipa.IPAddress.Set(m.IPAddress) + ipa.Port.SetInt(m.Port) + ipa.Ratio.SetIntPtr(m.Ratio) + + ipAddress = append(ipAddress, ipa) + } + + diags.Append(members.Targets.Set(ctx, ipAddress)...) + if diags.HasError() { + return nil, true, diags + } + } + + diags.Append(stateRefreshed.Members.Set(ctx, members)...) + if diags.HasError() { + return nil, true, diags + } + + // * Health + health := &PoolModelHealth{ + PassiveMonitoringEnabled: supertypes.NewBoolNull(), + Monitors: supertypes.NewListValueOfNull[string](ctx), + } + + health.PassiveMonitoringEnabled.SetPtr(data.PassiveMonitoringEnabled) + + // prevent unexpected new value: .health.monitors: was null, but now cty.ListValEmpty(cty.String). + if len(data.HealthMonitors) != 0 { + monitors := []string{} + for _, m := range data.HealthMonitors { + monitors = append(monitors, string(m.Type)) + } + + diags.Append(health.Monitors.Set(ctx, monitors)...) + if diags.HasError() { + return nil, true, diags + } + } + + diags.Append(stateRefreshed.Health.Set(ctx, health)...) + if diags.HasError() { + return nil, true, diags + } + + // * TLS + tls := &PoolModelTLS{ + Enabled: supertypes.NewBoolNull(), + DomainNames: supertypes.NewListValueOfNull[string](ctx), + CaCertificateRefs: supertypes.NewListValueOfNull[string](ctx), + CommonNameCheckEnabled: supertypes.NewBoolNull(), + } + + tls.Enabled.SetPtr(data.SSLEnabled) + tls.CommonNameCheckEnabled.SetPtr(data.CommonNameCheckEnabled) + diags.Append(tls.DomainNames.Set(ctx, data.DomainNames)...) + if diags.HasError() { + return nil, true, diags + } + + // prevent unexpected new value: .tls.ca_certificate_refs: was null, but now cty.ListValEmpty(cty.String). + if len(data.CaCertificateRefs) != 0 { + refs := []string{} + for _, ca := range data.CaCertificateRefs { + refs = append(refs, ca.ID) + } + + diags.Append(tls.CaCertificateRefs.Set(ctx, refs)...) + if diags.HasError() { + return nil, true, diags + } + } + + diags.Append(stateRefreshed.TLS.Set(ctx, tls)...) + if diags.HasError() { + return nil, true, diags + } + + // * Persistence + persistence := &PoolModelPersistence{ + Type: supertypes.NewStringNull(), + Value: supertypes.NewStringNull(), + } + + if data.PersistenceProfile != nil { + persistence.Type.Set(string(data.PersistenceProfile.Type)) + persistence.Value.Set(data.PersistenceProfile.Value) + } + diags.Append(stateRefreshed.Persistence.Set(ctx, persistence)...) + if diags.HasError() { + return nil, true, diags + } + + return stateRefreshed, true, nil +} diff --git a/internal/provider/elb/pool_schema.go b/internal/provider/elb/pool_schema.go new file mode 100644 index 00000000..48fd3885 --- /dev/null +++ b/internal/provider/elb/pool_schema.go @@ -0,0 +1,441 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +package elb + +import ( + "context" + "slices" + + superschema "github.com/orange-cloudavenue/terraform-plugin-framework-superschema" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + fstringvalidator "github.com/orange-cloudavenue/terraform-plugin-framework-validators/stringvalidator" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + schemaD "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + schemaR "github.com/hashicorp/terraform-plugin-framework/resource/schema" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + + "github.com/orange-cloudavenue/cloudavenue-sdk-go/pkg/urn" + "github.com/orange-cloudavenue/cloudavenue-sdk-go/v1/edgeloadbalancer" +) + +func poolSchema(ctx context.Context) superschema.Schema { + return superschema.Schema{ + Resource: superschema.SchemaDetails{ + MarkdownDescription: "The `cloudavenue_elb_pool` resource allows you to manage edgegateway load balancer pools. Pools maintain the list of servers assigned to them and perform health monitoring, load balancing, persistence. A pool may only be used or referenced by only one virtual service at a time.", + }, + DataSource: superschema.SchemaDetails{ + MarkdownDescription: "The `cloudavenue_elb_pool` data source allows you to retrieve information about an existing edgegateway load balancer pool.", + }, + Attributes: map[string]superschema.Attribute{ + "id": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + Computed: true, + MarkdownDescription: "The ID of the pool.", + }, + Resource: &schemaR.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + "name": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The name of the pool.", + Required: true, + }, + }, + "edge_gateway_name": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The name of the Edge Gateway.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("edge_gateway_name"), path.MatchRoot("edge_gateway_id")), + }, + }, + }, + "edge_gateway_id": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The ID of the Edge Gateway.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("edge_gateway_name"), path.MatchRoot("edge_gateway_id")), + }, + }, + }, + "description": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The name of the pool.", + }, + Resource: &schemaR.StringAttribute{ + Optional: true, + }, + DataSource: &schemaD.StringAttribute{ + Computed: true, + }, + }, + "enabled": superschema.SuperBoolAttribute{ + Common: &schemaR.BoolAttribute{ + MarkdownDescription: "Enable or disable the pool.", + Computed: true, + }, + Resource: &schemaR.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(true), + }, + }, + "algorithm": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The heart of a load balancer is its ability to effectively distribute traffic across healthy servers. If persistence is enabled, only the first connection from a client is load balanced. While the persistence remains in effect, subsequent connections or requests from a client are directed to the same server.", + Computed: true, + }, + Resource: &schemaR.StringAttribute{ + Optional: true, + Default: stringdefault.StaticString(string(edgeloadbalancer.PoolAlgorithmLeastConnections)), + Validators: []validator.String{ + fstringvalidator.OneOfWithDescription(func() (resp []fstringvalidator.OneOfWithDescriptionValues) { + x := []string{} + + for k := range edgeloadbalancer.PoolAlgorithms { + x = append(x, string(k)) + } + + slices.Sort(x) + + for _, v := range x { + resp = append(resp, fstringvalidator.OneOfWithDescriptionValues{ + Value: v, + Description: edgeloadbalancer.PoolAlgorithms[edgeloadbalancer.PoolAlgorithm(v)], + }) + } + return + }()...), + }, + }, + }, + "default_port": superschema.SuperInt64Attribute{ + Common: &schemaR.Int64Attribute{ + MarkdownDescription: "DefaultPort defines destination server port used by the traffic sent to the member.", + }, + Resource: &schemaR.Int64Attribute{ + Required: true, + }, + DataSource: &schemaD.Int64Attribute{ + Computed: true, + }, + }, + "members": superschema.SuperSingleNestedAttributeOf[PoolModelMembers]{ + Common: &schemaR.SingleNestedAttribute{ + Description: "The members of the pool.", + }, + Resource: &schemaR.SingleNestedAttribute{ + Required: true, + }, + DataSource: &schemaD.SingleNestedAttribute{ + Computed: true, + }, + Attributes: superschema.Attributes{ + "graceful_timeout_period": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "Maximum time (in minutes) to gracefully disable a member. Virtual service waits for the specified time before terminating the existing connections to the members that are disabled. Special values: `0` represents `Immediate` and `-1` represents `Infinite`. The maximum value is `7200` minutes.", + Computed: true, + }, + Resource: &schemaR.StringAttribute{ + Optional: true, + Default: stringdefault.StaticString("1"), + }, + }, + "target_group": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The group contains reference to the Edge Firewall Group representing destination servers which are used by the Load Balancer Pool to direct load balanced traffic. This permit to reference `IP Set` or `Static Group` ID.", + }, + Resource: &schemaR.StringAttribute{ + Optional: true, + Validators: []validator.String{ + fstringvalidator.PrefixContains(urn.SecurityGroup.String()), + stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("targets"), path.MatchRelative().AtParent().AtName("target_group")), + }, + }, + DataSource: &schemaD.StringAttribute{ + Computed: true, + }, + }, + "targets": superschema.SuperListNestedAttributeOf[PoolModelMembersIPAddress]{ + Common: &schemaR.ListNestedAttribute{ + MarkdownDescription: "targets field defines list of destination servers which are used by the Load Balancer Pool to direct load balanced traffic.", + }, + Resource: &schemaR.ListNestedAttribute{ + Optional: true, + Validators: []validator.List{ + listvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("targets"), path.MatchRelative().AtParent().AtName("target_group")), + }, + }, + DataSource: &schemaD.ListNestedAttribute{ + Computed: true, + }, + Attributes: superschema.Attributes{ + "enabled": superschema.SuperBoolAttribute{ + Common: &schemaR.BoolAttribute{ + MarkdownDescription: "Enable or disable the member.", + Computed: true, + }, + Resource: &schemaR.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(true), + }, + }, + "ip_address": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The IP address of the member.", + }, + Resource: &schemaR.StringAttribute{ + Required: true, + Validators: []validator.String{ + fstringvalidator.IsNetwork([]fstringvalidator.NetworkValidatorType{ + fstringvalidator.IPV4, + }, + false, + ), + }, + }, + DataSource: &schemaD.StringAttribute{ + Computed: true, + }, + }, + "port": superschema.SuperInt64Attribute{ + Common: &schemaR.Int64Attribute{ + MarkdownDescription: "The port of the member.", + }, + Resource: &schemaR.Int64Attribute{ + Required: true, + }, + DataSource: &schemaD.Int64Attribute{ + Computed: true, + }, + }, + "ratio": superschema.SuperInt64Attribute{ + Common: &schemaR.Int64Attribute{ + MarkdownDescription: "The ratio of the member. The ratio of each pool member denotes the traffic that goes to each server pool member. A server with a ratio of 2 gets twice as much traffic as a server with a ratio of 1.", + Computed: true, + }, + Resource: &schemaR.Int64Attribute{ + Optional: true, + Default: int64default.StaticInt64(1), + }, + }, + }, + }, + }, + }, + "health": superschema.SuperSingleNestedAttributeOf[PoolModelHealth]{ + Common: &schemaR.SingleNestedAttribute{ + Description: "Health check member servers health. It can be monitored by using one or more health monitors. Active monitors generate synthetic traffic and mark a server up or down based on the response.", + Computed: true, + }, + Resource: &schemaR.SingleNestedAttribute{ + Optional: true, + Default: objectdefault.StaticValue(supertypes.NewObjectValueOf[PoolModelHealth](ctx, &PoolModelHealth{ + PassiveMonitoringEnabled: supertypes.NewBoolValue(true), + Monitors: supertypes.NewListValueOfNull[string](ctx), + }).ObjectValue), + }, + Attributes: superschema.Attributes{ + "passive_monitoring_enabled": superschema.SuperBoolAttribute{ + Common: &schemaR.BoolAttribute{ + MarkdownDescription: "PassiveMonitoringEnabled sets if client traffic should be used to check if pool member is up or down.", + Computed: true, + }, + Resource: &schemaR.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(true), + }, + }, + "monitors": superschema.SuperListAttributeOf[string]{ + Common: &schemaR.ListAttribute{ + MarkdownDescription: "The active health monitors.", + }, + Resource: &schemaR.ListAttribute{ + Optional: true, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(func() (resp []string) { + for _, v := range edgeloadbalancer.PoolHealthMonitorTypes { + resp = append(resp, string(v)) + } + slices.Sort(resp) + return + }()...), + ), + }, + }, + DataSource: &schemaD.ListAttribute{ + Computed: true, + }, + }, + }, + }, + "tls": superschema.SuperSingleNestedAttributeOf[PoolModelTLS]{ + Common: &schemaR.SingleNestedAttribute{ + Description: "The TLS configuration of the pool.", + Computed: true, + }, + Resource: &schemaR.SingleNestedAttribute{ + Optional: true, + Default: objectdefault.StaticValue(supertypes.NewObjectValueOf[PoolModelTLS](ctx, &PoolModelTLS{ + Enabled: supertypes.NewBoolValue(false), + DomainNames: supertypes.NewListValueOfNull[string](ctx), + CaCertificateRefs: supertypes.NewListValueOfNull[string](ctx), + CommonNameCheckEnabled: supertypes.NewBoolValue(false), + }).ObjectValue), + }, + Attributes: superschema.Attributes{ + "enabled": superschema.SuperBoolAttribute{ + Common: &schemaR.BoolAttribute{ + MarkdownDescription: "Enable or disable the TLS.", + Computed: true, + }, + Resource: &schemaR.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(false), + }, + }, + "domain_names": superschema.SuperListAttributeOf[string]{ + Common: &schemaR.ListAttribute{ + MarkdownDescription: "The domain names of the TLS check. This attribute is taken into account if the `common_name_check_enabled` is set to `true`.", + }, + Resource: &schemaR.ListAttribute{ + Optional: true, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 10), + }, + }, + DataSource: &schemaD.ListAttribute{ + Computed: true, + }, + }, + "ca_certificate_refs": superschema.SuperListAttributeOf[string]{ + Common: &schemaR.ListAttribute{ + MarkdownDescription: "The CA certificate references point to root certificates to use when validating certificates presented by the pool members.", + }, + Resource: &schemaR.ListAttribute{ + MarkdownDescription: "Use `cloudavenue_org_certificate` resource to create a certificate and get the ID.", + Optional: true, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + fstringvalidator.PrefixContains(urn.CertificateLibraryItem.String()), + ), + }, + }, + DataSource: &schemaD.ListAttribute{ + Computed: true, + }, + }, + "common_name_check_enabled": superschema.SuperBoolAttribute{ + Common: &schemaR.BoolAttribute{ + MarkdownDescription: "Enable common name check for server certificate. If enabled and no explicit domain name is specified, the incoming host header will be used to do the match.", + Computed: true, + }, + Resource: &schemaR.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + }, + "persistence": superschema.SuperSingleNestedAttributeOf[PoolModelPersistence]{ + Common: &schemaR.SingleNestedAttribute{ + Description: "Persistence profile will ensure that the same user sticks to the same server for a desired duration of time. If the persistence profile is unmanaged by ELB, updates that leave the values unchanged will continue to use the same unmanaged profile. Any changes made to the persistence profile will cause ELB to switch the pool to a profile managed by ELB.", + Computed: true, + }, + Resource: &schemaR.SingleNestedAttribute{ + Optional: true, + Default: objectdefault.StaticValue(supertypes.NewObjectValueOf[PoolModelPersistence](ctx, &PoolModelPersistence{ + Type: supertypes.NewStringValue(string(edgeloadbalancer.PoolPersistenceProfileTypeClientIP)), + Value: supertypes.NewStringNull(), + }).ObjectValue), + }, + Attributes: superschema.Attributes{ + "type": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The type of the persistence.", + Computed: true, + }, + Resource: &schemaR.StringAttribute{ + Optional: true, + Default: stringdefault.StaticString(string(edgeloadbalancer.PoolPersistenceProfileTypeClientIP)), + Validators: []validator.String{ + fstringvalidator.OneOfWithDescription(func() (resp []fstringvalidator.OneOfWithDescriptionValues) { + x := []string{} + + for k := range edgeloadbalancer.PoolPersistenceProfileTypes { + x = append(x, string(k)) + } + + slices.Sort(x) + + for _, v := range x { + resp = append(resp, fstringvalidator.OneOfWithDescriptionValues{ + Value: v, + Description: edgeloadbalancer.PoolPersistenceProfileTypes[edgeloadbalancer.PoolPersistenceProfileType(v)], + }) + } + return + }()...), + }, + }, + }, + "value": superschema.SuperStringAttribute{ + Common: &schemaR.StringAttribute{ + MarkdownDescription: "The value of the persistence.", + }, + Resource: &schemaR.StringAttribute{ + Optional: true, + Validators: []validator.String{ + fstringvalidator.RequireIfAttributeIsOneOf(path.MatchRelative().AtParent().AtName("type"), func() (resp []attr.Value) { + resp = make([]attr.Value, 0) + resp = append(resp, types.StringValue(string(edgeloadbalancer.PoolPersistenceProfileTypeHTTPCookie))) + resp = append(resp, types.StringValue(string(edgeloadbalancer.PoolPersistenceProfileTypeCustomHTTPHeader))) + resp = append(resp, types.StringValue(string(edgeloadbalancer.PoolPersistenceProfileTypeAPPCookie))) + return + }()), + }, + }, + DataSource: &schemaD.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + } +} diff --git a/internal/provider/elb/pool_schema_test.go b/internal/provider/elb/pool_schema_test.go new file mode 100644 index 00000000..dedfbb6a --- /dev/null +++ b/internal/provider/elb/pool_schema_test.go @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +package elb_test + +import ( + "context" + "testing" + + // fwdatasource "github.com/hashicorp/terraform-plugin-framework/datasource". + fwdatasource "github.com/hashicorp/terraform-plugin-framework/datasource" + fwresource "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/elb" +) + +// Unit test for the schema of the resource cloudavenue_elb_pool. +func TestPoolResourceSchema(t *testing.T) { + t.Parallel() + + ctx := context.Background() + schemaResponse := &fwresource.SchemaResponse{} + + // Instantiate the resource.Resource and call its Schema method + elb.NewPoolResource().Schema(ctx, fwresource.SchemaRequest{}, schemaResponse) + + if schemaResponse.Diagnostics.HasError() { + t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) + } + + // Validate the schema + diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) + + if diagnostics.HasError() { + t.Fatalf("Schema validation diagnostics: %+v", diagnostics) + } +} + +// Unit test for the schema of the datasource cloudavenue_elb_pool + +func TestPoolDataSourceSchema(t *testing.T) { + t.Parallel() + + ctx := context.Background() + schemaResponse := &fwdatasource.SchemaResponse{} + + // Instantiate the datasource.Datasource and call its Schema method + elb.NewPoolDataSource().Schema(ctx, fwdatasource.SchemaRequest{}, schemaResponse) + + if schemaResponse.Diagnostics.HasError() { + t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) + } + + // Validate the schema + diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) + + if diagnostics.HasError() { + t.Fatalf("Schema validation diagnostics: %+v", diagnostics) + } +} diff --git a/internal/provider/elb/pool_types.go b/internal/provider/elb/pool_types.go new file mode 100644 index 00000000..8c0933a4 --- /dev/null +++ b/internal/provider/elb/pool_types.go @@ -0,0 +1,205 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +package elb + +import ( + "context" + "strconv" + + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + govcdtypes "github.com/vmware/go-vcloud-director/v2/types/v56" + + "github.com/hashicorp/terraform-plugin-framework/diag" + + "github.com/orange-cloudavenue/cloudavenue-sdk-go/v1/edgeloadbalancer" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/client" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/pkg/utils" +) + +type ( + PoolModel struct { + ID supertypes.StringValue `tfsdk:"id"` + Name supertypes.StringValue `tfsdk:"name"` + Description supertypes.StringValue `tfsdk:"description"` + EdgeGatewayID supertypes.StringValue `tfsdk:"edge_gateway_id"` + EdgeGatewayName supertypes.StringValue `tfsdk:"edge_gateway_name"` + Enabled supertypes.BoolValue `tfsdk:"enabled"` + Algorithm supertypes.StringValue `tfsdk:"algorithm"` + DefaultPort supertypes.Int64Value `tfsdk:"default_port"` + Members supertypes.SingleNestedObjectValueOf[PoolModelMembers] `tfsdk:"members"` + Health supertypes.SingleNestedObjectValueOf[PoolModelHealth] `tfsdk:"health"` + TLS supertypes.SingleNestedObjectValueOf[PoolModelTLS] `tfsdk:"tls"` + Persistence supertypes.SingleNestedObjectValueOf[PoolModelPersistence] `tfsdk:"persistence"` + } + + PoolModelMembers struct { + GracefulTimeoutPeriod supertypes.StringValue `tfsdk:"graceful_timeout_period"` + TargetGroup supertypes.StringValue `tfsdk:"target_group"` + Targets supertypes.ListNestedObjectValueOf[PoolModelMembersIPAddress] `tfsdk:"targets"` + } + + PoolModelMembersIPAddress struct { + Enabled supertypes.BoolValue `tfsdk:"enabled"` + IPAddress supertypes.StringValue `tfsdk:"ip_address"` + Port supertypes.Int64Value `tfsdk:"port"` + Ratio supertypes.Int64Value `tfsdk:"ratio"` + } + + PoolModelHealth struct { + PassiveMonitoringEnabled supertypes.BoolValue `tfsdk:"passive_monitoring_enabled"` + Monitors supertypes.ListValueOf[string] `tfsdk:"monitors"` + } + + PoolModelTLS struct { + Enabled supertypes.BoolValue `tfsdk:"enabled"` + DomainNames supertypes.ListValueOf[string] `tfsdk:"domain_names"` + CaCertificateRefs supertypes.ListValueOf[string] `tfsdk:"ca_certificate_refs"` + CommonNameCheckEnabled supertypes.BoolValue `tfsdk:"common_name_check_enabled"` + } + + PoolModelPersistence struct { + Type supertypes.StringValue `tfsdk:"type"` + Value supertypes.StringValue `tfsdk:"value"` + } +) + +func (rm *PoolModel) Copy() *PoolModel { + x := &PoolModel{} + utils.ModelCopy(rm, x) + return x +} + +// ToSDKPoolGroupModel converts the model to the SDK model. +func (rm *PoolModel) ToSDKPoolModelRequest(ctx context.Context, cavClient *client.CloudAvenue) (*edgeloadbalancer.PoolModelRequest, diag.Diagnostics) { + var diags diag.Diagnostics + + pool := &edgeloadbalancer.PoolModelRequest{ + Name: rm.Name.Get(), + Description: rm.Description.Get(), + Enabled: rm.Enabled.GetPtr(), + Algorithm: edgeloadbalancer.PoolAlgorithm(rm.Algorithm.Get()), + DefaultPort: rm.DefaultPort.GetIntPtr(), + GatewayRef: govcdtypes.OpenApiReference{ID: rm.EdgeGatewayID.Get(), Name: rm.EdgeGatewayName.Get()}, + } + + if rm.Members.IsKnown() { + x, d := rm.Members.Get(ctx) + diags = append(diags, d...) + if diags.HasError() { + return nil, diags + } + + if x.GracefulTimeoutPeriod.IsKnown() { + i, err := strconv.Atoi(x.GracefulTimeoutPeriod.Get()) + if err != nil { + diags.AddError("Error converting GracefulTimeoutPeriod to int", err.Error()) + return nil, diags + } + pool.GracefulTimeoutPeriod = &i + } + + if x.TargetGroup.IsKnown() { + pool.MemberGroupRef = &govcdtypes.OpenApiReference{ + ID: x.TargetGroup.Get(), + } + } + + if x.Targets.IsKnown() { + ipAddrs, d := x.Targets.Get(ctx) + diags = append(diags, d...) + if diags.HasError() { + return nil, diags + } + + members := make([]edgeloadbalancer.PoolModelMember, 0) + for _, m := range ipAddrs { + members = append(members, edgeloadbalancer.PoolModelMember{ + Enabled: m.Enabled.Get(), + IPAddress: m.IPAddress.Get(), + Port: m.Port.GetInt(), + Ratio: m.Ratio.GetIntPtr(), + }) + } + pool.Members = members + } + } + + if rm.Health.IsKnown() { + x, d := rm.Health.Get(ctx) + diags = append(diags, d...) + if diags.HasError() { + return nil, diags + } + + pool.PassiveMonitoringEnabled = x.PassiveMonitoringEnabled.GetPtr() + if x.Monitors.IsKnown() { + pool.HealthMonitors = make([]edgeloadbalancer.PoolModelHealthMonitor, 0) + monitors, d := x.Monitors.Get(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + for _, m := range monitors { + pool.HealthMonitors = append(pool.HealthMonitors, edgeloadbalancer.PoolModelHealthMonitor{ + Type: edgeloadbalancer.PoolHealthMonitorType(m), + }) + } + } + } + + if rm.TLS.IsKnown() { + x, d := rm.TLS.Get(ctx) + diags = append(diags, d...) + if diags.HasError() { + return nil, diags + } + + pool.SSLEnabled = x.Enabled.GetPtr() + pool.CommonNameCheckEnabled = x.CommonNameCheckEnabled.GetPtr() + if x.DomainNames.IsKnown() { + pool.DomainNames, d = x.DomainNames.Get(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + } + + if x.CaCertificateRefs.IsKnown() { + refs, d := x.CaCertificateRefs.Get(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + for _, ref := range refs { + pool.CaCertificateRefs = append(pool.CaCertificateRefs, govcdtypes.OpenApiReference{ + ID: ref, + }) + } + } + } + + if rm.Persistence.IsKnown() { + x, d := rm.Persistence.Get(ctx) + diags = append(diags, d...) + if diags.HasError() { + return nil, diags + } + + pool.PersistenceProfile = &edgeloadbalancer.PoolModelPersistenceProfile{ + Type: edgeloadbalancer.PoolPersistenceProfileType(x.Type.Get()), + Value: x.Value.Get(), + } + } + + return pool, diags +} diff --git a/internal/provider/provider_datasources.go b/internal/provider/provider_datasources.go index 00c11d8d..4ab08a00 100644 --- a/internal/provider/provider_datasources.go +++ b/internal/provider/provider_datasources.go @@ -35,10 +35,6 @@ import ( // DataSources defines the data sources implemented in the provider. func (p *cloudavenueProvider) DataSources(_ context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ - // * EdgeGateway LoadBalancer - elb.NewServiceEngineGroupDataSource, - elb.NewServiceEngineGroupsDataSource, - // * TIER0 vrf.NewTier0VrfsDataSource, vrf.NewTier0VrfDataSource, @@ -46,7 +42,7 @@ func (p *cloudavenueProvider) DataSources(_ context.Context) []func() datasource // * PUBLICIP publicip.NewPublicIPDataSource, - // * EDGE GATEWAY + // * EdgeGateway edgegw.NewEdgeGatewayDataSource, edgegw.NewEdgeGatewaysDataSource, edgegw.NewFirewallDataSource, @@ -58,6 +54,11 @@ func (p *cloudavenueProvider) DataSources(_ context.Context) []func() datasource edgegw.NewVPNIPSecDataSource, edgegw.NewAppPortProfileDataSource, + // * EdgeGateway LoadBalancer + elb.NewServiceEngineGroupDataSource, + elb.NewServiceEngineGroupsDataSource, + elb.NewPoolDataSource, + // * VDC vdc.NewVDCsDataSource, vdc.NewVDCDataSource, diff --git a/internal/provider/provider_resources.go b/internal/provider/provider_resources.go index 775cfd48..8c8e63f5 100644 --- a/internal/provider/provider_resources.go +++ b/internal/provider/provider_resources.go @@ -17,6 +17,7 @@ import ( "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/backup" "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/catalog" "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/edgegw" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/elb" "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/iam" "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/network" "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/provider/org" @@ -32,7 +33,7 @@ import ( // Resources defines the resources implemented in the provider. func (p *cloudavenueProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ - // * EDGE GATEWAY + // * EdgeGateway edgegw.NewEdgeGatewayResource, edgegw.NewFirewallResource, edgegw.NewAppPortProfileResource, @@ -43,6 +44,9 @@ func (p *cloudavenueProvider) Resources(_ context.Context) []func() resource.Res edgegw.NewNATRuleResource, edgegw.NewVPNIPSecResource, + // * EdgeGateway LoadBalancer + elb.NewPoolResource, + // * VDC vdc.NewVDCResource, vdc.NewACLResource, diff --git a/internal/testsacc/acctest_datasources_test.go b/internal/testsacc/acctest_datasources_test.go index 75380328..61fe3e69 100644 --- a/internal/testsacc/acctest_datasources_test.go +++ b/internal/testsacc/acctest_datasources_test.go @@ -53,6 +53,9 @@ func GetDataSourceConfig() map[testsacc.ResourceName]func() *testsacc.ResourceCo EdgeGatewayNATRuleDataSourceName: testsacc.NewResourceConfig(NewEdgeGatewayNATRuleDataSourceTest()), EdgeGatewayIPSetDataSourceName: testsacc.NewResourceConfig(NewEdgeGatewayIPSetDataSourceTest()), + // * EdgeGateway LoadBalancer (elb) + ELBPoolDataSourceName: testsacc.NewResourceConfig(NewELBPoolDataSourceTest()), + // * S3 S3BucketVersioningConfigurationDatasourceName: testsacc.NewResourceConfig(NewS3BucketVersioningConfigurationDatasourceTest()), S3BucketDatasourceName: testsacc.NewResourceConfig(NewS3BucketDatasourceTest()), diff --git a/internal/testsacc/acctest_resources_test.go b/internal/testsacc/acctest_resources_test.go index 9099d04a..84b8ba32 100644 --- a/internal/testsacc/acctest_resources_test.go +++ b/internal/testsacc/acctest_resources_test.go @@ -49,6 +49,9 @@ func GetResourceConfig() map[testsacc.ResourceName]func() *testsacc.ResourceConf EdgeGatewayNATRuleResourceName: testsacc.NewResourceConfig(NewEdgeGatewayNATRuleResourceTest()), EdgeGatewayIPSetResourceName: testsacc.NewResourceConfig(NewEdgeGatewayIPSetResourceTest()), + // * EdgeGateway LoadBalancer (elb) + ELBPoolResourceName: testsacc.NewResourceConfig(NewELBPoolResourceTest()), + // * Backup BackupResourceName: testsacc.NewResourceConfig(NewBackupResourceTest()), diff --git a/internal/testsacc/edgegw_edgegateway_datasource_test.go b/internal/testsacc/edgegw_edgegateway_datasource_test.go index d93c0b3a..754f2277 100644 --- a/internal/testsacc/edgegw_edgegateway_datasource_test.go +++ b/internal/testsacc/edgegw_edgegateway_datasource_test.go @@ -63,6 +63,19 @@ func (r *EdgeGatewayDataSource) Tests(ctx context.Context) map[testsacc.TestName Create: testsacc.TFConfig{ TFConfig: ` data "cloudavenue_edgegateway" "example_with_id" { + id = cloudavenue_edgegateway.example.id + }`, + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttrWith(resourceName, "id", urn.TestIsType(urn.Gateway)), + }, + }, + } + }, + "example_for_elb": func(_ context.Context, resourceName string) testsacc.Test { + return testsacc.Test{ + Create: testsacc.TFConfig{ + TFConfig: ` + data "cloudavenue_edgegateway" "example_for_elb" { name = "tn01e02ocb0006205spt101" }`, Checks: []resource.TestCheckFunc{ diff --git a/internal/testsacc/edgegw_ip_set_resource_test.go b/internal/testsacc/edgegw_ip_set_resource_test.go index ba071c20..92a0f95c 100644 --- a/internal/testsacc/edgegw_ip_set_resource_test.go +++ b/internal/testsacc/edgegw_ip_set_resource_test.go @@ -157,6 +157,28 @@ func (r *EdgeGatewayIPSetResource) Tests(ctx context.Context) map[testsacc.TestN }, } }, + "example_for_elb": func(_ context.Context, resourceName string) testsacc.Test { + return testsacc.Test{ + CommonDependencies: func() (resp testsacc.DependenciesConfigResponse) { + resp.Append(GetDataSourceConfig()[EdgeGatewayDataSourceName]().GetSpecificConfig("example_for_elb")) + return + }, + // ! Create testing + Create: testsacc.TFConfig{ + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_edgegateway_ip_set" "example_for_elb" { + name = {{ generate . "name" }} + description = {{ generate . "description" }} + ip_addresses = [ + "192.168.1.1", + "192.168.1.2", + "192.168.1.3", + ] + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + }`), + }, + } + }, } } diff --git a/internal/testsacc/elb_pool_datasource_test.go b/internal/testsacc/elb_pool_datasource_test.go new file mode 100644 index 00000000..3d4ddb93 --- /dev/null +++ b/internal/testsacc/elb_pool_datasource_test.go @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +package testsacc + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/helpers/testsacc" +) + +var _ testsacc.TestACC = &ELBPoolDataSource{} + +const ( + ELBPoolDataSourceName = testsacc.ResourceName("data.cloudavenue_elb_pool") +) + +type ELBPoolDataSource struct{} + +func NewELBPoolDataSourceTest() testsacc.TestACC { + return &ELBPoolDataSource{} +} + +// GetResourceName returns the name of the resource. +func (r *ELBPoolDataSource) GetResourceName() string { + return ELBPoolDataSourceName.String() +} + +func (r *ELBPoolDataSource) DependenciesConfig() (resp testsacc.DependenciesConfigResponse) { + // Add dependencies config to the resource + resp.Append(GetResourceConfig()[ELBPoolResourceName]().GetDefaultConfig) + return +} + +func (r *ELBPoolDataSource) Tests(ctx context.Context) map[testsacc.TestName]func(ctx context.Context, resourceName string) testsacc.Test { + return map[testsacc.TestName]func(ctx context.Context, resourceName string) testsacc.Test{ + // * Test One (example) + "example": func(_ context.Context, _ string) testsacc.Test { + return testsacc.Test{ + // ! Create testing + Create: testsacc.TFConfig{ + TFConfig: ` + data "cloudavenue_elb_pool" "example" { + name = cloudavenue_elb_pool.example.name + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + }`, + // Here use resource config test to test the data source + // the field example is the name of the test + Checks: GetResourceConfig()[ELBPoolResourceName]().GetDefaultChecks(), + }, + } + }, + } +} + +func TestAccELBPoolDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: TestAccProtoV6ProviderFactories, + Steps: testsacc.GenerateTests(&ELBPoolDataSource{}), + }) +} diff --git a/internal/testsacc/elb_pool_resource_test.go b/internal/testsacc/elb_pool_resource_test.go new file mode 100644 index 00000000..46bce99d --- /dev/null +++ b/internal/testsacc/elb_pool_resource_test.go @@ -0,0 +1,515 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange + * SPDX-License-Identifier: Mozilla Public License 2.0 + * + * This software is distributed under the MPL-2.0 license. + * the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/ + * or see the "LICENSE" file for more details. + */ + +package testsacc + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/orange-cloudavenue/cloudavenue-sdk-go/pkg/urn" + "github.com/orange-cloudavenue/terraform-provider-cloudavenue/internal/helpers/testsacc" +) + +var _ testsacc.TestACC = &ELBPoolResource{} + +const ( + ELBPoolResourceName = testsacc.ResourceName("cloudavenue_elb_pool") +) + +type ELBPoolResource struct{} + +func NewELBPoolResourceTest() testsacc.TestACC { + return &ELBPoolResource{} +} + +// GetResourceName returns the name of the resource. +func (r *ELBPoolResource) GetResourceName() string { + return ELBPoolResourceName.String() +} + +func (r *ELBPoolResource) DependenciesConfig() (resp testsacc.DependenciesConfigResponse) { + resp.Append(GetDataSourceConfig()[EdgeGatewayDataSourceName]().GetSpecificConfig("example_for_elb")) + return +} + +func (r *ELBPoolResource) Tests(ctx context.Context) map[testsacc.TestName]func(ctx context.Context, resourceName string) testsacc.Test { + return map[testsacc.TestName]func(ctx context.Context, resourceName string) testsacc.Test{ + "example": func(_ context.Context, resourceName string) testsacc.Test { + return testsacc.Test{ + CommonChecks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttrWith(resourceName, "id", urn.TestIsType(urn.LoadBalancerPool)), + }, + CommonDependencies: func() (resp testsacc.DependenciesConfigResponse) { + return + }, + // ! Create testing + Create: testsacc.TFConfig{ + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example" { + name = {{ generate . "name" }} + description = {{ generate . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckResourceAttr(resourceName, "members.targets.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "members.targets.*", map[string]string{ + "ip_address": "192.168.0.1", + "port": "80", + // Default values + "ratio": "1", + "enabled": "true", + }), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CLIENT_IP"), + }, + }, + // ! Updates testing + Updates: []testsacc.TFConfig{ + // * Update name and add a new disabled target + { + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example" { + name = {{ generate . "name" }} + description = {{ get . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + }, + { + ip_address = "192.168.0.2" + port = 8080 + enabled = false + ratio = 2 + } + ] + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckResourceAttr(resourceName, "members.targets.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "members.targets.*", map[string]string{ + "ip_address": "192.168.0.1", + "port": "80", + // Default values + "ratio": "1", + "enabled": "true", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "members.targets.*", map[string]string{ + "ip_address": "192.168.0.2", + "port": "8080", + "ratio": "2", + "enabled": "false", + }), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CLIENT_IP"), + }, + }, + // * Update remove the disabled target, change algorithm and add health monitors + { + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example" { + name = {{ generate . "name" }} + description = {{ get . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + algorithm = "ROUND_ROBIN" + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } + health = { + monitors = ["HTTP", "TCP"] + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "ROUND_ROBIN"), + resource.TestCheckResourceAttr(resourceName, "health.monitors.#", "2"), + resource.TestCheckResourceAttr(resourceName, "health.monitors.0", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "health.monitors.1", "TCP"), + resource.TestCheckResourceAttr(resourceName, "members.targets.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "members.targets.*", map[string]string{ + "ip_address": "192.168.0.1", + "port": "80", + // Default values + "ratio": "1", + "enabled": "true", + }), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CLIENT_IP"), + }, + }, + }, + // ! Imports testing + Imports: []testsacc.TFImport{ + { + ImportStateIDBuilder: []string{"edge_gateway_id", "id"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_name", "id"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_id", "name"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_name", "name"}, + ImportState: true, + ImportStateVerify: true, + }, + }, + } + }, + "example_with_edge_name": func(_ context.Context, resourceName string) testsacc.Test { + return testsacc.Test{ + CommonChecks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttrWith(resourceName, "id", urn.TestIsType(urn.LoadBalancerPool)), + }, + CommonDependencies: func() (resp testsacc.DependenciesConfigResponse) { + return + }, + // ! Create testing + Create: testsacc.TFConfig{ + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example_with_edge_name" { + name = {{ generate . "name" }} + description = {{ generate . "description" }} + edge_gateway_name = data.cloudavenue_edgegateway.example_for_elb.name + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckResourceAttr(resourceName, "members.targets.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "members.targets.*", map[string]string{ + "ip_address": "192.168.0.1", + "port": "80", + // Default values + "ratio": "1", + "enabled": "true", + }), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CLIENT_IP"), + }, + }, + // ! Imports testing + Imports: []testsacc.TFImport{ + { + ImportStateIDBuilder: []string{"edge_gateway_id", "id"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_name", "id"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_id", "name"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_name", "name"}, + ImportState: true, + ImportStateVerify: true, + }, + }, + } + }, + "example_with_ip_set": func(_ context.Context, resourceName string) testsacc.Test { + return testsacc.Test{ + CommonChecks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttrWith(resourceName, "id", urn.TestIsType(urn.LoadBalancerPool)), + }, + CommonDependencies: func() (resp testsacc.DependenciesConfigResponse) { + resp.Append(GetResourceConfig()[EdgeGatewayIPSetResourceName]().GetSpecificConfig("example_for_elb")) + resp.Append(GetResourceConfig()[ORGCertificateLibraryResourceName]().GetDefaultConfig) + return + }, + // ! Create testing + Create: testsacc.TFConfig{ + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example_with_ip_set" { + name = {{ generate . "name" }} + description = {{ generate . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + members = { + target_group = cloudavenue_edgegateway_ip_set.example_for_elb.id + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckNoResourceAttr(resourceName, "members.targets"), + resource.TestCheckResourceAttrWith(resourceName, "members.target_group", urn.TestIsType(urn.SecurityGroup)), + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CLIENT_IP"), + }, + }, + // ! Updates testing + Updates: []testsacc.TFConfig{ + // * Update add TLS + { + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example_with_ip_set" { + name = {{ get . "name" }} + description = {{ get . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + members = { + target_group = cloudavenue_edgegateway_ip_set.example_for_elb.id + } + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckNoResourceAttr(resourceName, "members.targets"), + resource.TestCheckResourceAttrWith(resourceName, "members.target_group", urn.TestIsType(urn.SecurityGroup)), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.ca_certificate_refs.#", "1"), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CLIENT_IP"), + }, + }, + // * Update add persistence + { + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example_with_ip_set" { + name = {{ get . "name" }} + description = {{ get . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + members = { + target_group = cloudavenue_edgegateway_ip_set.example_for_elb.id + } + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + } + persistence = { + type = "TLS" + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckNoResourceAttr(resourceName, "members.targets"), + resource.TestCheckResourceAttrWith(resourceName, "members.target_group", urn.TestIsType(urn.SecurityGroup)), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.ca_certificate_refs.#", "1"), + + resource.TestCheckResourceAttr(resourceName, "persistence.type", "TLS"), + resource.TestCheckNoResourceAttr(resourceName, "persistence.value"), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + }, + }, + // * Update change persistence + { + TFConfig: testsacc.GenerateFromTemplate(resourceName, ` + resource "cloudavenue_elb_pool" "example_with_ip_set" { + name = {{ get . "name" }} + description = {{ get . "description" }} + edge_gateway_id = data.cloudavenue_edgegateway.example_for_elb.id + enabled = true + default_port = 80 + members = { + target_group = cloudavenue_edgegateway_ip_set.example_for_elb.id + } + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + } + persistence = { + type = "CUSTOM_HTTP_HEADER" + value = "X-Custom" + } + }`), + Checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", testsacc.GetValueFromTemplate(resourceName, "name")), + resource.TestCheckResourceAttr(resourceName, "description", testsacc.GetValueFromTemplate(resourceName, "description")), + resource.TestCheckResourceAttrWith(resourceName, "edge_gateway_id", urn.TestIsType(urn.Gateway)), + resource.TestCheckResourceAttrSet(resourceName, "edge_gateway_name"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "default_port", "80"), + resource.TestCheckNoResourceAttr(resourceName, "members.targets"), + resource.TestCheckResourceAttrWith(resourceName, "members.target_group", urn.TestIsType(urn.SecurityGroup)), + resource.TestCheckResourceAttr(resourceName, "tls.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tls.common_name_check_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "tls.ca_certificate_refs.#", "1"), + + resource.TestCheckResourceAttr(resourceName, "persistence.type", "CUSTOM_HTTP_HEADER"), + resource.TestCheckResourceAttr(resourceName, "persistence.value", "X-Custom"), + + // Default values + resource.TestCheckResourceAttr(resourceName, "members.graceful_timeout_period", "1"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "LEAST_CONNECTIONS"), + resource.TestCheckResourceAttr(resourceName, "health.passive_monitoring_enabled", "true"), + }, + }, + }, + // ! Imports testing + Imports: []testsacc.TFImport{ + { + ImportStateIDBuilder: []string{"edge_gateway_id", "id"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_name", "id"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_id", "name"}, + ImportState: true, + ImportStateVerify: true, + }, + { + ImportStateIDBuilder: []string{"edge_gateway_name", "name"}, + ImportState: true, + ImportStateVerify: true, + }, + }, + } + }, + } +} + +func TestAccELBPoolResource(t *testing.T) { + cleanup := orgCertificateLibraryResourcePreCheck() + defer cleanup() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: TestAccProtoV6ProviderFactories, + Steps: testsacc.GenerateTests(&ELBPoolResource{}), + }) +} diff --git a/templates/data-sources/elb_pool.md.tmpl b/templates/data-sources/elb_pool.md.tmpl new file mode 100644 index 00000000..6812dc1a --- /dev/null +++ b/templates/data-sources/elb_pool.md.tmpl @@ -0,0 +1,25 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "ELB (EdgeGateway Load Balancer)" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile .ExampleFile }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: +{{ codefile "shell" .ImportFile }} +{{- end }} \ No newline at end of file diff --git a/templates/resources/elb_pool.md.tmpl b/templates/resources/elb_pool.md.tmpl new file mode 100644 index 00000000..dc288b42 --- /dev/null +++ b/templates/resources/elb_pool.md.tmpl @@ -0,0 +1,162 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "ELB (EdgeGateway Load Balancer)" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +Basic working example: + +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } +} +``` + + -> More examples can be found at the [Advanced Usage](#advanced-usage) section. + + + +{{ .SchemaMarkdown | trimspace }} + +## Advanced Usage + +### Multiple Members and health monitors +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + algorithm = "ROUND_ROBIN" + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + }, + { + ip_address = "192.168.0.2" + port = 80 + } + ] + } + health = { + monitors = ["HTTP", "TCP"] + } +} +``` + +### Setting TLS configuration +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + members = { + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + } + ] + } + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + } +} +``` + +### Use IPSet for members +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + members = { + graceful_timeout_period = 2 + target_group = cloudavenue_edgegateway_ip_set.example.id + } +} +``` + +### Full configuration +```hcl +resource "cloudavenue_elb_pool" "example" { + name = "example" + edge_gateway_id = data.cloudavenue_edgegateway.example.id + enabled = true + default_port = 80 + algorithm = "ROUND_ROBIN" + members = { + graceful_timeout_period = 2 + targets = [ + { + ip_address = "192.168.0.1" + port = 80 + enabled = true + ratio = 1 + }, + { + ip_address = "192.168.0.2" + port = 80 + enabled = true + ratio = 1 + }, + { + ip_address = "192.168.0.10" + port = 8080 + enabled = true + ratio = 10 + } + ] + } + + health = { + monitors = ["HTTP", "TCP"] + passive_monitoring_enabled = true + } + + tls = { + enabled = true + ca_certificate_refs = [ + cloudavenue_org_certificate_library.example.id + ] + common_name_check_enabled = true + } + + persistence = { + type = "CUSTOM_HTTP_HEADER" + value = "X-Custom" + } +} +``` + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: +{{ codefile "shell" .ImportFile }} +{{- end }} \ No newline at end of file