diff --git a/.changelog/9971.txt b/.changelog/9971.txt new file mode 100644 index 0000000000..58c9f340f4 --- /dev/null +++ b/.changelog/9971.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +made `google_notebooks_instance` wait for active state on creation and enable stopping/starting instances. +``` \ No newline at end of file diff --git a/google-beta/services/notebooks/resource_notebooks_instance.go b/google-beta/services/notebooks/resource_notebooks_instance.go index 803d460c0c..6e290328c8 100644 --- a/google-beta/services/notebooks/resource_notebooks_instance.go +++ b/google-beta/services/notebooks/resource_notebooks_instance.go @@ -27,6 +27,7 @@ import ( "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" @@ -70,6 +71,55 @@ func NotebooksInstanceKmsDiffSuppress(_, old, new string, _ *schema.ResourceData return false } +// waitForNotebooksInstanceActive waits for an Notebook instance to become "ACTIVE" +func waitForNotebooksInstanceActive(d *schema.ResourceData, config *transport_tpg.Config, timeout time.Duration) error { + return resource.Retry(timeout, func() *resource.RetryError { + if err := resourceNotebooksInstanceRead(d, config); err != nil { + return resource.NonRetryableError(err) + } + + name := d.Get("name").(string) + state := d.Get("state").(string) + if state == "ACTIVE" { + log.Printf("[DEBUG] Notebook Instance %q has state %q.", name, state) + return nil + } else { + return resource.RetryableError(fmt.Errorf("Notebook Instance %q has state %q. Waiting for ACTIVE state", name, state)) + } + + }) +} + +func modifyNotebooksInstanceState(config *transport_tpg.Config, d *schema.ResourceData, project string, billingProject string, userAgent string, state string) (map[string]interface{}, error) { + url, err := tpgresource.ReplaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/instances/{{name}}:"+state) + if err != nil { + return nil, err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return nil, fmt.Errorf("Unable to %q google_notebooks_instance %q: %s", state, d.Id(), err) + } + return res, nil +} + +func waitForNotebooksOperation(config *transport_tpg.Config, d *schema.ResourceData, project string, billingProject string, userAgent string, response map[string]interface{}) error { + var opRes map[string]interface{} + err := NotebooksOperationWaitTimeWithResponse( + config, response, &opRes, project, "Modifying Notebook Instance state", userAgent, + d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + return nil +} + func ResourceNotebooksInstance() *schema.Resource { return &schema.Resource{ Create: resourceNotebooksInstanceCreate, @@ -496,6 +546,12 @@ the population of this value.`, Optional: true, Description: `Instance update time.`, }, + "desired_state": { + Type: schema.TypeString, + Optional: true, + Default: "ACTIVE", + Description: `Desired state of the Notebook Instance. Set this field to 'ACTIVE' to start the Instance, and 'STOPPED' to stop the Instance.`, + }, "project": { Type: schema.TypeString, Optional: true, @@ -737,6 +793,20 @@ func resourceNotebooksInstanceCreate(d *schema.ResourceData, meta interface{}) e } d.SetId(id) + if err := waitForNotebooksInstanceActive(d, config, d.Timeout(schema.TimeoutCreate)-time.Minute); err != nil { + return fmt.Errorf("Notebook instance %q did not reach ACTIVE state: %q", d.Get("name").(string), err) + } + + if p, ok := d.GetOk("desired_state"); ok && p.(string) == "STOPPED" { + dRes, err := modifyNotebooksInstanceState(config, d, project, billingProject, userAgent, "stop") + if err != nil { + return err + } + if err := waitForNotebooksOperation(config, d, project, billingProject, userAgent, dRes); err != nil { + return fmt.Errorf("Error stopping Notebook Instance: %s", err) + } + } + log.Printf("[DEBUG] Finished creating Instance %q: %#v", d.Id(), res) return resourceNotebooksInstanceRead(d, meta) @@ -778,6 +848,12 @@ func resourceNotebooksInstanceRead(d *schema.ResourceData, meta interface{}) err return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("NotebooksInstance %q", d.Id())) } + // Explicitly set virtual fields to default values if unset + if _, ok := d.GetOkExists("desired_state"); !ok { + if err := d.Set("desired_state", "ACTIVE"); err != nil { + return fmt.Errorf("Error setting desired_state: %s", err) + } + } if err := d.Set("project", project); err != nil { return fmt.Errorf("Error reading Instance: %s", err) } @@ -972,6 +1048,27 @@ func resourceNotebooksInstanceUpdate(d *schema.ResourceData, meta interface{}) e d.Partial(false) + name := d.Get("name").(string) + state := d.Get("state").(string) + desired_state := d.Get("desired_state").(string) + + if state != desired_state { + verb := "start" + if desired_state == "STOPPED" { + verb = "stop" + } + pRes, err := modifyNotebooksInstanceState(config, d, project, billingProject, userAgent, verb) + if err != nil { + return err + } + + if err := waitForNotebooksOperation(config, d, project, billingProject, userAgent, pRes); err != nil { + return fmt.Errorf("Error waiting to modify Notebook Instance state: %s", err) + } + + } else { + log.Printf("[DEBUG] Notebook Instance %q has state %q.", name, state) + } return resourceNotebooksInstanceRead(d, meta) } @@ -1045,6 +1142,11 @@ func resourceNotebooksInstanceImport(d *schema.ResourceData, meta interface{}) ( } d.SetId(id) + // Explicitly set virtual fields to default values on import + if err := d.Set("desired_state", "ACTIVE"); err != nil { + return nil, fmt.Errorf("Error setting desired_state: %s", err) + } + return []*schema.ResourceData{d}, nil } diff --git a/google-beta/services/notebooks/resource_notebooks_instance_generated_test.go b/google-beta/services/notebooks/resource_notebooks_instance_generated_test.go index f5aa6ba731..6ad0da354d 100644 --- a/google-beta/services/notebooks/resource_notebooks_instance_generated_test.go +++ b/google-beta/services/notebooks/resource_notebooks_instance_generated_test.go @@ -70,6 +70,46 @@ resource "google_notebooks_instance" "instance" { `, context) } +func TestAccNotebooksInstance_notebookInstanceBasicStoppedExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckNotebooksInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstance_notebookInstanceBasicStoppedExample(context), + }, + { + ResourceName: "google_notebooks_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "instance_owners", "boot_disk_type", "boot_disk_size_gb", "data_disk_type", "data_disk_size_gb", "no_remove_data_disk", "metadata", "vm_image", "container_image", "location", "desired_state", "labels", "terraform_labels"}, + }, + }, + }) +} + +func testAccNotebooksInstance_notebookInstanceBasicStoppedExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_notebooks_instance" "instance" { + name = "tf-test-notebooks-instance%{random_suffix}" + location = "us-west1-a" + machine_type = "e2-medium" + vm_image { + project = "deeplearning-platform-release" + image_family = "tf-latest-cpu" + } + desired_state = "STOPPED" +} +`, context) +} + func TestAccNotebooksInstance_notebookInstanceBasicContainerExample(t *testing.T) { t.Parallel() @@ -225,6 +265,7 @@ resource "google_notebooks_instance" "instance" { ] disk_encryption = "CMEK" kms_key = "%{key_name}" + desired_state = "ACTIVE" } data "google_compute_network" "my_network" { diff --git a/google-beta/services/notebooks/resource_notebooks_instance_state_test.go b/google-beta/services/notebooks/resource_notebooks_instance_state_test.go new file mode 100644 index 0000000000..03bd28c0ad --- /dev/null +++ b/google-beta/services/notebooks/resource_notebooks_instance_state_test.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package notebooks_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-google-beta/google-beta/acctest" +) + +func TestAccNotebooksInstance_state(t *testing.T) { + t.Parallel() + + prefix := fmt.Sprintf("%d", acctest.RandInt(t)) + name := fmt.Sprintf("tf-%s", prefix) + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstance_basic_active(name), + }, + { + ResourceName: "google_notebooks_instance.test", + ImportState: true, + ImportStateVerify: true, + ExpectNonEmptyPlan: true, + ImportStateVerifyIgnore: []string{"container_image", "metadata", "vm_image", "desired_state", "update_time"}, + }, + { + Config: testAccNotebooksInstance_basic_stopped(name), + }, + { + ResourceName: "google_notebooks_instance.test", + ImportState: true, + ImportStateVerify: true, + ExpectNonEmptyPlan: true, + ImportStateVerifyIgnore: []string{"container_image", "metadata", "vm_image", "desired_state", "update_time"}, + }, + { + Config: testAccNotebooksInstance_basic_active(name), + }, + { + ResourceName: "google_notebooks_instance.test", + ImportState: true, + ImportStateVerify: true, + ExpectNonEmptyPlan: true, + ImportStateVerifyIgnore: []string{"container_image", "metadata", "vm_image", "desired_state", "update_time"}, + }, + }, + }) +} + +func testAccNotebooksInstance_basic_active(name string) string { + return fmt.Sprintf(` +resource "google_notebooks_instance" "test" { + name = "%s" + location = "us-west1-a" + machine_type = "e2-medium" + vm_image { + project = "deeplearning-platform-release" + image_family = "tf-latest-cpu" + } + desired_state = "ACTIVE" +} +`, name) +} + +func testAccNotebooksInstance_basic_stopped(name string) string { + return fmt.Sprintf(` +resource "google_notebooks_instance" "test" { + name = "%s" + location = "us-west1-a" + machine_type = "e2-medium" + vm_image { + project = "deeplearning-platform-release" + image_family = "tf-latest-cpu" + } + desired_state = "STOPPED" +} +`, name) +} diff --git a/website/docs/r/notebooks_instance.html.markdown b/website/docs/r/notebooks_instance.html.markdown index 3a3fee7b13..18066af238 100644 --- a/website/docs/r/notebooks_instance.html.markdown +++ b/website/docs/r/notebooks_instance.html.markdown @@ -53,6 +53,26 @@ resource "google_notebooks_instance" "instance" { } } ``` +
+ + Open in Cloud Shell + +
+## Example Usage - Notebook Instance Basic Stopped + + +```hcl +resource "google_notebooks_instance" "instance" { + name = "notebooks-instance" + location = "us-west1-a" + machine_type = "e2-medium" + vm_image { + project = "deeplearning-platform-release" + image_family = "tf-latest-cpu" + } + desired_state = "STOPPED" +} +```
Open in Cloud Shell @@ -143,6 +163,7 @@ resource "google_notebooks_instance" "instance" { ] disk_encryption = "CMEK" kms_key = "my-crypto-key" + desired_state = "ACTIVE" } data "google_compute_network" "my_network" { @@ -324,6 +345,8 @@ The following arguments are supported: * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +* `desired_state` - (Optional) Desired state of the Notebook Instance. Set this field to `ACTIVE` to start the Instance, and `STOPPED` to stop the Instance. + The `accelerator_config` block supports: