Repository: digitalocean/doctl
Pull request #1606 (2024-10-31 20:28:54)

Introduce support for droplet autoscaler
Droplet autoscaler is currently in closed alpha and enabled for select customer. 

Nit change also includes marking the new load balancer feature flags as visible in the cli text (now that these features are available since GA). 

TODO:
- [x] test compiled binary
- [x] add unit tests

asaha2: cc @andrewsomething when you have a chance 🙏 
asaha2: @andrewsomething Yeah should've perhaps checked in with you on this: it just seemed counter-intuitive that we accept `--id` as per command help text, but if we runt it with `--id=<id>` we seem to get a `Error: (droplet-autoscale.list-members) command is missing required arguments` E.g.:
```
asaha@C02FLDHYMD6M:doctl ./builds/doctl -t $DIGITALOCEAN_TOKEN -u https://api.s2r1.internal.digitalocean.com compute droplet-autoscale list-members -h
List all members of a Droplet autoscale pool

Usage:
  doctl compute droplet-autoscale list-members [flags]

Flags:
      --format ID   Columns for output in a comma-separated list. Possible values: ID, `Status`, `Health Status`, `Unhealthy Reason`, `CPU Util`, `Mem Util`.
  -h, --help        help for list-members
      --id string   ID of the Droplet autoscale pool (required)
      --no-header   Return raw data with no headers
asaha@C02FLDHYMD6M:doctl ./builds/doctl -t $DIGITALOCEAN_TOKEN -u https://api.s2r1.internal.digitalocean.com compute droplet-autoscale list-members --id=41fd76b0-3487-4ae6-8979-d26da4b7b122
Error: (droplet-autoscale.list-members) command is missing required arguments
asaha@C02FLDHYMD6M:doctl ./builds/doctl -t $DIGITALOCEAN_TOKEN -u https://api.s2r1.internal.digitalocean.com compute droplet-autoscale list-members 41fd76b0-3487-4ae6-8979-d26da4b7b122
ID          Status    Health Status    Unhealthy Reason    CPU Util    Mem Util
11735085    active    healthy                              0.001083    0.366714
11735086    active    healthy                              0.000704    0.382897
```

