diff --git a/digitalocean/dropletautoscale/datasource_droplet_autoscale.go b/digitalocean/dropletautoscale/datasource_droplet_autoscale.go new file mode 100644 index 000000000..166c76b06 --- /dev/null +++ b/digitalocean/dropletautoscale/datasource_droplet_autoscale.go @@ -0,0 +1,277 @@ +package dropletautoscale + +import ( + "context" + + "github.com/digitalocean/godo" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func DataSourceDigitalOceanDropletAutoscale() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceDigitalOceanDropletAutoscaleRead, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + Description: "ID of the Droplet autoscale pool", + ValidateFunc: validation.NoZeroValues, + ExactlyOneOf: []string{"id", "name"}, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the Droplet autoscale pool", + ValidateFunc: validation.NoZeroValues, + ExactlyOneOf: []string{"id", "name"}, + }, + "config": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "min_instances": { + Type: schema.TypeInt, + Computed: true, + Description: "Min number of members", + }, + "max_instances": { + Type: schema.TypeInt, + Computed: true, + Description: "Max number of members", + }, + "target_cpu_utilization": { + Type: schema.TypeFloat, + Computed: true, + Description: "CPU target threshold", + }, + "target_memory_utilization": { + Type: schema.TypeFloat, + Computed: true, + Description: "Memory target threshold", + }, + "cooldown_minutes": { + Type: schema.TypeInt, + Computed: true, + Description: "Cooldown duration", + }, + "target_number_instances": { + Type: schema.TypeInt, + Computed: true, + Description: "Target number of members", + }, + }, + }, + }, + "droplet_template": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "size": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet size", + }, + "region": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet region", + }, + "image": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet image", + }, + "tags": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Description: "Droplet tags", + }, + "ssh_keys": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Description: "Droplet SSH keys", + }, + "vpc_uuid": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet VPC UUID", + }, + "with_droplet_agent": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable droplet agent", + }, + "project_id": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet project ID", + }, + "ipv6": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable droplet IPv6", + }, + "user_data": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet user data", + }, + }, + }, + }, + "current_utilization": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "memory": { + Type: schema.TypeFloat, + Computed: true, + Description: "Average Memory utilization", + }, + "cpu": { + Type: schema.TypeFloat, + Computed: true, + Description: "Average CPU utilization", + }, + }, + }, + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet autoscale pool status", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet autoscale pool create timestamp", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet autoscale pool update timestamp", + }, + }, + } +} + +func dataSourceDigitalOceanDropletAutoscaleRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + + var foundDropletAutoscalePool *godo.DropletAutoscalePool + if id, ok := d.GetOk("id"); ok { + pool, _, err := client.DropletAutoscale.Get(context.Background(), id.(string)) + if err != nil { + return diag.Errorf("Error retrieving Droplet autoscale pool: %v", err) + } + foundDropletAutoscalePool = pool + } else if name, ok := d.GetOk("name"); ok { + dropletAutoscalePoolList := make([]*godo.DropletAutoscalePool, 0) + opts := &godo.ListOptions{ + Page: 1, + PerPage: 100, + } + // Paginate through all active resources + for { + pools, resp, err := client.DropletAutoscale.List(context.Background(), opts) + if err != nil { + return diag.Errorf("Error listing Droplet autoscale pools: %v", err) + } + dropletAutoscalePoolList = append(dropletAutoscalePoolList, pools...) + if resp.Links.IsLastPage() { + break + } + page, err := resp.Links.CurrentPage() + if err != nil { + break + } + opts.Page = page + 1 + } + // Scan through the list to find a resource name match + for i := range dropletAutoscalePoolList { + if dropletAutoscalePoolList[i].Name == name { + foundDropletAutoscalePool = dropletAutoscalePoolList[i] + break + } + } + } else { + return diag.Errorf("Need to specify either a name or an id to look up the Droplet autoscale pool") + } + if foundDropletAutoscalePool == nil { + return diag.Errorf("Droplet autoscale pool not found") + } + + d.SetId(foundDropletAutoscalePool.ID) + d.Set("name", foundDropletAutoscalePool.Name) + d.Set("config", flattenConfig(foundDropletAutoscalePool.Config)) + d.Set("droplet_template", flattenTemplate(foundDropletAutoscalePool.DropletTemplate)) + d.Set("current_utilization", flattenUtilization(foundDropletAutoscalePool.CurrentUtilization)) + d.Set("status", foundDropletAutoscalePool.Status) + d.Set("created_at", foundDropletAutoscalePool.CreatedAt.UTC().String()) + d.Set("updated_at", foundDropletAutoscalePool.UpdatedAt.UTC().String()) + + return nil +} + +func flattenConfig(config *godo.DropletAutoscaleConfiguration) []map[string]interface{} { + result := make([]map[string]interface{}, 0, 1) + if config != nil { + r := make(map[string]interface{}) + r["min_instances"] = config.MinInstances + r["max_instances"] = config.MaxInstances + r["target_cpu_utilization"] = config.TargetCPUUtilization + r["target_memory_utilization"] = config.TargetMemoryUtilization + r["cooldown_minutes"] = config.CooldownMinutes + r["target_number_instances"] = config.TargetNumberInstances + result = append(result, r) + } + return result +} + +func flattenTemplate(template *godo.DropletAutoscaleResourceTemplate) []map[string]interface{} { + result := make([]map[string]interface{}, 0, 1) + if template != nil { + r := make(map[string]interface{}) + r["size"] = template.Size + r["region"] = template.Region + r["image"] = template.Image + r["vpc_uuid"] = template.VpcUUID + r["with_droplet_agent"] = template.WithDropletAgent + r["project_id"] = template.ProjectID + r["ipv6"] = template.IPV6 + r["user_data"] = template.UserData + + tagSet := schema.NewSet(schema.HashString, []interface{}{}) + for _, tag := range template.Tags { + tagSet.Add(tag) + } + r["tags"] = tagSet + + keySet := schema.NewSet(schema.HashString, []interface{}{}) + for _, key := range template.SSHKeys { + keySet.Add(key) + } + r["ssh_keys"] = keySet + result = append(result, r) + } + return result +} + +func flattenUtilization(util *godo.DropletAutoscaleResourceUtilization) []map[string]interface{} { + result := make([]map[string]interface{}, 0, 1) + if util != nil { + r := make(map[string]interface{}) + r["memory"] = util.Memory + r["cpu"] = util.CPU + result = append(result, r) + } + return result +} diff --git a/digitalocean/dropletautoscale/datasource_droplet_autoscale_test.go b/digitalocean/dropletautoscale/datasource_droplet_autoscale_test.go new file mode 100644 index 000000000..2e10f1df5 --- /dev/null +++ b/digitalocean/dropletautoscale/datasource_droplet_autoscale_test.go @@ -0,0 +1,261 @@ +package dropletautoscale_test + +import ( + "testing" + + "github.com/digitalocean/godo" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceDigitalOceanDropletAutoscale_Static(t *testing.T) { + var autoscalePool godo.DropletAutoscalePool + name := acceptance.RandomTestName() + + createConfig := testAccCheckDigitalOceanDropletAutoscaleConfig_static(name, 1) + dataSourceIDConfig := ` +data "digitalocean_droplet_autoscale" "foo" { + id = digitalocean_droplet_autoscale.foobar.id +}` + dataSourceNameConfig := ` +data "digitalocean_droplet_autoscale" "foo" { + name = digitalocean_droplet_autoscale.foobar.name +}` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanDropletAutoscaleDestroy, + Steps: []resource.TestStep{ + { + // Test create + Config: createConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("digitalocean_droplet_autoscale.foobar", &autoscalePool), + ), + }, + { + // Import by id + Config: createConfig + dataSourceIDConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("data.digitalocean_droplet_autoscale.foo", &autoscalePool), + resource.TestCheckResourceAttrSet("data.digitalocean_droplet_autoscale.foo", "id"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "name", name), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.min_instances", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.max_instances", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_cpu_utilization", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_memory_utilization", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.cooldown_minutes", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_number_instances", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "status", "active"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "created_at"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "updated_at"), + ), + }, + { + // Import by name + Config: createConfig + dataSourceNameConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("data.digitalocean_droplet_autoscale.foo", &autoscalePool), + resource.TestCheckResourceAttrSet("data.digitalocean_droplet_autoscale.foo", "id"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "name", name), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.min_instances", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.max_instances", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_cpu_utilization", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_memory_utilization", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.cooldown_minutes", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_number_instances", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "status", "active"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "created_at"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "updated_at"), + ), + }, + }, + }) +} + +func TestAccDataSourceDigitalOceanDropletAutoscale_Dynamic(t *testing.T) { + var autoscalePool godo.DropletAutoscalePool + name := acceptance.RandomTestName() + + createConfig := testAccCheckDigitalOceanDropletAutoscaleConfig_dynamic(name, 1) + dataSourceIDConfig := ` +data "digitalocean_droplet_autoscale" "foo" { + id = digitalocean_droplet_autoscale.foobar.id +}` + dataSourceNameConfig := ` +data "digitalocean_droplet_autoscale" "foo" { + name = digitalocean_droplet_autoscale.foobar.name +}` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanDropletAutoscaleDestroy, + Steps: []resource.TestStep{ + { + // Test create + Config: createConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("digitalocean_droplet_autoscale.foobar", &autoscalePool), + ), + }, + { + // Import by id + Config: createConfig + dataSourceIDConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("data.digitalocean_droplet_autoscale.foo", &autoscalePool), + resource.TestCheckResourceAttrSet("data.digitalocean_droplet_autoscale.foo", "id"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "name", name), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.min_instances", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.max_instances", "3"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_cpu_utilization", "0.5"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_memory_utilization", "0.5"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.cooldown_minutes", "5"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_number_instances", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "status", "active"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "created_at"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "updated_at"), + ), + }, + { + // Import by name + Config: createConfig + dataSourceNameConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("data.digitalocean_droplet_autoscale.foo", &autoscalePool), + resource.TestCheckResourceAttrSet("data.digitalocean_droplet_autoscale.foo", "id"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "name", name), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.min_instances", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.max_instances", "3"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_cpu_utilization", "0.5"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_memory_utilization", "0.5"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.cooldown_minutes", "5"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "config.0.target_number_instances", "0"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "data.digitalocean_droplet_autoscale.foo", "status", "active"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "created_at"), + resource.TestCheckResourceAttrSet( + "data.digitalocean_droplet_autoscale.foo", "updated_at"), + ), + }, + }, + }) +} diff --git a/digitalocean/dropletautoscale/resource_droplet_autoscale.go b/digitalocean/dropletautoscale/resource_droplet_autoscale.go new file mode 100644 index 000000000..077305ec7 --- /dev/null +++ b/digitalocean/dropletautoscale/resource_droplet_autoscale.go @@ -0,0 +1,375 @@ +package dropletautoscale + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/digitalocean/godo" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func ResourceDigitalOceanDropletAutoscale() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDigitalOceanDropletAutoscaleCreate, + ReadContext: resourceDigitalOceanDropletAutoscaleRead, + UpdateContext: resourceDigitalOceanDropletAutoscaleUpdate, + DeleteContext: resourceDigitalOceanDropletAutoscaleDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the Droplet autoscale pool", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the Droplet autoscale pool", + }, + "config": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "min_instances": { + Type: schema.TypeInt, + Optional: true, + Description: "Min number of members", + ValidateFunc: validation.All(validation.NoZeroValues), + }, + "max_instances": { + Type: schema.TypeInt, + Optional: true, + Description: "Max number of members", + ValidateFunc: validation.All(validation.NoZeroValues), + }, + "target_cpu_utilization": { + Type: schema.TypeFloat, + Optional: true, + Description: "CPU target threshold", + ValidateFunc: validation.All(validation.FloatBetween(0, 1)), + }, + "target_memory_utilization": { + Type: schema.TypeFloat, + Optional: true, + Description: "Memory target threshold", + ValidateFunc: validation.All(validation.FloatBetween(0, 1)), + }, + "cooldown_minutes": { + Type: schema.TypeInt, + Optional: true, + Description: "Cooldown duration", + ValidateFunc: validation.All(validation.NoZeroValues), + }, + "target_number_instances": { + Type: schema.TypeInt, + Optional: true, + Description: "Target number of members", + ValidateFunc: validation.All(validation.NoZeroValues), + }, + }, + }, + }, + "droplet_template": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "size": { + Type: schema.TypeString, + Required: true, + Description: "Droplet size", + }, + "region": { + Type: schema.TypeString, + Required: true, + Description: "Droplet region", + }, + "image": { + Type: schema.TypeString, + Required: true, + Description: "Droplet image", + }, + "tags": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "Droplet tags", + }, + "ssh_keys": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "Droplet SSH keys", + }, + "vpc_uuid": { + Type: schema.TypeString, + Optional: true, + Description: "Droplet VPC UUID", + }, + "with_droplet_agent": { + Type: schema.TypeBool, + Optional: true, + Description: "Enable droplet agent", + }, + "project_id": { + Type: schema.TypeString, + Optional: true, + Description: "Droplet project ID", + }, + "ipv6": { + Type: schema.TypeBool, + Optional: true, + Description: "Enable droplet IPv6", + }, + "user_data": { + Type: schema.TypeString, + Optional: true, + Description: "Droplet user data", + }, + }, + }, + }, + "current_utilization": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "memory": { + Type: schema.TypeFloat, + Computed: true, + Description: "Average Memory utilization", + }, + "cpu": { + Type: schema.TypeFloat, + Computed: true, + Description: "Average CPU utilization", + }, + }, + }, + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet autoscale pool status", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet autoscale pool create timestamp", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Droplet autoscale pool update timestamp", + }, + }, + } +} + +func expandConfig(config []interface{}) *godo.DropletAutoscaleConfiguration { + if len(config) > 0 { + poolConfig := config[0].(map[string]interface{}) + return &godo.DropletAutoscaleConfiguration{ + MinInstances: uint64(poolConfig["min_instances"].(int)), + MaxInstances: uint64(poolConfig["max_instances"].(int)), + TargetCPUUtilization: poolConfig["target_cpu_utilization"].(float64), + TargetMemoryUtilization: poolConfig["target_memory_utilization"].(float64), + CooldownMinutes: uint32(poolConfig["cooldown_minutes"].(int)), + TargetNumberInstances: uint64(poolConfig["target_number_instances"].(int)), + } + } + return nil +} + +func expandTemplate(template []interface{}) *godo.DropletAutoscaleResourceTemplate { + if len(template) > 0 { + poolTemplate := template[0].(map[string]interface{}) + + var tags []string + if v, ok := poolTemplate["tags"]; ok { + for _, tag := range v.(*schema.Set).List() { + tags = append(tags, tag.(string)) + } + } + + var sshKeys []string + if v, ok := poolTemplate["ssh_keys"]; ok { + for _, key := range v.(*schema.Set).List() { + sshKeys = append(sshKeys, key.(string)) + } + } + + return &godo.DropletAutoscaleResourceTemplate{ + Size: poolTemplate["size"].(string), + Region: poolTemplate["region"].(string), + Image: poolTemplate["image"].(string), + Tags: tags, + SSHKeys: sshKeys, + VpcUUID: poolTemplate["vpc_uuid"].(string), + WithDropletAgent: poolTemplate["with_droplet_agent"].(bool), + ProjectID: poolTemplate["project_id"].(string), + IPV6: poolTemplate["ipv6"].(bool), + UserData: poolTemplate["user_data"].(string), + } + } + return nil +} + +func resourceDigitalOceanDropletAutoscaleCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + + pool, _, err := client.DropletAutoscale.Create(context.Background(), &godo.DropletAutoscalePoolRequest{ + Name: d.Get("name").(string), + Config: expandConfig(d.Get("config").([]interface{})), + DropletTemplate: expandTemplate(d.Get("droplet_template").([]interface{})), + }) + if err != nil { + return diag.Errorf("Error creating Droplet autoscale pool: %v", err) + } + d.SetId(pool.ID) + + // Setup to poll for autoscale pool scaling up to the desired count + stateConf := &retry.StateChangeConf{ + Delay: 5 * time.Second, + Pending: []string{"provisioning"}, + Target: []string{"active"}, + Refresh: dropletAutoscaleRefreshFunc(client, d.Id()), + MinTimeout: 15 * time.Second, + Timeout: 15 * time.Minute, + } + if _, err = stateConf.WaitForStateContext(ctx); err != nil { + return diag.Errorf("Error waiting for Droplet autoscale pool (%s) to become active: %v", pool.Name, err) + } + + return resourceDigitalOceanDropletAutoscaleRead(ctx, d, meta) +} + +func resourceDigitalOceanDropletAutoscaleRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + + pool, _, err := client.DropletAutoscale.Get(context.Background(), d.Id()) + if err != nil { + if strings.Contains(err.Error(), fmt.Sprintf("autoscale group with id %s not found", d.Id())) { + d.SetId("") + return nil + } + return diag.Errorf("Error retrieving Droplet autoscale pool: %v", err) + } + + d.Set("name", pool.Name) + d.Set("config", flattenConfig(pool.Config)) + d.Set("current_utilization", flattenUtilization(pool.CurrentUtilization)) + d.Set("status", pool.Status) + d.Set("created_at", pool.CreatedAt.UTC().String()) + d.Set("updated_at", pool.UpdatedAt.UTC().String()) + + // Persist existing image specification (id/slug) if it exists + if t, ok := d.GetOk("droplet_template"); ok { + tList := t.([]interface{}) + if len(tList) > 0 { + tMap := tList[0].(map[string]interface{}) + if tMap["image"] != "" { + pool.DropletTemplate.Image = tMap["image"].(string) + } + } + } + d.Set("droplet_template", flattenTemplate(pool.DropletTemplate)) + + return nil +} + +func resourceDigitalOceanDropletAutoscaleUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + + _, _, err := client.DropletAutoscale.Update(context.Background(), d.Id(), &godo.DropletAutoscalePoolRequest{ + Name: d.Get("name").(string), + Config: expandConfig(d.Get("config").([]interface{})), + DropletTemplate: expandTemplate(d.Get("droplet_template").([]interface{})), + }) + if err != nil { + return diag.Errorf("Error updating Droplet autoscale pool: %v", err) + } + + return resourceDigitalOceanDropletAutoscaleRead(ctx, d, meta) +} + +func resourceDigitalOceanDropletAutoscaleDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + + _, err := client.DropletAutoscale.DeleteDangerous(context.Background(), d.Id()) + if err != nil { + return diag.Errorf("Error updating Droplet autoscale pool: %v", err) + } + + // Setup to poll for autoscale pool deletion + stateConf := &retry.StateChangeConf{ + Delay: 5 * time.Second, + Pending: []string{http.StatusText(http.StatusOK)}, + Target: []string{http.StatusText(http.StatusNotFound)}, + Refresh: dropletAutoscaleRefreshFunc(client, d.Id()), + MinTimeout: 5 * time.Second, + Timeout: 1 * time.Minute, + } + if _, err = stateConf.WaitForStateContext(ctx); err != nil { + return diag.Errorf("Error waiting for Droplet autoscale pool (%s) to become be deleted: %v", d.Get("name"), err) + } + + d.SetId("") + return nil +} + +func dropletAutoscaleRefreshFunc(client *godo.Client, poolID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + // Check autoscale pool status + pool, _, err := client.DropletAutoscale.Get(context.Background(), poolID) + if err != nil { + if strings.Contains(err.Error(), fmt.Sprintf("autoscale group with id %s not found", poolID)) { + return pool, http.StatusText(http.StatusNotFound), nil + } + return nil, "", fmt.Errorf("Error retrieving Droplet autoscale pool: %v", err) + } + if pool.Status != "active" { + return pool, pool.Status, nil + } + members := make([]*godo.DropletAutoscaleResource, 0) + opts := &godo.ListOptions{ + Page: 1, + PerPage: 100, + } + // Paginate through autoscale pool members and validate status + for { + m, resp, err := client.DropletAutoscale.ListMembers(context.Background(), poolID, opts) + if err != nil { + return nil, "", fmt.Errorf("Error listing Droplet autoscale pool members: %v", err) + } + members = append(members, m...) + if resp.Links.IsLastPage() { + break + } + page, err := resp.Links.CurrentPage() + if err != nil { + break + } + opts.Page = page + 1 + } + // Scan through the list to find a non-active provision state + for i := range members { + if members[i].Status != "active" { + return members, members[i].Status, nil + } + } + return members, "active", nil + } +} diff --git a/digitalocean/dropletautoscale/resource_droplet_autoscale_test.go b/digitalocean/dropletautoscale/resource_droplet_autoscale_test.go new file mode 100644 index 000000000..95565edaf --- /dev/null +++ b/digitalocean/dropletautoscale/resource_droplet_autoscale_test.go @@ -0,0 +1,397 @@ +package dropletautoscale_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/digitalocean/godo" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDigitalOceanDropletAutoscale_Static(t *testing.T) { + var autoscalePool godo.DropletAutoscalePool + name := acceptance.RandomTestName() + + createConfig := testAccCheckDigitalOceanDropletAutoscaleConfig_static(name, 1) + updateConfig := strings.ReplaceAll(createConfig, "target_number_instances = 1", "target_number_instances = 2") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanDropletAutoscaleDestroy, + Steps: []resource.TestStep{ + { + // Test create + Config: createConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("digitalocean_droplet_autoscale.foobar", &autoscalePool), + resource.TestCheckResourceAttrSet("digitalocean_droplet_autoscale.foobar", "id"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "name", name), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.min_instances", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.max_instances", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_cpu_utilization", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_memory_utilization", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.cooldown_minutes", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_number_instances", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "status", "active"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "created_at"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "updated_at"), + ), + }, + { + // Test update (static scale up) + Config: updateConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("digitalocean_droplet_autoscale.foobar", &autoscalePool), + resource.TestCheckResourceAttrSet("digitalocean_droplet_autoscale.foobar", "id"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "name", name), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.min_instances", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.max_instances", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_cpu_utilization", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_memory_utilization", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.cooldown_minutes", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_number_instances", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "status", "active"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "created_at"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "updated_at"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanDropletAutoscale_Dynamic(t *testing.T) { + var autoscalePool godo.DropletAutoscalePool + name := acceptance.RandomTestName() + + createConfig := testAccCheckDigitalOceanDropletAutoscaleConfig_dynamic(name, 1) + updateConfig := strings.ReplaceAll(createConfig, "min_instances = 1", "min_instances = 2") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanDropletAutoscaleDestroy, + Steps: []resource.TestStep{ + { + // Test create + Config: createConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("digitalocean_droplet_autoscale.foobar", &autoscalePool), + resource.TestCheckResourceAttrSet("digitalocean_droplet_autoscale.foobar", "id"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "name", name), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.min_instances", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.max_instances", "3"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_cpu_utilization", "0.5"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_memory_utilization", "0.5"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.cooldown_minutes", "5"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_number_instances", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "status", "active"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "created_at"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "updated_at"), + ), + }, + { + // Test update (dynamic scale up) + Config: updateConfig, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDigitalOceanDropletAutoscaleExists("digitalocean_droplet_autoscale.foobar", &autoscalePool), + resource.TestCheckResourceAttrSet("digitalocean_droplet_autoscale.foobar", "id"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "name", name), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.min_instances", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.max_instances", "3"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_cpu_utilization", "0.5"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_memory_utilization", "0.5"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.cooldown_minutes", "5"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "config.0.target_number_instances", "0"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.#", "1"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.size", "c-2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.region", "nyc3"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.image"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.with_droplet_agent", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ipv6", "true"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.user_data", "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.tags.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "droplet_template.0.ssh_keys.#", "2"), + resource.TestCheckResourceAttr( + "digitalocean_droplet_autoscale.foobar", "status", "active"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "created_at"), + resource.TestCheckResourceAttrSet( + "digitalocean_droplet_autoscale.foobar", "updated_at"), + ), + }, + }, + }) +} + +func testAccCheckDigitalOceanDropletAutoscaleExists(n string, autoscalePool *godo.DropletAutoscalePool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Resource not found: %v", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("Resource ID not set") + } + // Check for valid ID response to validate that the resource has been created + client := acceptance.TestAccProvider.Meta().(*config.CombinedConfig).GodoClient() + pool, _, err := client.DropletAutoscale.Get(context.Background(), rs.Primary.ID) + if err != nil { + return err + } + if pool.ID != rs.Primary.ID { + return fmt.Errorf("Droplet autoscale pool not found") + } + *autoscalePool = *pool + return nil + } +} + +func testAccCheckDigitalOceanDropletAutoscaleDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "digitalocean_droplet_autoscale" { + continue + } + client := acceptance.TestAccProvider.Meta().(*config.CombinedConfig).GodoClient() + _, _, err := client.DropletAutoscale.Get(context.Background(), rs.Primary.ID) + if err != nil { + if strings.Contains(err.Error(), fmt.Sprintf("autoscale group with id %s not found", rs.Primary.ID)) { + return nil + } + return fmt.Errorf("Droplet autoscale pool still exists") + } + } + return nil +} + +func testAccCheckDigitalOceanDropletAutoscaleConfig_static(name string, size int) string { + pubKey1, _, err := acctest.RandSSHKeyPair("digitalocean@acceptance-test") + if err != nil { + fmt.Println("Unable to generate public key", err) + return "" + } + + pubKey2, _, err := acctest.RandSSHKeyPair("digitalocean@acceptance-test") + if err != nil { + fmt.Println("Unable to generate public key", err) + return "" + } + + return fmt.Sprintf(` +resource "digitalocean_ssh_key" "foo" { + name = "%s" + public_key = "%s" +} + +resource "digitalocean_ssh_key" "bar" { + name = "%s" + public_key = "%s" +} + +resource "digitalocean_tag" "foo" { + name = "%s" +} + +resource "digitalocean_tag" "bar" { + name = "%s" +} + +resource "digitalocean_droplet_autoscale" "foobar" { + name = "%s" + + config { + target_number_instances = %d + } + + droplet_template { + size = "c-2" + region = "nyc3" + image = "ubuntu-24-04-x64" + tags = [digitalocean_tag.foo.id, digitalocean_tag.bar.id] + ssh_keys = [digitalocean_ssh_key.foo.id, digitalocean_ssh_key.bar.id] + with_droplet_agent = true + ipv6 = true + user_data = "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + } +}`, + acceptance.RandomTestName("sshKey1"), pubKey1, + acceptance.RandomTestName("sshKey2"), pubKey2, + acceptance.RandomTestName("tag1"), + acceptance.RandomTestName("tag2"), + name, size) +} + +func testAccCheckDigitalOceanDropletAutoscaleConfig_dynamic(name string, size int) string { + pubKey1, _, err := acctest.RandSSHKeyPair("digitalocean@acceptance-test") + if err != nil { + fmt.Println("Unable to generate public key", err) + return "" + } + + pubKey2, _, err := acctest.RandSSHKeyPair("digitalocean@acceptance-test") + if err != nil { + fmt.Println("Unable to generate public key", err) + return "" + } + + return fmt.Sprintf(` +resource "digitalocean_ssh_key" "foo" { + name = "%s" + public_key = "%s" +} + +resource "digitalocean_ssh_key" "bar" { + name = "%s" + public_key = "%s" +} + +resource "digitalocean_tag" "foo" { + name = "%s" +} + +resource "digitalocean_tag" "bar" { + name = "%s" +} + +resource "digitalocean_droplet_autoscale" "foobar" { + name = "%s" + + config { + min_instances = %d + max_instances = 3 + target_cpu_utilization = 0.5 + target_memory_utilization = 0.5 + cooldown_minutes = 5 + } + + droplet_template { + size = "c-2" + region = "nyc3" + image = "ubuntu-24-04-x64" + tags = [digitalocean_tag.foo.id, digitalocean_tag.bar.id] + ssh_keys = [digitalocean_ssh_key.foo.id, digitalocean_ssh_key.bar.id] + with_droplet_agent = true + ipv6 = true + user_data = "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + } +}`, + acceptance.RandomTestName("sshKey1"), pubKey1, + acceptance.RandomTestName("sshKey2"), pubKey2, + acceptance.RandomTestName("tag1"), + acceptance.RandomTestName("tag2"), + name, size) +} diff --git a/digitalocean/dropletautoscale/sweep.go b/digitalocean/dropletautoscale/sweep.go new file mode 100644 index 000000000..7e7fcb904 --- /dev/null +++ b/digitalocean/dropletautoscale/sweep.go @@ -0,0 +1,40 @@ +package dropletautoscale + +import ( + "context" + "log" + "strings" + + "github.com/digitalocean/godo" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/sweep" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func init() { + resource.AddTestSweepers("digitalocean_droplet_autoscale", &resource.Sweeper{ + Name: "digitalocean_droplet_autoscale", + F: sweepDropletAutoscale, + }) +} + +func sweepDropletAutoscale(region string) error { + meta, err := sweep.SharedConfigForRegion(region) + if err != nil { + return err + } + client := meta.(*config.CombinedConfig).GodoClient() + pools, _, err := client.DropletAutoscale.List(context.Background(), &godo.ListOptions{PerPage: 200}) + if err != nil { + return err + } + for _, pool := range pools { + if strings.HasPrefix(pool.Name, sweep.TestNamePrefix) { + log.Printf("Destroying droplet autoscale pool %s", pool.Name) + if _, err = client.DropletAutoscale.DeleteDangerous(context.Background(), pool.ID); err != nil { + return err + } + } + } + return nil +} diff --git a/digitalocean/provider.go b/digitalocean/provider.go index f0d682d5c..1c67a4e99 100644 --- a/digitalocean/provider.go +++ b/digitalocean/provider.go @@ -11,6 +11,7 @@ import ( "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/database" "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/domain" "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/droplet" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/dropletautoscale" "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/firewall" "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/image" "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/kubernetes" @@ -109,6 +110,7 @@ func Provider() *schema.Provider { "digitalocean_domain": domain.DataSourceDigitalOceanDomain(), "digitalocean_domains": domain.DataSourceDigitalOceanDomains(), "digitalocean_droplet": droplet.DataSourceDigitalOceanDroplet(), + "digitalocean_droplet_autoscale": dropletautoscale.DataSourceDigitalOceanDropletAutoscale(), "digitalocean_droplets": droplet.DataSourceDigitalOceanDroplets(), "digitalocean_droplet_snapshot": snapshot.DataSourceDigitalOceanDropletSnapshot(), "digitalocean_firewall": firewall.DataSourceDigitalOceanFirewall(), @@ -161,6 +163,7 @@ func Provider() *schema.Provider { "digitalocean_database_kafka_topic": database.ResourceDigitalOceanDatabaseKafkaTopic(), "digitalocean_domain": domain.ResourceDigitalOceanDomain(), "digitalocean_droplet": droplet.ResourceDigitalOceanDroplet(), + "digitalocean_droplet_autoscale": dropletautoscale.ResourceDigitalOceanDropletAutoscale(), "digitalocean_droplet_snapshot": snapshot.ResourceDigitalOceanDropletSnapshot(), "digitalocean_firewall": firewall.ResourceDigitalOceanFirewall(), "digitalocean_floating_ip": reservedip.ResourceDigitalOceanFloatingIP(), diff --git a/docs/data-sources/droplet_autoscale.md b/docs/data-sources/droplet_autoscale.md new file mode 100644 index 000000000..2d81a68ce --- /dev/null +++ b/docs/data-sources/droplet_autoscale.md @@ -0,0 +1,41 @@ +--- +page_title: "DigitalOcean: digitalocean_droplet_autoscale" +subcategory: "Droplets" +--- + +# digitalocean\_droplet\_autoscale + +Get information on a Droplet Autoscale pool for use with other managed resources. This datasource provides all the +Droplet Autoscale pool properties as configured on the DigitalOcean account. This is useful if the Droplet Autoscale +pool in question is not managed by Terraform, or any of the relevant data would need to referenced in other managed +resources. + +## Example Usage + +Get the Droplet Autoscale pool by name: + +```hcl +data "digitalocean_droplet_autoscale" "my-imported-autoscale-pool" { + name = digitalocean_droplet_autoscale.my-existing-autoscale-pool.name +} +``` + +Get the Droplet Autoscale pool by ID: + +```hcl +data "digitalocean_droplet_autoscale" "my-imported-autoscale-pool" { + id = digitalocean_droplet_autoscale.my-existing-autoscale-pool.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Optional) The name of Droplet Autoscale pool. +* `id` - (Optional) The ID of Droplet Autoscale pool. + +## Attributes Reference + +See the [Droplet Autoscale Resource](../resources/droplet_autoscale.md) for details on the +returned attributes - they are identical. diff --git a/docs/resources/droplet_autoscale.md b/docs/resources/droplet_autoscale.md new file mode 100644 index 000000000..a0e3150b5 --- /dev/null +++ b/docs/resources/droplet_autoscale.md @@ -0,0 +1,102 @@ +--- +page_title: "DigitalOcean: digitalocean_droplet_autoscale" +subcategory: "Droplets" +--- + +# digitalocean\_droplet\_autoscale + +Provides a DigitalOcean Droplet Autoscale resource. This can be used to create, modify, +read and delete Droplet Autoscale pools. + +## Example Usage + +```hcl +resource "digitalocean_ssh_key" "my-ssh-key" { + name = "terraform-example" + public_key = file("/Users/terraform/.ssh/id_rsa.pub") +} + +resource "digitalocean_tag" "my-tag" { + name = "terraform-example" +} + +resource "digitalocean_droplet_autoscale" "my-autoscale-pool" { + name = "terraform-example" + + config { + min_instances = 10 + max_instances = 50 + target_cpu_utilization = 0.5 + target_memory_utilization = 0.5 + cooldown_minutes = 5 + } + + droplet_template { + size = "c-2" + region = "nyc3" + image = "ubuntu-24-04-x64" + tags = [digitalocean_tag.my-tag.id] + ssh_keys = [digitalocean_ssh_key.my-ssh-key.id] + with_droplet_agent = true + ipv6 = true + user_data = "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Droplet Autoscale pool. +* `config` - (Required) The configuration parameters for Droplet Autoscale pool, the supported arguments are +documented below. +* `droplet_template` - (Required) The droplet template parameters for Droplet Autoscale pool, the supported arguments +are documented below. + +`config` supports the following: + +* `min_instances` - The minimum number of instances to maintain in the Droplet Autoscale pool. +* `max_instances` - The maximum number of instances to maintain in the Droplet Autoscale pool. +* `target_cpu_utilization` - The target average CPU load (in range `[0, 1]`) to maintain in the Droplet Autoscale pool. +* `target_memory_utilization` - The target average Memory load (in range `[0, 1]`) to maintain in the Droplet Autoscale +pool. +* `cooldown_minutes` - The cooldown duration between scaling events for the Droplet Autoscale pool. +* `target_number_instances` - The static number of instances to maintain in the pool Droplet Autoscale pool. This +argument cannot be used with any other config options. + +`droplet_template` supports the following: + +* `size` - (Required) Size slug of the Droplet Autoscale pool underlying resource(s). +* `region` - (Required) Region slug of the Droplet Autoscale pool underlying resource(s). +* `image` - (Required) Image slug of the Droplet Autoscale pool underlying resource(s). +* `tags` - List of tags to add to the Droplet Autoscale pool underlying resource(s). +* `ssh_keys` - (Required) SSH fingerprints to add to the Droplet Autoscale pool underlying resource(s). +* `vpc_uuid` - VPC UUID to create the Droplet Autoscale pool underlying resource(s). If not provided, this is inferred +from the specified `region` (default VPC). +* `with_droplet_agent` - Boolean flag to enable metric agent on the Droplet Autoscale pool underlying resource(s). The +metric agent enables collecting resource utilization metrics, which allows making resource based scaling decisions. +* `project_id` - Project UUID to create the Droplet Autoscale pool underlying resource(s). +* `ipv6` - Boolean flag to enable IPv6 networking on the Droplet Autoscale pool underlying resource(s). +* `user_data` - Custom user data that can be added to the Droplet Autoscale pool underlying resource(s). This can be a +cloud init script that user may configure to setup their application workload. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Droplet Autoscale pool. +* `current_utilization` - The current average resource utilization of the Droplet Autoscale pool, this attribute further +embeds `memory` and `cpu` attributes to respectively report utilization data. +* `status` - Droplet Autoscale pool health status; this reflects if the pool is currently healthy and ready to accept +traffic, or in an error state and needs user intervention. +* `created_at` - Created at timestamp for the Droplet Autoscale pool. +* `updated_at` - Updated at timestamp for the Droplet Autoscale pool. + +## Import + +Droplet Autoscale pools can be imported using their `id`, e.g. + +``` +terraform import digitalocean_droplet_autoscale.my-autoscale-pool 38e66834-d741-47ec-88e7-c70cbdcz0445 +```