diff --git a/digitalocean/loadbalancer.go b/digitalocean/loadbalancer.go index 2e5f1213..3861e3f6 100644 --- a/digitalocean/loadbalancer.go +++ b/digitalocean/loadbalancer.go @@ -3,10 +3,10 @@ package digitalocean import ( "context" "fmt" - "strconv" "github.com/digitalocean/godo" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" ) func loadbalancerStateRefreshFunc(client *godo.Client, loadbalancerId string) resource.StateRefreshFunc { @@ -82,13 +82,12 @@ func expandForwardingRules(config []interface{}) []godo.ForwardingRule { return forwardingRules } -func flattenDropletIds(list []int) []interface{} { - flatList := make([]interface{}, 0, len(list)) +func flattenDropletIds(list []int) *schema.Set { + flatSet := schema.NewSet(schema.HashInt, []interface{}{}) for _, v := range list { - vStr := strconv.Itoa(v) - flatList = append(flatList, vStr) + flatSet.Add(v) } - return flatList + return flatSet } func flattenHealthChecks(health *godo.HealthCheck) []map[string]interface{} { diff --git a/digitalocean/resource_digitalocean_loadbalancer.go b/digitalocean/resource_digitalocean_loadbalancer.go index d1f365c6..1e1dd19f 100644 --- a/digitalocean/resource_digitalocean_loadbalancer.go +++ b/digitalocean/resource_digitalocean_loadbalancer.go @@ -4,12 +4,12 @@ import ( "context" "fmt" "log" - "strconv" "time" "github.com/digitalocean/godo" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func resourceDigitalOceanLoadbalancer() *schema.Resource { @@ -23,21 +23,26 @@ func resourceDigitalOceanLoadbalancer() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "region": { Type: schema.TypeString, Required: true, ForceNew: true, }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "algorithm": { Type: schema.TypeString, Optional: true, Default: "round_robin", + ValidateFunc: validation.StringInSlice([]string{ + "round_robin", + "least_connections", + }, false), }, "forwarding_rule": { @@ -49,22 +54,37 @@ func resourceDigitalOceanLoadbalancer() *schema.Resource { "entry_protocol": { Type: schema.TypeString, Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "http", + "https", + "http2", + "tcp", + }, false), }, "entry_port": { - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), }, "target_protocol": { Type: schema.TypeString, Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "http", + "https", + "http2", + "tcp", + }, false), }, "target_port": { - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), }, "certificate_id": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.NoZeroValues, }, "tls_passthrough": { Type: schema.TypeBool, @@ -78,40 +98,51 @@ func resourceDigitalOceanLoadbalancer() *schema.Resource { "healthcheck": { Type: schema.TypeList, Optional: true, + Computed: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "protocol": { Type: schema.TypeString, Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "http", + "tcp", + }, false), }, "port": { - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), }, "path": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.NoZeroValues, }, "check_interval_seconds": { - Type: schema.TypeInt, - Optional: true, - Default: 10, + Type: schema.TypeInt, + Optional: true, + Default: 10, + ValidateFunc: validation.IntBetween(3, 300), }, "response_timeout_seconds": { - Type: schema.TypeInt, - Optional: true, - Default: 5, + Type: schema.TypeInt, + Optional: true, + Default: 5, + ValidateFunc: validation.IntBetween(3, 300), }, "unhealthy_threshold": { - Type: schema.TypeInt, - Optional: true, - Default: 3, + Type: schema.TypeInt, + Optional: true, + Default: 3, + ValidateFunc: validation.IntBetween(2, 10), }, "healthy_threshold": { - Type: schema.TypeInt, - Optional: true, - Default: 5, + Type: schema.TypeInt, + Optional: true, + Default: 5, + ValidateFunc: validation.IntBetween(2, 10), }, }, }, @@ -128,24 +159,31 @@ func resourceDigitalOceanLoadbalancer() *schema.Resource { Type: schema.TypeString, Optional: true, Default: "none", + ValidateFunc: validation.StringInSlice([]string{ + "cookies", + "none", + }, false), }, "cookie_name": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(2, 40), }, "cookie_ttl_seconds": { - Type: schema.TypeInt, - Optional: true, + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(1), }, }, }, }, "droplet_ids": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Computed: true, + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeInt}, + Optional: true, + Computed: true, + ConflictsWith: []string{"droplet_tag"}, }, "droplet_tag": { @@ -166,6 +204,48 @@ func resourceDigitalOceanLoadbalancer() *schema.Resource { Computed: true, }, }, + + CustomizeDiff: func(diff *schema.ResourceDiff, v interface{}) error { + + if _, hasHealthCheck := diff.GetOk("healthcheck"); hasHealthCheck { + + healthCheckProtocol := diff.Get("healthcheck.0.protocol").(string) + _, hasPath := diff.GetOk("healthcheck.0.path") + if healthCheckProtocol == "http" { + if !hasPath { + return fmt.Errorf("health check `path` is required for when protocol is `http`") + } + } else { + if hasPath { + return fmt.Errorf("health check `path` is not allowed for when protocol is `tcp`") + } + } + } + + if _, hasStickySession := diff.GetOk("sticky_sessions.#"); hasStickySession { + + sessionType := diff.Get("sticky_sessions.0.type").(string) + _, hasCookieName := diff.GetOk("sticky_sessions.0.cookie_name") + _, hasTtlSeconds := diff.GetOk("sticky_sessions.0.cookie_ttl_seconds") + if sessionType == "cookies" { + if !hasCookieName { + return fmt.Errorf("sticky sessions `cookie_name` is required for when type is `cookie`") + } + if !hasTtlSeconds { + return fmt.Errorf("sticky sessions `cookie_ttl_seconds` is required for when type is `cookie`") + } + } else { + if hasCookieName { + return fmt.Errorf("sticky sessions `cookie_name` is not allowed for when type is `none`") + } + if hasTtlSeconds { + return fmt.Errorf("sticky sessions `cookie_ttl_seconds` is not allowed for when type is `none`") + } + } + } + + return nil + }, } } @@ -178,23 +258,17 @@ func buildLoadBalancerRequest(d *schema.ResourceData) (*godo.LoadBalancerRequest ForwardingRules: expandForwardingRules(d.Get("forwarding_rule").([]interface{})), } - if v, ok := d.GetOk("droplet_ids"); ok { + if v, ok := d.GetOk("droplet_tag"); ok { + opts.Tag = v.(string) + } else if v, ok := d.GetOk("droplet_ids"); ok { var droplets []int - for _, id := range v.([]interface{}) { - i, err := strconv.Atoi(id.(string)) - if err != nil { - return nil, err - } - droplets = append(droplets, i) + for _, id := range v.(*schema.Set).List() { + droplets = append(droplets, id.(int)) } opts.DropletIDs = droplets } - if v, ok := d.GetOk("droplet_tag"); ok { - opts.Tag = v.(string) - } - if v, ok := d.GetOk("healthcheck"); ok { opts.HealthCheck = expandHealthCheck(v.([]interface{})) } @@ -288,6 +362,8 @@ func resourceDigitalOceanLoadbalancerUpdate(d *schema.ResourceData, meta interfa return err } + log.Printf("UIIIII: %v", lbOpts) + log.Printf("[DEBUG] Load Balancer Update: %#v", lbOpts) _, _, err = client.LoadBalancers.Update(context.Background(), d.Id(), lbOpts) if err != nil { diff --git a/website/docs/r/loadbalancer.html.markdown b/website/docs/r/loadbalancer.html.markdown index 6e2a4988..bd3cfcf6 100644 --- a/website/docs/r/loadbalancer.html.markdown +++ b/website/docs/r/loadbalancer.html.markdown @@ -42,6 +42,53 @@ resource "digitalocean_loadbalancer" "public" { } ``` +When managing certificates attached to the load balancer, make sure to add the `create_before_destroy` +lifecycle property in order to ensure the certificate is correctly updated when changed. The order of +operations will then be: `Create new certificate` -> `Update loadbalancer with new certificate` -> +`Delete old certificate`. When doing so, you must also change the name of the certificate, +as there cannot be multiple certificates with the same name in an account. + +```hcl +resource "digitalocean_certificate" "cert" { + name = "cert" + private_key = "${file("key.pem")}" + leaf_certificate = "${file("cert.pem")}" + + lifecycle { + create_before_destroy = true + } +} + +resource "digitalocean_droplet" "web" { + name = "web-1" + size = "s-1vcpu-1gb" + image = "centos-7-x64" + region = "nyc3" +} + +resource "digitalocean_loadbalancer" "public" { + name = "loadbalancer-1" + region = "nyc3" + + forwarding_rule { + entry_port = 443 + entry_protocol = "https" + + target_port = 80 + target_protocol = "http" + + certificate_id = "${digitalocean_certificate.cert.id}" + } + + healthcheck { + port = 22 + protocol = "tcp" + } + + droplet_ids = ["${digitalocean_droplet.web.id}"] +} +``` + ## Argument Reference The following arguments are supported: