Repository: digitalocean/doctl
Pull request #1526 (2024-05-08 23:17:26)

Add support for GLB active-passive failover config

asaha2: cc @andrewsomething 
danaelhe: Thanks for the contribution! I'm not too familiar with this capability. Is it documented in our API [reference](https://docs.digitalocean.com/reference/api/api-reference/#section/Introduction)? If so, could you point me to it- it'll be easier for me to review this PR with some context :)

Patch:
diff --git a/commands/load_balancers.go b/commands/load_balancers.go
index 089ef59a..476bd3ba 100644
--- a/commands/load_balancers.go
+++ b/commands/load_balancers.go
@@ -490,6 +490,18 @@ func fillStructFromStringSliceArgs(obj any, s string, delimiter string) error {
 				}
 			case reflect.String:
 				f.Set(reflect.ValueOf(val))
+			case reflect.Map:
+				for _, kvPair := range strings.Split(val, " ") {
+					kv := strings.Split(kvPair, "=")
+					if len(kv) == 2 {
+						if v32, err := strconv.ParseUint(kv[1], 10, 32); err == nil {
+							if f.IsZero() {
+								f.Set(reflect.MakeMap(f.Type()))
+							}
+							f.SetMapIndex(reflect.ValueOf(kv[0]), reflect.ValueOf(uint32(v32)))
+						}
+					}
+				}
 			default:
 				return fmt.Errorf("Unexpected type for struct field %v", val)
 			}
diff --git a/go.mod b/go.mod
index 9791df11..255ef672 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.21
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/creack/pty v1.1.11
-	github.com/digitalocean/godo v1.113.0
+	github.com/digitalocean/godo v1.115.0
 	github.com/docker/cli v24.0.5+incompatible
 	github.com/docker/docker v24.0.9+incompatible
 	github.com/docker/docker-credential-helpers v0.7.0 // indirect
diff --git a/go.sum b/go.sum
index 971ff6db..877a4d1d 100644
--- a/go.sum
+++ b/go.sum
@@ -86,8 +86,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/digitalocean/godo v1.113.0 h1:CLtCxlP4wDAjKIQ+Hshht/UNbgAp8/J/XBH1ZtDCF9Y=
-github.com/digitalocean/godo v1.113.0/go.mod h1:Z2mTP848Vi3IXXl5YbPekUgr4j4tOePomA+OE1Ag98w=
+github.com/digitalocean/godo v1.115.0 h1:Xv0gwN0t7ldD61QKeYeHHwCEKfTXzAOmnUDgGAzoZw4=
+github.com/digitalocean/godo v1.115.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
 github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
 github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index d1051832..e1fb20fe 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Change Log
 
+## [v1.115.0] - 2024-05-08
+
+- #688 - @asaha2 - load balancers: support glb active-passive fail-over settings, currently in closed beta
+
+## [v1.114.0] - 2024-04-12
+
+- #686 - @greeshmapill - APPS-8386: Add comments to mark deprecation of unused instance size fields
+- #685 - @jcodybaker - APPS-8711: container termination controls
+- #682 - @dependabot[bot] - Bump golang.org/x/net from 0.17.0 to 0.23.0
+
 ## [v1.113.0] - 2024-04-12
 
 - #679 - @bhardwajRahul - Enable ui_connection parameter for Opensearch
diff --git a/vendor/github.com/digitalocean/godo/apps.gen.go b/vendor/github.com/digitalocean/godo/apps.gen.go
index c037559d..f734ab02 100644
--- a/vendor/github.com/digitalocean/godo/apps.gen.go
+++ b/vendor/github.com/digitalocean/godo/apps.gen.go
@@ -388,6 +388,7 @@ type AppJobSpec struct {
 	Alerts []*AppAlertSpec `json:"alerts,omitempty"`
 	// A list of configured log forwarding destinations.
 	LogDestinations []*AppLogDestinationSpec `json:"log_destinations,omitempty"`
+	Termination     *AppJobSpecTermination   `json:"termination,omitempty"`
 }
 
 // AppJobSpecKind  - UNSPECIFIED: Default job type, will auto-complete to POST_DEPLOY kind.  - PRE_DEPLOY: Indicates a job that runs before an app deployment.  - POST_DEPLOY: Indicates a job that runs after an app deployment.  - FAILED_DEPLOY: Indicates a job that runs after a component fails to deploy.
