diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 72090147..ad356da5 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -123,6 +123,7 @@ func Provider() *schema.Provider { "cloudstack_firewall": resourceCloudStackFirewall(), "cloudstack_host": resourceCloudStackHost(), "cloudstack_instance": resourceCloudStackInstance(), + "cloudstack_internal_loadbalancer": resourceCloudStackInternalLoadBalancer(), "cloudstack_ipaddress": resourceCloudStackIPAddress(), "cloudstack_kubernetes_cluster": resourceCloudStackKubernetesCluster(), "cloudstack_kubernetes_version": resourceCloudStackKubernetesVersion(), diff --git a/cloudstack/resource_cloudstack_internal_loadbalancer.go b/cloudstack/resource_cloudstack_internal_loadbalancer.go new file mode 100644 index 00000000..bbd268ab --- /dev/null +++ b/cloudstack/resource_cloudstack_internal_loadbalancer.go @@ -0,0 +1,238 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// resourceCloudStackInternalLoadBalancer manages an Internal LB rule on a VPC +// tier. Unlike cloudstack_loadbalancer_rule (a PUBLIC rule created via +// createLoadBalancerRule), the Internal LB uses createLoadBalancer with +// scheme=Internal: create / read / update (members) / delete / import. +func resourceCloudStackInternalLoadBalancer() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackInternalLoadBalancerCreate, + Read: resourceCloudStackInternalLoadBalancerRead, + Update: resourceCloudStackInternalLoadBalancerUpdate, + Delete: resourceCloudStackInternalLoadBalancerDelete, + + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "algorithm": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "source_port": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "instance_port": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + // The guest network the internal LB serves. + "network_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + // Network for the source IP; defaults to network_id when omitted. + "source_network_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "source_ip": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "member_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func ilbMemberIds(d *schema.ResourceData) []string { + set := d.Get("member_ids").(*schema.Set) + ids := make([]string, 0, set.Len()) + for _, id := range set.List() { + ids = append(ids, id.(string)) + } + return ids +} + +func resourceCloudStackInternalLoadBalancerCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + networkid := d.Get("network_id").(string) + sourceNetwork := networkid + if v, ok := d.GetOk("source_network_id"); ok && v.(string) != "" { + sourceNetwork = v.(string) + } + + p := cs.LoadBalancer.NewCreateLoadBalancerParams( + d.Get("algorithm").(string), + d.Get("instance_port").(int), + d.Get("name").(string), + networkid, + "Internal", + sourceNetwork, + d.Get("source_port").(int), + ) + if v, ok := d.GetOk("description"); ok { + p.SetDescription(v.(string)) + } + if v, ok := d.GetOk("source_ip"); ok && v.(string) != "" { + p.SetSourceipaddress(v.(string)) + } + + r, err := cs.LoadBalancer.CreateLoadBalancer(p) + if err != nil { + return fmt.Errorf("Error creating internal load balancer %s: %s", d.Get("name").(string), err) + } + d.SetId(r.Id) + + if ids := ilbMemberIds(d); len(ids) > 0 { + ap := cs.LoadBalancer.NewAssignToLoadBalancerRuleParams(r.Id) + ap.SetVirtualmachineids(ids) + if _, err := cs.LoadBalancer.AssignToLoadBalancerRule(ap); err != nil { + return fmt.Errorf("Error assigning instances to internal LB %s: %s", r.Id, err) + } + } + + return resourceCloudStackInternalLoadBalancerRead(d, meta) +} + +func resourceCloudStackInternalLoadBalancerRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + lb, count, err := cs.LoadBalancer.GetLoadBalancerByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Internal LB with ID %s no longer exists", d.Id()) + d.SetId("") + return nil + } + return err + } + + d.Set("name", lb.Name) + d.Set("algorithm", lb.Algorithm) + d.Set("description", lb.Description) + d.Set("network_id", lb.Networkid) + d.Set("source_network_id", lb.Sourceipaddressnetworkid) + d.Set("source_ip", lb.Sourceipaddress) + + members := make([]string, 0, len(lb.Loadbalancerinstance)) + for _, m := range lb.Loadbalancerinstance { + members = append(members, m.Id) + } + d.Set("member_ids", members) + + // NOTE: the listLoadBalancers response does not return source/instance ports, + // so they keep their configured values (both are ForceNew anyway). + return nil +} + +func resourceCloudStackInternalLoadBalancerUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + if d.HasChange("member_ids") { + o, n := d.GetChange("member_ids") + oldSet := o.(*schema.Set) + newSet := n.(*schema.Set) + + toAdd := toStringList(newSet.Difference(oldSet).List()) + toRemove := toStringList(oldSet.Difference(newSet).List()) + + if len(toRemove) > 0 { + rp := cs.LoadBalancer.NewRemoveFromLoadBalancerRuleParams(d.Id()) + rp.SetVirtualmachineids(toRemove) + if _, err := cs.LoadBalancer.RemoveFromLoadBalancerRule(rp); err != nil { + return fmt.Errorf("Error removing instances from internal LB %s: %s", d.Id(), err) + } + } + if len(toAdd) > 0 { + ap := cs.LoadBalancer.NewAssignToLoadBalancerRuleParams(d.Id()) + ap.SetVirtualmachineids(toAdd) + if _, err := cs.LoadBalancer.AssignToLoadBalancerRule(ap); err != nil { + return fmt.Errorf("Error assigning instances to internal LB %s: %s", d.Id(), err) + } + } + } + + return resourceCloudStackInternalLoadBalancerRead(d, meta) +} + +func resourceCloudStackInternalLoadBalancerDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.LoadBalancer.NewDeleteLoadBalancerParams(d.Id()) + if _, err := cs.LoadBalancer.DeleteLoadBalancer(p); err != nil { + // Ignore the error if the internal LB is already gone. + log.Printf("[DEBUG] Error deleting internal LB %s: %s", d.Id(), err) + return err + } + return nil +} + +func toStringList(in []interface{}) []string { + out := make([]string, 0, len(in)) + for _, v := range in { + out = append(out, v.(string)) + } + return out +} diff --git a/website/docs/r/internal_loadbalancer.html.markdown b/website/docs/r/internal_loadbalancer.html.markdown new file mode 100644 index 00000000..fb20a848 --- /dev/null +++ b/website/docs/r/internal_loadbalancer.html.markdown @@ -0,0 +1,74 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_internal_loadbalancer" +sidebar_current: "docs-cloudstack-resource-internal-loadbalancer" +description: |- + Creates an internal load balancer rule. +--- + +# cloudstack_internal_loadbalancer + +Creates an internal load balancer rule (`createLoadBalancer` with +`scheme = Internal`). An internal load balancer distributes traffic to a set of +instances within a VPC tier, reached through a private source IP on that tier's +network rather than a public IP. + +## Example Usage + +```hcl +resource "cloudstack_internal_loadbalancer" "app" { + name = "app-ilb" + algorithm = "roundrobin" + source_port = 80 + instance_port = 8080 + network_id = cloudstack_network.app_tier.id + member_ids = cloudstack_instance.app.*.id + # source_ip is optional — CloudStack auto-assigns one from the tier when omitted. +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the internal load balancer rule. + +* `description` - (Optional) The description of the internal load balancer rule. + +* `algorithm` - (Required) The load balancing algorithm. Valid values are + `source`, `roundrobin` and `leastconn`. + +* `source_port` - (Required) The source (front-end) port the rule listens on. + +* `instance_port` - (Required) The instance (back-end) port traffic is forwarded + to. + +* `network_id` - (Required) The ID of the network (VPC tier) the internal load + balancer is created on. Changing this forces a new resource to be created. + +* `source_network_id` - (Optional) The ID of the network the source IP is taken + from. Defaults to `network_id`. Changing this forces a new resource to be + created. + +* `source_ip` - (Optional) The private source IP the rule listens on. When + omitted CloudStack assigns one automatically. Changing this forces a new + resource to be created. + +* `member_ids` - (Optional) A set of instance IDs assigned to (load balanced by) + this rule. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The id of the internal load balancer rule. +* `source_ip` - The private source IP the rule listens on (computed when not set). + +## Import + +Internal load balancer rules can be imported; use `` as the +import ID. For example: + +```shell +terraform import cloudstack_internal_loadbalancer.app 6226ea4d-9cbe-4cc9-b30c-b9532146da5b +```