Patch:
diff --git a/args.go b/args.go
index 4b5290bb..c651f339 100644
--- a/args.go
+++ b/args.go
@@ -96,6 +96,24 @@ const (
 	ArgMajorVersion = "major-version"
 	// ArgAutoUpgrade is a cluster's auto-upgrade argument.
 	ArgAutoUpgrade = "auto-upgrade"
+
+	// ArgAutoscaleID is an autoscale id argument.
+	ArgAutoscaleID = "id"
+	// ArgAutoscaleName is an autoscale name argument.
+	ArgAutoscaleName = "name"
+	// ArgAutoscaleMinInstances is an autoscale min instance argument.
+	ArgAutoscaleMinInstances = "min-instances"
+	// ArgAutoscaleMaxInstances is an autoscale max instance argument.
+	ArgAutoscaleMaxInstances = "max-instances"
+	// ArgAutoscaleCpuTarget is an autoscale target cpu utilization argument.
+	ArgAutoscaleCpuTarget = "cpu-target"
+	// ArgAutoscaleMemTarget is an autoscale target memory utilization argument.
+	ArgAutoscaleMemTarget = "mem-target"
+	// ArgAutoscaleCooldownMinutes is an autoscale cooldown duration (minutes) argument.
+	ArgAutoscaleCooldownMinutes = "cooldown-minutes"
+	// ArgAutoscaleTargetInstances is an autoscale target instance argument.
+	ArgAutoscaleTargetInstances = "target-instances"
+
 	// ArgHA is a cluster's highly available control plane argument.
 	ArgHA = "ha"
 	// ArgEnableControlPlaneFirewall enable control plane firewall.
diff --git a/commands/command_config.go b/commands/command_config.go
index 943e7df0..91fe3d8a 100644
--- a/commands/command_config.go
+++ b/commands/command_config.go
@@ -48,6 +48,7 @@ type CmdConfig struct {
 	ReservedIPActions func() do.ReservedIPActionsService
 	Droplets          func() do.DropletsService
 	DropletActions    func() do.DropletActionsService
+	DropletAutoscale  func() do.DropletAutoscaleService
 	Domains           func() do.DomainsService
 	Actions           func() do.ActionsService
 	Account           func() do.AccountService
@@ -99,6 +100,7 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init
 			c.ReservedIPActions = func() do.ReservedIPActionsService { return do.NewReservedIPActionsService(godoClient) }
 			c.Droplets = func() do.DropletsService { return do.NewDropletsService(godoClient) }
 			c.DropletActions = func() do.DropletActionsService { return do.NewDropletActionsService(godoClient) }
+			c.DropletAutoscale = func() do.DropletAutoscaleService { return do.NewDropletAutoscaleService(godoClient) }
 			c.Domains = func() do.DomainsService { return do.NewDomainsService(godoClient) }
 			c.Actions = func() do.ActionsService { return do.NewActionsService(godoClient) }
 			c.Account = func() do.AccountService { return do.NewAccountService(godoClient) }
diff --git a/commands/displayers/droplet_autoscale.go b/commands/displayers/droplet_autoscale.go
new file mode 100644
index 00000000..f5750108
--- /dev/null
+++ b/commands/displayers/droplet_autoscale.go
@@ -0,0 +1,173 @@
+package displayers
+
+import (
+	"io"
+
+	"github.com/digitalocean/godo"
+)
+
+type DropletAutoscalePools struct {
+	AutoscalePools []*godo.DropletAutoscalePool `json:"autoscale_pools"`
+}
+
+var _ Displayable = &DropletAutoscalePools{}
+
+func (d *DropletAutoscalePools) Cols() []string {
+	return []string{
+		"ID",
+		"Name",
+		"Region",
+		"Status",
+		"Min Instance",
+		"Max Instance",
+		"Target Instance",
+		"Avg CPU Util",
+		"Avg Mem Util",
+		"Target CPU Util",
+		"Target Mem Util",
+	}
+}
+
+func (d *DropletAutoscalePools) ColMap() map[string]string {
+	return map[string]string{
+		"ID":              "ID",
+		"Name":            "Name",
+		"Region":          "Region",
+		"Status":          "Status",
+		"Min Instance":    "Min Instance",
+		"Max Instance":    "Max Instance",
+		"Target Instance": "Target Instance",
+		"Avg CPU Util":    "Avg CPU Util",
+		"Avg Mem Util":    "Avg Mem Util",
+		"Target CPU Util": "Target CPU Util",
+		"Target Mem Util": "Target Mem Util",
+	}
+}
+
+func (d *DropletAutoscalePools) KV() []map[string]any {
+	out := make([]map[string]any, 0, len(d.AutoscalePools))
+	for _, pool := range d.AutoscalePools {
+		var cpuUtil, memUtil any
+		if pool.CurrentUtilization != nil {
+			cpuUtil = pool.CurrentUtilization.CPU
+			memUtil = pool.CurrentUtilization.Memory
+		}
+		out = append(out, map[string]any{
+			"ID":              pool.ID,
+			"Name":            pool.Name,
+			"Region":          pool.DropletTemplate.Region,
+			"Status":          pool.Status,
+			"Min Instance":    pool.Config.MinInstances,
+			"Max Instance":    pool.Config.MaxInstances,
+			"Target Instance": pool.Config.TargetNumberInstances,
+			"Avg CPU Util":    cpuUtil,
+			"Avg Mem Util":    memUtil,
+			"Target CPU Util": pool.Config.TargetCPUUtilization,
+			"Target Mem Util": pool.Config.TargetMemoryUtilization,
+		})
+	}
+	return out
+}
+
+func (d *DropletAutoscalePools) JSON(out io.Writer) error {
+	return writeJSON(d.AutoscalePools, out)
+}
+
+type DropletAutoscaleResources struct {
+	Droplets []*godo.DropletAutoscaleResource `json:"droplets"`
+}
+
+var _ Displayable = &DropletAutoscaleResources{}
+
+func (d *DropletAutoscaleResources) Cols() []string {
+	return []string{
+		"ID",
+		"Status",
+		"Health Status",
+		"Unhealthy Reason",
+		"CPU Util",
+		"Mem Util",
+	}
+}
+
+func (d *DropletAutoscaleResources) ColMap() map[string]string {
+	return map[string]string{
+		"ID":               "ID",
+		"Status":           "Status",
+		"Health Status":    "Health Status",
+		"Unhealthy Reason": "Unhealthy Reason",
+		"CPU Util":         "CPU Util",
+		"Mem Util":         "Mem Util",
+	}
+}
+
+func (d *DropletAutoscaleResources) KV() []map[string]any {
+	out := make([]map[string]any, 0, len(d.Droplets))
+	for _, droplet := range d.Droplets {
+		var cpuUtil, memUtil any
+		if droplet.CurrentUtilization != nil {
+			cpuUtil = droplet.CurrentUtilization.CPU
+			memUtil = droplet.CurrentUtilization.Memory
+		}
+		out = append(out, map[string]any{
+			"ID":               droplet.DropletID,
+			"Status":           droplet.Status,
+			"Health Status":    droplet.HealthStatus,
+			"Unhealthy Reason": droplet.UnhealthyReason,
+			"CPU Util":         cpuUtil,
+			"Mem Util":         memUtil,
+		})
+	}
+	return out
+}
+
+func (d *DropletAutoscaleResources) JSON(out io.Writer) error {
+	return writeJSON(d.Droplets, out)
+}
+
+type DropletAutoscaleHistoryEvents struct {
+	History []*godo.DropletAutoscaleHistoryEvent `json:"history"`
+}
+
+var _ Displayable = &DropletAutoscaleHistoryEvents{}
+
+func (d *DropletAutoscaleHistoryEvents) Cols() []string {
+	return []string{
+		"ID",
+		"Current Instance",
+		"Target Instance",
+		"Status",
+		"Reason",
+		"Error Reason",
+	}
+}
+
+func (d *DropletAutoscaleHistoryEvents) ColMap() map[string]string {
+	return map[string]string{
+		"ID":               "ID",
+		"Current Instance": "Current Instance",
+		"Target Instance":  "Target Instance",
+		"Status":           "Status",
+		"Reason":           "Reason",
+		"Error Reason":     "Error Reason",
+	}
+}
+
+func (d *DropletAutoscaleHistoryEvents) KV() []map[string]any {
+	out := make([]map[string]any, 0, len(d.History))
+	for _, history := range d.History {
+		out = append(out, map[string]any{
+			"ID":               history.HistoryEventID,
+			"Current Instance": history.CurrentInstanceCount,
+			"Target Instance":  history.DesiredInstanceCount,
+			"Status":           history.Status,
+			"Reason":           history.Reason,
+			"Error Reason":     history.ErrorReason,
+		})
+	}
+	return out
+}
+
+func (d *DropletAutoscaleHistoryEvents) JSON(out io.Writer) error {
+	return writeJSON(d.History, out)
+}
diff --git a/commands/doit.go b/commands/doit.go
index 4a2d3ef0..4ab3e66c 100644
--- a/commands/doit.go
+++ b/commands/doit.go
@@ -206,6 +206,7 @@ func computeCmd() *Command {
 	cmd.AddCommand(Certificate())
 	cmd.AddCommand(DropletAction())
 	cmd.AddCommand(Droplet())
+	cmd.AddCommand(DropletAutoscale())
 	cmd.AddCommand(Domain())
 	cmd.AddCommand(Firewall())
 	cmd.AddCommand(ReservedIP())
diff --git a/commands/droplet_autoscale.go b/commands/droplet_autoscale.go
new file mode 100644
index 00000000..31b30c13
--- /dev/null
+++ b/commands/droplet_autoscale.go
@@ -0,0 +1,373 @@
+/*
+Copyright 2018 The Doctl Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"strconv"
+
+	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/doctl/commands/displayers"
+	"github.com/digitalocean/godo"
+	"github.com/spf13/cobra"
+)
+
+func DropletAutoscale() *Command {
+	cmd := &Command{
+		Command: &cobra.Command{
+			Hidden:  true,
+			Use:     "droplet-autoscale",
+			Aliases: []string{"das"},
+			Short:   "Display commands to manage Droplet autoscale pools",
+			Long: `Use the subcommands of ` + "`" + `doctl compute droplet-autoscale` + "`" + ` to perform actions on Droplet Autoscale Pools.
+
+You can use droplet-autoscale to perform CRUD operations on a Droplet Autoscale Pools.`,
+		},
+	}
+	cmdDropletAutoscaleCreate := CmdBuilder(cmd, RunDropletAutoscaleCreate, "create", "Create a new Droplet autoscale pool", "", Writer, displayerType(&displayers.DropletAutoscalePools{}))
+
+	cmdDropletAutoscaleUpdate := CmdBuilder(cmd, RunDropletAutoscaleUpdate, "update <autoscale-pool-id>", "Update an active Droplet autoscale pool", "", Writer, displayerType(&displayers.DropletAutoscalePools{}))
+
+	for _, c := range []*Command{
+		cmdDropletAutoscaleCreate,
+		cmdDropletAutoscaleUpdate,
+	} {
+		AddStringFlag(c, doctl.ArgAutoscaleName, "", "", "Name of the Droplet autoscale pool", requiredOpt())
+		AddIntFlag(c, doctl.ArgAutoscaleMinInstances, "", 0, "Min number of members")
+		AddIntFlag(c, doctl.ArgAutoscaleMaxInstances, "", 0, "Max number of members")
+		AddStringFlag(c, doctl.ArgAutoscaleCpuTarget, "", "", "CPU target threshold")
+		AddStringFlag(c, doctl.ArgAutoscaleMemTarget, "", "", "Memory target threshold")
+		AddIntFlag(c, doctl.ArgAutoscaleCooldownMinutes, "", 0, "Cooldown duration")
+		AddIntFlag(c, doctl.ArgAutoscaleTargetInstances, "", 0, "Target number of members")
+
+		AddStringFlag(c, doctl.ArgSizeSlug, "", "", "Droplet size")
+		AddStringFlag(c, doctl.ArgRegionSlug, "", "", "Droplet region")
+		AddStringFlag(c, doctl.ArgImage, "", "", "Droplet image")
+		AddStringSliceFlag(c, doctl.ArgTag, "", []string{}, "Droplet tags")
+		AddStringSliceFlag(c, doctl.ArgSSHKeys, "", []string{}, "Droplet SSH keys")
+		AddStringFlag(c, doctl.ArgVPCUUID, "", "", "Droplet VPC UUID")
+		AddBoolFlag(c, doctl.ArgDropletAgent, "", true, "Enable droplet agent")
+		AddStringFlag(c, doctl.ArgProjectID, "", "", "Droplet project ID")
+		AddBoolFlag(c, doctl.ArgIPv6, "", true, "Enable droplet IPv6")
+		AddStringFlag(c, doctl.ArgUserData, "", "", "Droplet user data")
+	}
+
+	CmdBuilder(cmd, RunDropletAutoscaleGet, "get <autoscale-pool-id>", "Get an active Droplet autoscale pool", "", Writer, displayerType(&displayers.DropletAutoscalePools{}))
+
+	CmdBuilder(cmd, RunDropletAutoscaleList, "list", "List all active Droplet autoscale pools", "", Writer, displayerType(&displayers.DropletAutoscalePools{}), aliasOpt("ls"))
+
+	CmdBuilder(cmd, RunDropletAutoscaleListMembers, "list-members <autoscale-pool-id>", "List all members of a Droplet autoscale pool", "", Writer, displayerType(&displayers.DropletAutoscaleResources{}))
+
+	CmdBuilder(cmd, RunDropletAutoscaleListHistory, "list-history <autoscale-pool-id>", "List all history events for a Droplet autoscale pool", "", Writer, displayerType(&displayers.DropletAutoscaleHistoryEvents{}))
+
+	cmdDropletAutoscaleDelete := CmdBuilder(cmd, RunDropletAutoscaleDelete, "delete <autoscale-pool-id>", "Delete an active Droplet autoscale pool", "", Writer, aliasOpt("d", "rm"))
+	AddBoolFlag(cmdDropletAutoscaleDelete, doctl.ArgForce, "", false, "Force delete without a confirmation prompt")
+
+	cmdDropletAutoscaleDeleteDangerous := CmdBuilder(cmd, RunDropletAutoscaleDeleteDangerous, "delete-dangerous <autoscale-pool-id>", "Delete an active Droplet autoscale pool and all its members", "", Writer)
+	AddBoolFlag(cmdDropletAutoscaleDeleteDangerous, doctl.ArgForce, "", false, "Force delete without a confirmation prompt")
+
+	return cmd
+}
+
+func buildDropletAutoscaleRequestFromArgs(c *CmdConfig, r *godo.DropletAutoscalePoolRequest) error {
+	var hydrators = []func() error{
+		func() error {
+			name, err := c.Doit.GetString(c.NS, doctl.ArgAutoscaleName)
+			if err != nil {
+				return err
+			}
+			r.Name = name
+			return nil
+		},
+		func() error {
+			minCount, err := c.Doit.GetInt(c.NS, doctl.ArgAutoscaleMinInstances)
+			if err != nil {
+				return err
+			}
+			r.Config.MinInstances = uint64(minCount)
+			return nil
+		},
+		func() error {
+			maxCount, err := c.Doit.GetInt(c.NS, doctl.ArgAutoscaleMaxInstances)
+			if err != nil {
+				return err
+			}
+			r.Config.MaxInstances = uint64(maxCount)
+			return nil
+		},
+		func() error {
+			cpuStr, err := c.Doit.GetString(c.NS, doctl.ArgAutoscaleCpuTarget)
+			if err != nil {
+				return err
+			}
+			if cpuStr != "" {
+				cpuTarget, err := strconv.ParseFloat(cpuStr, 64)
+				if err != nil {
+					return err
+				}
+				r.Config.TargetCPUUtilization = cpuTarget
+			}
+			return nil
+		},
+		func() error {
+			memStr, err := c.Doit.GetString(c.NS, doctl.ArgAutoscaleMemTarget)
+			if err != nil {
+				return err
+			}
+			if memStr != "" {
+				memTarget, err := strconv.ParseFloat(memStr, 64)
+				if err != nil {
+					return err
+				}
+				r.Config.TargetMemoryUtilization = memTarget
+			}
+			return nil
+		},
+		func() error {
+			cooldown, err := c.Doit.GetInt(c.NS, doctl.ArgAutoscaleCooldownMinutes)
+			if err != nil {
+				return err
+			}
+			r.Config.CooldownMinutes = uint32(cooldown)
+			return nil
+		},
+		func() error {
+			targetCount, err := c.Doit.GetInt(c.NS, doctl.ArgAutoscaleTargetInstances)
+			if err != nil {
+				return err
+			}
+			r.Config.TargetNumberInstances = uint64(targetCount)
+			return nil
+		},
+		func() error {
+			size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.Size = size
+			return nil
+		},
+		func() error {
+			region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.Region = region
+			return nil
+		},
+		func() error {
+			image, err := c.Doit.GetString(c.NS, doctl.ArgImage)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.Image = image
+			return nil
+		},
+		func() error {
+			tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgTag)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.Tags = tags
+			return nil
+		},
+		func() error {
+			sshKeys, err := c.Doit.GetStringSlice(c.NS, doctl.ArgSSHKeys)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.SSHKeys = sshKeys
+			return nil
+		},
+		func() error {
+			vpcUUID, err := c.Doit.GetString(c.NS, doctl.ArgVPCUUID)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.VpcUUID = vpcUUID
+			return nil
+		},
+		func() error {
+			enableAgent, err := c.Doit.GetBool(c.NS, doctl.ArgDropletAgent)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.WithDropletAgent = enableAgent
+			return nil
+		},
+		func() error {
+			projectID, err := c.Doit.GetString(c.NS, doctl.ArgProjectID)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.ProjectID = projectID
+			return nil
+		},
+		func() error {
+			enableIPv6, err := c.Doit.GetBool(c.NS, doctl.ArgIPv6)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.IPV6 = enableIPv6
+			return nil
+		},
+		func() error {
+			userData, err := c.Doit.GetString(c.NS, doctl.ArgUserData)
+			if err != nil {
+				return err
+			}
+			r.DropletTemplate.UserData = userData
+			return nil
+		},
+	}
+	for _, h := range hydrators {
+		if err := h(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// RunDropletAutoscaleCreate creates an autoscale pool
+func RunDropletAutoscaleCreate(c *CmdConfig) error {
+	createReq := new(godo.DropletAutoscalePoolRequest)
+	createReq.Config = new(godo.DropletAutoscaleConfiguration)
+	createReq.DropletTemplate = new(godo.DropletAutoscaleResourceTemplate)
+	if err := buildDropletAutoscaleRequestFromArgs(c, createReq); err != nil {
+		return err
+	}
+	pool, err := c.DropletAutoscale().Create(createReq)
+	if err != nil {
+		return err
+	}
+	item := &displayers.DropletAutoscalePools{AutoscalePools: []*godo.DropletAutoscalePool{pool}}
+	return c.Display(item)
+}
+
+// RunDropletAutoscaleUpdate updates an autoscale pool
+func RunDropletAutoscaleUpdate(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	id := c.Args[0]
+	updateReq := new(godo.DropletAutoscalePoolRequest)
+	updateReq.Config = new(godo.DropletAutoscaleConfiguration)
+	updateReq.DropletTemplate = new(godo.DropletAutoscaleResourceTemplate)
+	if err := buildDropletAutoscaleRequestFromArgs(c, updateReq); err != nil {
+		return err
+	}
+	pool, err := c.DropletAutoscale().Update(id, updateReq)
+	if err != nil {
+		return err
+	}
+	item := &displayers.DropletAutoscalePools{AutoscalePools: []*godo.DropletAutoscalePool{pool}}
+	return c.Display(item)
+}
+
+// RunDropletAutoscaleGet retrieves an autoscale pool
+func RunDropletAutoscaleGet(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	id := c.Args[0]
+	pool, err := c.DropletAutoscale().Get(id)
+	if err != nil {
+		return err
+	}
+	item := &displayers.DropletAutoscalePools{AutoscalePools: []*godo.DropletAutoscalePool{pool}}
+	return c.Display(item)
+}
+
+// RunDropletAutoscaleList lists all autoscale pools
+func RunDropletAutoscaleList(c *CmdConfig) error {
+	pools, err := c.DropletAutoscale().List()
+	if err != nil {
+		return err
+	}
+	item := &displayers.DropletAutoscalePools{AutoscalePools: pools}
+	return c.Display(item)
+}
+
+// RunDropletAutoscaleListMembers lists autoscale pool members
+func RunDropletAutoscaleListMembers(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	id := c.Args[0]
+	members, err := c.DropletAutoscale().ListMembers(id)
+	if err != nil {
+		return err
+	}
+	item := &displayers.DropletAutoscaleResources{Droplets: members}
+	return c.Display(item)
+}
+
+// RunDropletAutoscaleListHistory lists autoscale pool history events
+func RunDropletAutoscaleListHistory(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	id := c.Args[0]
+	history, err := c.DropletAutoscale().ListHistory(id)
+	if err != nil {
+		return err
+	}
+	item := &displayers.DropletAutoscaleHistoryEvents{History: history}
+	return c.Display(item)
+}
+
+// RunDropletAutoscaleDelete deletes an autoscale pool
+func RunDropletAutoscaleDelete(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	id := c.Args[0]
+	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
+	if err != nil {
+		return err
+	}
+	if force || AskForConfirmDelete("droplet autoscale pool", 1) == nil {
+		if err = c.DropletAutoscale().Delete(id); err != nil {
+			return err
+		}
+	} else {
+		return errOperationAborted
+	}
+	return nil
+}
+
+// RunDropletAutoscaleDeleteDangerous deletes an autoscale pool and all underlying members
+func RunDropletAutoscaleDeleteDangerous(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	id := c.Args[0]
+	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
+	if err != nil {
+		return err
+	}
+	if force || AskForConfirmDelete("droplet autoscale pool", 1) == nil {
+		if err = c.DropletAutoscale().DeleteDangerous(id); err != nil {
+			return err
+		}
+	} else {
+		return errOperationAborted
+	}
+	return nil
+}
diff --git a/commands/load_balancers.go b/commands/load_balancers.go
index bc648eaa..ce562fbf 100644
--- a/commands/load_balancers.go
+++ b/commands/load_balancers.go
@@ -101,8 +101,6 @@ With the load-balancer command, you can list, create, or delete load balancers,
 	AddStringSliceFlag(cmdLoadBalancerCreate, doctl.ArgTargetLoadBalancerIDs, "", []string{},
 		"A comma-separated list of Load Balancer IDs to add as target to the global load balancer ")
 	AddStringFlag(cmdLoadBalancerCreate, doctl.ArgLoadBalancerNetwork, "", "", "The type of network the load balancer is accessible from, e.g.: `EXTERNAL` or `INTERNAL`")
-	cmdLoadBalancerCreate.Flags().MarkHidden(doctl.ArgLoadBalancerType)
-	cmdLoadBalancerCreate.Flags().MarkHidden(doctl.ArgLoadBalancerNetwork)
 
 	cmdRecordUpdate := CmdBuilder(cmd, RunLoadBalancerUpdate, "update <load-balancer-id>",
 		"Update a load balancer's configuration", `Use this command to update the configuration of a specified load balancer. Using all applicable flags, the command should contain a full representation of the load balancer including existing attributes, such as the load balancer's name, region, forwarding rules, and Droplet IDs. Any attribute that is not provided is reset to its default value.`, Writer, aliasOpt("u"))
diff --git a/do/droplet_autoscale.go b/do/droplet_autoscale.go
new file mode 100644
index 00000000..5eaefee6
--- /dev/null
+++ b/do/droplet_autoscale.go
@@ -0,0 +1,152 @@
+/*
+Copyright 2018 The Doctl Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+	http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package do
+
+import (
+	"context"
+
+	"github.com/digitalocean/godo"
+)
+
+type DropletAutoscaleService interface {
+	Create(*godo.DropletAutoscalePoolRequest) (*godo.DropletAutoscalePool, error)
+	Get(string) (*godo.DropletAutoscalePool, error)
+	List() ([]*godo.DropletAutoscalePool, error)
+	ListMembers(string) ([]*godo.DropletAutoscaleResource, error)
+	ListHistory(string) ([]*godo.DropletAutoscaleHistoryEvent, error)
+	Update(string, *godo.DropletAutoscalePoolRequest) (*godo.DropletAutoscalePool, error)
+	Delete(string) error
+	DeleteDangerous(string) error
+}
+
+var _ DropletAutoscaleService = &dropletAutoscaleService{}
+
+type dropletAutoscaleService struct {
+	client *godo.Client
+}
+
+// NewDropletAutoscaleService creates an instance of DropletAutoscaleService
+func NewDropletAutoscaleService(client *godo.Client) DropletAutoscaleService {
+	return &dropletAutoscaleService{
+		client: client,
+	}
+}
+
+// Create creates a new droplet autoscale pool
+func (d *dropletAutoscaleService) Create(createReq *godo.DropletAutoscalePoolRequest) (*godo.DropletAutoscalePool, error) {
+	pool, _, err := d.client.DropletAutoscale.Create(context.Background(), createReq)
+	if err != nil {
+		return nil, err
+	}
+	return pool, nil
+}
+
+// Get retrieves an existing droplet autoscale pool
+func (d *dropletAutoscaleService) Get(poolID string) (*godo.DropletAutoscalePool, error) {
+	pool, _, err := d.client.DropletAutoscale.Get(context.Background(), poolID)
+	if err != nil {
+		return nil, err
+	}
+	return pool, nil
+}
+
+// List lists all existing droplet autoscale pools
+func (d *dropletAutoscaleService) List() ([]*godo.DropletAutoscalePool, error) {
+	listResp, err := PaginateResp(func(opts *godo.ListOptions) ([]any, *godo.Response, error) {
+		pools, resp, err := d.client.DropletAutoscale.List(context.Background(), opts)
+		if err != nil {
+			return nil, nil, err
+		}
+		anyResp := make([]any, 0, len(pools))
+		for _, pool := range pools {
+			anyResp = append(anyResp, pool)
+		}
+		return anyResp, resp, err
+	})
+	if err != nil {
+		return nil, err
+	}
+	pools := make([]*godo.DropletAutoscalePool, 0, len(listResp))
+	for _, pool := range listResp {
+		pools = append(pools, pool.(*godo.DropletAutoscalePool))
+	}
+	return pools, nil
+}
+
+// ListMembers lists all droplet autoscale resources for an existing pool
+func (d *dropletAutoscaleService) ListMembers(poolID string) ([]*godo.DropletAutoscaleResource, error) {
+	listResp, err := PaginateResp(func(opts *godo.ListOptions) ([]any, *godo.Response, error) {
+		members, resp, err := d.client.DropletAutoscale.ListMembers(context.Background(), poolID, opts)
+		if err != nil {
+			return nil, nil, err
+		}
+		anyResp := make([]any, 0, len(members))
+		for _, member := range members {
+			anyResp = append(anyResp, member)
+		}
+		return anyResp, resp, err
+	})
+	if err != nil {
+		return nil, err
+	}
+	members := make([]*godo.DropletAutoscaleResource, 0, len(listResp))
+	for _, member := range listResp {
+		members = append(members, member.(*godo.DropletAutoscaleResource))
+	}
+	return members, nil
+}
+
+// ListHistory lists all droplet autoscale history events for an existing pool
+func (d *dropletAutoscaleService) ListHistory(poolID string) ([]*godo.DropletAutoscaleHistoryEvent, error) {
+	listResp, err := PaginateResp(func(opts *godo.ListOptions) ([]any, *godo.Response, error) {
+		events, resp, err := d.client.DropletAutoscale.ListHistory(context.Background(), poolID, opts)
+		if err != nil {
+			return nil, nil, err
+		}
+		anyResp := make([]any, 0, len(events))
+		for _, event := range events {
+			anyResp = append(anyResp, event)
+		}
+		return anyResp, resp, err
+	})
+	if err != nil {
+		return nil, err
+	}
+	events := make([]*godo.DropletAutoscaleHistoryEvent, 0, len(listResp))
+	for _, event := range listResp {
+		events = append(events, event.(*godo.DropletAutoscaleHistoryEvent))
+	}
+	return events, nil
+}
+
+// Update updates an existing droplet autoscale pool
+func (d *dropletAutoscaleService) Update(poolID string, updateReq *godo.DropletAutoscalePoolRequest) (*godo.DropletAutoscalePool, error) {
+	pool, _, err := d.client.DropletAutoscale.Update(context.Background(), poolID, updateReq)
+	if err != nil {
+		return nil, err
+	}
+	return pool, nil
+}
+
+// Delete deletes an existing droplet autoscale pool
+func (d *dropletAutoscaleService) Delete(poolID string) error {
+	_, err := d.client.DropletAutoscale.Delete(context.Background(), poolID)
+	return err
+}
+
+// DeleteDangerous deletes an existing droplet autoscale pool and all its underlying resources
+func (d *dropletAutoscaleService) DeleteDangerous(poolID string) error {
+	_, err := d.client.DropletAutoscale.DeleteDangerous(context.Background(), poolID)
+	return err
+}
diff --git a/do/mocks/AccountService.go b/do/mocks/AccountService.go
index 2a98fcd0..53bbf828 100644
--- a/do/mocks/AccountService.go
+++ b/do/mocks/AccountService.go
@@ -20,6 +20,7 @@ import (
 type MockAccountService struct {
 	ctrl     *gomock.Controller
 	recorder *MockAccountServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockAccountServiceMockRecorder is the mock recorder for MockAccountService.
diff --git a/do/mocks/ActionService.go b/do/mocks/ActionService.go
index 8dbf872f..d571cc94 100644
--- a/do/mocks/ActionService.go
+++ b/do/mocks/ActionService.go
@@ -20,6 +20,7 @@ import (
 type MockActionsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockActionsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockActionsServiceMockRecorder is the mock recorder for MockActionsService.
diff --git a/do/mocks/AppsService.go b/do/mocks/AppsService.go
index c1f9421f..f0774d7d 100644
--- a/do/mocks/AppsService.go
+++ b/do/mocks/AppsService.go
@@ -20,6 +20,7 @@ import (
 type MockAppsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockAppsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockAppsServiceMockRecorder is the mock recorder for MockAppsService.
diff --git a/do/mocks/BalanceService.go b/do/mocks/BalanceService.go
index ea9a08ff..81384906 100644
--- a/do/mocks/BalanceService.go
+++ b/do/mocks/BalanceService.go
@@ -20,6 +20,7 @@ import (
 type MockBalanceService struct {
 	ctrl     *gomock.Controller
 	recorder *MockBalanceServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockBalanceServiceMockRecorder is the mock recorder for MockBalanceService.
diff --git a/do/mocks/BillingHistoryService.go b/do/mocks/BillingHistoryService.go
index 79203d17..65cf2522 100644
--- a/do/mocks/BillingHistoryService.go
+++ b/do/mocks/BillingHistoryService.go
@@ -20,6 +20,7 @@ import (
 type MockBillingHistoryService struct {
 	ctrl     *gomock.Controller
 	recorder *MockBillingHistoryServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockBillingHistoryServiceMockRecorder is the mock recorder for MockBillingHistoryService.
diff --git a/do/mocks/CDNsService.go b/do/mocks/CDNsService.go
index d2aaf34c..72cdc451 100644
--- a/do/mocks/CDNsService.go
+++ b/do/mocks/CDNsService.go
@@ -21,6 +21,7 @@ import (
 type MockCDNsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockCDNsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockCDNsServiceMockRecorder is the mock recorder for MockCDNsService.
diff --git a/do/mocks/CertificatesService.go b/do/mocks/CertificatesService.go
index 33cecdf0..b4d42f4a 100644
--- a/do/mocks/CertificatesService.go
+++ b/do/mocks/CertificatesService.go
@@ -21,6 +21,7 @@ import (
 type MockCertificatesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockCertificatesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockCertificatesServiceMockRecorder is the mock recorder for MockCertificatesService.
diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go
index b9f2a99c..b36985a5 100644
--- a/do/mocks/DatabasesService.go
+++ b/do/mocks/DatabasesService.go
@@ -21,6 +21,7 @@ import (
 type MockDatabasesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockDatabasesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockDatabasesServiceMockRecorder is the mock recorder for MockDatabasesService.
diff --git a/do/mocks/DomainService.go b/do/mocks/DomainService.go
index ae1c24f4..1528b28d 100644
--- a/do/mocks/DomainService.go
+++ b/do/mocks/DomainService.go
@@ -21,6 +21,7 @@ import (
 type MockDomainsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockDomainsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockDomainsServiceMockRecorder is the mock recorder for MockDomainsService.
diff --git a/do/mocks/DropletActionService.go b/do/mocks/DropletActionService.go
index 0aacf090..1141c508 100644
--- a/do/mocks/DropletActionService.go
+++ b/do/mocks/DropletActionService.go
@@ -20,6 +20,7 @@ import (
 type MockDropletActionsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockDropletActionsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockDropletActionsServiceMockRecorder is the mock recorder for MockDropletActionsService.
diff --git a/do/mocks/DropletAutoscaleService.go b/do/mocks/DropletAutoscaleService.go
new file mode 100644
index 00000000..3fa30c05
--- /dev/null
+++ b/do/mocks/DropletAutoscaleService.go
@@ -0,0 +1,159 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: droplet_autoscale.go
+//
+// Generated by this command:
+//
+//	mockgen -source droplet_autoscale.go -package=mocks DropletAutoscaleService
+//
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	godo "github.com/digitalocean/godo"
+	gomock "go.uber.org/mock/gomock"
+)
+
+// MockDropletAutoscaleService is a mock of DropletAutoscaleService interface.
+type MockDropletAutoscaleService struct {
+	ctrl     *gomock.Controller
+	recorder *MockDropletAutoscaleServiceMockRecorder
+	isgomock struct{}
+}
+
+// MockDropletAutoscaleServiceMockRecorder is the mock recorder for MockDropletAutoscaleService.
+type MockDropletAutoscaleServiceMockRecorder struct {
+	mock *MockDropletAutoscaleService
+}
+
+// NewMockDropletAutoscaleService creates a new mock instance.
+func NewMockDropletAutoscaleService(ctrl *gomock.Controller) *MockDropletAutoscaleService {
+	mock := &MockDropletAutoscaleService{ctrl: ctrl}
+	mock.recorder = &MockDropletAutoscaleServiceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockDropletAutoscaleService) EXPECT() *MockDropletAutoscaleServiceMockRecorder {
+	return m.recorder
+}
+
+// Create mocks base method.
+func (m *MockDropletAutoscaleService) Create(arg0 *godo.DropletAutoscalePoolRequest) (*godo.DropletAutoscalePool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Create", arg0)
+	ret0, _ := ret[0].(*godo.DropletAutoscalePool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockDropletAutoscaleServiceMockRecorder) Create(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDropletAutoscaleService)(nil).Create), arg0)
+}
+
+// Delete mocks base method.
+func (m *MockDropletAutoscaleService) Delete(arg0 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockDropletAutoscaleServiceMockRecorder) Delete(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDropletAutoscaleService)(nil).Delete), arg0)
+}
+
+// DeleteDangerous mocks base method.
+func (m *MockDropletAutoscaleService) DeleteDangerous(arg0 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteDangerous", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteDangerous indicates an expected call of DeleteDangerous.
+func (mr *MockDropletAutoscaleServiceMockRecorder) DeleteDangerous(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDangerous", reflect.TypeOf((*MockDropletAutoscaleService)(nil).DeleteDangerous), arg0)
+}
+
+// Get mocks base method.
+func (m *MockDropletAutoscaleService) Get(arg0 string) (*godo.DropletAutoscalePool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0)
+	ret0, _ := ret[0].(*godo.DropletAutoscalePool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockDropletAutoscaleServiceMockRecorder) Get(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDropletAutoscaleService)(nil).Get), arg0)
+}
+
+// List mocks base method.
+func (m *MockDropletAutoscaleService) List() ([]*godo.DropletAutoscalePool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List")
+	ret0, _ := ret[0].([]*godo.DropletAutoscalePool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockDropletAutoscaleServiceMockRecorder) List() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockDropletAutoscaleService)(nil).List))
+}
+
+// ListHistory mocks base method.
+func (m *MockDropletAutoscaleService) ListHistory(arg0 string) ([]*godo.DropletAutoscaleHistoryEvent, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListHistory", arg0)
+	ret0, _ := ret[0].([]*godo.DropletAutoscaleHistoryEvent)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListHistory indicates an expected call of ListHistory.
+func (mr *MockDropletAutoscaleServiceMockRecorder) ListHistory(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListHistory", reflect.TypeOf((*MockDropletAutoscaleService)(nil).ListHistory), arg0)
+}
+
+// ListMembers mocks base method.
+func (m *MockDropletAutoscaleService) ListMembers(arg0 string) ([]*godo.DropletAutoscaleResource, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListMembers", arg0)
+	ret0, _ := ret[0].([]*godo.DropletAutoscaleResource)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListMembers indicates an expected call of ListMembers.
+func (mr *MockDropletAutoscaleServiceMockRecorder) ListMembers(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMembers", reflect.TypeOf((*MockDropletAutoscaleService)(nil).ListMembers), arg0)
+}
+
+// Update mocks base method.
+func (m *MockDropletAutoscaleService) Update(arg0 string, arg1 *godo.DropletAutoscalePoolRequest) (*godo.DropletAutoscalePool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Update", arg0, arg1)
+	ret0, _ := ret[0].(*godo.DropletAutoscalePool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockDropletAutoscaleServiceMockRecorder) Update(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockDropletAutoscaleService)(nil).Update), arg0, arg1)
+}
diff --git a/do/mocks/FirewallsService.go b/do/mocks/FirewallsService.go
index 8a318691..28046e32 100644
--- a/do/mocks/FirewallsService.go
+++ b/do/mocks/FirewallsService.go
@@ -21,6 +21,7 @@ import (
 type MockFirewallsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockFirewallsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockFirewallsServiceMockRecorder is the mock recorder for MockFirewallsService.
diff --git a/do/mocks/ImageActionsService.go b/do/mocks/ImageActionsService.go
index 0ca645aa..733c536f 100644
--- a/do/mocks/ImageActionsService.go
+++ b/do/mocks/ImageActionsService.go
@@ -21,6 +21,7 @@ import (
 type MockImageActionsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockImageActionsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockImageActionsServiceMockRecorder is the mock recorder for MockImageActionsService.
diff --git a/do/mocks/ImageService.go b/do/mocks/ImageService.go
index 4dacd4e2..d8aa1ee5 100644
--- a/do/mocks/ImageService.go
+++ b/do/mocks/ImageService.go
@@ -21,6 +21,7 @@ import (
 type MockImagesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockImagesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockImagesServiceMockRecorder is the mock recorder for MockImagesService.
diff --git a/do/mocks/InvoicesService.go b/do/mocks/InvoicesService.go
index f7e2625f..827f587d 100644
--- a/do/mocks/InvoicesService.go
+++ b/do/mocks/InvoicesService.go
@@ -20,6 +20,7 @@ import (
 type MockInvoicesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockInvoicesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockInvoicesServiceMockRecorder is the mock recorder for MockInvoicesService.
diff --git a/do/mocks/KeysService.go b/do/mocks/KeysService.go
index a4fd9002..1a16868f 100644
--- a/do/mocks/KeysService.go
+++ b/do/mocks/KeysService.go
@@ -21,6 +21,7 @@ import (
 type MockKeysService struct {
 	ctrl     *gomock.Controller
 	recorder *MockKeysServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockKeysServiceMockRecorder is the mock recorder for MockKeysService.
diff --git a/do/mocks/KubernetesService.go b/do/mocks/KubernetesService.go
index 5ec1063c..d4b44e98 100644
--- a/do/mocks/KubernetesService.go
+++ b/do/mocks/KubernetesService.go
@@ -21,6 +21,7 @@ import (
 type MockKubernetesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockKubernetesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockKubernetesServiceMockRecorder is the mock recorder for MockKubernetesService.
diff --git a/do/mocks/Listen.go b/do/mocks/Listen.go
index 49e600c1..94882a4e 100644
--- a/do/mocks/Listen.go
+++ b/do/mocks/Listen.go
@@ -19,6 +19,7 @@ import (
 type MockListenerService struct {
 	ctrl     *gomock.Controller
 	recorder *MockListenerServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockListenerServiceMockRecorder is the mock recorder for MockListenerService.
diff --git a/do/mocks/LoadBalancersService.go b/do/mocks/LoadBalancersService.go
index ac964404..d58affbf 100644
--- a/do/mocks/LoadBalancersService.go
+++ b/do/mocks/LoadBalancersService.go
@@ -21,6 +21,7 @@ import (
 type MockLoadBalancersService struct {
 	ctrl     *gomock.Controller
 	recorder *MockLoadBalancersServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockLoadBalancersServiceMockRecorder is the mock recorder for MockLoadBalancersService.
diff --git a/do/mocks/MonitoringService.go b/do/mocks/MonitoringService.go
index 42d33f7e..a2c29be3 100644
--- a/do/mocks/MonitoringService.go
+++ b/do/mocks/MonitoringService.go
@@ -21,6 +21,7 @@ import (
 type MockMonitoringService struct {
 	ctrl     *gomock.Controller
 	recorder *MockMonitoringServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockMonitoringServiceMockRecorder is the mock recorder for MockMonitoringService.
diff --git a/do/mocks/OAuthService.go b/do/mocks/OAuthService.go
index eba9c703..56c5c817 100644
--- a/do/mocks/OAuthService.go
+++ b/do/mocks/OAuthService.go
@@ -20,6 +20,7 @@ import (
 type MockOAuthService struct {
 	ctrl     *gomock.Controller
 	recorder *MockOAuthServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockOAuthServiceMockRecorder is the mock recorder for MockOAuthService.
diff --git a/do/mocks/OneClickService.go b/do/mocks/OneClickService.go
index 825b19ea..7dd68384 100644
--- a/do/mocks/OneClickService.go
+++ b/do/mocks/OneClickService.go
@@ -20,6 +20,7 @@ import (
 type MockOneClickService struct {
 	ctrl     *gomock.Controller
 	recorder *MockOneClickServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockOneClickServiceMockRecorder is the mock recorder for MockOneClickService.
diff --git a/do/mocks/ProjectsService.go b/do/mocks/ProjectsService.go
index f50b07e7..28005dee 100644
--- a/do/mocks/ProjectsService.go
+++ b/do/mocks/ProjectsService.go
@@ -21,6 +21,7 @@ import (
 type MockProjectsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockProjectsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockProjectsServiceMockRecorder is the mock recorder for MockProjectsService.
diff --git a/do/mocks/RegionsService.go b/do/mocks/RegionsService.go
index 8c6d2165..28b1b41a 100644
--- a/do/mocks/RegionsService.go
+++ b/do/mocks/RegionsService.go
@@ -20,6 +20,7 @@ import (
 type MockRegionsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockRegionsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockRegionsServiceMockRecorder is the mock recorder for MockRegionsService.
diff --git a/do/mocks/RegistryService.go b/do/mocks/RegistryService.go
index 74d41b9b..ddb9165d 100644
--- a/do/mocks/RegistryService.go
+++ b/do/mocks/RegistryService.go
@@ -21,6 +21,7 @@ import (
 type MockRegistryService struct {
 	ctrl     *gomock.Controller
 	recorder *MockRegistryServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockRegistryServiceMockRecorder is the mock recorder for MockRegistryService.
diff --git a/do/mocks/ReservedIPActionsService.go b/do/mocks/ReservedIPActionsService.go
index 5725b1ca..0b835976 100644
--- a/do/mocks/ReservedIPActionsService.go
+++ b/do/mocks/ReservedIPActionsService.go
@@ -21,6 +21,7 @@ import (
 type MockReservedIPActionsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockReservedIPActionsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockReservedIPActionsServiceMockRecorder is the mock recorder for MockReservedIPActionsService.
diff --git a/do/mocks/ReservedIPsService.go b/do/mocks/ReservedIPsService.go
index 1a16d115..fb828b94 100644
--- a/do/mocks/ReservedIPsService.go
+++ b/do/mocks/ReservedIPsService.go
@@ -21,6 +21,7 @@ import (
 type MockReservedIPsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockReservedIPsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockReservedIPsServiceMockRecorder is the mock recorder for MockReservedIPsService.
diff --git a/do/mocks/Runner.go b/do/mocks/Runner.go
index 55a32a8e..8bfcb48f 100644
--- a/do/mocks/Runner.go
+++ b/do/mocks/Runner.go
@@ -19,6 +19,7 @@ import (
 type MockRunner struct {
 	ctrl     *gomock.Controller
 	recorder *MockRunnerMockRecorder
+	isgomock struct{}
 }
 
 // MockRunnerMockRecorder is the mock recorder for MockRunner.
diff --git a/do/mocks/ServerlessService.go b/do/mocks/ServerlessService.go
index 9a14a899..f26c5c7a 100644
--- a/do/mocks/ServerlessService.go
+++ b/do/mocks/ServerlessService.go
@@ -23,6 +23,7 @@ import (
 type MockServerlessService struct {
 	ctrl     *gomock.Controller
 	recorder *MockServerlessServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockServerlessServiceMockRecorder is the mock recorder for MockServerlessService.
diff --git a/do/mocks/SizesService.go b/do/mocks/SizesService.go
index c9ffe805..a2c57b03 100644
--- a/do/mocks/SizesService.go
+++ b/do/mocks/SizesService.go
@@ -20,6 +20,7 @@ import (
 type MockSizesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockSizesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockSizesServiceMockRecorder is the mock recorder for MockSizesService.
diff --git a/do/mocks/SnapshotsService.go b/do/mocks/SnapshotsService.go
index 09086ae8..cb69ec7a 100644
--- a/do/mocks/SnapshotsService.go
+++ b/do/mocks/SnapshotsService.go
@@ -20,6 +20,7 @@ import (
 type MockSnapshotsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockSnapshotsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockSnapshotsServiceMockRecorder is the mock recorder for MockSnapshotsService.
diff --git a/do/mocks/TagsService.go b/do/mocks/TagsService.go
index 5a6ed17a..5f31ce66 100644
--- a/do/mocks/TagsService.go
+++ b/do/mocks/TagsService.go
@@ -21,6 +21,7 @@ import (
 type MockTagsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockTagsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockTagsServiceMockRecorder is the mock recorder for MockTagsService.
diff --git a/do/mocks/UptimeChecksService.go b/do/mocks/UptimeChecksService.go
index 926c34e4..b4baf99b 100644
--- a/do/mocks/UptimeChecksService.go
+++ b/do/mocks/UptimeChecksService.go
@@ -21,6 +21,7 @@ import (
 type MockUptimeChecksService struct {
 	ctrl     *gomock.Controller
 	recorder *MockUptimeChecksServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockUptimeChecksServiceMockRecorder is the mock recorder for MockUptimeChecksService.
diff --git a/do/mocks/VPCsService.go b/do/mocks/VPCsService.go
index 33d86691..a4f4873f 100644
--- a/do/mocks/VPCsService.go
+++ b/do/mocks/VPCsService.go
@@ -21,6 +21,7 @@ import (
 type MockVPCsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockVPCsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockVPCsServiceMockRecorder is the mock recorder for MockVPCsService.
diff --git a/do/mocks/VolumeActionsService.go b/do/mocks/VolumeActionsService.go
index 60a22b67..423dc5d8 100644
--- a/do/mocks/VolumeActionsService.go
+++ b/do/mocks/VolumeActionsService.go
@@ -20,6 +20,7 @@ import (
 type MockVolumeActionsService struct {
 	ctrl     *gomock.Controller
 	recorder *MockVolumeActionsServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockVolumeActionsServiceMockRecorder is the mock recorder for MockVolumeActionsService.
diff --git a/do/mocks/VolumesService.go b/do/mocks/VolumesService.go
index 48920d66..bad84e71 100644
--- a/do/mocks/VolumesService.go
+++ b/do/mocks/VolumesService.go
@@ -21,6 +21,7 @@ import (
 type MockVolumesService struct {
 	ctrl     *gomock.Controller
 	recorder *MockVolumesServiceMockRecorder
+	isgomock struct{}
 }
 
 // MockVolumesServiceMockRecorder is the mock recorder for MockVolumesService.
diff --git a/scripts/regenmocks.sh b/scripts/regenmocks.sh
index 1e53bd73..93599c94 100755
--- a/scripts/regenmocks.sh
+++ b/scripts/regenmocks.sh
@@ -18,6 +18,7 @@ mockgen -source certificates.go -package=mocks CertificateSservice > mocks/Certi
 mockgen -source databases.go -package=mocks DatabasesService > mocks/DatabasesService.go
 mockgen -source domains.go -package=mocks DomainService > mocks/DomainService.go
 mockgen -source droplet_actions.go -package=mocks DropletActionsService > mocks/DropletActionService.go
+mockgen -source droplet_autoscale.go -package=mocks DropletAutoscaleService > mocks/DropletAutoscaleService.go
 mockgen -source droplets.go -package=mocks DropletsService > mocks/DropletsService.go
 mockgen -source firewalls.go -package=mocks FirewallsService > mocks/FirewallsService.go
 mockgen -source image_actions.go -package=mocks ImageActionsService > mocks/ImageActionsService.go


Test patch:
diff --git a/commands/commands_test.go b/commands/commands_test.go
index 0991468e..1837453f 100644
--- a/commands/commands_test.go
+++ b/commands/commands_test.go
@@ -152,6 +152,7 @@ type tcMocks struct {
 	billingHistory        *domocks.MockBillingHistoryService
 	databases             *domocks.MockDatabasesService
 	dropletActions        *domocks.MockDropletActionsService
+	dropletAutoscale      *domocks.MockDropletAutoscaleService
 	droplets              *domocks.MockDropletsService
 	keys                  *domocks.MockKeysService
 	sizes                 *domocks.MockSizesService
@@ -206,6 +207,7 @@ func withTestClient(t *testing.T, tFn testFn) {
 		reservedIPActions:     domocks.NewMockReservedIPActionsService(ctrl),
 		droplets:              domocks.NewMockDropletsService(ctrl),
 		dropletActions:        domocks.NewMockDropletActionsService(ctrl),
+		dropletAutoscale:      domocks.NewMockDropletAutoscaleService(ctrl),
 		domains:               domocks.NewMockDomainsService(ctrl),
 		tags:                  domocks.NewMockTagsService(ctrl),
 		uptimeChecks:          domocks.NewMockUptimeChecksService(ctrl),
@@ -260,6 +262,7 @@ func withTestClient(t *testing.T, tFn testFn) {
 		ReservedIPActions: func() do.ReservedIPActionsService { return tm.reservedIPActions },
 		Droplets:          func() do.DropletsService { return tm.droplets },
 		DropletActions:    func() do.DropletActionsService { return tm.dropletActions },
+		DropletAutoscale:  func() do.DropletAutoscaleService { return tm.dropletAutoscale },
 		Domains:           func() do.DomainsService { return tm.domains },
 		Actions:           func() do.ActionsService { return tm.actions },
 		Account:           func() do.AccountService { return tm.account },
diff --git a/commands/droplet_autoscale_test.go b/commands/droplet_autoscale_test.go
new file mode 100644
index 00000000..b9d08b66
--- /dev/null
+++ b/commands/droplet_autoscale_test.go
@@ -0,0 +1,224 @@
+package commands
+
+import (
+	"testing"
+
+	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/godo"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	testAutoscalePools = []*godo.DropletAutoscalePool{
+		{
+			ID:   "51154959-e07b-4093-98fb-828590ecc76d",
+			Name: "test-droplet-autoscale-pool-01",
+			Config: &godo.DropletAutoscaleConfiguration{
+				TargetNumberInstances: 3,
+			},
+			DropletTemplate: &godo.DropletAutoscaleResourceTemplate{
+				Size:             "s-1vcpu-512mb-10gb",
+				Region:           "s2r1",
+				Image:            "547864",
+				Tags:             []string{"test-pool-01"},
+				SSHKeys:          []string{"key-1", "key-2"},
+				VpcUUID:          "05790d02-c7e0-47d6-a917-5b4cf68cf5b7",
+				WithDropletAgent: true,
+				UserData:         "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n",
+			},
+		},
+		{
+			ID:   "4d99fb28-b33d-4791-aff5-bf30f8f4f917",
+			Name: "test-droplet-autoscale-pool-02",
+			Config: &godo.DropletAutoscaleConfiguration{
+				TargetNumberInstances: 3,
+			},
+			DropletTemplate: &godo.DropletAutoscaleResourceTemplate{
+				Size:             "s-1vcpu-512mb-10gb",
+				Region:           "s2r1",
+				Image:            "547864",
+				Tags:             []string{"test-pool-02"},
+				SSHKeys:          []string{"key-1", "key-2"},
+				VpcUUID:          "05790d02-c7e0-47d6-a917-5b4cf68cf5b7",
+				WithDropletAgent: true,
+				UserData:         "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n",
+			},
+		},
+	}
+	testAutoscaleMembers = []*godo.DropletAutoscaleResource{
+		{
+			DropletID:    1,
+			HealthStatus: "healthy",
+			Status:       "active",
+		},
+		{
+			DropletID:    2,
+			HealthStatus: "healthy",
+			Status:       "active",
+		},
+	}
+	testAutoscaleHistory = []*godo.DropletAutoscaleHistoryEvent{
+		{
+			HistoryEventID:       "c4686a63-4996-484d-b269-b329b7e97051",
+			CurrentInstanceCount: 0,
+			DesiredInstanceCount: 3,
+			Reason:               "configuration update",
+			Status:               "success",
+		},
+		{
+			HistoryEventID:       "cb676933-73ba-43da-9c43-bb286a5af545",
+			CurrentInstanceCount: 3,
+			DesiredInstanceCount: 3,
+			Reason:               "scale up",
+			Status:               "success",
+		},
+	}
+)
+
+func TestDropletAutoscaleCommand(t *testing.T) {
+	cmd := DropletAutoscale()
+	assert.NotNil(t, cmd)
+	assertCommandNames(t, cmd, "create", "update", "get", "list", "list-members", "list-history", "delete", "delete-dangerous")
+}
+
+func TestDropletAutoscaleCreate(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		createReq := godo.DropletAutoscalePoolRequest{
+			Name: "test-droplet-autoscale-pool-01",
+			Config: &godo.DropletAutoscaleConfiguration{
+				TargetNumberInstances: 3,
+			},
+			DropletTemplate: &godo.DropletAutoscaleResourceTemplate{
+				Size:             "s-1vcpu-512mb-10gb",
+				Region:           "s2r1",
+				Image:            "547864",
+				Tags:             []string{"test-pool-01"},
+				SSHKeys:          []string{"key-1", "key-2"},
+				VpcUUID:          "05790d02-c7e0-47d6-a917-5b4cf68cf5b7",
+				WithDropletAgent: true,
+				UserData:         "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n",
+			},
+		}
+
+		tm.dropletAutoscale.EXPECT().Create(&createReq).Return(testAutoscalePools[0], nil)
+
+		c.Doit.Set(c.NS, doctl.ArgAutoscaleName, "test-droplet-autoscale-pool-01")
+		c.Doit.Set(c.NS, doctl.ArgAutoscaleTargetInstances, "3")
+		c.Doit.Set(c.NS, doctl.ArgSizeSlug, "s-1vcpu-512mb-10gb")
+		c.Doit.Set(c.NS, doctl.ArgRegionSlug, "s2r1")
+		c.Doit.Set(c.NS, doctl.ArgImage, "547864")
+		c.Doit.Set(c.NS, doctl.ArgTag, "test-pool-01")
+		c.Doit.Set(c.NS, doctl.ArgSSHKeys, []string{"key-1", "key-2"})
+		c.Doit.Set(c.NS, doctl.ArgVPCUUID, "05790d02-c7e0-47d6-a917-5b4cf68cf5b7")
+		c.Doit.Set(c.NS, doctl.ArgDropletAgent, "true")
+		c.Doit.Set(c.NS, doctl.ArgUserData, "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n")
+
+		err := RunDropletAutoscaleCreate(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleUpdate(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		poolID := "51154959-e07b-4093-98fb-828590ecc76d"
+		updateReq := godo.DropletAutoscalePoolRequest{
+			Name: "test-droplet-autoscale-pool-01",
+			Config: &godo.DropletAutoscaleConfiguration{
+				TargetNumberInstances: 3,
+			},
+			DropletTemplate: &godo.DropletAutoscaleResourceTemplate{
+				Size:             "s-1vcpu-512mb-10gb",
+				Region:           "s2r1",
+				Image:            "547864",
+				Tags:             []string{"test-pool-01"},
+				SSHKeys:          []string{"key-1", "key-2"},
+				VpcUUID:          "05790d02-c7e0-47d6-a917-5b4cf68cf5b7",
+				WithDropletAgent: true,
+				UserData:         "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n",
+			},
+		}
+
+		tm.dropletAutoscale.EXPECT().Update(poolID, &updateReq).Return(testAutoscalePools[0], nil)
+		c.Args = append(c.Args, poolID)
+
+		c.Doit.Set(c.NS, doctl.ArgAutoscaleName, "test-droplet-autoscale-pool-01")
+		c.Doit.Set(c.NS, doctl.ArgAutoscaleTargetInstances, "3")
+		c.Doit.Set(c.NS, doctl.ArgSizeSlug, "s-1vcpu-512mb-10gb")
+		c.Doit.Set(c.NS, doctl.ArgRegionSlug, "s2r1")
+		c.Doit.Set(c.NS, doctl.ArgImage, "547864")
+		c.Doit.Set(c.NS, doctl.ArgTag, "test-pool-01")
+		c.Doit.Set(c.NS, doctl.ArgSSHKeys, []string{"key-1", "key-2"})
+		c.Doit.Set(c.NS, doctl.ArgVPCUUID, "05790d02-c7e0-47d6-a917-5b4cf68cf5b7")
+		c.Doit.Set(c.NS, doctl.ArgDropletAgent, "true")
+		c.Doit.Set(c.NS, doctl.ArgUserData, "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n")
+
+		err := RunDropletAutoscaleUpdate(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleGet(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		poolID := "51154959-e07b-4093-98fb-828590ecc76d"
+		tm.dropletAutoscale.EXPECT().Get(poolID).Return(testAutoscalePools[0], nil)
+		c.Args = append(c.Args, poolID)
+
+		err := RunDropletAutoscaleGet(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleList(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		tm.dropletAutoscale.EXPECT().List().Return(testAutoscalePools, nil)
+
+		err := RunDropletAutoscaleList(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleListMembers(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		poolID := "51154959-e07b-4093-98fb-828590ecc76d"
+		tm.dropletAutoscale.EXPECT().ListMembers(poolID).Return(testAutoscaleMembers, nil)
+		c.Args = append(c.Args, poolID)
+
+		err := RunDropletAutoscaleListMembers(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleListHistory(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		poolID := "51154959-e07b-4093-98fb-828590ecc76d"
+		tm.dropletAutoscale.EXPECT().ListHistory(poolID).Return(testAutoscaleHistory, nil)
+		c.Args = append(c.Args, poolID)
+
+		err := RunDropletAutoscaleListHistory(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleDelete(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		poolID := "51154959-e07b-4093-98fb-828590ecc76d"
+		tm.dropletAutoscale.EXPECT().Delete(poolID).Return(nil)
+		c.Args = append(c.Args, poolID)
+		c.Doit.Set(c.NS, doctl.ArgForce, "true")
+
+		err := RunDropletAutoscaleDelete(c)
+		assert.NoError(t, err)
+	})
+}
+
+func TestDropletAutoscaleDeleteDangerous(t *testing.T) {
+	withTestClient(t, func(c *CmdConfig, tm *tcMocks) {
+		poolID := "51154959-e07b-4093-98fb-828590ecc76d"
+		tm.dropletAutoscale.EXPECT().DeleteDangerous(poolID).Return(nil)
+		c.Args = append(c.Args, poolID)
+		c.Doit.Set(c.NS, doctl.ArgForce, "true")
+
+		err := RunDropletAutoscaleDeleteDangerous(c)
+		assert.NoError(t, err)
+	})
+}