@@ -401,6 +402,12 @@ const (
 	AppJobSpecKind_FailedDeploy AppJobSpecKind = "FAILED_DEPLOY"
 )
 
+// AppJobSpecTermination struct for AppJobSpecTermination
+type AppJobSpecTermination struct {
+	// The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.
+	GracePeriodSeconds int32 `json:"grace_period_seconds,omitempty"`
+}
+
 // AppLogDestinationSpec struct for AppLogDestinationSpec
 type AppLogDestinationSpec struct {
 	// Name of the log destination.
@@ -484,22 +491,23 @@ type AppServiceSpec struct {
 	// A list of configured alerts which apply to the component.
 	Alerts []*AppAlertSpec `json:"alerts,omitempty"`
 	// A list of configured log forwarding destinations.
-	LogDestinations []*AppLogDestinationSpec `json:"log_destinations,omitempty"`
+	LogDestinations []*AppLogDestinationSpec   `json:"log_destinations,omitempty"`
+	Termination     *AppServiceSpecTermination `json:"termination,omitempty"`
 }
 
 // AppServiceSpecHealthCheck struct for AppServiceSpecHealthCheck
 type AppServiceSpecHealthCheck struct {
 	// Deprecated. Use http_path instead.
 	Path string `json:"path,omitempty"`
-	// The number of seconds to wait before beginning health checks. Default: 0 seconds; start health checks as soon as the service starts.
+	// The number of seconds to wait before beginning health checks. Default: 0 seconds, Minimum 0, Maximum 3600.
 	InitialDelaySeconds int32 `json:"initial_delay_seconds,omitempty"`
-	// The number of seconds to wait between health checks. Default: 10 seconds.
+	// The number of seconds to wait between health checks. Default: 10 seconds, Minimum 1, Maximum 300.
 	PeriodSeconds int32 `json:"period_seconds,omitempty"`
-	// The number of seconds after which the check times out. Default: 1 second.
+	// The number of seconds after which the check times out. Default: 1 second, Minimum 1, Maximum 120.
 	TimeoutSeconds int32 `json:"timeout_seconds,omitempty"`
-	// The number of successful health checks before considered healthy. Default: 1.
+	// The number of successful health checks before considered healthy. Default: 1, Minimum 1, Maximum 50.
 	SuccessThreshold int32 `json:"success_threshold,omitempty"`
-	// The number of failed health checks before considered unhealthy. Default: 9.
+	// The number of failed health checks before considered unhealthy. Default: 9, Minimum 1, Maximum 50.
 	FailureThreshold int32 `json:"failure_threshold,omitempty"`
 	// The route path used for the HTTP health check ping. If not set, the HTTP health check will be disabled and a TCP health check used instead.
 	HTTPPath string `json:"http_path,omitempty"`
@@ -507,6 +515,14 @@ type AppServiceSpecHealthCheck struct {
 	Port int64 `json:"port,omitempty"`
 }
 
+// AppServiceSpecTermination struct for AppServiceSpecTermination
+type AppServiceSpecTermination struct {
+	// The number of seconds to wait between selecting a container instance for termination and issuing the TERM signal. Selecting a container instance for termination begins an asynchronous drain of new requests on upstream load-balancers. Default: 15 seconds, Minimum 1, Maximum 110.
+	DrainSeconds int32 `json:"drain_seconds,omitempty"`
+	// The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.
+	GracePeriodSeconds int32 `json:"grace_period_seconds,omitempty"`
+}
+
 // AppSpec The desired configuration of an application.
 type AppSpec struct {
 	// The name of the app. Must be unique across all apps in the same account.
@@ -601,7 +617,14 @@ type AppWorkerSpec struct {
 	// A list of configured alerts which apply to the component.
 	Alerts []*AppAlertSpec `json:"alerts,omitempty"`
 	// A list of configured log forwarding destinations.
-	LogDestinations []*AppLogDestinationSpec `json:"log_destinations,omitempty"`
+	LogDestinations []*AppLogDestinationSpec  `json:"log_destinations,omitempty"`
+	Termination     *AppWorkerSpecTermination `json:"termination,omitempty"`
+}
+
+// AppWorkerSpecTermination struct for AppWorkerSpecTermination
+type AppWorkerSpecTermination struct {
+	// The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.
+	GracePeriodSeconds int32 `json:"grace_period_seconds,omitempty"`
 }
 
 // Buildpack struct for Buildpack
@@ -1109,16 +1132,18 @@ const (
 
 // AppInstanceSize struct for AppInstanceSize
 type AppInstanceSize struct {
-	Name            string                 `json:"name,omitempty"`
-	Slug            string                 `json:"slug,omitempty"`
-	CPUType         AppInstanceSizeCPUType `json:"cpu_type,omitempty"`
-	CPUs            string                 `json:"cpus,omitempty"`
-	MemoryBytes     string                 `json:"memory_bytes,omitempty"`
-	USDPerMonth     string                 `json:"usd_per_month,omitempty"`
-	USDPerSecond    string                 `json:"usd_per_second,omitempty"`
-	TierSlug        string                 `json:"tier_slug,omitempty"`
-	TierUpgradeTo   string                 `json:"tier_upgrade_to,omitempty"`
-	TierDowngradeTo string                 `json:"tier_downgrade_to,omitempty"`
+	Name         string                 `json:"name,omitempty"`
+	Slug         string                 `json:"slug,omitempty"`
+	CPUType      AppInstanceSizeCPUType `json:"cpu_type,omitempty"`
+	CPUs         string                 `json:"cpus,omitempty"`
+	MemoryBytes  string                 `json:"memory_bytes,omitempty"`
+	USDPerMonth  string                 `json:"usd_per_month,omitempty"`
+	USDPerSecond string                 `json:"usd_per_second,omitempty"`
+	TierSlug     string                 `json:"tier_slug,omitempty"`
+	// (Deprecated) The slug of the corresponding upgradable instance size on the higher tier.
+	TierUpgradeTo string `json:"tier_upgrade_to,omitempty"`
+	// (Deprecated) The slug of the corresponding downgradable instance size on the lower tier.
+	TierDowngradeTo string `json:"tier_downgrade_to,omitempty"`
 	// Indicates if the tier instance size can enable autoscaling.
 	Scalable       bool `json:"scalable,omitempty"`
 	FeaturePreview bool `json:"feature_preview,omitempty"`
@@ -1168,9 +1193,9 @@ type AppProposeResponse struct {
 	Spec              *AppSpec `json:"spec,omitempty"`
 	// The monthly cost of the proposed app in USD.
 	AppCost float32 `json:"app_cost,omitempty"`
-	// The monthly cost of the proposed app in USD using the next pricing plan tier. For example, if you propose an app that uses the Basic tier, the `app_tier_upgrade_cost` field displays the monthly cost of the app if it were to use the Professional tier. If the proposed app already uses the most expensive tier, the field is empty.
+	// (Deprecated) The monthly cost of the proposed app in USD using the next pricing plan tier. For example, if you propose an app that uses the Basic tier, the `app_tier_upgrade_cost` field displays the monthly cost of the app if it were to use the Professional tier. If the proposed app already uses the most expensive tier, the field is empty.
 	AppTierUpgradeCost float32 `json:"app_tier_upgrade_cost,omitempty"`
-	// The monthly cost of the proposed app in USD using the previous pricing plan tier. For example, if you propose an app that uses the Professional tier, the `app_tier_downgrade_cost` field displays the monthly cost of the app if it were to use the Basic tier. If the proposed app already uses the lest expensive tier, the field is empty.
+	// (Deprecated) The monthly cost of the proposed app in USD using the previous pricing plan tier. For example, if you propose an app that uses the Professional tier, the `app_tier_downgrade_cost` field displays the monthly cost of the app if it were to use the Basic tier. If the proposed app already uses the lest expensive tier, the field is empty.
 	AppTierDowngradeCost float32 `json:"app_tier_downgrade_cost,omitempty"`
 	// The number of existing starter tier apps the account has.
 	ExistingStarterApps string `json:"existing_starter_apps,omitempty"`
diff --git a/vendor/github.com/digitalocean/godo/apps_accessors.go b/vendor/github.com/digitalocean/godo/apps_accessors.go
index 0d655d84..734c27ea 100644
--- a/vendor/github.com/digitalocean/godo/apps_accessors.go
+++ b/vendor/github.com/digitalocean/godo/apps_accessors.go
@@ -1261,6 +1261,22 @@ func (a *AppJobSpec) GetSourceDir() string {
 	return a.SourceDir
 }
 
+// GetTermination returns the Termination field.
+func (a *AppJobSpec) GetTermination() *AppJobSpecTermination {
+	if a == nil {
+		return nil
+	}
+	return a.Termination
+}
+
+// GetGracePeriodSeconds returns the GracePeriodSeconds field.
+func (a *AppJobSpecTermination) GetGracePeriodSeconds() int32 {
+	if a == nil {
+		return 0
+	}
+	return a.GracePeriodSeconds
+}
+
 // GetDatadog returns the Datadog field.
 func (a *AppLogDestinationSpec) GetDatadog() *AppLogDestinationSpecDataDog {
 	if a == nil {
@@ -1725,6 +1741,14 @@ func (a *AppServiceSpec) GetSourceDir() string {
 	return a.SourceDir
 }
 
+// GetTermination returns the Termination field.
+func (a *AppServiceSpec) GetTermination() *AppServiceSpecTermination {
+	if a == nil {
+		return nil
+	}
+	return a.Termination
+}
+
 // GetFailureThreshold returns the FailureThreshold field.
 func (a *AppServiceSpecHealthCheck) GetFailureThreshold() int32 {
 	if a == nil {
@@ -1789,6 +1813,22 @@ func (a *AppServiceSpecHealthCheck) GetTimeoutSeconds() int32 {
 	return a.TimeoutSeconds
 }
 
+// GetDrainSeconds returns the DrainSeconds field.
+func (a *AppServiceSpecTermination) GetDrainSeconds() int32 {
+	if a == nil {
+		return 0
+	}
+	return a.DrainSeconds
+}
+
+// GetGracePeriodSeconds returns the GracePeriodSeconds field.
+func (a *AppServiceSpecTermination) GetGracePeriodSeconds() int32 {
+	if a == nil {
+		return 0
+	}
+	return a.GracePeriodSeconds
+}
+
 // GetAlerts returns the Alerts field.
 func (a *AppSpec) GetAlerts() []*AppAlertSpec {
 	if a == nil {
@@ -2237,6 +2277,22 @@ func (a *AppWorkerSpec) GetSourceDir() string {
 	return a.SourceDir
 }
 
+// GetTermination returns the Termination field.
+func (a *AppWorkerSpec) GetTermination() *AppWorkerSpecTermination {
+	if a == nil {
+		return nil
+	}
+	return a.Termination
+}
+
+// GetGracePeriodSeconds returns the GracePeriodSeconds field.
+func (a *AppWorkerSpecTermination) GetGracePeriodSeconds() int32 {
+	if a == nil {
+		return 0
+	}
+	return a.GracePeriodSeconds
+}
+
 // GetDescription returns the Description field.
 func (b *Buildpack) GetDescription() []string {
 	if b == nil {
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index d71e7193..0713df51 100644
--- a/vendor/github.com/digitalocean/godo/godo.go
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -21,7 +21,7 @@ import (
 )
 
 const (
-	libraryVersion = "1.113.0"
+	libraryVersion = "1.115.0"
 	defaultBaseURL = "https://api.digitalocean.com/"
 	userAgent      = "godo/" + libraryVersion
 	mediaType      = "application/json"
diff --git a/vendor/github.com/digitalocean/godo/load_balancers.go b/vendor/github.com/digitalocean/godo/load_balancers.go
index a044adae..39679053 100644
--- a/vendor/github.com/digitalocean/godo/load_balancers.go
+++ b/vendor/github.com/digitalocean/godo/load_balancers.go
@@ -290,6 +290,10 @@ type GLBSettings struct {
 	TargetPort uint32 `json:"target_port"`
 	// CDNSettings is the CDN configurations
 	CDN *CDNSettings `json:"cdn"`
+	// RegionPriorities embeds regional priority information for regional active-passive failover policy
+	RegionPriorities map[string]uint32 `json:"region_priorities,omitempty"`
+	// FailoverThreshold embeds failover threshold percentage for regional active-passive failover policy
+	FailoverThreshold uint32 `json:"failover_threshold,omitempty"`
 }
 
 // String creates a human-readable description of a GLBSettings
@@ -299,8 +303,10 @@ func (s GLBSettings) String() string {
 
 func (s GLBSettings) deepCopy() *GLBSettings {
 	settings := &GLBSettings{
-		TargetProtocol: s.TargetProtocol,
-		TargetPort:     s.TargetPort,
+		TargetProtocol:    s.TargetProtocol,
+		TargetPort:        s.TargetPort,
+		RegionPriorities:  s.RegionPriorities,
+		FailoverThreshold: s.FailoverThreshold,
 	}
 	if s.CDN != nil {
 		settings.CDN = &CDNSettings{IsEnabled: s.CDN.IsEnabled}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a2df2477..cb7dee75 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -58,7 +58,7 @@ github.com/creack/pty
 # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 ## explicit
 github.com/davecgh/go-spew/spew
-# github.com/digitalocean/godo v1.113.0
+# github.com/digitalocean/godo v1.115.0
 ## explicit; go 1.20
 github.com/digitalocean/godo
 github.com/digitalocean/godo/metrics


Test patch:
diff --git a/commands/load_balancers_test.go b/commands/load_balancers_test.go
index 20fe19e5..3d0a1d63 100644
--- a/commands/load_balancers_test.go
+++ b/commands/load_balancers_test.go
@@ -159,9 +159,11 @@ func TestLoadBalancerCreateGLB(t *testing.T) {
 				UnhealthyThreshold:     3,
 			},
 			GLBSettings: &godo.GLBSettings{
-				TargetProtocol: "http",
-				TargetPort:     80,
-				CDN:            &godo.CDNSettings{IsEnabled: true},
+				TargetProtocol:    "http",
+				TargetPort:        80,
+				CDN:               &godo.CDNSettings{IsEnabled: true},
+				RegionPriorities:  map[string]uint32{"nyc1": 1, "nyc2": 2},
+				FailoverThreshold: 10,
 			},
 			DropletIDs: []int{1, 2},
 			Domains: []*godo.LBDomain{
@@ -189,7 +191,7 @@ func TestLoadBalancerCreateGLB(t *testing.T) {
 		config.Doit.Set(config.NS, doctl.ArgLoadBalancerType, "GLOBAL")
 		config.Doit.Set(config.NS, doctl.ArgStickySessions, "type:none")
 		config.Doit.Set(config.NS, doctl.ArgHealthCheck, "protocol:http,port:80,path:/,check_interval_seconds:10,response_timeout_seconds:5,healthy_threshold:5,unhealthy_threshold:3")
-		config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerSettings, "target_protocol:http,target_port:80")
+		config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerSettings, "target_protocol:http,target_port:80,region_priorities:nyc1=1 nyc2=2,failover_threshold:10")
 		config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerCDNSettings, "is_enabled:true")
 		config.Doit.Set(config.NS, doctl.ArgDropletIDs, []string{"1", "2"})
 		config.Doit.Set(config.NS, doctl.ArgLoadBalancerDomains, []string{
@@ -286,9 +288,11 @@ func TestLoadBalancerUpdateGLB(t *testing.T) {
 				UnhealthyThreshold:     3,
 			},
 			GLBSettings: &godo.GLBSettings{
-				TargetProtocol: "http",
-				TargetPort:     80,
-				CDN:            &godo.CDNSettings{IsEnabled: true},
+				TargetProtocol:    "http",
+				TargetPort:        80,
+				CDN:               &godo.CDNSettings{IsEnabled: true},
+				RegionPriorities:  map[string]uint32{"nyc1": 1, "nyc2": 2},
+				FailoverThreshold: 10,
 			},
 			DropletIDs: []int{1, 2},
 			Domains: []*godo.LBDomain{
@@ -317,7 +321,7 @@ func TestLoadBalancerUpdateGLB(t *testing.T) {
 		config.Doit.Set(config.NS, doctl.ArgLoadBalancerType, "GLOBAL")
 		config.Doit.Set(config.NS, doctl.ArgStickySessions, "type:none")
 		config.Doit.Set(config.NS, doctl.ArgHealthCheck, "protocol:http,port:80,path:/,check_interval_seconds:10,response_timeout_seconds:5,healthy_threshold:5,unhealthy_threshold:3")
-		config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerSettings, "target_protocol:http,target_port:80")
+		config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerSettings, "target_protocol:http,target_port:80,region_priorities:nyc1=1 nyc2=2,failover_threshold:10")
 		config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerCDNSettings, "is_enabled:true")
 		config.Doit.Set(config.NS, doctl.ArgDropletIDs, []string{"1", "2"})
 		config.Doit.Set(config.NS, doctl.ArgLoadBalancerDomains, []string{