From 861706921cba2e5178366bdea4df2256beffb1f9 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Thu, 23 Feb 2017 23:41:20 +0200 Subject: [PATCH] provider/digitalocean: Add support for LoadBalancers (#12077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * provider/digitalocean: Add support for LoadBalancers Fixes: #11945 ``` % make testacc TEST=./builtin/providers/digitalocean TESTARGS='-run=TestAccDigitalOceanLoadbalancer_' 2 ↵ ✹ ✭ ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2017/02/18 21:49:11 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/digitalocean -v -run=TestAccDigitalOceanLoadbalancer_ -timeout 120m === RUN TestAccDigitalOceanLoadbalancer_Basic --- PASS: TestAccDigitalOceanLoadbalancer_Basic (121.18s) === RUN TestAccDigitalOceanLoadbalancer_Updated --- PASS: TestAccDigitalOceanLoadbalancer_Updated (168.35s) === RUN TestAccDigitalOceanLoadbalancer_dropletTag --- PASS: TestAccDigitalOceanLoadbalancer_dropletTag (131.31s) PASS ok github.com/hashicorp/terraform/builtin/providers/digitalocean 420.851s ``` * provider/digitalocean: Addressing PR feedback from @catsby --- .../providers/digitalocean/loadbalancer.go | 145 ++++++++ builtin/providers/digitalocean/provider.go | 15 +- .../resource_digitalocean_loadbalancer.go | 297 ++++++++++++++++ ...resource_digitalocean_loadbalancer_test.go | 316 ++++++++++++++++++ .../providers/do/r/loadbalancer.html.markdown | 98 ++++++ website/source/layouts/digitalocean.erb | 4 + 6 files changed, 868 insertions(+), 7 deletions(-) create mode 100644 builtin/providers/digitalocean/loadbalancer.go create mode 100644 builtin/providers/digitalocean/resource_digitalocean_loadbalancer.go create mode 100644 builtin/providers/digitalocean/resource_digitalocean_loadbalancer_test.go create mode 100644 website/source/docs/providers/do/r/loadbalancer.html.markdown diff --git a/builtin/providers/digitalocean/loadbalancer.go b/builtin/providers/digitalocean/loadbalancer.go new file mode 100644 index 000000000..abf868055 --- /dev/null +++ b/builtin/providers/digitalocean/loadbalancer.go @@ -0,0 +1,145 @@ +package digitalocean + +import ( + "fmt" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/resource" +) + +func loadbalancerStateRefreshFunc(client *godo.Client, loadbalancerId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + lb, _, err := client.LoadBalancers.Get(loadbalancerId) + if err != nil { + return nil, "", fmt.Errorf("Error issuing read request in LoadbalancerStateRefreshFunc to DigitalOcean for Load Balancer '%s': %s", loadbalancerId, err) + } + + return lb, lb.Status, nil + } +} + +func expandStickySessions(config []interface{}) *godo.StickySessions { + stickysessionConfig := config[0].(map[string]interface{}) + + stickySession := &godo.StickySessions{ + Type: stickysessionConfig["type"].(string), + } + + if v, ok := stickysessionConfig["cookie_name"]; ok { + stickySession.CookieName = v.(string) + } + + if v, ok := stickysessionConfig["cookie_ttl_seconds"]; ok { + stickySession.CookieTtlSeconds = v.(int) + } + + return stickySession +} + +func expandHealthCheck(config []interface{}) *godo.HealthCheck { + healthcheckConfig := config[0].(map[string]interface{}) + + healthcheck := &godo.HealthCheck{ + Protocol: healthcheckConfig["protocol"].(string), + Port: healthcheckConfig["port"].(int), + CheckIntervalSeconds: healthcheckConfig["check_interval_seconds"].(int), + ResponseTimeoutSeconds: healthcheckConfig["response_timeout_seconds"].(int), + UnhealthyThreshold: healthcheckConfig["unhealthy_threshold"].(int), + HealthyThreshold: healthcheckConfig["healthy_threshold"].(int), + } + + if v, ok := healthcheckConfig["path"]; ok { + healthcheck.Path = v.(string) + } + + return healthcheck +} + +func expandForwardingRules(config []interface{}) []godo.ForwardingRule { + forwardingRules := make([]godo.ForwardingRule, 0, len(config)) + + for _, rawRule := range config { + rule := rawRule.(map[string]interface{}) + + r := godo.ForwardingRule{ + EntryPort: rule["entry_port"].(int), + EntryProtocol: rule["entry_protocol"].(string), + TargetPort: rule["target_port"].(int), + TargetProtocol: rule["target_protocol"].(string), + TlsPassthrough: rule["tls_passthrough"].(bool), + } + + if v, ok := rule["certificate_id"]; ok { + r.CertificateID = v.(string) + } + + forwardingRules = append(forwardingRules, r) + + } + + return forwardingRules +} + +func flattenDropletIds(list []int) []interface{} { + vs := make([]interface{}, 0, len(list)) + for _, v := range list { + vs = append(vs, v) + } + return vs +} + +func flattenHealthChecks(health *godo.HealthCheck) []map[string]interface{} { + result := make([]map[string]interface{}, 0, 1) + + if health != nil { + + r := make(map[string]interface{}) + r["protocol"] = (*health).Protocol + r["port"] = (*health).Port + r["path"] = (*health).Path + r["check_interval_seconds"] = (*health).CheckIntervalSeconds + r["response_timeout_seconds"] = (*health).ResponseTimeoutSeconds + r["unhealthy_threshold"] = (*health).UnhealthyThreshold + r["healthy_threshold"] = (*health).HealthyThreshold + + result = append(result, r) + } + + return result +} + +func flattenStickySessions(session *godo.StickySessions) []map[string]interface{} { + result := make([]map[string]interface{}, 0, 1) + + if session != nil { + + r := make(map[string]interface{}) + r["type"] = (*session).Type + r["cookie_name"] = (*session).CookieName + r["cookie_ttl_seconds"] = (*session).CookieTtlSeconds + + result = append(result, r) + } + + return result +} + +func flattenForwardingRules(rules []godo.ForwardingRule) []map[string]interface{} { + result := make([]map[string]interface{}, 0, 1) + + if rules != nil { + for _, rule := range rules { + r := make(map[string]interface{}) + r["entry_protocol"] = rule.EntryProtocol + r["entry_port"] = rule.EntryPort + r["target_protocol"] = rule.TargetProtocol + r["target_port"] = rule.TargetPort + r["certificate_id"] = rule.CertificateID + r["tls_passthrough"] = rule.TlsPassthrough + + result = append(result, r) + } + } + + return result +} diff --git a/builtin/providers/digitalocean/provider.go b/builtin/providers/digitalocean/provider.go index fbc550f57..5ab2cab43 100644 --- a/builtin/providers/digitalocean/provider.go +++ b/builtin/providers/digitalocean/provider.go @@ -18,13 +18,14 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "digitalocean_domain": resourceDigitalOceanDomain(), - "digitalocean_droplet": resourceDigitalOceanDroplet(), - "digitalocean_floating_ip": resourceDigitalOceanFloatingIp(), - "digitalocean_record": resourceDigitalOceanRecord(), - "digitalocean_ssh_key": resourceDigitalOceanSSHKey(), - "digitalocean_tag": resourceDigitalOceanTag(), - "digitalocean_volume": resourceDigitalOceanVolume(), + "digitalocean_domain": resourceDigitalOceanDomain(), + "digitalocean_droplet": resourceDigitalOceanDroplet(), + "digitalocean_floating_ip": resourceDigitalOceanFloatingIp(), + "digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(), + "digitalocean_record": resourceDigitalOceanRecord(), + "digitalocean_ssh_key": resourceDigitalOceanSSHKey(), + "digitalocean_tag": resourceDigitalOceanTag(), + "digitalocean_volume": resourceDigitalOceanVolume(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/digitalocean/resource_digitalocean_loadbalancer.go b/builtin/providers/digitalocean/resource_digitalocean_loadbalancer.go new file mode 100644 index 000000000..73aee03bc --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_loadbalancer.go @@ -0,0 +1,297 @@ +package digitalocean + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDigitalOceanLoadbalancer() *schema.Resource { + return &schema.Resource{ + Create: resourceDigitalOceanLoadbalancerCreate, + Read: resourceDigitalOceanLoadbalancerRead, + Update: resourceDigitalOceanLoadbalancerUpdate, + Delete: resourceDigitalOceanLoadbalancerDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "region": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "algorithm": { + Type: schema.TypeString, + Optional: true, + Default: "round_robin", + }, + + "forwarding_rule": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "entry_protocol": { + Type: schema.TypeString, + Required: true, + }, + "entry_port": { + Type: schema.TypeInt, + Required: true, + }, + "target_protocol": { + Type: schema.TypeString, + Required: true, + }, + "target_port": { + Type: schema.TypeInt, + Required: true, + }, + "certificate_id": { + Type: schema.TypeString, + Optional: true, + }, + "tls_passthrough": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + + "healthcheck": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Required: true, + }, + "port": { + Type: schema.TypeInt, + Required: true, + }, + "path": { + Type: schema.TypeString, + Optional: true, + }, + "check_interval_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 10, + }, + "response_timeout_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + }, + "unhealthy_threshold": { + Type: schema.TypeInt, + Optional: true, + Default: 3, + }, + "healthy_threshold": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + }, + }, + }, + }, + + "sticky_sessions": { + Type: schema.TypeList, + Optional: true, + Computed: true, //this needs to be computed as the API returns a struct with none as the type + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + Default: "none", + }, + "cookie_name": { + Type: schema.TypeString, + Optional: true, + }, + "cookie_ttl_seconds": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + + "droplet_ids": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + + "droplet_tag": { + Type: schema.TypeString, + Optional: true, + }, + + "redirect_http_to_https": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "ip": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func buildLoadBalancerRequest(d *schema.ResourceData) (*godo.LoadBalancerRequest, error) { + opts := &godo.LoadBalancerRequest{ + Name: d.Get("name").(string), + Region: d.Get("region").(string), + Algorithm: d.Get("algorithm").(string), + RedirectHttpToHttps: d.Get("redirect_http_to_https").(bool), + ForwardingRules: expandForwardingRules(d.Get("forwarding_rule").([]interface{})), + } + + 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) + } + + 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{})) + } + + if v, ok := d.GetOk("sticky_sessions"); ok { + opts.StickySessions = expandStickySessions(v.([]interface{})) + } + + return opts, nil +} + +func resourceDigitalOceanLoadbalancerCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + log.Printf("[INFO] Create a Loadbalancer Request") + + lbOpts, err := buildLoadBalancerRequest(d) + if err != nil { + return err + } + + log.Printf("[DEBUG] Loadbalancer Create: %#v", lbOpts) + loadbalancer, _, err := client.LoadBalancers.Create(lbOpts) + if err != nil { + return fmt.Errorf("Error creating Load Balancer: %s", err) + } + + d.SetId(loadbalancer.ID) + + log.Printf("[DEBUG] Waiting for Load Balancer (%s) to become active", d.Get("name")) + stateConf := &resource.StateChangeConf{ + Pending: []string{"new"}, + Target: []string{"active"}, + Refresh: loadbalancerStateRefreshFunc(client, d.Id()), + Timeout: 10 * time.Minute, + MinTimeout: 15 * time.Second, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Load Balancer (%s) to become active: %s", d.Get("name"), err) + } + + return resourceDigitalOceanLoadbalancerRead(d, meta) +} + +func resourceDigitalOceanLoadbalancerRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + log.Printf("[INFO] Reading the details of the Loadbalancer %s", d.Id()) + loadbalancer, _, err := client.LoadBalancers.Get(d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving Loadbalancer: %s", err) + } + + d.Set("name", loadbalancer.Name) + d.Set("ip", loadbalancer.IP) + d.Set("algorithm", loadbalancer.Algorithm) + d.Set("region", loadbalancer.Region.Slug) + d.Set("redirect_http_to_https", loadbalancer.RedirectHttpToHttps) + d.Set("droplet_ids", flattenDropletIds(loadbalancer.DropletIDs)) + d.Set("droplet_tag", loadbalancer.Tag) + + if err := d.Set("sticky_sessions", flattenStickySessions(loadbalancer.StickySessions)); err != nil { + return fmt.Errorf("[DEBUG] Error setting Load Balancer sticky_sessions - error: %#v", err) + } + + if err := d.Set("healthcheck", flattenHealthChecks(loadbalancer.HealthCheck)); err != nil { + return fmt.Errorf("[DEBUG] Error setting Load Balancer healthcheck - error: %#v", err) + } + + if err := d.Set("forwarding_rule", flattenForwardingRules(loadbalancer.ForwardingRules)); err != nil { + return fmt.Errorf("[DEBUG] Error setting Load Balancer forwarding_rule - error: %#v", err) + } + + return nil + +} + +func resourceDigitalOceanLoadbalancerUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + lbOpts, err := buildLoadBalancerRequest(d) + if err != nil { + return err + } + + log.Printf("[DEBUG] Load Balancer Update: %#v", lbOpts) + _, _, err = client.LoadBalancers.Update(d.Id(), lbOpts) + if err != nil { + return fmt.Errorf("Error updating Load Balancer: %s", err) + } + + return resourceDigitalOceanLoadbalancerRead(d, meta) +} + +func resourceDigitalOceanLoadbalancerDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + log.Printf("[INFO] Deleting Load Balancer: %s", d.Id()) + _, err := client.LoadBalancers.Delete(d.Id()) + if err != nil { + return fmt.Errorf("Error deleting Load Balancer: %s", err) + } + + d.SetId("") + return nil + +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_loadbalancer_test.go b/builtin/providers/digitalocean/resource_digitalocean_loadbalancer_test.go new file mode 100644 index 000000000..7dc5626f2 --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_loadbalancer_test.go @@ -0,0 +1,316 @@ +package digitalocean + +import ( + "fmt" + "strings" + "testing" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDigitalOceanLoadbalancer_Basic(t *testing.T) { + var loadbalancer godo.LoadBalancer + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanLoadbalancerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanLoadbalancerConfig_basic(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanLoadbalancerExists("digitalocean_loadbalancer.foobar", &loadbalancer), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "name", fmt.Sprintf("loadbalancer-%d", rInt)), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "region", "nyc3"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_port", "80"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_port", "80"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.port", "22"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "droplet_ids.#", "1"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanLoadbalancer_Updated(t *testing.T) { + var loadbalancer godo.LoadBalancer + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanLoadbalancerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanLoadbalancerConfig_basic(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanLoadbalancerExists("digitalocean_loadbalancer.foobar", &loadbalancer), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "name", fmt.Sprintf("loadbalancer-%d", rInt)), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "region", "nyc3"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_port", "80"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_port", "80"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.port", "22"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "droplet_ids.#", "1"), + ), + }, + { + Config: testAccCheckDigitalOceanLoadbalancerConfig_updated(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanLoadbalancerExists("digitalocean_loadbalancer.foobar", &loadbalancer), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "name", fmt.Sprintf("loadbalancer-%d", rInt)), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "region", "nyc3"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_port", "81"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_port", "81"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.port", "22"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "droplet_ids.#", "2"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanLoadbalancer_dropletTag(t *testing.T) { + var loadbalancer godo.LoadBalancer + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanLoadbalancerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanLoadbalancerConfig_dropletTag(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanLoadbalancerExists("digitalocean_loadbalancer.foobar", &loadbalancer), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "name", fmt.Sprintf("loadbalancer-%d", rInt)), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "region", "nyc3"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_port", "80"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.entry_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_port", "80"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "forwarding_rule.0.target_protocol", "http"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.port", "22"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "healthcheck.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "digitalocean_loadbalancer.foobar", "droplet_tag", "sample"), + ), + }, + }, + }) +} + +func testAccCheckDigitalOceanLoadbalancerDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*godo.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "digitalocean_loadbalancer" { + continue + } + + _, _, err := client.LoadBalancers.Get(rs.Primary.ID) + + if err != nil && !strings.Contains(err.Error(), "404") { + return fmt.Errorf( + "Error waiting for loadbalancer (%s) to be destroyed: %s", + rs.Primary.ID, err) + } + } + + return nil +} + +func testAccCheckDigitalOceanLoadbalancerExists(n string, loadbalancer *godo.LoadBalancer) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Loadbalancer ID is set") + } + + client := testAccProvider.Meta().(*godo.Client) + + lb, _, err := client.LoadBalancers.Get(rs.Primary.ID) + + if err != nil { + return err + } + + if lb.ID != rs.Primary.ID { + return fmt.Errorf("Loabalancer not found") + } + + *loadbalancer = *lb + + return nil + } +} + +func testAccCheckDigitalOceanLoadbalancerConfig_basic(rInt int) string { + return fmt.Sprintf(` +resource "digitalocean_droplet" "foobar" { + name = "foo-%d" + size = "512mb" + image = "centos-7-x64" + region = "nyc3" +} + +resource "digitalocean_loadbalancer" "foobar" { + name = "loadbalancer-%d" + region = "nyc3" + + forwarding_rule { + entry_port = 80 + entry_protocol = "http" + + target_port = 80 + target_protocol = "http" + } + + healthcheck { + port = 22 + protocol = "tcp" + } + + droplet_ids = ["${digitalocean_droplet.foobar.id}"] +}`, rInt, rInt) +} + +func testAccCheckDigitalOceanLoadbalancerConfig_updated(rInt int) string { + return fmt.Sprintf(` +resource "digitalocean_droplet" "foobar" { + name = "foo-%d" + size = "512mb" + image = "centos-7-x64" + region = "nyc3" +} + +resource "digitalocean_droplet" "foo" { + name = "foo-%d" + size = "512mb" + image = "centos-7-x64" + region = "nyc3" +} + +resource "digitalocean_loadbalancer" "foobar" { + name = "loadbalancer-%d" + region = "nyc3" + + forwarding_rule { + entry_port = 81 + entry_protocol = "http" + + target_port = 81 + target_protocol = "http" + } + + healthcheck { + port = 22 + protocol = "tcp" + } + + droplet_ids = ["${digitalocean_droplet.foobar.id}","${digitalocean_droplet.foo.id}"] +}`, rInt, rInt, rInt) +} + +func testAccCheckDigitalOceanLoadbalancerConfig_dropletTag(rInt int) string { + return fmt.Sprintf(` +resource "digitalocean_tag" "barbaz" { + name = "sample" +} + +resource "digitalocean_droplet" "foobar" { + name = "foo-%d" + size = "512mb" + image = "centos-7-x64" + region = "nyc3" + tags = ["${digitalocean_tag.barbaz.id}"] +} + +resource "digitalocean_loadbalancer" "foobar" { + name = "loadbalancer-%d" + region = "nyc3" + + forwarding_rule { + entry_port = 80 + entry_protocol = "http" + + target_port = 80 + target_protocol = "http" + } + + healthcheck { + port = 22 + protocol = "tcp" + } + + droplet_tag = "${digitalocean_tag.barbaz.name}" + + depends_on = ["digitalocean_droplet.foobar"] +}`, rInt, rInt) +} diff --git a/website/source/docs/providers/do/r/loadbalancer.html.markdown b/website/source/docs/providers/do/r/loadbalancer.html.markdown new file mode 100644 index 000000000..dd36ff917 --- /dev/null +++ b/website/source/docs/providers/do/r/loadbalancer.html.markdown @@ -0,0 +1,98 @@ +--- +layout: "digitalocean" +page_title: "DigitalOcean: digitalocean_loadbalancer" +sidebar_current: "docs-do-resource-loadbalancer" +description: |- + Provides a DigitalOcean Load Balancer resource. This can be used to create, modify, and delete Load Balancers. +--- + +# digitalocean\_loadbalancer + +Provides a DigitalOcean Load Balancer resource. This can be used to create, +modify, and delete Load Balancers. + +## Example Usage + +``` +resource "digitalocean_droplet" "web" { + name = "web-1" + size = "512mb" + image = "centos-7-x64" + region = "nyc3" +} + +resource "digitalocean_loadbalancer" "public" { + name = "loadbalancer-1" + region = "nyc3" + + forwarding_rule { + entry_port = 80 + entry_protocol = "http" + + target_port = 80 + target_protocol = "http" + } + + healthcheck { + port = 22 + protocol = "tcp" + } + + droplet_ids = ["${digitalocean_droplet.web.id}"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The Load Balancer name +* `region` - (Required) The region to start in +* `algorithm` - (Optional) The load balancing algorithm used to determine +which backend Droplet will be selected by a client. It must be either `round_robin` +or `least_connections`. The default value is `round_robin`. +* `forwarding_rule` - (Required) A list of `forwarding_rule` to be assigned to the +Load Balancer. The `forwarding_rule` block is documented below. +* `healthcheck` - (Optional) A `healthcheck` block to be assigned to the +Load Balancer. The `healthcheck` block is documented below. Only 1 healthcheck is allowed. +* `sticky_sessions` - (Optional) A `sticky_sessions` block to be assigned to the +Load Balancer. The `sticky_sessions` block is documented below. Only 1 sticky_sessions block is allowed. +* `redirect_http_to_https` - (Optional) A boolean value indicating whether +HTTP requests to the Load Balancer on port 80 will be redirected to HTTPS on port 443. +Default value is `false`. +* `droplet_ids` (Optional) - A list of the IDs of each droplet to be attached to the Load Balancer. +* `droplet_tag` (Optional) - The name of a Droplet tag corresponding to Droplets to be assigned to the Load Balancer. + +`forwarding_rule` supports the following: + +* `entry_protocol` - (Required) The protocol used for traffic to the Load Balancer. The possible values are: `http`, `https`, or `tcp`. +* `entry_port` - (Required) An integer representing the port on which the Load Balancer instance will listen. +* `target_protocol` - (Required) The protocol used for traffic from the Load Balancer to the backend Droplets. The possible values are: `http`, `https`, or `tcp`. +* `target_port` - (Required) An integer representing the port on the backend Droplets to which the Load Balancer will send traffic. +* `certificate_id` - (Optional) The ID of the TLS certificate to be used for SSL termination. +* `tls_passthrough` - (Optional) A boolean value indicating whether SSL encrypted traffic will be passed through to the backend Droplets. The default value is `false`. + +`sticky_sessions` supports the following: + +* `type` - (Required) An attribute indicating how and if requests from a client will be persistently served by the same backend Droplet. The possible values are `cookies` or `none`. If not specified, the default value is `none`. +* `cookie_name` - (Optional) The name to be used for the cookie sent to the client. This attribute is required when using `cookies` for the sticky sessions type. +* `cookie_ttl_seconds` - (Optional) The number of seconds until the cookie set by the Load Balancer expires. This attribute is required when using `cookies` for the sticky sessions type. + + +`healthcheck` supports the following: + +* `protocol` - (Required) The protocol used for health checks sent to the backend Droplets. The possible values are `http` or `tcp`. +* `port` - (Optional) An integer representing the port on the backend Droplets on which the health check will attempt a connection. +* `path` - (Optional) The path on the backend Droplets to which the Load Balancer instance will send a request. +* `check_interval_seconds` - (Optional) The number of seconds between between two consecutive health checks. If not specified, the default value is `10`. +* `response_timeout_seconds` - (Optional) The number of seconds the Load Balancer instance will wait for a response until marking a health check as failed. If not specified, the default value is `5`. +* `unhealthy_threshold` - (Optional) The number of times a health check must fail for a backend Droplet to be marked "unhealthy" and be removed from the pool. If not specified, the default value is `3`. +* `healthy_threshold` - (Optional) The number of times a health check must pass for a backend Droplet to be marked "healthy" and be re-added to the pool. If not specified, the default value is `5`. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Load Balancer +* `ip`- The ip of the Load Balancer diff --git a/website/source/layouts/digitalocean.erb b/website/source/layouts/digitalocean.erb index 701ed3597..aea2e6517 100644 --- a/website/source/layouts/digitalocean.erb +++ b/website/source/layouts/digitalocean.erb @@ -25,6 +25,10 @@ digitalocean_floating_ip + > + digitalocean_loadbalancer + + > digitalocean_record