diff --git a/CHANGELOG.md b/CHANGELOG.md index 3565bdeea..8b4a13527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 2.45.0 + +IMPROVEMENTS: + +- #1279 - @jvasilevsky - LBAAS-3552: add lb ipv6 field + +BUG FIXES: + +- #1276 - @andrewsomething - apps: support removing ingress rules. + +MISC: + +- #1283 - @loosla - [documentation]: update contributing: move terraformrc section to reb… +- #1282 - @loosla - [documentation]: update contributing doc + +## 2.44.1 + +BUG FIXES: + +- #1273 - @andrewsomething - opensearch_config: follow PATCH semantics + +MISC: + +- #1265 - @brianhelba - Fix docs for "digitalocean_database_opensearch_config" +- #1270 - @dduportal - docs(droplet) details `user_data` behavior (resource forces recreate) + ## 2.44.0 IMPROVEMENTS: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 16539b2de..323854d0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,70 +1,163 @@ -Developing the Provider ---------------------------- +# Developing the Provider -If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.11+ is *required*). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`. +Testing Your Changes Locally +---------------------------- + +### 1. Rebuilding the Provider +1.1 If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.11+ is *required*). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`. To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. ```sh $ make build ... -$ $GOPATH/bin/terraform-provider-digitalocean +$ ls -la $GOPATH/bin/terraform-provider-digitalocean ... ``` +Remember to rebuild the provider with `make build` to apply any new changes. + +1.2 In order to check changes you made locally to the provider, you can use the binary you just compiled by adding the following +to your `~/.terraformrc` file. This is valid for Terraform 0.14+. Please see +[Terraform's documentation](https://www.terraform.io/docs/cli/config/config-file.html#development-overrides-for-provider-developers) +for more details. + +``` +provider_installation { + + # Use /home/developer/go/bin as an overridden package directory + # for the digitalocean/digitalocean provider. This disables the version and checksum + # verifications for this provider and forces Terraform to look for the + # digitalocean provider plugin in the given directory. + dev_overrides { + "digitalocean/digitalocean" = "/home/developer/go/bin" + } + + # For all other providers, install them directly from their origin provider + # registries as normal. If you omit this, Terraform will _only_ use + # the dev_overrides block, and so no other providers will be available. + direct {} +} +``` + +### 2. Creating a Sample Terraform Configuration +2.1 From the root of the project, create a directory for your Terraform configuration: + +```console +mkdir -p examples/my-tf +``` + +2.2 Create a new Terraform configuration file: + +``` console +touch examples/my-tf/main.tf +``` + +2.3 Populate the main.tf file with the following example configuration. +* [Available versions for the DigitalOcean Terraform provider](https://registry.terraform.io/providers/digitalocean/digitalocean/latest) +* Make sure to update the token value with your own [DigitalOcean token](https://docs.digitalocean.com/reference/api/create-personal-access-token). + +```console +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = ">= 2.44.1" + } + } +} + +provider "digitalocean" { + token = "dop_v1_12345d7ce104413a59023656565" +} + +resource "digitalocean_droplet" "foobar-my-tf" { + image = "ubuntu-22-04-x64" + name = "tf-acc-test-my-tf" + region = "nyc3" + size = "s-1vcpu-1gb" +} +``` +2.4 Before using the Terraform configuration, you need to initialize your working directory. +```console +cd examples/my-tf +terraform init +``` + +### 3. Running Terraform Commands +3.1 Navigate to the working directory: + +```console +cd examples/my-tf +``` + +3.2 To see the changes that will be applied run: + +```console +terraform plan +``` + +3.3 To apply the changes run: + +```console +terraform apply +``` +This will interact with your DigitalOcean account, using the token you provided in the `main.tf` configuration file. +Once you've finished testing, it's a good practice to clean up any resources you created to avoid incurring charges. + +### 4. Debugging and Logging +You can add logs to your code with `log.Printf()`. Remember to run `make build` to apply changes. + +If you'd like to see more detailed logs for debugging, you can set the `TF_LOG` environment variable to `DEBUG` or `TRACE`. + +``` console +export TF_LOG=DEBUG +export TF_LOG=TRACE +``` + +After setting the log level, you can run `terraform plan` or `terraform apply` again to see more detailed output. + +Provider Automation Tests +-------------------- +### Running unit tests In order to test the provider, you can simply run `make test`. ```sh $ make test ``` -In order to run the full suite of acceptance tests, run `make testacc`. +### Running Acceptance Tests -*Note:* Acceptance tests create real resources, and often cost money to run. +Rebuild the provider before running acceptance tests. -```sh -$ make testacc -``` +Please be aware that **running ALL acceptance tests will take a significant amount of time and could be expensive**, as they interact with your **real DigitalOcean account**. For this reason, it is highly recommended to run only one acceptance test at a time to minimize both time and cost. +- It is preferable to run one acceptance test at a time. In order to run a specific acceptance test, use the `TESTARGS` environment variable. For example, the following command will run `TestAccDigitalOceanDomain_Basic` acceptance test only: ```sh $ make testacc TESTARGS='-run=TestAccDigitalOceanDomain_Basic' ``` -All acceptance tests for a specific package can be run by setting the `PKG_NAME` environment variable. For example: +- All acceptance tests for a specific package can be run by setting the `PKG_NAME` environment variable. For example: ```sh $ make testacc PKG_NAME=digitalocean/account ``` -In order to check changes you made locally to the provider, you can use the binary you just compiled by adding the following -to your `~/.terraformrc` file. This is valid for Terraform 0.14+. Please see -[Terraform's documentation](https://www.terraform.io/docs/cli/config/config-file.html#development-overrides-for-provider-developers) -for more details. +- In order to run the full suite of acceptance tests, run `make testacc`. -``` -provider_installation { - - # Use /home/developer/go/bin as an overridden package directory - # for the digitalocean/digitalocean provider. This disables the version and checksum - # verifications for this provider and forces Terraform to look for the - # digitalocean provider plugin in the given directory. - dev_overrides { - "digitalocean/digitalocean" = "/home/developer/go/bin" - } +**Note:** Acceptance tests create real resources, and often cost money to run. - # For all other providers, install them directly from their origin provider - # registries as normal. If you omit this, Terraform will _only_ use - # the dev_overrides block, and so no other providers will be available. - direct {} -} +```sh +$ make testacc ``` For information about writing acceptance tests, see the main Terraform [contributing guide](https://github.com/hashicorp/terraform/blob/master/.github/CONTRIBUTING.md#writing-acceptance-tests). Releasing the Provider ---------------------- +The dedicated DigitalOcean team is responsible for releasing the provider. To release the provider: diff --git a/digitalocean/app/app_spec.go b/digitalocean/app/app_spec.go index 60b9ce638..dbbaedc2d 100644 --- a/digitalocean/app/app_spec.go +++ b/digitalocean/app/app_spec.go @@ -1033,7 +1033,6 @@ func appSpecIngressSchema() *schema.Resource { "rule": { Type: schema.TypeList, Optional: true, - Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "match": { diff --git a/digitalocean/app/resource_app_test.go b/digitalocean/app/resource_app_test.go index 63caec9e9..41095cf4a 100644 --- a/digitalocean/app/resource_app_test.go +++ b/digitalocean/app/resource_app_test.go @@ -865,6 +865,55 @@ func TestAccDigitalOceanApp_TimeoutConfig(t *testing.T) { }) } +func TestAccDigitalOceanApp_makeServiceInternal(t *testing.T) { + var app godo.App + appName := acceptance.RandomTestName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccCheckDigitalOceanAppDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCheckDigitalOceanAppConfig_minimalService, appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName, + ), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/", + ), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.http_port", "8080", + ), + ), + }, + { + Config: fmt.Sprintf(testAccCheckDigitalOceanAppConfig_internalOnlyService, appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName, + ), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.http_port", "0", + ), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.internal_ports.#", "1", + ), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.internal_ports.0", "8080", + ), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.#", "0", + ), + ), + }, + }, + }) +} + func testAccCheckDigitalOceanAppDestroy(s *terraform.State) error { client := acceptance.TestAccProvider.Meta().(*config.CombinedConfig).GodoClient() @@ -1577,3 +1626,46 @@ resource "digitalocean_app" "foobar" { } } }` + +var testAccCheckDigitalOceanAppConfig_minimalService = ` +resource "digitalocean_app" "foobar" { + spec { + name = "%s" + region = "nyc" + + service { + name = "go-service" + instance_count = 1 + instance_size_slug = "apps-d-1vcpu-0.5gb" + + git { + repo_clone_url = "https://github.com/digitalocean/sample-golang.git" + branch = "main" + } + } + } +}` + +var testAccCheckDigitalOceanAppConfig_internalOnlyService = ` +resource "digitalocean_app" "foobar" { + spec { + name = "%s" + region = "nyc" + + service { + name = "go-service" + instance_count = 1 + instance_size_slug = "apps-d-1vcpu-0.5gb" + + http_port = 0 + internal_ports = [8080] + + git { + repo_clone_url = "https://github.com/digitalocean/sample-golang.git" + branch = "main" + } + } + + ingress {} + } +}` diff --git a/digitalocean/database/resource_database_opensearch_config.go b/digitalocean/database/resource_database_opensearch_config.go index bd7bd02a5..e1e528694 100644 --- a/digitalocean/database/resource_database_opensearch_config.go +++ b/digitalocean/database/resource_database_opensearch_config.go @@ -280,165 +280,234 @@ func updateOpensearchConfig(ctx context.Context, d *schema.ResourceData, client opts := &godo.OpensearchConfig{} - if v, ok := d.GetOkExists("ism_enabled"); ok { - opts.IsmEnabled = godo.PtrTo(v.(bool)) - } + if d.HasChanges("ism_enabled", "ism_history_enabled", "ism_history_max_age_hours", + "ism_history_max_docs", "ism_history_rollover_check_period_hours", + "ism_history_rollover_retention_period_days") { - if v, ok := d.GetOkExists("ism_history_enabled"); ok { - opts.IsmHistoryEnabled = godo.PtrTo(v.(bool)) - } + if v, ok := d.GetOkExists("ism_enabled"); ok { + opts.IsmEnabled = godo.PtrTo(v.(bool)) + } - if v, ok := d.GetOk("ism_history_max_age_hours"); ok { - opts.IsmHistoryMaxAgeHours = godo.PtrTo(v.(int)) - } + if v, ok := d.GetOkExists("ism_history_enabled"); ok { + opts.IsmHistoryEnabled = godo.PtrTo(v.(bool)) + } - if v, ok := d.GetOk("ism_history_max_docs"); ok { - opts.IsmHistoryMaxDocs = godo.PtrTo(int64(v.(int))) - } + if v, ok := d.GetOk("ism_history_max_age_hours"); ok { + opts.IsmHistoryMaxAgeHours = godo.PtrTo(v.(int)) + } - if v, ok := d.GetOk("ism_history_rollover_check_period_hours"); ok { - opts.IsmHistoryRolloverCheckPeriodHours = godo.PtrTo(v.(int)) - } + if v, ok := d.GetOk("ism_history_max_docs"); ok { + opts.IsmHistoryMaxDocs = godo.PtrTo(int64(v.(int))) + } + + if v, ok := d.GetOk("ism_history_rollover_check_period_hours"); ok { + opts.IsmHistoryRolloverCheckPeriodHours = godo.PtrTo(v.(int)) + } - if v, ok := d.GetOk("ism_history_rollover_retention_period_days"); ok { - opts.IsmHistoryRolloverRetentionPeriodDays = godo.PtrTo(v.(int)) + if v, ok := d.GetOk("ism_history_rollover_retention_period_days"); ok { + opts.IsmHistoryRolloverRetentionPeriodDays = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("http_max_content_length_bytes"); ok { - opts.HttpMaxContentLengthBytes = godo.PtrTo(v.(int)) + if d.HasChange("http_max_content_length_bytes") { + if v, ok := d.GetOk("http_max_content_length_bytes"); ok { + opts.HttpMaxContentLengthBytes = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("http_max_header_size_bytes"); ok { - opts.HttpMaxHeaderSizeBytes = godo.PtrTo(v.(int)) + if d.HasChange("http_max_header_size_bytes") { + if v, ok := d.GetOk("http_max_header_size_bytes"); ok { + opts.HttpMaxHeaderSizeBytes = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("http_max_initial_line_length_bytes"); ok { - opts.HttpMaxInitialLineLengthBytes = godo.PtrTo(v.(int)) + if d.HasChange("http_max_initial_line_length_bytes") { + if v, ok := d.GetOk("http_max_initial_line_length_bytes"); ok { + opts.HttpMaxInitialLineLengthBytes = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_query_bool_max_clause_count"); ok { - opts.IndicesQueryBoolMaxClauseCount = godo.PtrTo(v.(int)) + if d.HasChange("indices_query_bool_max_clause_count") { + if v, ok := d.GetOk("indices_query_bool_max_clause_count"); ok { + opts.IndicesQueryBoolMaxClauseCount = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("search_max_buckets"); ok { - opts.SearchMaxBuckets = godo.PtrTo(v.(int)) + if d.HasChange("search_max_buckets") { + if v, ok := d.GetOk("search_max_buckets"); ok { + opts.SearchMaxBuckets = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_fielddata_cache_size_percentage"); ok { - opts.IndicesFielddataCacheSizePercentage = godo.PtrTo(v.(int)) + if d.HasChange("indices_fielddata_cache_size_percentage") { + if v, ok := d.GetOk("indices_fielddata_cache_size_percentage"); ok { + opts.IndicesFielddataCacheSizePercentage = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_memory_index_buffer_size_percentage"); ok { - opts.IndicesMemoryIndexBufferSizePercentage = godo.PtrTo(v.(int)) + if d.HasChange("indices_memory_index_buffer_size_percentage") { + if v, ok := d.GetOk("indices_memory_index_buffer_size_percentage"); ok { + opts.IndicesMemoryIndexBufferSizePercentage = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_memory_min_index_buffer_size_mb"); ok { - opts.IndicesMemoryMinIndexBufferSizeMb = godo.PtrTo(v.(int)) + if d.HasChange("indices_memory_min_index_buffer_size_mb") { + if v, ok := d.GetOk("indices_memory_min_index_buffer_size_mb"); ok { + opts.IndicesMemoryMinIndexBufferSizeMb = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_memory_max_index_buffer_size_mb"); ok { - opts.IndicesMemoryMaxIndexBufferSizeMb = godo.PtrTo(v.(int)) + if d.HasChange("indices_memory_max_index_buffer_size_mb") { + if v, ok := d.GetOk("indices_memory_max_index_buffer_size_mb"); ok { + opts.IndicesMemoryMaxIndexBufferSizeMb = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_queries_cache_size_percentage"); ok { - opts.IndicesQueriesCacheSizePercentage = godo.PtrTo(v.(int)) + if d.HasChange("indices_queries_cache_size_percentage") { + if v, ok := d.GetOk("indices_queries_cache_size_percentage"); ok { + opts.IndicesQueriesCacheSizePercentage = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_recovery_max_mb_per_sec"); ok { - opts.IndicesRecoveryMaxMbPerSec = godo.PtrTo(v.(int)) + if d.HasChange("indices_recovery_max_mb_per_sec") { + if v, ok := d.GetOk("indices_recovery_max_mb_per_sec"); ok { + opts.IndicesRecoveryMaxMbPerSec = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("indices_recovery_max_concurrent_file_chunks"); ok { - opts.IndicesRecoveryMaxConcurrentFileChunks = godo.PtrTo(v.(int)) + if d.HasChange("indices_recovery_max_concurrent_file_chunks") { + if v, ok := d.GetOk("indices_recovery_max_concurrent_file_chunks"); ok { + opts.IndicesRecoveryMaxConcurrentFileChunks = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOkExists("action_auto_create_index_enabled"); ok { - opts.ActionAutoCreateIndexEnabled = godo.PtrTo(v.(bool)) + if d.HasChange("action_auto_create_index_enabled") { + if v, ok := d.GetOkExists("action_auto_create_index_enabled"); ok { + opts.ActionAutoCreateIndexEnabled = godo.PtrTo(v.(bool)) + } } - if v, ok := d.GetOkExists("action_destructive_requires_name"); ok { - opts.ActionDestructiveRequiresName = godo.PtrTo(v.(bool)) + if d.HasChange("action_destructive_requires_name") { + if v, ok := d.GetOkExists("action_destructive_requires_name"); ok { + opts.ActionDestructiveRequiresName = godo.PtrTo(v.(bool)) + } } - if v, ok := d.GetOkExists("enable_security_audit"); ok { - opts.EnableSecurityAudit = godo.PtrTo(v.(bool)) + if d.HasChange("enable_security_audit") { + if v, ok := d.GetOkExists("enable_security_audit"); ok { + opts.EnableSecurityAudit = godo.PtrTo(v.(bool)) + } } - if v, ok := d.GetOk("thread_pool_search_size"); ok { - opts.ThreadPoolSearchSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_search_size") { + if v, ok := d.GetOk("thread_pool_search_size"); ok { + opts.ThreadPoolSearchSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_search_throttled_size"); ok { - opts.ThreadPoolSearchThrottledSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_search_throttled_size") { + if v, ok := d.GetOk("thread_pool_search_throttled_size"); ok { + opts.ThreadPoolSearchThrottledSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_search_throttled_queue_size"); ok { - opts.ThreadPoolSearchThrottledQueueSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_search_throttled_queue_size") { + if v, ok := d.GetOk("thread_pool_search_throttled_queue_size"); ok { + opts.ThreadPoolSearchThrottledQueueSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_search_queue_size"); ok { - opts.ThreadPoolSearchQueueSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_search_queue_size") { + if v, ok := d.GetOk("thread_pool_search_queue_size"); ok { + opts.ThreadPoolSearchQueueSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_get_size"); ok { - opts.ThreadPoolGetSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_get_size") { + if v, ok := d.GetOk("thread_pool_get_size"); ok { + opts.ThreadPoolGetSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_get_queue_size"); ok { - opts.ThreadPoolGetQueueSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_get_queue_size") { + if v, ok := d.GetOk("thread_pool_get_queue_size"); ok { + opts.ThreadPoolGetQueueSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_analyze_size"); ok { - opts.ThreadPoolAnalyzeSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_analyze_size") { + if v, ok := d.GetOk("thread_pool_analyze_size"); ok { + opts.ThreadPoolAnalyzeSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_analyze_queue_size"); ok { - opts.ThreadPoolAnalyzeQueueSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_analyze_queue_size") { + if v, ok := d.GetOk("thread_pool_analyze_queue_size"); ok { + opts.ThreadPoolAnalyzeQueueSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_write_size"); ok { - opts.ThreadPoolWriteSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_write_size") { + if v, ok := d.GetOk("thread_pool_write_size"); ok { + opts.ThreadPoolWriteSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_write_queue_size"); ok { - opts.ThreadPoolWriteQueueSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_write_queue_size") { + if v, ok := d.GetOk("thread_pool_write_queue_size"); ok { + opts.ThreadPoolWriteQueueSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("thread_pool_force_merge_size"); ok { - opts.ThreadPoolForceMergeSize = godo.PtrTo(v.(int)) + if d.HasChange("thread_pool_force_merge_size") { + if v, ok := d.GetOk("thread_pool_force_merge_size"); ok { + opts.ThreadPoolForceMergeSize = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOkExists("override_main_response_version"); ok { - opts.OverrideMainResponseVersion = godo.PtrTo(v.(bool)) + if d.HasChange("override_main_response_version") { + if v, ok := d.GetOkExists("override_main_response_version"); ok { + opts.OverrideMainResponseVersion = godo.PtrTo(v.(bool)) + } } - if v, ok := d.GetOk("script_max_compilations_rate"); ok { - opts.ScriptMaxCompilationsRate = godo.PtrTo(v.(string)) + if d.HasChange("script_max_compilations_rate") { + if v, ok := d.GetOk("script_max_compilations_rate"); ok { + opts.ScriptMaxCompilationsRate = godo.PtrTo(v.(string)) + } } - if v, ok := d.GetOk("cluster_max_shards_per_node"); ok { - opts.ClusterMaxShardsPerNode = godo.PtrTo(v.(int)) + if d.HasChange("cluster_max_shards_per_node") { + if v, ok := d.GetOk("cluster_max_shards_per_node"); ok { + opts.ClusterMaxShardsPerNode = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOk("cluster_routing_allocation_node_concurrent_recoveries"); ok { - opts.ClusterRoutingAllocationNodeConcurrentRecoveries = godo.PtrTo(v.(int)) + if d.HasChange("cluster_routing_allocation_node_concurrent_recoveries") { + if v, ok := d.GetOk("cluster_routing_allocation_node_concurrent_recoveries"); ok { + opts.ClusterRoutingAllocationNodeConcurrentRecoveries = godo.PtrTo(v.(int)) + } } - if v, ok := d.GetOkExists("plugins_alerting_filter_by_backend_roles_enabled"); ok { - opts.PluginsAlertingFilterByBackendRolesEnabled = godo.PtrTo(v.(bool)) + if d.HasChange("plugins_alerting_filter_by_backend_roles_enabled") { + if v, ok := d.GetOkExists("plugins_alerting_filter_by_backend_roles_enabled"); ok { + opts.PluginsAlertingFilterByBackendRolesEnabled = godo.PtrTo(v.(bool)) + } } - if v, ok := d.GetOk("reindex_remote_whitelist"); ok { - if exampleSet, ok := v.(*schema.Set); ok { - var items []string - for _, item := range exampleSet.List() { - if str, ok := item.(string); ok { - items = append(items, str) - } else { - return fmt.Errorf("non-string item found in set") // todo: anna update err message + if d.HasChange("reindex_remote_whitelist") { + if v, ok := d.GetOk("reindex_remote_whitelist"); ok { + if exampleSet, ok := v.(*schema.Set); ok { + var items []string + for _, item := range exampleSet.List() { + if str, ok := item.(string); ok { + items = append(items, str) + } else { + return fmt.Errorf("non-string item found in set") + } } + opts.ReindexRemoteWhitelist = items } - opts.ReindexRemoteWhitelist = items } } 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/loadbalancer/datasource_loadbalancer.go b/digitalocean/loadbalancer/datasource_loadbalancer.go index 9a3fc316a..b6df571c7 100644 --- a/digitalocean/loadbalancer/datasource_loadbalancer.go +++ b/digitalocean/loadbalancer/datasource_loadbalancer.go @@ -251,7 +251,7 @@ func DataSourceDigitalOceanLoadbalancer() *schema.Resource { "type": { Type: schema.TypeString, Computed: true, - Description: "the type of the load balancer (GLOBAL or REGIONAL)", + Description: "the type of the load balancer (GLOBAL, REGIONAL, or REGIONAL_NETWORK)", }, "domains": { Type: schema.TypeSet, @@ -432,6 +432,10 @@ func dataSourceDigitalOceanLoadbalancerRead(ctx context.Context, d *schema.Resou d.Set("type", foundLoadbalancer.Type) d.Set("network", foundLoadbalancer.Network) + if foundLoadbalancer.IPv6 != "" { + d.Set("ipv6", foundLoadbalancer.IPv6) + } + if err := d.Set("droplet_ids", flattenDropletIds(foundLoadbalancer.DropletIDs)); err != nil { return diag.Errorf("[DEBUG] Error setting Load Balancer droplet_ids - error: %#v", err) } diff --git a/digitalocean/loadbalancer/resource_loadbalancer.go b/digitalocean/loadbalancer/resource_loadbalancer.go index 460277a77..6b3f46027 100644 --- a/digitalocean/loadbalancer/resource_loadbalancer.go +++ b/digitalocean/loadbalancer/resource_loadbalancer.go @@ -132,7 +132,7 @@ func loadbalancerDiffCheck(ctx context.Context, d *schema.ResourceDiff, v interf if regionSet && region.(string) != "" { return fmt.Errorf("'region' must be empty or not set when 'type' is '%s'", typStr) } - case "REGIONAL": + case "REGIONAL", "REGIONAL_NETWORK": if !regionSet || region.(string) == "" { return fmt.Errorf("'region' must be set and not be empty when 'type' is '%s'", typStr) } @@ -180,7 +180,7 @@ func resourceDigitalOceanLoadBalancerV0() *schema.Resource { Type: schema.TypeInt, Optional: true, Computed: true, - ValidateFunc: validation.IntBetween(1, 100), + ValidateFunc: validation.IntBetween(1, 200), }, "name": { Type: schema.TypeString, @@ -394,6 +394,11 @@ func resourceDigitalOceanLoadBalancerV0() *schema.Resource { Computed: true, }, + "ipv6": { + Type: schema.TypeString, + Computed: true, + }, + "status": { Type: schema.TypeString, Computed: true, @@ -438,8 +443,8 @@ func resourceDigitalOceanLoadBalancerV0() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{"REGIONAL", "GLOBAL"}, true), - Description: "the type of the load balancer (GLOBAL or REGIONAL)", + ValidateFunc: validation.StringInSlice([]string{"REGIONAL", "GLOBAL", "REGIONAL_NETWORK"}, true), + Description: "the type of the load balancer (GLOBAL, REGIONAL, or REGIONAL_NETWORK)", }, "domains": { @@ -741,6 +746,10 @@ func resourceDigitalOceanLoadbalancerRead(ctx context.Context, d *schema.Resourc d.Set("http_idle_timeout_seconds", loadbalancer.HTTPIdleTimeoutSeconds) d.Set("project_id", loadbalancer.ProjectID) + if loadbalancer.IPv6 != "" { + d.Set("ipv6", loadbalancer.IPv6) + } + if loadbalancer.SizeUnit > 0 { d.Set("size_unit", loadbalancer.SizeUnit) } else { diff --git a/digitalocean/provider.go b/digitalocean/provider.go index 9eb4741b0..6e45e11f3 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" @@ -110,6 +111,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(), @@ -163,6 +165,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/database_opensearch_config.md b/docs/resources/database_opensearch_config.md index 4e615f177..57ec45d43 100644 --- a/docs/resources/database_opensearch_config.md +++ b/docs/resources/database_opensearch_config.md @@ -90,7 +90,7 @@ for additional details on each option. * `indices_queries_cache_size_percentage` - (Optional) Maximum amount of heap used for query cache. Too low value will decrease query performance and increase performance for other operations; too high value will cause issues with other functionality. Default: `10` * `indices_recovery_max_mb_per_sec` - (Optional) Limits total inbound and outbound recovery traffic for each node, expressed in mb per second. Applies to both peer recoveries as well as snapshot recoveries (i.e., restores from a snapshot). Default: `40` * `indices_recovery_max_concurrent_file_chunks` - (Optional) Maximum number of file chunks sent in parallel for each recovery. Default: `2` -* `action_auto_create_index_enabled` - (Optional) Specifies whether ISM is enabled or not. Default: `true` +* `action_auto_create_index_enabled` - (Optional) Specifices whether to allow automatic creation of indices. Default: `true` * `action_destructive_requires_name` - (Optional) Specifies whether to require explicit index names when deleting indices. * `enable_security_audit` - (Optional) Specifies whether to allow security audit logging. Default: `false` * `thread_pool_search_size` - (Optional) Number of workers in the search operation thread pool. Do note this may have maximum value depending on CPU count - value is automatically lowered if set to higher than maximum value. 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 +``` diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 94e9f8ea8..91d341bdb 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -93,7 +93,7 @@ The following arguments are supported: * `name` - (Required) The Load Balancer name * `region` - (Required) The region to start in * `size` - (Optional) The size of the Load Balancer. It must be either `lb-small`, `lb-medium`, or `lb-large`. Defaults to `lb-small`. Only one of `size` or `size_unit` may be provided. -* `size_unit` - (Optional) The size of the Load Balancer. It must be in the range (1, 100). Defaults to `1`. Only one of `size` or `size_unit` may be provided. +* `size_unit` - (Optional) The size of the Load Balancer. It must be in the range (1, 200). Defaults to `1`. Only one of `size` or `size_unit` may be provided. * `algorithm` - (Optional) **Deprecated** This field has been deprecated. You can no longer specify an algorithm for load balancers. or `least_connections`. The default value is `round_robin`. * `forwarding_rule` - (Required) A list of `forwarding_rule` to be assigned to the @@ -119,10 +119,9 @@ the backend service. Default value is `false`. * `domains` (Optional) - A list of `domains` required to ingress traffic to a Global Load Balancer. The `domains` block is documented below. * `glb_settings` (Optional) - A block containing `glb_settings` required to define target rules for a Global Load Balancer. The `glb_settings` block is documented below. * `target_load_balancer_ids` (Optional) - A list of Load Balancer IDs to be attached behind a Global Load Balancer. -* `type` - (Optional) The type of the Load Balancer. It must be either of `REGIONAL` or `GLOBAL`. Defaults to `REGIONAL`. +* `type` - (Optional) The type of the Load Balancer. It must be either of `REGIONAL`, `REGIONAL_NETWORK`, or `GLOBAL`. Defaults to `REGIONAL`. **NOTE**: non-`REGIONAL/GLOBAL` type may be part of closed beta feature and not available for public use. * `network` - (Optional) The type of network the Load Balancer is accessible from. It must be either of `INTERNAL` or `EXTERNAL`. Defaults to `EXTERNAL`. -**NOTE**: non-`EXTERNAL` type may be part of closed beta feature and not available for public use. `forwarding_rule` supports the following: diff --git a/vendor/github.com/digitalocean/godo/reserved_ipv6.go b/vendor/github.com/digitalocean/godo/reserved_ipv6.go index 119c6bde3..ccc4a80af 100644 --- a/vendor/github.com/digitalocean/godo/reserved_ipv6.go +++ b/vendor/github.com/digitalocean/godo/reserved_ipv6.go @@ -34,6 +34,7 @@ type ReservedIPV6 struct { ReservedAt time.Time `json:"reserved_at"` Droplet *Droplet `json:"droplet,omitempty"` } + type reservedIPV6Root struct { ReservedIPV6 *ReservedIPV6 `json:"reserved_ipv6"` } @@ -76,7 +77,8 @@ func (r *ReservedIPV6sServiceOp) List(ctx context.Context, opt *ListOptions) ([] if err != nil { return nil, nil, err } - if root.Meta != nil { + + if root.Meta != nil { resp.Meta = root.Meta } if root.Links != nil {