From 6eba8de71dc9d646ff35ab5ab1bd41654013a7e3 Mon Sep 17 00:00:00 2001 From: "evgeniy.michurin" Date: Wed, 13 Dec 2023 18:56:34 +0400 Subject: [PATCH] (CLOUDDEV-354): lb pools --- edgecenter/converter/list.go | 77 ++++++ edgecenter/instance/change.go | 6 +- edgecenter/instance/instances.go | 14 +- edgecenter/instance/set.go | 2 +- .../lblistener/datasource_lblistener.go | 2 +- edgecenter/lblistener/lblistener.go | 21 +- edgecenter/lblistener/resource_lblistener.go | 2 +- edgecenter/lbpool/datasource_lbpool.go | 259 ++++++++++++++++++ edgecenter/lbpool/lbpool.go | 231 ++++++++++++++++ edgecenter/lbpool/resource_lbpool.go | 186 +++++++++++++ edgecenter/lbpool/set.go | 54 ++++ edgecenter/provider.go | 3 + go.mod | 2 +- go.sum | 12 + 14 files changed, 851 insertions(+), 20 deletions(-) create mode 100644 edgecenter/lbpool/datasource_lbpool.go create mode 100644 edgecenter/lbpool/lbpool.go create mode 100644 edgecenter/lbpool/resource_lbpool.go create mode 100644 edgecenter/lbpool/set.go diff --git a/edgecenter/converter/list.go b/edgecenter/converter/list.go index 2dee0250..9a518852 100644 --- a/edgecenter/converter/list.go +++ b/edgecenter/converter/list.go @@ -1,6 +1,8 @@ package converter import ( + "net" + edgecloud "github.com/Edge-Center/edgecentercloud-go" ) @@ -59,3 +61,78 @@ func ListInterfaceToListInstanceInterface(interfaces []interface{}) ([]edgecloud return ifs, nil } + +// ListInterfaceToLoadbalancerSessionPersistence creates a session persistence options struct. +func ListInterfaceToLoadbalancerSessionPersistence(sessionPersistence []interface{}) *edgecloud.LoadbalancerSessionPersistence { + var sp *edgecloud.LoadbalancerSessionPersistence + + if len(sessionPersistence) > 0 { + sessionPersistenceMap := sessionPersistence[0].(map[string]interface{}) + sp = &edgecloud.LoadbalancerSessionPersistence{ + Type: edgecloud.SessionPersistence(sessionPersistenceMap["type"].(string)), + } + + if granularity, ok := sessionPersistenceMap["persistence_granularity"].(string); ok { + sp.PersistenceGranularity = granularity + } + + if timeout, ok := sessionPersistenceMap["persistence_timeout"].(int); ok { + sp.PersistenceTimeout = timeout + } + + if cookieName, ok := sessionPersistenceMap["cookie_name"].(string); ok { + sp.CookieName = cookieName + } + } + + return sp +} + +// ListInterfaceToHealthMonitor creates a heath monitor options struct. +func ListInterfaceToHealthMonitor(healthMonitor []interface{}) edgecloud.HealthMonitorCreateRequest { + var hm edgecloud.HealthMonitorCreateRequest + + if len(healthMonitor) > 0 { + healthMonitorMap := healthMonitor[0].(map[string]interface{}) + hm = edgecloud.HealthMonitorCreateRequest{ + Timeout: healthMonitorMap["timeout"].(int), + Delay: healthMonitorMap["delay"].(int), + Type: edgecloud.HealthMonitorType(healthMonitorMap["type"].(string)), + MaxRetries: healthMonitorMap["max_retries"].(int), + } + + if httpMethod, ok := healthMonitorMap["http_method"].(string); ok { + hm.HTTPMethod = edgecloud.HTTPMethod(httpMethod) + } + + if urlPath, ok := healthMonitorMap["url_path"].(string); ok { + hm.URLPath = urlPath + } + + if maxRetriesDown, ok := healthMonitorMap["max_retries_down"].(int); ok { + hm.MaxRetriesDown = maxRetriesDown + } + + if expectedCodes, ok := healthMonitorMap["expected_codes"].(string); ok { + hm.ExpectedCodes = expectedCodes + } + } + + return hm +} + +func ListInterfaceToListPoolMember(poolMembers []interface{}) ([]edgecloud.PoolMemberCreateRequest, error) { + members := make([]edgecloud.PoolMemberCreateRequest, len(poolMembers)) + for i, member := range poolMembers { + m := member.(map[string]interface{}) + address := m["address"].(string) + m["address"] = net.ParseIP(address) + var M edgecloud.PoolMemberCreateRequest + if err := MapStructureDecoder(&M, &m, decoderConfig); err != nil { + return nil, err + } + members[i] = M + } + + return members, nil +} diff --git a/edgecenter/instance/change.go b/edgecenter/instance/change.go index 4bdfe735..1be3df3f 100644 --- a/edgecenter/instance/change.go +++ b/edgecenter/instance/change.go @@ -182,11 +182,11 @@ func attachInterface(ctx context.Context, d *schema.ResourceData, client *edgecl attachInterfaceRequest := &edgecloud.InstanceAttachInterfaceRequest{Type: iType} switch iType { //nolint: exhaustive - case edgecloud.SubnetInterfaceType: + case edgecloud.InterfaceTypeSubnet: attachInterfaceRequest.SubnetID = ifs["subnet_id"].(string) - case edgecloud.AnySubnetInterfaceType: + case edgecloud.InterfaceTypeAnySubnet: attachInterfaceRequest.NetworkID = ifs["network_id"].(string) - case edgecloud.ReservedFixedIPType: + case edgecloud.InterfaceTypeReservedFixedIP: attachInterfaceRequest.PortID = ifs["port_id"].(string) } attachInterfaceRequest.SecurityGroups = getSecurityGroupsIDs(ifs["security_groups"].([]interface{})) diff --git a/edgecenter/instance/instances.go b/edgecenter/instance/instances.go index 352b4e2b..a40a9308 100644 --- a/edgecenter/instance/instances.go +++ b/edgecenter/instance/instances.go @@ -52,10 +52,8 @@ func instanceSchema() map[string]*schema.Schema { Required: true, Description: fmt.Sprintf( "available values are '%s', '%s', '%s', '%s'", - edgecloud.SubnetInterfaceType, - edgecloud.AnySubnetInterfaceType, - edgecloud.ExternalInterfaceType, - edgecloud.ReservedFixedIPType, + edgecloud.InterfaceTypeSubnet, edgecloud.InterfaceTypeAnySubnet, + edgecloud.InterfaceTypeExternal, edgecloud.InterfaceTypeReservedFixedIP, ), }, "security_groups": { @@ -72,8 +70,8 @@ func instanceSchema() map[string]*schema.Schema { ValidateFunc: validation.IsUUID, Description: fmt.Sprintf( "ID of the network that the subnet belongs to, required if type is '%s' or '%s'", - edgecloud.SubnetInterfaceType, - edgecloud.AnySubnetInterfaceType, + edgecloud.InterfaceTypeSubnet, + edgecloud.InterfaceTypeAnySubnet, ), }, "subnet_id": { @@ -81,7 +79,7 @@ func instanceSchema() map[string]*schema.Schema { Optional: true, Computed: true, ValidateFunc: validation.IsUUID, - Description: fmt.Sprintf("required if type is '%s'", edgecloud.SubnetInterfaceType), + Description: fmt.Sprintf("required if type is '%s'", edgecloud.InterfaceTypeSubnet), }, "floating_ip_source": { Type: schema.TypeString, @@ -100,7 +98,7 @@ func instanceSchema() map[string]*schema.Schema { Optional: true, Computed: true, ValidateFunc: validation.IsUUID, - Description: fmt.Sprintf("required if type is '%s'", edgecloud.ReservedFixedIPType), + Description: fmt.Sprintf("required if type is '%s'", edgecloud.InterfaceTypeReservedFixedIP), }, "ip_address": { Type: schema.TypeString, diff --git a/edgecenter/instance/set.go b/edgecenter/instance/set.go index 982eef9b..e8bad4c2 100644 --- a/edgecenter/instance/set.go +++ b/edgecenter/instance/set.go @@ -65,7 +65,7 @@ func setInterfaces(ctx context.Context, d *schema.ResourceData, client *edgeclou ifs := v.(map[string]interface{}) ifsType := edgecloud.InterfaceType(ifs["type"].(string)) switch ifsType { //nolint: exhaustive - case edgecloud.SubnetInterfaceType, edgecloud.AnySubnetInterfaceType: + case edgecloud.InterfaceTypeSubnet, edgecloud.InterfaceTypeAnySubnet: currentInterfaces[i].(map[string]interface{})["subnet_id"] = ipAssignments[0].SubnetID } currentInterfaces[i].(map[string]interface{})["port_id"] = portID diff --git a/edgecenter/lblistener/datasource_lblistener.go b/edgecenter/lblistener/datasource_lblistener.go index 342367df..fe1bb71b 100644 --- a/edgecenter/lblistener/datasource_lblistener.go +++ b/edgecenter/lblistener/datasource_lblistener.go @@ -86,7 +86,7 @@ then the first one will be used. it is recommended to use "id"`, "allowed_cidrs": { Type: schema.TypeList, Computed: true, - Description: "allowed CIDRs for listener.", + Description: "allowed CIDRs for listener", Elem: &schema.Schema{Type: schema.TypeString}, }, }, diff --git a/edgecenter/lblistener/lblistener.go b/edgecenter/lblistener/lblistener.go index 28e7ee21..31abed91 100644 --- a/edgecenter/lblistener/lblistener.go +++ b/edgecenter/lblistener/lblistener.go @@ -1,6 +1,8 @@ package lblistener import ( + "fmt" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -33,10 +35,14 @@ func lblistenerSchema() map[string]*schema.Schema { ValidateFunc: validation.IsUUID, }, "protocol": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Available values are 'HTTP', 'HTTPS', 'TCP', 'UDP' and 'Terminated HTTPS'", + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: fmt.Sprintf( + "available values are '%s', '%s', '%s', '%s' and '%s'", + edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS, + edgecloud.ListenerProtocolTCP, edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolTerminatedHTTPS, + ), ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { v := val.(string) switch edgecloud.LoadbalancerListenerProtocol(v) { @@ -44,7 +50,12 @@ func lblistenerSchema() map[string]*schema.Schema { edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolTerminatedHTTPS: return diag.Diagnostics{} default: - return diag.Errorf("wrong protocol %s, available values are 'HTTP', 'HTTPS', 'TCP', 'UDP' and 'Terminated HTTPS'", v) + return diag.Errorf( + "wrong protocol %s, available values are '%s', '%s', '%s', '%s', '%s'", v, + edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS, + edgecloud.ListenerProtocolTCP, edgecloud.ListenerProtocolUDP, + edgecloud.ListenerProtocolTerminatedHTTPS, + ) } }, }, diff --git a/edgecenter/lblistener/resource_lblistener.go b/edgecenter/lblistener/resource_lblistener.go index e47caa3c..123b8c50 100644 --- a/edgecenter/lblistener/resource_lblistener.go +++ b/edgecenter/lblistener/resource_lblistener.go @@ -200,7 +200,7 @@ func resourceEdgeCenterLbListenerUpdate(ctx context.Context, d *schema.ResourceD } if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Error while waiting for loadbalancer listener: %s", err) + return diag.Errorf("Error while waiting for loadbalancer listener update: %s", err) } } diff --git a/edgecenter/lbpool/datasource_lbpool.go b/edgecenter/lbpool/datasource_lbpool.go new file mode 100644 index 00000000..307b3785 --- /dev/null +++ b/edgecenter/lbpool/datasource_lbpool.go @@ -0,0 +1,259 @@ +package lbpool + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/util" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" +) + +func DataSourceEdgeCenterLbPool() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceEdgeCenterLbPoolRead, + Description: `A pool is a list of virtual machines to which the listener will redirect incoming traffic`, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the project", + }, + "region_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the region", + }, + "id": { + Type: schema.TypeString, + Optional: true, + Description: "lb pool uuid", + ValidateFunc: validation.IsUUID, + ExactlyOneOf: []string{"id", "name"}, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: `lb pool name. this parameter is not unique, if there is more than one lb pool with the same name, +then the first one will be used. it is recommended to use "id"`, + ExactlyOneOf: []string{"id", "name"}, + }, + "loadbalancer_id": { + Type: schema.TypeString, + Required: true, + Description: "ID of the load balancer", + }, + // computed attributes + "listener_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the load balancer listener", + }, + "lb_algorithm": { + Type: schema.TypeString, + Computed: true, + Description: "algorithm of the load balancer", + }, + "provisioning_status": { + Type: schema.TypeString, + Computed: true, + Description: "lifecycle status of the pool", + }, + "session_persistence": { + Type: schema.TypeList, + Computed: true, + Description: `configuration that enables the load balancer to bind a user's session to a specific backend member. +this ensures that all requests from the user during the session are sent to the same member.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Computed: true, + }, + "cookie_name": { + Type: schema.TypeString, + Computed: true, + }, + "persistence_granularity": { + Type: schema.TypeString, + Computed: true, + }, + "persistence_timeout": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + "timeout_member_connect": { + Type: schema.TypeInt, + Computed: true, + Description: "timeout for the backend member connection (in milliseconds)", + }, + "timeout_member_data": { + Type: schema.TypeInt, + Computed: true, + Description: "timeout for the backend member inactivity (in milliseconds)", + }, + "timeout_client_data": { + Type: schema.TypeInt, + Computed: true, + Description: "timeout for the frontend client inactivity (in milliseconds)", + }, + "healthmonitor": { + Type: schema.TypeList, + Computed: true, + Description: `configuration for health checks to test the health and state of the backend members. +it determines how the load balancer identifies whether the backend members are healthy or unhealthy`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "delay": { + Type: schema.TypeInt, + Computed: true, + }, + "timeout": { + Type: schema.TypeInt, + Computed: true, + }, + "max_retries": { + Type: schema.TypeInt, + Computed: true, + }, + "max_retries_down": { + Type: schema.TypeInt, + Computed: true, + }, + "url_path": { + Type: schema.TypeString, + Computed: true, + }, + "expected_codes": { + Type: schema.TypeString, + Computed: true, + }, + "http_method": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "operating status of the pool", + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + Description: "protocol of the load balancer", + }, + "member": { + Type: schema.TypeList, + Computed: true, + Description: "members of the Pool", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "weight": { + Type: schema.TypeInt, + Computed: true, + }, + "address": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "protocol_port": { + Type: schema.TypeInt, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + }, + "instance_id": { + Type: schema.TypeString, + Computed: true, + }, + "admin_state_up": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceEdgeCenterLbPoolRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + loadbalancerID := d.Get("loadbalancer_id").(string) + + var foundPool *edgecloud.Pool + + if id, ok := d.GetOk("id"); ok { + pool, _, err := client.Loadbalancers.PoolGet(ctx, id.(string)) + if err != nil { + return diag.FromErr(err) + } + + foundPool = pool + } else if poolName, ok := d.GetOk("name"); ok { + pool, err := util.LBPoolGetByName(ctx, client, poolName.(string), loadbalancerID) + if err != nil { + return diag.FromErr(err) + } + + foundPool = pool + } else { + return diag.Errorf("Error: specify either id or a name to lookup the lb pool") + } + + d.SetId(foundPool.ID) + d.Set("name", foundPool.Name) + d.Set("lb_algorithm", foundPool.LoadbalancerAlgorithm) + d.Set("protocol", foundPool.Protocol) + d.Set("provisioning_status", foundPool.ProvisioningStatus) + d.Set("operating_status", foundPool.OperatingStatus) + d.Set("listener_id", foundPool.Listeners[0].ID) + d.Set("timeout_member_connect", foundPool.TimeoutMemberConnect) + d.Set("timeout_member_data", foundPool.TimeoutMemberData) + d.Set("timeout_client_data", foundPool.TimeoutClientData) + + if err := setHealthMonitor(ctx, d, foundPool); err != nil { + return diag.FromErr(err) + } + + if err := setSessionPersistence(ctx, d, foundPool); err != nil { + return diag.FromErr(err) + } + + if err := setMembers(ctx, d, foundPool); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/edgecenter/lbpool/lbpool.go b/edgecenter/lbpool/lbpool.go new file mode 100644 index 00000000..0242137e --- /dev/null +++ b/edgecenter/lbpool/lbpool.go @@ -0,0 +1,231 @@ +package lbpool + +import ( + "fmt" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func lbpoolSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the project", + }, + "region_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the region", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: `lb pool name`, + }, + "lb_algorithm": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf( + "algorithm of the load balancer. available values are '%s', '%s', '%s', '%s'", + edgecloud.LoadbalancerAlgorithmRoundRobin, edgecloud.LoadbalancerAlgorithmLeastConnections, + edgecloud.LoadbalancerAlgorithmSourceIP, edgecloud.LoadbalancerAlgorithmSourceIPPort, + ), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch edgecloud.LoadbalancerAlgorithm(v) { + case edgecloud.LoadbalancerAlgorithmRoundRobin, edgecloud.LoadbalancerAlgorithmLeastConnections, edgecloud.LoadbalancerAlgorithmSourceIP, edgecloud.LoadbalancerAlgorithmSourceIPPort: + return diag.Diagnostics{} + } + + return diag.Errorf( + "wrong type %s, available values are '%s', '%s', '%s', '%s'", v, + edgecloud.LoadbalancerAlgorithmRoundRobin, edgecloud.LoadbalancerAlgorithmLeastConnections, + edgecloud.LoadbalancerAlgorithmSourceIP, edgecloud.LoadbalancerAlgorithmSourceIPPort, + ) + }, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: fmt.Sprintf( + "available values are '%s', '%s', '%s', '%s' and '%s'", + edgecloud.LBPoolProtocolHTTP, edgecloud.LBPoolProtocolHTTPS, edgecloud.LBPoolProtocolTCP, + edgecloud.LBPoolProtocolUDP, edgecloud.LBPoolProtocolProxy, + ), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch edgecloud.LoadbalancerPoolProtocol(v) { + case edgecloud.LBPoolProtocolHTTP, edgecloud.LBPoolProtocolHTTPS, edgecloud.LBPoolProtocolTCP, + edgecloud.LBPoolProtocolUDP, edgecloud.LBPoolProtocolProxy: + return diag.Diagnostics{} + default: + return diag.Errorf( + "wrong protocol %s, available values are '%s', '%s', '%s', '%s', '%s'", v, + edgecloud.LBPoolProtocolHTTP, edgecloud.LBPoolProtocolHTTPS, edgecloud.LBPoolProtocolTCP, + edgecloud.LBPoolProtocolUDP, edgecloud.LBPoolProtocolProxy, + ) + } + }, + }, + "loadbalancer_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the load balancer", + }, + "listener_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the load balancer listener", + }, + "session_persistence": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: `configuration that enables the load balancer to bind a user's session to a specific backend member. +this ensures that all requests from the user during the session are sent to the same member.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + }, + "cookie_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "persistence_granularity": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "persistence_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + }, + }, + "timeout_member_connect": { + Type: schema.TypeInt, + Optional: true, + Default: 5000, //nolint: gomnd + Description: "timeout for the backend member connection (in milliseconds)", + }, + "timeout_member_data": { + Type: schema.TypeInt, + Optional: true, + Default: 5000, //nolint: gomnd + Description: "timeout for the backend member inactivity (in milliseconds)", + }, + "timeout_client_data": { + Type: schema.TypeInt, + Optional: true, + Default: 5000, //nolint: gomnd + Description: "timeout for the frontend client inactivity (in milliseconds)", + }, + "healthmonitor": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Description: `configuration for health checks to test the health and state of the backend members. +it determines how the load balancer identifies whether the backend members are healthy or unhealthy.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf( + "available values are '%s', '%s', '%s', '%s', '%s', '%s", + edgecloud.HealthMonitorTypeHTTP, edgecloud.HealthMonitorTypeHTTPS, + edgecloud.HealthMonitorTypePING, edgecloud.HealthMonitorTypeTCP, + edgecloud.HealthMonitorTypeTLSHello, edgecloud.HealthMonitorTypeUDPConnect), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch edgecloud.HealthMonitorType(v) { + case edgecloud.HealthMonitorTypeHTTP, edgecloud.HealthMonitorTypeHTTPS, + edgecloud.HealthMonitorTypePING, edgecloud.HealthMonitorTypeTCP, + edgecloud.HealthMonitorTypeTLSHello, edgecloud.HealthMonitorTypeUDPConnect: + return diag.Diagnostics{} + } + + return diag.Errorf( + "wrong type %s, available values is '%s', '%s', '%s', '%s', '%s', '%s", v, + edgecloud.HealthMonitorTypeHTTP, edgecloud.HealthMonitorTypeHTTPS, + edgecloud.HealthMonitorTypePING, edgecloud.HealthMonitorTypeTCP, + edgecloud.HealthMonitorTypeTLSHello, edgecloud.HealthMonitorTypeUDPConnect, + ) + }, + }, + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 5, //nolint: gomnd + Description: "Response time (in sec)", + }, + "delay": { + Type: schema.TypeInt, + Optional: true, + Default: 60, //nolint: gomnd + Description: "check interval (in sec)", + }, + "max_retries": { + Type: schema.TypeInt, + Optional: true, + Default: 10, //nolint: gomnd + Description: "healthy thresholds", + }, + "max_retries_down": { + Type: schema.TypeInt, + Optional: true, + Default: 5, //nolint: gomnd + Description: "unhealthy thresholds", + }, + "http_method": { + Type: schema.TypeString, + Optional: true, + }, + "url_path": { + Type: schema.TypeString, + Optional: true, + }, + "expected_codes": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + // computed attributes + "id": { + Type: schema.TypeString, + Computed: true, + Description: "lb pool uuid", + }, + "provisioning_status": { + Type: schema.TypeString, + Computed: true, + Description: "lifecycle status of the pool", + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "operating status of the pool", + }, + } +} diff --git a/edgecenter/lbpool/resource_lbpool.go b/edgecenter/lbpool/resource_lbpool.go new file mode 100644 index 00000000..0ec96b80 --- /dev/null +++ b/edgecenter/lbpool/resource_lbpool.go @@ -0,0 +1,186 @@ +package lbpool + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/util" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" +) + +func ResourceEdgeCenterLbPool() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceEdgeCenterLbPoolCreate, + ReadContext: resourceEdgeCenterLbPoolRead, + UpdateContext: resourceEdgeCenterLbPoolUpdate, + DeleteContext: resourceEdgeCenterLbPoolDelete, + Description: `A pool is a list of virtual machines to which the listener will redirect incoming traffic`, + Schema: lbpoolSchema(), + } +} + +func resourceEdgeCenterLbPoolCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + sessionPersistence := converter.ListInterfaceToLoadbalancerSessionPersistence(d.Get("session_persistence").([]interface{})) + healthMonitor := converter.ListInterfaceToHealthMonitor(d.Get("healthmonitor").([]interface{})) + + opts := &edgecloud.PoolCreateRequest{ + LoadbalancerPoolCreateRequest: edgecloud.LoadbalancerPoolCreateRequest{ + Name: d.Get("name").(string), + Protocol: edgecloud.LoadbalancerPoolProtocol(d.Get("protocol").(string)), + LoadbalancerAlgorithm: edgecloud.LoadbalancerAlgorithm(d.Get("lb_algorithm").(string)), + LoadbalancerID: d.Get("loadbalancer_id").(string), + ListenerID: d.Get("listener_id").(string), + TimeoutClientData: d.Get("timeout_client_data").(int), + TimeoutMemberData: d.Get("timeout_member_data").(int), + TimeoutMemberConnect: d.Get("timeout_member_connect").(int), + SessionPersistence: sessionPersistence, + HealthMonitor: healthMonitor, + }, + } + + log.Printf("[DEBUG] Loadbalancer pool create configuration: %#v", opts) + + taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Loadbalancers.PoolCreate, opts, client) + if err != nil { + return diag.Errorf("error creating loadbalancer pool: %s", err) + } + + d.SetId(taskResult.Pools[0]) + + log.Printf("[INFO] Pool: %s", d.Id()) + + return resourceEdgeCenterLbPoolRead(ctx, d, meta) +} + +func resourceEdgeCenterLbPoolRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + // Retrieve the loadbalancer pool properties for updating the state + pool, resp, err := client.Loadbalancers.PoolGet(ctx, d.Id()) + if err != nil { + // check if the loadbalancer pool no longer exists. + if resp != nil && resp.StatusCode == 404 { + log.Printf("[WARN] EdgeCenter Pool (%s) not found", d.Id()) + d.SetId("") + return nil + } + + return diag.Errorf("Error retrieving loadbalancer pool: %s", err) + } + + d.Set("name", pool.Name) + d.Set("lb_algorithm", pool.LoadbalancerAlgorithm) + d.Set("protocol", pool.Protocol) + d.Set("timeout_member_connect", pool.TimeoutMemberConnect) + d.Set("timeout_member_data", pool.TimeoutMemberData) + d.Set("timeout_client_data", pool.TimeoutClientData) + d.Set("provisioning_status", pool.ProvisioningStatus) + d.Set("operating_status", pool.OperatingStatus) + + if len(pool.Loadbalancers) > 0 { + d.Set("loadbalancer_id", pool.Loadbalancers[0].ID) + } + + if len(pool.Listeners) > 0 { + d.Set("listener_id", pool.Listeners[0].ID) + } + + if err := setHealthMonitor(ctx, d, pool); err != nil { + return diag.FromErr(err) + } + + if err := setSessionPersistence(ctx, d, pool); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceEdgeCenterLbPoolUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + var changed bool + opts := &edgecloud.PoolUpdateRequest{ + Name: d.Get("name").(string), + HealthMonitor: converter.ListInterfaceToHealthMonitor(d.Get("healthmonitor").([]interface{})), + } + + if d.HasChange("name") || d.HasChange("healthmonitor") { + changed = true + } + + if d.HasChange("timeout_client_data") { + opts.TimeoutClientData = d.Get("timeout_client_data").(int) + changed = true + } + + if d.HasChange("timeout_member_data") { + opts.TimeoutMemberData = d.Get("timeout_member_data").(int) + changed = true + } + + if d.HasChange("timeout_member_connect") { + opts.TimeoutMemberConnect = d.Get("timeout_member_connect").(int) + changed = true + } + + if d.HasChange("lb_algorithm") { + opts.LoadbalancerAlgorithm = edgecloud.LoadbalancerAlgorithm(d.Get("lb_algorithm").(string)) + changed = true + } + + if d.HasChange("session_persistence") { + opts.SessionPersistence = converter.ListInterfaceToLoadbalancerSessionPersistence(d.Get("session_persistence").([]interface{})) + changed = true + } + + if changed { + task, _, err := client.Loadbalancers.PoolUpdate(ctx, d.Id(), opts) + if err != nil { + return diag.Errorf("Error when changing the loadbalancer pool: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Error while waiting for loadbalancer pool update: %s", err) + } + } + + return resourceEdgeCenterLbPoolRead(ctx, d, meta) +} + +func resourceEdgeCenterLbPoolDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + log.Printf("[INFO] Deleting loadbalancer pool: %s", d.Id()) + task, _, err := client.Loadbalancers.PoolDelete(ctx, d.Id()) + if err != nil { + return diag.Errorf("Error deleting loadbalancer pool: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Delete loadbalancer pool task failed with error: %s", err) + } + + if err = util.ResourceIsDeleted(ctx, client.Loadbalancers.PoolGet, d.Id()); err != nil { + return diag.Errorf("Loadbalancer pool with id %s was not deleted: %s", d.Id(), err) + } + + d.SetId("") + + return nil +} diff --git a/edgecenter/lbpool/set.go b/edgecenter/lbpool/set.go new file mode 100644 index 00000000..c0a8f2f6 --- /dev/null +++ b/edgecenter/lbpool/set.go @@ -0,0 +1,54 @@ +package lbpool + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func setHealthMonitor(_ context.Context, d *schema.ResourceData, pool *edgecloud.Pool) error { + healthMonitor := []map[string]interface{}{{ + "id": pool.HealthMonitor.ID, + "type": pool.HealthMonitor.Type, + "delay": pool.HealthMonitor.Delay, + "timeout": pool.HealthMonitor.Timeout, + "max_retries": pool.HealthMonitor.MaxRetries, + "max_retries_down": pool.HealthMonitor.MaxRetriesDown, + "url_path": pool.HealthMonitor.URLPath, + "expected_codes": pool.HealthMonitor.ExpectedCodes, + }} + + return d.Set("healthmonitor", healthMonitor) +} + +func setSessionPersistence(_ context.Context, d *schema.ResourceData, pool *edgecloud.Pool) error { + sessionPersistence := []map[string]interface{}{{ + "type": pool.SessionPersistence.Type, + "cookie_name": pool.SessionPersistence.CookieName, + "persistence_timeout": pool.SessionPersistence.PersistenceTimeout, + "persistence_granularity": pool.SessionPersistence.PersistenceGranularity, + }} + + return d.Set("session_persistence", sessionPersistence) +} + +func setMembers(_ context.Context, d *schema.ResourceData, pool *edgecloud.Pool) error { + members := make([]map[string]interface{}, 0, len(pool.Members)) + for _, m := range pool.Members { + member := map[string]interface{}{ + "id": m.ID, + "operating_status": m.OperatingStatus, + "weight": m.Weight, + "address": m.Address.String(), + "protocol_port": m.ProtocolPort, + "subnet_id": m.SubnetID, + "instance_id": m.InstanceID, + "admin_state_up": m.AdminStateUP, + } + members = append(members, member) + } + + return d.Set("member", members) +} diff --git a/edgecenter/provider.go b/edgecenter/provider.go index 8e371b1e..c625c444 100644 --- a/edgecenter/provider.go +++ b/edgecenter/provider.go @@ -10,6 +10,7 @@ import ( "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/floatingip" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/instance" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/lblistener" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/lbpool" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/loadbalancer" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/volume" ) @@ -36,6 +37,7 @@ func Provider() *schema.Provider { "edgecenter_floatingip": floatingip.DataSourceEdgeCenterFloatingIP(), "edgecenter_instance": instance.DataSourceEdgeCenterInstance(), "edgecenter_lblistener": lblistener.DataSourceEdgeCenterLbListener(), + "edgecenter_lbpool": lbpool.DataSourceEdgeCenterLbPool(), "edgecenter_loadbalancer": loadbalancer.DataSourceEdgeCenterLoadbalancer(), "edgecenter_volume": volume.DataSourceEdgeCenterVolume(), }, @@ -43,6 +45,7 @@ func Provider() *schema.Provider { "edgecenter_floatingip": floatingip.ResourceEdgeCenterFloatingIP(), "edgecenter_instance": instance.ResourceEdgeCenterInstance(), "edgecenter_lblistener": lblistener.ResourceEdgeCenterLbListener(), + "edgecenter_lbpool": lbpool.ResourceEdgeCenterLbPool(), "edgecenter_loadbalancer": loadbalancer.ResourceEdgeCenterLoadbalancer(), "edgecenter_volume": volume.ResourceEdgeCenterVolume(), }, diff --git a/go.mod b/go.mod index 4e92baef..c6249ef5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Edge-Center/terraform-provider-edgecenter go 1.21 require ( - github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212080222-fcada367a8d8 + github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213140925-4fccd8fc8ae4 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 ) diff --git a/go.sum b/go.sum index 2d02bd72..af4c0130 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,18 @@ github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212073651-b3c52a55f6ba h github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212073651-b3c52a55f6ba/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212080222-fcada367a8d8 h1:Eq22tmuTqpXHoUHH1Jow54uy2C785RlWTY7WZcTisnA= github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212080222-fcada367a8d8/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213080155-c94c402d66fc h1:34ly0Q2IxmdHYGgofZKp9T/xOSSGvQpaFm3oYcbh37o= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213080155-c94c402d66fc/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213131059-8eb0d845fa97 h1:UvHYk5j0e3DKyasIFEEgYFqjfmcUAuZ7d3dCqBcao8M= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213131059-8eb0d845fa97/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213133542-09e1ba6209fd h1:qRP88Br1FNKIrklPNAcQgKlRsVAzD5p24x8vHbS9iJ4= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213133542-09e1ba6209fd/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213134036-2d2626ad8217 h1:vdxUsVIGqBwVw6llu3A8PK7Kk3SGcJu/vIiTuiNiNLI= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213134036-2d2626ad8217/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213135504-c30b40454895 h1:NIVlqu/Hhq+yCwNqjoLrNPgVos/uaYTtzKK49mXs/zY= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213135504-c30b40454895/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213140925-4fccd8fc8ae4 h1:bZmO3pDzsh7VhBWDFsXnPr1TQUKwEaYT1AMvQEIH5RA= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231213140925-4fccd8fc8ae4/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=