terraform-provider-greenhost/digitalocean/resource_digitalocean_loadb...

519 lines
15 KiB
Go

package digitalocean
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func resourceDigitalOceanLoadbalancer() *schema.Resource {
return &schema.Resource{
Create: resourceDigitalOceanLoadbalancerCreate,
Read: resourceDigitalOceanLoadbalancerRead,
Update: resourceDigitalOceanLoadbalancerUpdate,
Delete: resourceDigitalOceanLoadbalancerDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
SchemaVersion: 1,
StateUpgraders: []schema.StateUpgrader{
{
Type: resourceDigitalOceanLoadBalancerV0().CoreConfigSchema().ImpliedType(),
Upgrade: migrateLoadBalancerStateV0toV1,
Version: 0,
},
},
Schema: resourceDigitalOceanLoadBalancerV1(),
CustomizeDiff: func(ctx context.Context, 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 healthCheckProtocol == "https" {
if !hasPath {
return fmt.Errorf("health check `path` is required for when protocol is `https`")
}
} 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
},
}
}
func resourceDigitalOceanLoadBalancerV1() map[string]*schema.Schema {
loadBalancerV0Schema := resourceDigitalOceanLoadBalancerV0().Schema
loadBalancerV1Schema := map[string]*schema.Schema{}
forwardingRuleSchema := map[string]*schema.Schema{
"certificate_name": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.NoZeroValues,
},
}
for k, v := range loadBalancerV0Schema["forwarding_rule"].Elem.(*schema.Resource).Schema {
forwardingRuleSchema[k] = v
}
forwardingRuleSchema["certificate_id"].Computed = true
forwardingRuleSchema["certificate_id"].Deprecated = "Certificate IDs may change, for example when a Let's Encrypt certificate is auto-renewed. Please specify 'certificate_name' instead."
for k, v := range loadBalancerV0Schema {
loadBalancerV1Schema[k] = v
}
loadBalancerV1Schema["forwarding_rule"].Elem.(*schema.Resource).Schema = forwardingRuleSchema
return loadBalancerV1Schema
}
func resourceDigitalOceanLoadBalancerV0() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"region": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: func(val interface{}) string {
// DO API V2 region slug is always lowercase
return strings.ToLower(val.(string))
},
},
"name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.NoZeroValues,
},
"urn": {
Type: schema.TypeString,
Computed: true,
Description: "the uniform resource name for the load balancer",
},
"algorithm": {
Type: schema.TypeString,
Optional: true,
Default: "round_robin",
ValidateFunc: validation.StringInSlice([]string{
"round_robin",
"least_connections",
}, false),
},
"forwarding_rule": {
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"entry_protocol": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
"http",
"https",
"http2",
"tcp",
}, false),
},
"entry_port": {
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,
ValidateFunc: validation.IntBetween(1, 65535),
},
"certificate_id": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.NoZeroValues,
},
"tls_passthrough": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
},
Set: hashForwardingRules,
},
"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",
"https",
"tcp",
}, false),
},
"port": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntBetween(1, 65535),
},
"path": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.NoZeroValues,
},
"check_interval_seconds": {
Type: schema.TypeInt,
Optional: true,
Default: 10,
ValidateFunc: validation.IntBetween(3, 300),
},
"response_timeout_seconds": {
Type: schema.TypeInt,
Optional: true,
Default: 5,
ValidateFunc: validation.IntBetween(3, 300),
},
"unhealthy_threshold": {
Type: schema.TypeInt,
Optional: true,
Default: 3,
ValidateFunc: validation.IntBetween(2, 10),
},
"healthy_threshold": {
Type: schema.TypeInt,
Optional: true,
Default: 5,
ValidateFunc: validation.IntBetween(2, 10),
},
},
},
},
"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",
ValidateFunc: validation.StringInSlice([]string{
"cookies",
"none",
}, false),
},
"cookie_name": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(2, 40),
},
"cookie_ttl_seconds": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(1),
},
},
},
},
"droplet_ids": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeInt},
Optional: true,
Computed: true,
ConflictsWith: []string{"droplet_tag"},
},
"droplet_tag": {
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: CaseSensitive,
ValidateFunc: validateTag,
},
"redirect_http_to_https": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"enable_proxy_protocol": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"enable_backend_keepalive": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"vpc_uuid": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
ValidateFunc: validation.NoZeroValues,
},
"ip": {
Type: schema.TypeString,
Computed: true,
},
"status": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func migrateLoadBalancerStateV0toV1(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
if len(rawState) == 0 {
log.Println("[DEBUG] Empty state; nothing to migrate.")
return rawState, nil
}
log.Println("[DEBUG] Migrating load balancer schema from v0 to v1.")
client := meta.(*CombinedConfig).godoClient()
// When the certificate type is lets_encrypt, the certificate
// ID will change when it's renewed, so we have to rely on the
// certificate name as the primary identifier instead.
for _, forwardingRule := range rawState["forwarding_rule"].([]interface{}) {
fw := forwardingRule.(map[string]interface{})
if fw["certificate_id"].(string) == "" {
continue
}
cert, _, err := client.Certificates.Get(context.Background(), fw["certificate_id"].(string))
if err != nil {
return rawState, err
}
fw["certificate_id"] = cert.Name
fw["certificate_name"] = cert.Name
}
return rawState, nil
}
func buildLoadBalancerRequest(client *godo.Client, d *schema.ResourceData) (*godo.LoadBalancerRequest, error) {
forwardingRules, err := expandForwardingRules(client, d.Get("forwarding_rule").(*schema.Set).List())
if err != nil {
return nil, err
}
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),
EnableProxyProtocol: d.Get("enable_proxy_protocol").(bool),
EnableBackendKeepalive: d.Get("enable_backend_keepalive").(bool),
ForwardingRules: forwardingRules,
}
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.(*schema.Set).List() {
droplets = append(droplets, id.(int))
}
opts.DropletIDs = droplets
}
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{}))
}
if v, ok := d.GetOk("vpc_uuid"); ok {
opts.VPCUUID = v.(string)
}
return opts, nil
}
func resourceDigitalOceanLoadbalancerCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*CombinedConfig).godoClient()
log.Printf("[INFO] Create a Loadbalancer Request")
lbOpts, err := buildLoadBalancerRequest(client, d)
if err != nil {
return err
}
log.Printf("[DEBUG] Loadbalancer Create: %#v", lbOpts)
loadbalancer, _, err := client.LoadBalancers.Create(context.Background(), 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.(*CombinedConfig).godoClient()
log.Printf("[INFO] Reading the details of the Loadbalancer %s", d.Id())
loadbalancer, resp, err := client.LoadBalancers.Get(context.Background(), d.Id())
if err != nil {
if resp != nil && resp.StatusCode == 404 {
log.Printf("[WARN] DigitalOcean Load Balancer (%s) not found", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving Loadbalancer: %s", err)
}
d.Set("name", loadbalancer.Name)
d.Set("urn", loadbalancer.URN())
d.Set("ip", loadbalancer.IP)
d.Set("status", loadbalancer.Status)
d.Set("algorithm", loadbalancer.Algorithm)
d.Set("region", loadbalancer.Region.Slug)
d.Set("redirect_http_to_https", loadbalancer.RedirectHttpToHttps)
d.Set("enable_proxy_protocol", loadbalancer.EnableProxyProtocol)
d.Set("enable_backend_keepalive", loadbalancer.EnableBackendKeepalive)
d.Set("droplet_tag", loadbalancer.Tag)
d.Set("vpc_uuid", loadbalancer.VPCUUID)
if err := d.Set("droplet_ids", flattenDropletIds(loadbalancer.DropletIDs)); err != nil {
return fmt.Errorf("[DEBUG] Error setting Load Balancer droplet_ids - error: %#v", err)
}
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)
}
forwardingRules, err := flattenForwardingRules(client, loadbalancer.ForwardingRules)
if err != nil {
return fmt.Errorf("[DEBUG] Error building Load Balancer forwarding rules - error: %#v", err)
}
if err := d.Set("forwarding_rule", 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.(*CombinedConfig).godoClient()
lbOpts, err := buildLoadBalancerRequest(client, d)
if err != nil {
return err
}
log.Printf("[DEBUG] Load Balancer Update: %#v", lbOpts)
_, _, err = client.LoadBalancers.Update(context.Background(), 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.(*CombinedConfig).godoClient()
log.Printf("[INFO] Deleting Load Balancer: %s", d.Id())
_, err := client.LoadBalancers.Delete(context.Background(), d.Id())
if err != nil {
return fmt.Errorf("Error deleting Load Balancer: %s", err)
}
d.SetId("")
return nil
}