Skip to content

Commit

Permalink
Make notebooks wait for active state and enable stopping/starting ins…
Browse files Browse the repository at this point in the history
…… (#9971) (#6965)

* Make notebooks wait for active state and enable stopping/starting instances using the desired_state field

* ignore update_time

[upstream:18072d2b0665b0182e8bcd677fc1d0ca75693507]

Signed-off-by: Modular Magician <[email protected]>
  • Loading branch information
modular-magician authored Feb 13, 2024
1 parent 887b319 commit b652fb0
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/9971.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
made `google_notebooks_instance` wait for active state on creation and enable stopping/starting instances.
```
102 changes: 102 additions & 0 deletions google-beta/services/notebooks/resource_notebooks_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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" {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
23 changes: 23 additions & 0 deletions website/docs/r/notebooks_instance.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ resource "google_notebooks_instance" "instance" {
}
}
```
<div class = "oics-button" style="float: right; margin: 0 0 -15px">
<a href="https://console.cloud.google.com/cloudshell/open?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fdocs-examples.git&cloudshell_working_dir=notebook_instance_basic_stopped&cloudshell_image=gcr.io%2Fcloudshell-images%2Fcloudshell%3Alatest&open_in_editor=main.tf&cloudshell_print=.%2Fmotd&cloudshell_tutorial=.%2Ftutorial.md" target="_blank">
<img alt="Open in Cloud Shell" src="//gstatic.com/cloudssh/images/open-btn.svg" style="max-height: 44px; margin: 32px auto; max-width: 100%;">
</a>
</div>
## 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"
}
```
<div class = "oics-button" style="float: right; margin: 0 0 -15px">
<a href="https://console.cloud.google.com/cloudshell/open?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fdocs-examples.git&cloudshell_working_dir=notebook_instance_basic_container&cloudshell_image=gcr.io%2Fcloudshell-images%2Fcloudshell%3Alatest&open_in_editor=main.tf&cloudshell_print=.%2Fmotd&cloudshell_tutorial=.%2Ftutorial.md" target="_blank">
<img alt="Open in Cloud Shell" src="//gstatic.com/cloudssh/images/open-btn.svg" style="max-height: 44px; margin: 32px auto; max-width: 100%;">
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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.


<a name="nested_accelerator_config"></a>The `accelerator_config` block supports:

Expand Down

0 comments on commit b652fb0

Please sign in to comment.