2017-02-23 21:41:20 +00:00
package digitalocean
import (
2017-05-15 13:54:16 +00:00
"context"
2017-02-23 21:41:20 +00:00
"fmt"
"log"
2019-04-14 11:36:30 +00:00
"strings"
2017-02-23 21:41:20 +00:00
"time"
"github.com/digitalocean/godo"
2019-10-22 21:44:03 +00:00
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
2017-02-23 21:41:20 +00:00
)
func resourceDigitalOceanLoadbalancer ( ) * schema . Resource {
return & schema . Resource {
Create : resourceDigitalOceanLoadbalancerCreate ,
Read : resourceDigitalOceanLoadbalancerRead ,
Update : resourceDigitalOceanLoadbalancerUpdate ,
Delete : resourceDigitalOceanLoadbalancerDelete ,
2018-06-25 23:38:10 +00:00
Importer : & schema . ResourceImporter {
State : schema . ImportStatePassthrough ,
} ,
2017-02-23 21:41:20 +00:00
2020-10-13 19:45:34 +00:00
SchemaVersion : 1 ,
StateUpgraders : [ ] schema . StateUpgrader {
{
Type : resourceDigitalOceanLoadBalancerV0 ( ) . CoreConfigSchema ( ) . ImpliedType ( ) ,
Upgrade : migrateLoadBalancerStateV0toV1 ,
Version : 0 ,
} ,
} ,
Schema : resourceDigitalOceanLoadBalancerV1 ( ) ,
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 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 {
2017-02-23 21:41:20 +00:00
Schema : map [ string ] * schema . Schema {
"region" : {
Type : schema . TypeString ,
Required : true ,
ForceNew : true ,
2019-04-14 11:36:30 +00:00
StateFunc : func ( val interface { } ) string {
// DO API V2 region slug is always lowercase
return strings . ToLower ( val . ( string ) )
} ,
2017-02-23 21:41:20 +00:00
} ,
2018-08-28 14:47:40 +00:00
"name" : {
Type : schema . TypeString ,
Required : true ,
ValidateFunc : validation . NoZeroValues ,
} ,
2019-04-15 11:30:01 +00:00
"urn" : {
Type : schema . TypeString ,
Computed : true ,
Description : "the uniform resource name for the load balancer" ,
} ,
2017-02-23 21:41:20 +00:00
"algorithm" : {
Type : schema . TypeString ,
Optional : true ,
Default : "round_robin" ,
2018-08-28 14:47:40 +00:00
ValidateFunc : validation . StringInSlice ( [ ] string {
"round_robin" ,
"least_connections" ,
} , false ) ,
2017-02-23 21:41:20 +00:00
} ,
"forwarding_rule" : {
2020-04-28 19:25:57 +00:00
Type : schema . TypeSet ,
2017-02-23 21:41:20 +00:00
Required : true ,
MinItems : 1 ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"entry_protocol" : {
Type : schema . TypeString ,
Required : true ,
2018-08-28 14:47:40 +00:00
ValidateFunc : validation . StringInSlice ( [ ] string {
"http" ,
"https" ,
"http2" ,
"tcp" ,
} , false ) ,
2017-02-23 21:41:20 +00:00
} ,
"entry_port" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Required : true ,
ValidateFunc : validation . IntBetween ( 1 , 65535 ) ,
2017-02-23 21:41:20 +00:00
} ,
"target_protocol" : {
Type : schema . TypeString ,
Required : true ,
2018-08-28 14:47:40 +00:00
ValidateFunc : validation . StringInSlice ( [ ] string {
"http" ,
"https" ,
"http2" ,
"tcp" ,
} , false ) ,
2017-02-23 21:41:20 +00:00
} ,
"target_port" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Required : true ,
ValidateFunc : validation . IntBetween ( 1 , 65535 ) ,
2017-02-23 21:41:20 +00:00
} ,
"certificate_id" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeString ,
Optional : true ,
ValidateFunc : validation . NoZeroValues ,
2017-02-23 21:41:20 +00:00
} ,
"tls_passthrough" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
} ,
} ,
} ,
2020-04-28 19:25:57 +00:00
Set : hashForwardingRules ,
2017-02-23 21:41:20 +00:00
} ,
"healthcheck" : {
Type : schema . TypeList ,
Optional : true ,
2018-08-28 14:47:40 +00:00
Computed : true ,
2017-02-23 21:41:20 +00:00
MaxItems : 1 ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"protocol" : {
Type : schema . TypeString ,
Required : true ,
2018-08-28 14:47:40 +00:00
ValidateFunc : validation . StringInSlice ( [ ] string {
"http" ,
2020-07-14 19:57:41 +00:00
"https" ,
2018-08-28 14:47:40 +00:00
"tcp" ,
} , false ) ,
2017-02-23 21:41:20 +00:00
} ,
"port" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Required : true ,
ValidateFunc : validation . IntBetween ( 1 , 65535 ) ,
2017-02-23 21:41:20 +00:00
} ,
"path" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeString ,
Optional : true ,
ValidateFunc : validation . NoZeroValues ,
2017-02-23 21:41:20 +00:00
} ,
"check_interval_seconds" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Optional : true ,
Default : 10 ,
ValidateFunc : validation . IntBetween ( 3 , 300 ) ,
2017-02-23 21:41:20 +00:00
} ,
"response_timeout_seconds" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Optional : true ,
Default : 5 ,
ValidateFunc : validation . IntBetween ( 3 , 300 ) ,
2017-02-23 21:41:20 +00:00
} ,
"unhealthy_threshold" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Optional : true ,
Default : 3 ,
ValidateFunc : validation . IntBetween ( 2 , 10 ) ,
2017-02-23 21:41:20 +00:00
} ,
"healthy_threshold" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Optional : true ,
Default : 5 ,
ValidateFunc : validation . IntBetween ( 2 , 10 ) ,
2017-02-23 21:41:20 +00:00
} ,
} ,
} ,
} ,
"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" ,
2018-08-28 14:47:40 +00:00
ValidateFunc : validation . StringInSlice ( [ ] string {
"cookies" ,
"none" ,
} , false ) ,
2017-02-23 21:41:20 +00:00
} ,
"cookie_name" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeString ,
Optional : true ,
ValidateFunc : validation . StringLenBetween ( 2 , 40 ) ,
2017-02-23 21:41:20 +00:00
} ,
"cookie_ttl_seconds" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeInt ,
Optional : true ,
ValidateFunc : validation . IntAtLeast ( 1 ) ,
2017-02-23 21:41:20 +00:00
} ,
} ,
} ,
} ,
"droplet_ids" : {
2018-08-28 14:47:40 +00:00
Type : schema . TypeSet ,
Elem : & schema . Schema { Type : schema . TypeInt } ,
Optional : true ,
Computed : true ,
ConflictsWith : [ ] string { "droplet_tag" } ,
2017-02-23 21:41:20 +00:00
} ,
"droplet_tag" : {
2018-08-24 18:58:53 +00:00
Type : schema . TypeString ,
Optional : true ,
DiffSuppressFunc : CaseSensitive ,
ValidateFunc : validateTag ,
2017-02-23 21:41:20 +00:00
} ,
"redirect_http_to_https" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
} ,
2019-03-25 22:39:33 +00:00
"enable_proxy_protocol" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
} ,
2020-05-04 16:59:05 +00:00
"enable_backend_keepalive" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
} ,
2020-04-13 22:09:44 +00:00
"vpc_uuid" : {
Type : schema . TypeString ,
Optional : true ,
ForceNew : true ,
Computed : true ,
ValidateFunc : validation . NoZeroValues ,
} ,
2017-02-23 21:41:20 +00:00
"ip" : {
Type : schema . TypeString ,
Computed : true ,
} ,
2020-04-13 22:09:44 +00:00
2018-09-04 11:37:12 +00:00
"status" : {
Type : schema . TypeString ,
Computed : true ,
} ,
2017-02-23 21:41:20 +00:00
} ,
2020-10-13 19:45:34 +00:00
}
}
2018-08-28 14:47:40 +00:00
2020-10-13 19:45:34 +00:00
func migrateLoadBalancerStateV0toV1 ( 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 ( )
2018-08-28 14:47:40 +00:00
2020-10-13 19:45:34 +00:00
// 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
}
2018-08-28 14:47:40 +00:00
2020-10-13 19:45:34 +00:00
cert , _ , err := client . Certificates . Get ( context . Background ( ) , fw [ "certificate_id" ] . ( string ) )
if err != nil {
return rawState , err
}
2018-08-28 14:47:40 +00:00
2020-10-13 19:45:34 +00:00
fw [ "certificate_id" ] = cert . Name
fw [ "certificate_name" ] = cert . Name
}
2018-08-28 14:47:40 +00:00
2020-10-13 19:45:34 +00:00
return rawState , nil
}
2018-08-28 14:47:40 +00:00
2020-10-13 19:45:34 +00:00
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
2017-02-23 21:41:20 +00:00
}
opts := & godo . LoadBalancerRequest {
2020-05-04 16:59:05 +00:00
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 ) ,
2020-10-13 19:45:34 +00:00
ForwardingRules : forwardingRules ,
2017-02-23 21:41:20 +00:00
}
2018-08-28 14:47:40 +00:00
if v , ok := d . GetOk ( "droplet_tag" ) ; ok {
opts . Tag = v . ( string )
} else if v , ok := d . GetOk ( "droplet_ids" ) ; ok {
2017-02-23 21:41:20 +00:00
var droplets [ ] int
2018-08-28 14:47:40 +00:00
for _ , id := range v . ( * schema . Set ) . List ( ) {
droplets = append ( droplets , id . ( int ) )
2017-02-23 21:41:20 +00:00
}
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 { } ) )
}
2020-04-13 22:09:44 +00:00
if v , ok := d . GetOk ( "vpc_uuid" ) ; ok {
opts . VPCUUID = v . ( string )
}
2017-02-23 21:41:20 +00:00
return opts , nil
}
func resourceDigitalOceanLoadbalancerCreate ( d * schema . ResourceData , meta interface { } ) error {
2019-01-07 23:48:17 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2017-02-23 21:41:20 +00:00
log . Printf ( "[INFO] Create a Loadbalancer Request" )
2020-10-13 19:45:34 +00:00
lbOpts , err := buildLoadBalancerRequest ( client , d )
2017-02-23 21:41:20 +00:00
if err != nil {
return err
}
log . Printf ( "[DEBUG] Loadbalancer Create: %#v" , lbOpts )
2017-05-15 13:54:16 +00:00
loadbalancer , _ , err := client . LoadBalancers . Create ( context . Background ( ) , lbOpts )
2017-02-23 21:41:20 +00:00
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 {
2019-01-07 23:48:17 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2017-02-23 21:41:20 +00:00
log . Printf ( "[INFO] Reading the details of the Loadbalancer %s" , d . Id ( ) )
2017-05-30 16:21:53 +00:00
loadbalancer , resp , err := client . LoadBalancers . Get ( context . Background ( ) , d . Id ( ) )
2017-02-23 21:41:20 +00:00
if err != nil {
2017-05-30 16:21:53 +00:00
if resp != nil && resp . StatusCode == 404 {
log . Printf ( "[WARN] DigitalOcean Load Balancer (%s) not found" , d . Id ( ) )
d . SetId ( "" )
return nil
}
2017-02-23 21:41:20 +00:00
return fmt . Errorf ( "Error retrieving Loadbalancer: %s" , err )
}
d . Set ( "name" , loadbalancer . Name )
2019-04-15 11:30:01 +00:00
d . Set ( "urn" , loadbalancer . URN ( ) )
2017-02-23 21:41:20 +00:00
d . Set ( "ip" , loadbalancer . IP )
2018-09-04 11:37:12 +00:00
d . Set ( "status" , loadbalancer . Status )
2017-02-23 21:41:20 +00:00
d . Set ( "algorithm" , loadbalancer . Algorithm )
d . Set ( "region" , loadbalancer . Region . Slug )
d . Set ( "redirect_http_to_https" , loadbalancer . RedirectHttpToHttps )
2019-03-25 22:39:33 +00:00
d . Set ( "enable_proxy_protocol" , loadbalancer . EnableProxyProtocol )
2020-05-04 16:59:05 +00:00
d . Set ( "enable_backend_keepalive" , loadbalancer . EnableBackendKeepalive )
2017-02-23 21:41:20 +00:00
d . Set ( "droplet_tag" , loadbalancer . Tag )
2020-04-13 22:09:44 +00:00
d . Set ( "vpc_uuid" , loadbalancer . VPCUUID )
2017-02-23 21:41:20 +00:00
2018-06-25 23:38:10 +00:00
if err := d . Set ( "droplet_ids" , flattenDropletIds ( loadbalancer . DropletIDs ) ) ; err != nil {
return fmt . Errorf ( "[DEBUG] Error setting Load Balancer droplet_ids - error: %#v" , err )
}
2017-02-23 21:41:20 +00:00
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 )
}
2020-10-13 19:45:34 +00:00
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 {
2017-02-23 21:41:20 +00:00
return fmt . Errorf ( "[DEBUG] Error setting Load Balancer forwarding_rule - error: %#v" , err )
}
return nil
}
func resourceDigitalOceanLoadbalancerUpdate ( d * schema . ResourceData , meta interface { } ) error {
2019-01-07 23:48:17 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2017-02-23 21:41:20 +00:00
2020-10-13 19:45:34 +00:00
lbOpts , err := buildLoadBalancerRequest ( client , d )
2017-02-23 21:41:20 +00:00
if err != nil {
return err
}
log . Printf ( "[DEBUG] Load Balancer Update: %#v" , lbOpts )
2017-05-15 13:54:16 +00:00
_ , _ , err = client . LoadBalancers . Update ( context . Background ( ) , d . Id ( ) , lbOpts )
2017-02-23 21:41:20 +00:00
if err != nil {
return fmt . Errorf ( "Error updating Load Balancer: %s" , err )
}
return resourceDigitalOceanLoadbalancerRead ( d , meta )
}
func resourceDigitalOceanLoadbalancerDelete ( d * schema . ResourceData , meta interface { } ) error {
2019-01-07 23:48:17 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2017-02-23 21:41:20 +00:00
log . Printf ( "[INFO] Deleting Load Balancer: %s" , d . Id ( ) )
2017-05-15 13:54:16 +00:00
_ , err := client . LoadBalancers . Delete ( context . Background ( ) , d . Id ( ) )
2017-02-23 21:41:20 +00:00
if err != nil {
return fmt . Errorf ( "Error deleting Load Balancer: %s" , err )
}
d . SetId ( "" )
return nil
}