diff --git a/.changelog/4253.txt b/.changelog/4253.txt new file mode 100644 index 00000000000..e3cf8f7d65c --- /dev/null +++ b/.changelog/4253.txt @@ -0,0 +1,4 @@ +```release-note:enhancement +notebooks: promoted `google_notebooks_instance` to GA +notebooks: promoted `google_notebooks_environment` to GA +``` diff --git a/google/config.go b/google/config.go index 8d1f713406c..32337f3e0db 100644 --- a/google/config.go +++ b/google/config.go @@ -119,6 +119,7 @@ type Config struct { MLEngineBasePath string MonitoringBasePath string NetworkManagementBasePath string + NotebooksBasePath string OSConfigBasePath string OSLoginBasePath string PubsubBasePath string @@ -193,6 +194,7 @@ var LoggingDefaultBasePath = "https://logging.googleapis.com/v2/" var MLEngineDefaultBasePath = "https://ml.googleapis.com/v1/" var MonitoringDefaultBasePath = "https://monitoring.googleapis.com/" var NetworkManagementDefaultBasePath = "https://networkmanagement.googleapis.com/v1/" +var NotebooksDefaultBasePath = "https://notebooks.googleapis.com/v1/" var OSConfigDefaultBasePath = "https://osconfig.googleapis.com/v1/" var OSLoginDefaultBasePath = "https://oslogin.googleapis.com/v1/" var PubsubDefaultBasePath = "https://pubsub.googleapis.com/v1/" @@ -977,6 +979,7 @@ func ConfigureBasePaths(c *Config) { c.MLEngineBasePath = MLEngineDefaultBasePath c.MonitoringBasePath = MonitoringDefaultBasePath c.NetworkManagementBasePath = NetworkManagementDefaultBasePath + c.NotebooksBasePath = NotebooksDefaultBasePath c.OSConfigBasePath = OSConfigDefaultBasePath c.OSLoginBasePath = OSLoginDefaultBasePath c.PubsubBasePath = PubsubDefaultBasePath diff --git a/google/iam_notebooks_instance.go b/google/iam_notebooks_instance.go new file mode 100644 index 00000000000..0760a4ebdb5 --- /dev/null +++ b/google/iam_notebooks_instance.go @@ -0,0 +1,222 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- +package google + +import ( + "fmt" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" +) + +var NotebooksInstanceIamSchema = map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "location": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "instance_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + }, +} + +type NotebooksInstanceIamUpdater struct { + project string + location string + instanceName string + d *schema.ResourceData + Config *Config +} + +func NotebooksInstanceIamUpdaterProducer(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) { + values := make(map[string]string) + + project, _ := getProject(d, config) + if project != "" { + if err := d.Set("project", project); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + } + values["project"] = project + location, _ := getLocation(d, config) + if location != "" { + if err := d.Set("location", location); err != nil { + return nil, fmt.Errorf("Error setting location: %s", err) + } + } + values["location"] = location + if v, ok := d.GetOk("instance_name"); ok { + values["instance_name"] = v.(string) + } + + // We may have gotten either a long or short name, so attempt to parse long name if possible + m, err := getImportIdQualifiers([]string{"projects/(?P[^/]+)/locations/(?P[^/]+)/instances/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config, d.Get("instance_name").(string)) + if err != nil { + return nil, err + } + + for k, v := range m { + values[k] = v + } + + u := &NotebooksInstanceIamUpdater{ + project: values["project"], + location: values["location"], + instanceName: values["instance_name"], + d: d, + Config: config, + } + + if err := d.Set("project", u.project); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("location", u.location); err != nil { + return nil, fmt.Errorf("Error setting location: %s", err) + } + if err := d.Set("instance_name", u.GetResourceId()); err != nil { + return nil, fmt.Errorf("Error setting instance_name: %s", err) + } + + return u, nil +} + +func NotebooksInstanceIdParseFunc(d *schema.ResourceData, config *Config) error { + values := make(map[string]string) + + project, _ := getProject(d, config) + if project != "" { + values["project"] = project + } + + location, _ := getLocation(d, config) + if location != "" { + values["location"] = location + } + + m, err := getImportIdQualifiers([]string{"projects/(?P[^/]+)/locations/(?P[^/]+)/instances/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config, d.Id()) + if err != nil { + return err + } + + for k, v := range m { + values[k] = v + } + + u := &NotebooksInstanceIamUpdater{ + project: values["project"], + location: values["location"], + instanceName: values["instance_name"], + d: d, + Config: config, + } + if err := d.Set("instance_name", u.GetResourceId()); err != nil { + return fmt.Errorf("Error setting instance_name: %s", err) + } + d.SetId(u.GetResourceId()) + return nil +} + +func (u *NotebooksInstanceIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + url, err := u.qualifyInstanceUrl("getIamPolicy") + if err != nil { + return nil, err + } + + project, err := getProject(u.d, u.Config) + if err != nil { + return nil, err + } + var obj map[string]interface{} + + userAgent, err := generateUserAgentString(u.d, u.Config.userAgent) + if err != nil { + return nil, err + } + + policy, err := sendRequest(u.Config, "GET", project, url, userAgent, obj) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + out := &cloudresourcemanager.Policy{} + err = Convert(policy, out) + if err != nil { + return nil, errwrap.Wrapf("Cannot convert a policy to a resource manager policy: {{err}}", err) + } + + return out, nil +} + +func (u *NotebooksInstanceIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + json, err := ConvertToMap(policy) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + obj["policy"] = json + + url, err := u.qualifyInstanceUrl("setIamPolicy") + if err != nil { + return err + } + project, err := getProject(u.d, u.Config) + if err != nil { + return err + } + + userAgent, err := generateUserAgentString(u.d, u.Config.userAgent) + if err != nil { + return err + } + + _, err = sendRequestWithTimeout(u.Config, "POST", project, url, userAgent, obj, u.d.Timeout(schema.TimeoutCreate)) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + return nil +} + +func (u *NotebooksInstanceIamUpdater) qualifyInstanceUrl(methodIdentifier string) (string, error) { + urlTemplate := fmt.Sprintf("{{NotebooksBasePath}}%s:%s", fmt.Sprintf("projects/%s/locations/%s/instances/%s", u.project, u.location, u.instanceName), methodIdentifier) + url, err := replaceVars(u.d, u.Config, urlTemplate) + if err != nil { + return "", err + } + return url, nil +} + +func (u *NotebooksInstanceIamUpdater) GetResourceId() string { + return fmt.Sprintf("projects/%s/locations/%s/instances/%s", u.project, u.location, u.instanceName) +} + +func (u *NotebooksInstanceIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-notebooks-instance-%s", u.GetResourceId()) +} + +func (u *NotebooksInstanceIamUpdater) DescribeResource() string { + return fmt.Sprintf("notebooks instance %q", u.GetResourceId()) +} diff --git a/google/iam_notebooks_instance_generated_test.go b/google/iam_notebooks_instance_generated_test.go new file mode 100644 index 00000000000..6e7e69d94d2 --- /dev/null +++ b/google/iam_notebooks_instance_generated_test.go @@ -0,0 +1,235 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccNotebooksInstanceIamBindingGenerated(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + "role": "roles/viewer", + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstanceIamBinding_basicGenerated(context), + }, + { + ResourceName: "google_notebooks_instance_iam_binding.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/instances/%s roles/viewer", getTestProjectFromEnv(), "us-west1-a", fmt.Sprintf("tf-test-notebooks-instance%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Binding update + Config: testAccNotebooksInstanceIamBinding_updateGenerated(context), + }, + { + ResourceName: "google_notebooks_instance_iam_binding.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/instances/%s roles/viewer", getTestProjectFromEnv(), "us-west1-a", fmt.Sprintf("tf-test-notebooks-instance%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNotebooksInstanceIamMemberGenerated(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + "role": "roles/viewer", + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccNotebooksInstanceIamMember_basicGenerated(context), + }, + { + ResourceName: "google_notebooks_instance_iam_member.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/instances/%s roles/viewer user:admin@hashicorptest.com", getTestProjectFromEnv(), "us-west1-a", fmt.Sprintf("tf-test-notebooks-instance%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNotebooksInstanceIamPolicyGenerated(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + "role": "roles/viewer", + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstanceIamPolicy_basicGenerated(context), + }, + { + ResourceName: "google_notebooks_instance_iam_policy.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/instances/%s", getTestProjectFromEnv(), "us-west1-a", fmt.Sprintf("tf-test-notebooks-instance%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccNotebooksInstanceIamPolicy_emptyBinding(context), + }, + { + ResourceName: "google_notebooks_instance_iam_policy.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/instances/%s", getTestProjectFromEnv(), "us-west1-a", fmt.Sprintf("tf-test-notebooks-instance%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccNotebooksInstanceIamMember_basicGenerated(context map[string]interface{}) string { + return 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" + } +} + +resource "google_notebooks_instance_iam_member" "foo" { + project = google_notebooks_instance.instance.project + location = google_notebooks_instance.instance.location + instance_name = google_notebooks_instance.instance.name + role = "%{role}" + member = "user:admin@hashicorptest.com" +} +`, context) +} + +func testAccNotebooksInstanceIamPolicy_basicGenerated(context map[string]interface{}) string { + return 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" + } +} + +data "google_iam_policy" "foo" { + binding { + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + } +} + +resource "google_notebooks_instance_iam_policy" "foo" { + project = google_notebooks_instance.instance.project + location = google_notebooks_instance.instance.location + instance_name = google_notebooks_instance.instance.name + policy_data = data.google_iam_policy.foo.policy_data +} +`, context) +} + +func testAccNotebooksInstanceIamPolicy_emptyBinding(context map[string]interface{}) string { + return 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" + } +} + +data "google_iam_policy" "foo" { +} + +resource "google_notebooks_instance_iam_policy" "foo" { + project = google_notebooks_instance.instance.project + location = google_notebooks_instance.instance.location + instance_name = google_notebooks_instance.instance.name + policy_data = data.google_iam_policy.foo.policy_data +} +`, context) +} + +func testAccNotebooksInstanceIamBinding_basicGenerated(context map[string]interface{}) string { + return 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" + } +} + +resource "google_notebooks_instance_iam_binding" "foo" { + project = google_notebooks_instance.instance.project + location = google_notebooks_instance.instance.location + instance_name = google_notebooks_instance.instance.name + role = "%{role}" + members = ["user:admin@hashicorptest.com"] +} +`, context) +} + +func testAccNotebooksInstanceIamBinding_updateGenerated(context map[string]interface{}) string { + return 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" + } +} + +resource "google_notebooks_instance_iam_binding" "foo" { + project = google_notebooks_instance.instance.project + location = google_notebooks_instance.instance.location + instance_name = google_notebooks_instance.instance.name + role = "%{role}" + members = ["user:admin@hashicorptest.com", "user:paddy@hashicorp.com"] +} +`, context) +} diff --git a/google/notebooks_operation.go b/google/notebooks_operation.go new file mode 100644 index 00000000000..2e78777e335 --- /dev/null +++ b/google/notebooks_operation.go @@ -0,0 +1,75 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- +package google + +import ( + "encoding/json" + "fmt" + "time" +) + +type NotebooksOperationWaiter struct { + Config *Config + UserAgent string + Project string + CommonOperationWaiter +} + +func (w *NotebooksOperationWaiter) QueryOp() (interface{}, error) { + if w == nil { + return nil, fmt.Errorf("Cannot query operation, it's unset or nil.") + } + // Returns the proper get. + url := fmt.Sprintf("https://notebooks.googleapis.com/v1/%s", w.CommonOperationWaiter.Op.Name) + + return sendRequest(w.Config, "GET", w.Project, url, w.UserAgent, nil) +} + +func createNotebooksWaiter(config *Config, op map[string]interface{}, project, activity, userAgent string) (*NotebooksOperationWaiter, error) { + if val, ok := op["name"]; !ok || val == "" { + // This was a synchronous call - there is no operation to wait for. + return nil, nil + } + w := &NotebooksOperationWaiter{ + Config: config, + UserAgent: userAgent, + Project: project, + } + if err := w.CommonOperationWaiter.SetOp(op); err != nil { + return nil, err + } + return w, nil +} + +// nolint: deadcode,unused +func notebooksOperationWaitTimeWithResponse(config *Config, op map[string]interface{}, response *map[string]interface{}, project, activity, userAgent string, timeout time.Duration) error { + w, err := createNotebooksWaiter(config, op, project, activity, userAgent) + if err != nil || w == nil { + // If w is nil, the op was synchronous. + return err + } + if err := OperationWait(w, activity, timeout, config.PollInterval); err != nil { + return err + } + return json.Unmarshal([]byte(w.CommonOperationWaiter.Op.Response), response) +} + +func notebooksOperationWaitTime(config *Config, op map[string]interface{}, project, activity, userAgent string, timeout time.Duration) error { + w, err := createNotebooksWaiter(config, op, project, activity, userAgent) + if err != nil || w == nil { + // If w is nil, the op was synchronous. + return err + } + return OperationWait(w, activity, timeout, config.PollInterval) +} diff --git a/google/provider.go b/google/provider.go index 08173ddc084..df1fe8cded6 100644 --- a/google/provider.go +++ b/google/provider.go @@ -423,6 +423,14 @@ func Provider() *schema.Provider { "GOOGLE_NETWORK_MANAGEMENT_CUSTOM_ENDPOINT", }, NetworkManagementDefaultBasePath), }, + "notebooks_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCustomEndpoint, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_NOTEBOOKS_CUSTOM_ENDPOINT", + }, NotebooksDefaultBasePath), + }, "os_config_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -670,9 +678,9 @@ func Provider() *schema.Provider { return provider } -// Generated resources: 171 -// Generated IAM resources: 72 -// Total generated resources: 243 +// Generated resources: 174 +// Generated IAM resources: 75 +// Total generated resources: 249 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -886,6 +894,12 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_monitoring_uptime_check_config": resourceMonitoringUptimeCheckConfig(), "google_monitoring_metric_descriptor": resourceMonitoringMetricDescriptor(), "google_network_management_connectivity_test": resourceNetworkManagementConnectivityTest(), + "google_notebooks_environment": resourceNotebooksEnvironment(), + "google_notebooks_instance": resourceNotebooksInstance(), + "google_notebooks_instance_iam_binding": ResourceIamBinding(NotebooksInstanceIamSchema, NotebooksInstanceIamUpdaterProducer, NotebooksInstanceIdParseFunc), + "google_notebooks_instance_iam_member": ResourceIamMember(NotebooksInstanceIamSchema, NotebooksInstanceIamUpdaterProducer, NotebooksInstanceIdParseFunc), + "google_notebooks_instance_iam_policy": ResourceIamPolicy(NotebooksInstanceIamSchema, NotebooksInstanceIamUpdaterProducer, NotebooksInstanceIdParseFunc), + "google_notebooks_location": resourceNotebooksLocation(), "google_os_config_patch_deployment": resourceOSConfigPatchDeployment(), "google_os_login_ssh_public_key": resourceOSLoginSSHPublicKey(), "google_pubsub_topic": resourcePubsubTopic(), @@ -1145,6 +1159,7 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.MLEngineBasePath = d.Get("ml_engine_custom_endpoint").(string) config.MonitoringBasePath = d.Get("monitoring_custom_endpoint").(string) config.NetworkManagementBasePath = d.Get("network_management_custom_endpoint").(string) + config.NotebooksBasePath = d.Get("notebooks_custom_endpoint").(string) config.OSConfigBasePath = d.Get("os_config_custom_endpoint").(string) config.OSLoginBasePath = d.Get("os_login_custom_endpoint").(string) config.PubsubBasePath = d.Get("pubsub_custom_endpoint").(string) diff --git a/google/resource_notebooks_environment.go b/google/resource_notebooks_environment.go new file mode 100644 index 00000000000..4abc29f655d --- /dev/null +++ b/google/resource_notebooks_environment.go @@ -0,0 +1,587 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceNotebooksEnvironment() *schema.Resource { + return &schema.Resource{ + Create: resourceNotebooksEnvironmentCreate, + Read: resourceNotebooksEnvironmentRead, + Update: resourceNotebooksEnvironmentUpdate, + Delete: resourceNotebooksEnvironmentDelete, + + Importer: &schema.ResourceImporter{ + State: resourceNotebooksEnvironmentImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(4 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Minute), + Delete: schema.DefaultTimeout(4 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "location": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + Description: `A reference to the zone where the machine resides.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name specified for the Environment instance. +Format: projects/{project_id}/locations/{location}/environments/{environmentId}`, + }, + "container_image": { + Type: schema.TypeList, + Optional: true, + Description: `Use a container image to start the notebook instance.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + Description: `The path to the container image repository. +For example: gcr.io/{project_id}/{imageName}`, + }, + "tag": { + Type: schema.TypeString, + Optional: true, + Description: `The tag of the container image. If not specified, this defaults to the latest tag.`, + }, + }, + }, + ExactlyOneOf: []string{"vm_image", "container_image"}, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: `A brief description of this environment.`, + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Description: `Display name of this environment for the UI.`, + }, + "post_startup_script": { + Type: schema.TypeString, + Optional: true, + Description: `Path to a Bash script that automatically runs after a notebook instance fully boots up. +The path must be a URL or Cloud Storage path. Example: "gs://path-to-file/file-name"`, + }, + "vm_image": { + Type: schema.TypeList, + Optional: true, + Description: `Use a Compute Engine VM image to start the notebook instance.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Required: true, + Description: `The name of the Google Cloud project that this VM image belongs to. +Format: projects/{project_id}`, + }, + "image_family": { + Type: schema.TypeString, + Optional: true, + Description: `Use this VM image family to find the image; the newest image in this family will be used.`, + }, + "image_name": { + Type: schema.TypeString, + Optional: true, + Description: `Use VM image name to find the image.`, + }, + }, + }, + ExactlyOneOf: []string{"vm_image", "container_image"}, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `Instance creation time`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceNotebooksEnvironmentCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + displayNameProp, err := expandNotebooksEnvironmentDisplayName(d.Get("display_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("display_name"); !isEmptyValue(reflect.ValueOf(displayNameProp)) && (ok || !reflect.DeepEqual(v, displayNameProp)) { + obj["displayName"] = displayNameProp + } + descriptionProp, err := expandNotebooksEnvironmentDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + postStartupScriptProp, err := expandNotebooksEnvironmentPostStartupScript(d.Get("post_startup_script"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("post_startup_script"); !isEmptyValue(reflect.ValueOf(postStartupScriptProp)) && (ok || !reflect.DeepEqual(v, postStartupScriptProp)) { + obj["postStartupScript"] = postStartupScriptProp + } + vmImageProp, err := expandNotebooksEnvironmentVmImage(d.Get("vm_image"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("vm_image"); !isEmptyValue(reflect.ValueOf(vmImageProp)) && (ok || !reflect.DeepEqual(v, vmImageProp)) { + obj["vmImage"] = vmImageProp + } + containerImageProp, err := expandNotebooksEnvironmentContainerImage(d.Get("container_image"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("container_image"); !isEmptyValue(reflect.ValueOf(containerImageProp)) && (ok || !reflect.DeepEqual(v, containerImageProp)) { + obj["containerImage"] = containerImageProp + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/environments?environmentId={{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Environment: %#v", obj) + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Environment: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Environment: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = notebooksOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating Environment", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + return fmt.Errorf("Error waiting to create Environment: %s", err) + } + + // This may have caused the ID to update - update it if so. + id, err = replaceVars(d, config, "projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating Environment %q: %#v", d.Id(), res) + + return resourceNotebooksEnvironmentRead(d, meta) +} + +func resourceNotebooksEnvironmentRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Environment: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "GET", billingProject, url, userAgent, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("NotebooksEnvironment %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + + if err := d.Set("display_name", flattenNotebooksEnvironmentDisplayName(res["displayName"], d, config)); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + if err := d.Set("description", flattenNotebooksEnvironmentDescription(res["description"], d, config)); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + if err := d.Set("post_startup_script", flattenNotebooksEnvironmentPostStartupScript(res["postStartupScript"], d, config)); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + if err := d.Set("create_time", flattenNotebooksEnvironmentCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + if err := d.Set("vm_image", flattenNotebooksEnvironmentVmImage(res["vmImage"], d, config)); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + if err := d.Set("container_image", flattenNotebooksEnvironmentContainerImage(res["containerImage"], d, config)); err != nil { + return fmt.Errorf("Error reading Environment: %s", err) + } + + return nil +} + +func resourceNotebooksEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Environment: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + displayNameProp, err := expandNotebooksEnvironmentDisplayName(d.Get("display_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("display_name"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, displayNameProp)) { + obj["displayName"] = displayNameProp + } + descriptionProp, err := expandNotebooksEnvironmentDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + postStartupScriptProp, err := expandNotebooksEnvironmentPostStartupScript(d.Get("post_startup_script"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("post_startup_script"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, postStartupScriptProp)) { + obj["postStartupScript"] = postStartupScriptProp + } + vmImageProp, err := expandNotebooksEnvironmentVmImage(d.Get("vm_image"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("vm_image"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, vmImageProp)) { + obj["vmImage"] = vmImageProp + } + containerImageProp, err := expandNotebooksEnvironmentContainerImage(d.Get("container_image"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("container_image"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, containerImageProp)) { + obj["containerImage"] = containerImageProp + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Environment %q: %#v", d.Id(), obj) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "PUT", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Environment %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Environment %q: %#v", d.Id(), res) + } + + err = notebooksOperationWaitTime( + config, res, project, "Updating Environment", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + + return resourceNotebooksEnvironmentRead(d, meta) +} + +func resourceNotebooksEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Environment: %s", err) + } + billingProject = project + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Environment %q", d.Id()) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "DELETE", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Environment") + } + + err = notebooksOperationWaitTime( + config, res, project, "Deleting Environment", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting Environment %q: %#v", d.Id(), res) + return nil +} + +func resourceNotebooksEnvironmentImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/locations/(?P[^/]+)/environments/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenNotebooksEnvironmentDisplayName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentDescription(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentPostStartupScript(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentCreateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentVmImage(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["project"] = + flattenNotebooksEnvironmentVmImageProject(original["project"], d, config) + transformed["image_name"] = + flattenNotebooksEnvironmentVmImageImageName(original["imageName"], d, config) + transformed["image_family"] = + flattenNotebooksEnvironmentVmImageImageFamily(original["imageFamily"], d, config) + return []interface{}{transformed} +} +func flattenNotebooksEnvironmentVmImageProject(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentVmImageImageName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentVmImageImageFamily(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentContainerImage(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["repository"] = + flattenNotebooksEnvironmentContainerImageRepository(original["repository"], d, config) + transformed["tag"] = + flattenNotebooksEnvironmentContainerImageTag(original["tag"], d, config) + return []interface{}{transformed} +} +func flattenNotebooksEnvironmentContainerImageRepository(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksEnvironmentContainerImageTag(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func expandNotebooksEnvironmentDisplayName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentPostStartupScript(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentVmImage(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedProject, err := expandNotebooksEnvironmentVmImageProject(original["project"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedProject); val.IsValid() && !isEmptyValue(val) { + transformed["project"] = transformedProject + } + + transformedImageName, err := expandNotebooksEnvironmentVmImageImageName(original["image_name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedImageName); val.IsValid() && !isEmptyValue(val) { + transformed["imageName"] = transformedImageName + } + + transformedImageFamily, err := expandNotebooksEnvironmentVmImageImageFamily(original["image_family"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedImageFamily); val.IsValid() && !isEmptyValue(val) { + transformed["imageFamily"] = transformedImageFamily + } + + return transformed, nil +} + +func expandNotebooksEnvironmentVmImageProject(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentVmImageImageName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentVmImageImageFamily(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentContainerImage(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedRepository, err := expandNotebooksEnvironmentContainerImageRepository(original["repository"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedRepository); val.IsValid() && !isEmptyValue(val) { + transformed["repository"] = transformedRepository + } + + transformedTag, err := expandNotebooksEnvironmentContainerImageTag(original["tag"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedTag); val.IsValid() && !isEmptyValue(val) { + transformed["tag"] = transformedTag + } + + return transformed, nil +} + +func expandNotebooksEnvironmentContainerImageRepository(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksEnvironmentContainerImageTag(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google/resource_notebooks_environment_generated_test.go b/google/resource_notebooks_environment_generated_test.go new file mode 100644 index 00000000000..186f0e14631 --- /dev/null +++ b/google/resource_notebooks_environment_generated_test.go @@ -0,0 +1,92 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccNotebooksEnvironment_notebookEnvironmentBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + }, + CheckDestroy: testAccCheckNotebooksEnvironmentDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksEnvironment_notebookEnvironmentBasicExample(context), + }, + { + ResourceName: "google_notebooks_environment.environment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location"}, + }, + }, + }) +} + +func testAccNotebooksEnvironment_notebookEnvironmentBasicExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_notebooks_environment" "environment" { + provider = google-beta + name = "tf-test-notebooks-environment%{random_suffix}" + location = "us-west1-a" + container_image { + repository = "gcr.io/deeplearning-platform-release/base-cpu" + } +} +`, context) +} + +func testAccCheckNotebooksEnvironmentDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_notebooks_environment" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := googleProviderConfig(t) + + url, err := replaceVarsForTest(config, rs, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/environments/{{name}}") + if err != nil { + return err + } + + _, err = sendRequest(config, "GET", "", url, config.userAgent, nil) + if err == nil { + return fmt.Errorf("NotebooksEnvironment still exists at %s", url) + } + } + + return nil + } +} diff --git a/google/resource_notebooks_environment_sweeper_test.go b/google/resource_notebooks_environment_sweeper_test.go new file mode 100644 index 00000000000..1e4a971f0b6 --- /dev/null +++ b/google/resource_notebooks_environment_sweeper_test.go @@ -0,0 +1,124 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func init() { + resource.AddTestSweepers("NotebooksEnvironment", &resource.Sweeper{ + Name: "NotebooksEnvironment", + F: testSweepNotebooksEnvironment, + }) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepNotebooksEnvironment(region string) error { + resourceName := "NotebooksEnvironment" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := getTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://notebooks.googleapis.com/v1/projects/{{project}}/locations/{{location}}/environments", "?")[0] + listUrl, err := replaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := sendRequest(config, "GET", config.Project, listUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["environments"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + if obj["name"] == nil { + log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName) + return nil + } + + name := GetResourceNameFromSelfLink(obj["name"].(string)) + // Skip resources that shouldn't be sweeped + if !isSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://notebooks.googleapis.com/v1/projects/{{project}}/locations/{{location}}/environments/{{name}}" + deleteUrl, err := replaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/google/resource_notebooks_instance.go b/google/resource_notebooks_instance.go new file mode 100644 index 00000000000..bdfee083e77 --- /dev/null +++ b/google/resource_notebooks_instance.go @@ -0,0 +1,1050 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceNotebooksInstance() *schema.Resource { + return &schema.Resource{ + Create: resourceNotebooksInstanceCreate, + Read: resourceNotebooksInstanceRead, + Update: resourceNotebooksInstanceUpdate, + Delete: resourceNotebooksInstanceDelete, + + Importer: &schema.ResourceImporter{ + State: resourceNotebooksInstanceImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(15 * time.Minute), + Update: schema.DefaultTimeout(15 * time.Minute), + Delete: schema.DefaultTimeout(15 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "location": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + Description: `A reference to the zone where the machine resides.`, + }, + "machine_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + Description: `A reference to a machine type which defines VM kind.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name specified for the Notebook instance.`, + }, + "accelerator_config": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `The hardware accelerator used on this instance. If you use accelerators, +make sure that your configuration has enough vCPUs and memory to support the +machineType you have selected.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "core_count": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: `Count of cores of this accelerator.`, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"ACCELERATOR_TYPE_UNSPECIFIED", "NVIDIA_TESLA_K80", "NVIDIA_TESLA_P100", "NVIDIA_TESLA_V100", "NVIDIA_TESLA_P4", "NVIDIA_TESLA_T4", "NVIDIA_TESLA_T4_VWS", "NVIDIA_TESLA_P100_VWS", "NVIDIA_TESLA_P4_VWS", "TPU_V2", "TPU_V3"}, false), + Description: `Type of this accelerator. Possible values: ["ACCELERATOR_TYPE_UNSPECIFIED", "NVIDIA_TESLA_K80", "NVIDIA_TESLA_P100", "NVIDIA_TESLA_V100", "NVIDIA_TESLA_P4", "NVIDIA_TESLA_T4", "NVIDIA_TESLA_T4_VWS", "NVIDIA_TESLA_P100_VWS", "NVIDIA_TESLA_P4_VWS", "TPU_V2", "TPU_V3"]`, + }, + }, + }, + }, + "boot_disk_size_gb": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: `The size of the boot disk in GB attached to this instance, +up to a maximum of 64000 GB (64 TB). The minimum recommended value is 100 GB. +If not specified, this defaults to 100.`, + }, + "boot_disk_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"DISK_TYPE_UNSPECIFIED", "PD_STANDARD", "PD_SSD", "PD_BALANCED", ""}, false), + Description: `Possible disk types for notebook instances. Possible values: ["DISK_TYPE_UNSPECIFIED", "PD_STANDARD", "PD_SSD", "PD_BALANCED"]`, + }, + "container_image": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `Use a container image to start the notebook instance.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The path to the container image repository. +For example: gcr.io/{project_id}/{imageName}`, + }, + "tag": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The tag of the container image. If not specified, this defaults to the latest tag.`, + }, + }, + }, + ExactlyOneOf: []string{"vm_image", "container_image"}, + }, + "custom_gpu_driver_path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Specify a custom Cloud Storage path where the GPU driver is stored. +If not specified, we'll automatically choose from official GPU drivers.`, + }, + "data_disk_size_gb": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: `The size of the data disk in GB attached to this instance, +up to a maximum of 64000 GB (64 TB). +You can choose the size of the data disk based on how big your notebooks and data are. +If not specified, this defaults to 100.`, + }, + "data_disk_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"DISK_TYPE_UNSPECIFIED", "PD_STANDARD", "PD_SSD", "PD_BALANCED", ""}, false), + DiffSuppressFunc: emptyOrDefaultStringSuppress("DISK_TYPE_UNSPECIFIED"), + Description: `Possible disk types for notebook instances. Possible values: ["DISK_TYPE_UNSPECIFIED", "PD_STANDARD", "PD_SSD", "PD_BALANCED"]`, + }, + "disk_encryption": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"DISK_ENCRYPTION_UNSPECIFIED", "GMEK", "CMEK", ""}, false), + DiffSuppressFunc: emptyOrDefaultStringSuppress("DISK_ENCRYPTION_UNSPECIFIED"), + Description: `Disk encryption method used on the boot and data disks, defaults to GMEK. Possible values: ["DISK_ENCRYPTION_UNSPECIFIED", "GMEK", "CMEK"]`, + }, + "install_gpu_driver": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Description: `Whether the end user authorizes Google Cloud to install GPU driver +on this instance. If this field is empty or set to false, the GPU driver +won't be installed. Only applicable to instances with GPUs.`, + }, + "instance_owners": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `The list of owners of this instance after creation. +Format: alias@example.com. +Currently supports one owner only. +If not specified, all of the service account users of +your VM instance's service account can use the instance.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "kms_key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The KMS key used to encrypt the disks, only applicable if diskEncryption is CMEK. +Format: projects/{project_id}/locations/{location}/keyRings/{key_ring_id}/cryptoKeys/{key_id}`, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + Description: `Labels to apply to this instance. These can be later modified by the setLabels method. +An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "metadata": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Description: `Custom metadata to apply to this instance. +An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "network": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + Description: `The name of the VPC that this instance is in. +Format: projects/{project_id}/global/networks/{network_id}`, + }, + "no_proxy_access": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Description: `the notebook instance will not register with the proxy..`, + }, + "no_public_ip": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Description: `no public IP will be assigned to this instance.`, + }, + "no_remove_data_disk": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Description: `If true, the data disk will not be auto deleted when deleting the instance.`, + }, + "post_startup_script": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Path to a Bash script that automatically runs after a +notebook instance fully boots up. The path must be a URL +or Cloud Storage path (gs://path-to-file/file-name).`, + }, + "service_account": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + Description: `The service account on this instance, giving access to other +Google Cloud services. You can use any service account within +the same project, but you must have the service account user +permission to use the instance. If not specified, +the Compute Engine default service account is used.`, + }, + "subnet": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + Description: `The name of the subnet that this instance is in. +Format: projects/{project_id}/regions/{region}/subnetworks/{subnetwork_id}`, + }, + "vm_image": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `Use a Compute Engine VM image to start the notebook instance.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name of the Google Cloud project that this VM image belongs to. +Format: projects/{project_id}`, + }, + "image_family": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Use this VM image family to find the image; the newest image in this family will be used.`, + }, + "image_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Use VM image name to find the image.`, + }, + }, + }, + ExactlyOneOf: []string{"vm_image", "container_image"}, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Instance creation time`, + }, + "proxy_uri": { + Type: schema.TypeString, + Computed: true, + Description: `The proxy endpoint that is used to access the Jupyter notebook.`, + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: `The state of this instance.`, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Instance update time.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceNotebooksInstanceCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + machineTypeProp, err := expandNotebooksInstanceMachineType(d.Get("machine_type"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("machine_type"); !isEmptyValue(reflect.ValueOf(machineTypeProp)) && (ok || !reflect.DeepEqual(v, machineTypeProp)) { + obj["machineType"] = machineTypeProp + } + postStartupScriptProp, err := expandNotebooksInstancePostStartupScript(d.Get("post_startup_script"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("post_startup_script"); !isEmptyValue(reflect.ValueOf(postStartupScriptProp)) && (ok || !reflect.DeepEqual(v, postStartupScriptProp)) { + obj["postStartupScript"] = postStartupScriptProp + } + instanceOwnersProp, err := expandNotebooksInstanceInstanceOwners(d.Get("instance_owners"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("instance_owners"); !isEmptyValue(reflect.ValueOf(instanceOwnersProp)) && (ok || !reflect.DeepEqual(v, instanceOwnersProp)) { + obj["instanceOwners"] = instanceOwnersProp + } + serviceAccountProp, err := expandNotebooksInstanceServiceAccount(d.Get("service_account"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("service_account"); !isEmptyValue(reflect.ValueOf(serviceAccountProp)) && (ok || !reflect.DeepEqual(v, serviceAccountProp)) { + obj["serviceAccount"] = serviceAccountProp + } + acceleratorConfigProp, err := expandNotebooksInstanceAcceleratorConfig(d.Get("accelerator_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("accelerator_config"); !isEmptyValue(reflect.ValueOf(acceleratorConfigProp)) && (ok || !reflect.DeepEqual(v, acceleratorConfigProp)) { + obj["acceleratorConfig"] = acceleratorConfigProp + } + installGpuDriverProp, err := expandNotebooksInstanceInstallGpuDriver(d.Get("install_gpu_driver"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("install_gpu_driver"); !isEmptyValue(reflect.ValueOf(installGpuDriverProp)) && (ok || !reflect.DeepEqual(v, installGpuDriverProp)) { + obj["installGpuDriver"] = installGpuDriverProp + } + customGpuDriverPathProp, err := expandNotebooksInstanceCustomGpuDriverPath(d.Get("custom_gpu_driver_path"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("custom_gpu_driver_path"); !isEmptyValue(reflect.ValueOf(customGpuDriverPathProp)) && (ok || !reflect.DeepEqual(v, customGpuDriverPathProp)) { + obj["customGpuDriverPath"] = customGpuDriverPathProp + } + bootDiskTypeProp, err := expandNotebooksInstanceBootDiskType(d.Get("boot_disk_type"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("boot_disk_type"); !isEmptyValue(reflect.ValueOf(bootDiskTypeProp)) && (ok || !reflect.DeepEqual(v, bootDiskTypeProp)) { + obj["bootDiskType"] = bootDiskTypeProp + } + bootDiskSizeGbProp, err := expandNotebooksInstanceBootDiskSizeGb(d.Get("boot_disk_size_gb"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("boot_disk_size_gb"); !isEmptyValue(reflect.ValueOf(bootDiskSizeGbProp)) && (ok || !reflect.DeepEqual(v, bootDiskSizeGbProp)) { + obj["bootDiskSizeGb"] = bootDiskSizeGbProp + } + dataDiskTypeProp, err := expandNotebooksInstanceDataDiskType(d.Get("data_disk_type"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("data_disk_type"); !isEmptyValue(reflect.ValueOf(dataDiskTypeProp)) && (ok || !reflect.DeepEqual(v, dataDiskTypeProp)) { + obj["dataDiskType"] = dataDiskTypeProp + } + dataDiskSizeGbProp, err := expandNotebooksInstanceDataDiskSizeGb(d.Get("data_disk_size_gb"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("data_disk_size_gb"); !isEmptyValue(reflect.ValueOf(dataDiskSizeGbProp)) && (ok || !reflect.DeepEqual(v, dataDiskSizeGbProp)) { + obj["dataDiskSizeGb"] = dataDiskSizeGbProp + } + noRemoveDataDiskProp, err := expandNotebooksInstanceNoRemoveDataDisk(d.Get("no_remove_data_disk"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("no_remove_data_disk"); !isEmptyValue(reflect.ValueOf(noRemoveDataDiskProp)) && (ok || !reflect.DeepEqual(v, noRemoveDataDiskProp)) { + obj["noRemoveDataDisk"] = noRemoveDataDiskProp + } + diskEncryptionProp, err := expandNotebooksInstanceDiskEncryption(d.Get("disk_encryption"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("disk_encryption"); !isEmptyValue(reflect.ValueOf(diskEncryptionProp)) && (ok || !reflect.DeepEqual(v, diskEncryptionProp)) { + obj["diskEncryption"] = diskEncryptionProp + } + kmsKeyProp, err := expandNotebooksInstanceKmsKey(d.Get("kms_key"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("kms_key"); !isEmptyValue(reflect.ValueOf(kmsKeyProp)) && (ok || !reflect.DeepEqual(v, kmsKeyProp)) { + obj["kmsKey"] = kmsKeyProp + } + noPublicIpProp, err := expandNotebooksInstanceNoPublicIp(d.Get("no_public_ip"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("no_public_ip"); !isEmptyValue(reflect.ValueOf(noPublicIpProp)) && (ok || !reflect.DeepEqual(v, noPublicIpProp)) { + obj["noPublicIp"] = noPublicIpProp + } + noProxyAccessProp, err := expandNotebooksInstanceNoProxyAccess(d.Get("no_proxy_access"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("no_proxy_access"); !isEmptyValue(reflect.ValueOf(noProxyAccessProp)) && (ok || !reflect.DeepEqual(v, noProxyAccessProp)) { + obj["noProxyAccess"] = noProxyAccessProp + } + networkProp, err := expandNotebooksInstanceNetwork(d.Get("network"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("network"); !isEmptyValue(reflect.ValueOf(networkProp)) && (ok || !reflect.DeepEqual(v, networkProp)) { + obj["network"] = networkProp + } + subnetProp, err := expandNotebooksInstanceSubnet(d.Get("subnet"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("subnet"); !isEmptyValue(reflect.ValueOf(subnetProp)) && (ok || !reflect.DeepEqual(v, subnetProp)) { + obj["subnet"] = subnetProp + } + labelsProp, err := expandNotebooksInstanceLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + metadataProp, err := expandNotebooksInstanceMetadata(d.Get("metadata"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("metadata"); !isEmptyValue(reflect.ValueOf(metadataProp)) && (ok || !reflect.DeepEqual(v, metadataProp)) { + obj["metadata"] = metadataProp + } + vmImageProp, err := expandNotebooksInstanceVmImage(d.Get("vm_image"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("vm_image"); !isEmptyValue(reflect.ValueOf(vmImageProp)) && (ok || !reflect.DeepEqual(v, vmImageProp)) { + obj["vmImage"] = vmImageProp + } + containerImageProp, err := expandNotebooksInstanceContainerImage(d.Get("container_image"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("container_image"); !isEmptyValue(reflect.ValueOf(containerImageProp)) && (ok || !reflect.DeepEqual(v, containerImageProp)) { + obj["containerImage"] = containerImageProp + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/instances?instanceId={{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Instance: %#v", obj) + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Instance: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Instance: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/instances/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = notebooksOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating Instance", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + return fmt.Errorf("Error waiting to create Instance: %s", err) + } + + // This may have caused the ID to update - update it if so. + id, err = replaceVars(d, config, "projects/{{project}}/locations/{{location}}/instances/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating Instance %q: %#v", d.Id(), res) + + return resourceNotebooksInstanceRead(d, meta) +} + +func resourceNotebooksInstanceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/instances/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Instance: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "GET", billingProject, url, userAgent, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("NotebooksInstance %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + + if err := d.Set("machine_type", flattenNotebooksInstanceMachineType(res["machineType"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("post_startup_script", flattenNotebooksInstancePostStartupScript(res["postStartupScript"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("proxy_uri", flattenNotebooksInstanceProxyUri(res["proxyUri"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("service_account", flattenNotebooksInstanceServiceAccount(res["serviceAccount"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("accelerator_config", flattenNotebooksInstanceAcceleratorConfig(res["acceleratorConfig"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("state", flattenNotebooksInstanceState(res["state"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("install_gpu_driver", flattenNotebooksInstanceInstallGpuDriver(res["installGpuDriver"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("custom_gpu_driver_path", flattenNotebooksInstanceCustomGpuDriverPath(res["customGpuDriverPath"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("data_disk_type", flattenNotebooksInstanceDataDiskType(res["dataDiskType"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("no_remove_data_disk", flattenNotebooksInstanceNoRemoveDataDisk(res["noRemoveDataDisk"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("disk_encryption", flattenNotebooksInstanceDiskEncryption(res["diskEncryption"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("kms_key", flattenNotebooksInstanceKmsKey(res["kmsKey"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("no_public_ip", flattenNotebooksInstanceNoPublicIp(res["noPublicIp"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("no_proxy_access", flattenNotebooksInstanceNoProxyAccess(res["noProxyAccess"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("network", flattenNotebooksInstanceNetwork(res["network"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("subnet", flattenNotebooksInstanceSubnet(res["subnet"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("labels", flattenNotebooksInstanceLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("create_time", flattenNotebooksInstanceCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("update_time", flattenNotebooksInstanceUpdateTime(res["updateTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + + return nil +} + +func resourceNotebooksInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Instance: %s", err) + } + billingProject = project + + d.Partial(true) + + if d.HasChange("labels") { + obj := make(map[string]interface{}) + + labelsProp, err := expandNotebooksInstanceLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/instances/{{name}}:setLabels") + if err != nil { + return err + } + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return fmt.Errorf("Error updating Instance %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Instance %q: %#v", d.Id(), res) + } + + err = notebooksOperationWaitTime( + config, res, project, "Updating Instance", userAgent, + d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + } + + d.Partial(false) + + return resourceNotebooksInstanceRead(d, meta) +} + +func resourceNotebooksInstanceDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Instance: %s", err) + } + billingProject = project + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/instances/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Instance %q", d.Id()) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "DELETE", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Instance") + } + + err = notebooksOperationWaitTime( + config, res, project, "Deleting Instance", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting Instance %q: %#v", d.Id(), res) + return nil +} + +func resourceNotebooksInstanceImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/locations/(?P[^/]+)/instances/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/instances/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenNotebooksInstanceMachineType(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + return NameFromSelfLinkStateFunc(v) +} + +func flattenNotebooksInstancePostStartupScript(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceProxyUri(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceServiceAccount(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceAcceleratorConfig(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["type"] = + flattenNotebooksInstanceAcceleratorConfigType(original["type"], d, config) + transformed["core_count"] = + flattenNotebooksInstanceAcceleratorConfigCoreCount(original["coreCount"], d, config) + return []interface{}{transformed} +} +func flattenNotebooksInstanceAcceleratorConfigType(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceAcceleratorConfigCoreCount(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenNotebooksInstanceState(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceInstallGpuDriver(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceCustomGpuDriverPath(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceDataDiskType(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceNoRemoveDataDisk(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceDiskEncryption(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceKmsKey(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceNoPublicIp(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceNoProxyAccess(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceNetwork(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceSubnet(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceLabels(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceCreateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenNotebooksInstanceUpdateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func expandNotebooksInstanceMachineType(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstancePostStartupScript(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceInstanceOwners(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceServiceAccount(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceAcceleratorConfig(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedType, err := expandNotebooksInstanceAcceleratorConfigType(original["type"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedType); val.IsValid() && !isEmptyValue(val) { + transformed["type"] = transformedType + } + + transformedCoreCount, err := expandNotebooksInstanceAcceleratorConfigCoreCount(original["core_count"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedCoreCount); val.IsValid() && !isEmptyValue(val) { + transformed["coreCount"] = transformedCoreCount + } + + return transformed, nil +} + +func expandNotebooksInstanceAcceleratorConfigType(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceAcceleratorConfigCoreCount(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceInstallGpuDriver(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceCustomGpuDriverPath(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceBootDiskType(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceBootDiskSizeGb(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceDataDiskType(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceDataDiskSizeGb(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceNoRemoveDataDisk(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceDiskEncryption(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceKmsKey(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceNoPublicIp(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceNoProxyAccess(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceNetwork(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceSubnet(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceLabels(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) + } + return m, nil +} + +func expandNotebooksInstanceMetadata(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) + } + return m, nil +} + +func expandNotebooksInstanceVmImage(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedProject, err := expandNotebooksInstanceVmImageProject(original["project"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedProject); val.IsValid() && !isEmptyValue(val) { + transformed["project"] = transformedProject + } + + transformedImageFamily, err := expandNotebooksInstanceVmImageImageFamily(original["image_family"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedImageFamily); val.IsValid() && !isEmptyValue(val) { + transformed["imageFamily"] = transformedImageFamily + } + + transformedImageName, err := expandNotebooksInstanceVmImageImageName(original["image_name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedImageName); val.IsValid() && !isEmptyValue(val) { + transformed["imageName"] = transformedImageName + } + + return transformed, nil +} + +func expandNotebooksInstanceVmImageProject(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceVmImageImageFamily(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceVmImageImageName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceContainerImage(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedRepository, err := expandNotebooksInstanceContainerImageRepository(original["repository"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedRepository); val.IsValid() && !isEmptyValue(val) { + transformed["repository"] = transformedRepository + } + + transformedTag, err := expandNotebooksInstanceContainerImageTag(original["tag"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedTag); val.IsValid() && !isEmptyValue(val) { + transformed["tag"] = transformedTag + } + + return transformed, nil +} + +func expandNotebooksInstanceContainerImageRepository(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandNotebooksInstanceContainerImageTag(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google/resource_notebooks_instance_generated_test.go b/google/resource_notebooks_instance_generated_test.go new file mode 100644 index 00000000000..95c1c38a5ca --- /dev/null +++ b/google/resource_notebooks_instance_generated_test.go @@ -0,0 +1,263 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccNotebooksInstance_notebookInstanceBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + }, + CheckDestroy: testAccCheckNotebooksInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstance_notebookInstanceBasicExample(context), + }, + { + ResourceName: "google_notebooks_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "instance_owners", "boot_disk_type", "boot_disk_size_gb", "data_disk_size_gb", "metadata", "vm_image", "container_image", "location"}, + }, + }, + }) +} + +func testAccNotebooksInstance_notebookInstanceBasicExample(context map[string]interface{}) string { + return 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" + } +} +`, context) +} + +func TestAccNotebooksInstance_notebookInstanceBasicContainerExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + }, + CheckDestroy: testAccCheckNotebooksInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstance_notebookInstanceBasicContainerExample(context), + }, + { + ResourceName: "google_notebooks_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "instance_owners", "boot_disk_type", "boot_disk_size_gb", "data_disk_size_gb", "metadata", "vm_image", "container_image", "location"}, + }, + }, + }) +} + +func testAccNotebooksInstance_notebookInstanceBasicContainerExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_notebooks_instance" "instance" { + name = "tf-test-notebooks-instance%{random_suffix}" + location = "us-west1-a" + machine_type = "e2-medium" + metadata = { + proxy-mode = "service_account" + terraform = "true" + } + container_image { + repository = "gcr.io/deeplearning-platform-release/base-cpu" + tag = "latest" + } +} +`, context) +} + +func TestAccNotebooksInstance_notebookInstanceBasicGpuExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + }, + CheckDestroy: testAccCheckNotebooksInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstance_notebookInstanceBasicGpuExample(context), + }, + { + ResourceName: "google_notebooks_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "instance_owners", "boot_disk_type", "boot_disk_size_gb", "data_disk_size_gb", "metadata", "vm_image", "container_image", "location"}, + }, + }, + }) +} + +func testAccNotebooksInstance_notebookInstanceBasicGpuExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_notebooks_instance" "instance" { + name = "tf-test-notebooks-instance%{random_suffix}" + location = "us-west1-a" + machine_type = "n1-standard-1" // can't be e2 because of accelerator + + install_gpu_driver = true + accelerator_config { + type = "NVIDIA_TESLA_T4" + core_count = 1 + } + vm_image { + project = "deeplearning-platform-release" + image_family = "tf-latest-gpu" + } +} +`, context) +} + +func TestAccNotebooksInstance_notebookInstanceFullExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "service_account": getTestServiceAccountFromEnv(t), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + }, + CheckDestroy: testAccCheckNotebooksInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNotebooksInstance_notebookInstanceFullExample(context), + }, + { + ResourceName: "google_notebooks_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "instance_owners", "boot_disk_type", "boot_disk_size_gb", "data_disk_size_gb", "metadata", "vm_image", "container_image", "location"}, + }, + }, + }) +} + +func testAccNotebooksInstance_notebookInstanceFullExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_notebooks_instance" "instance" { + name = "tf-test-notebooks-instance%{random_suffix}" + location = "us-central1-a" + machine_type = "e2-medium" + + vm_image { + project = "deeplearning-platform-release" + image_family = "tf-latest-cpu" + } + + instance_owners = ["admin@hashicorptest.com"] + service_account = "%{service_account}" + + install_gpu_driver = true + boot_disk_type = "PD_SSD" + boot_disk_size_gb = 110 + + no_public_ip = true + no_proxy_access = true + + network = data.google_compute_network.my_network.id + subnet = data.google_compute_subnetwork.my_subnetwork.id + + labels = { + k = "val" + } + + metadata = { + terraform = "true" + } +} + +data "google_compute_network" "my_network" { + provider = google-beta + name = "default" +} + +data "google_compute_subnetwork" "my_subnetwork" { + provider = google-beta + name = "default" + region = "us-central1" +} +`, context) +} + +func testAccCheckNotebooksInstanceDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_notebooks_instance" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := googleProviderConfig(t) + + url, err := replaceVarsForTest(config, rs, "{{NotebooksBasePath}}projects/{{project}}/locations/{{location}}/instances/{{name}}") + if err != nil { + return err + } + + _, err = sendRequest(config, "GET", "", url, config.userAgent, nil) + if err == nil { + return fmt.Errorf("NotebooksInstance still exists at %s", url) + } + } + + return nil + } +} diff --git a/google/resource_notebooks_instance_sweeper_test.go b/google/resource_notebooks_instance_sweeper_test.go new file mode 100644 index 00000000000..f3e641496f6 --- /dev/null +++ b/google/resource_notebooks_instance_sweeper_test.go @@ -0,0 +1,124 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func init() { + resource.AddTestSweepers("NotebooksInstance", &resource.Sweeper{ + Name: "NotebooksInstance", + F: testSweepNotebooksInstance, + }) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepNotebooksInstance(region string) error { + resourceName := "NotebooksInstance" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := getTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://notebooks.googleapis.com/v1/projects/{{project}}/locations/{{location}}/instances", "?")[0] + listUrl, err := replaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := sendRequest(config, "GET", config.Project, listUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["instances"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + if obj["name"] == nil { + log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName) + return nil + } + + name := GetResourceNameFromSelfLink(obj["name"].(string)) + // Skip resources that shouldn't be sweeped + if !isSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://notebooks.googleapis.com/v1/projects/{{project}}/locations/{{location}}/instances/{{name}}" + deleteUrl, err := replaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/google/resource_notebooks_location.go b/google/resource_notebooks_location.go new file mode 100644 index 00000000000..9bcfdf2ae1c --- /dev/null +++ b/google/resource_notebooks_location.go @@ -0,0 +1,309 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceNotebooksLocation() *schema.Resource { + return &schema.Resource{ + Create: resourceNotebooksLocationCreate, + Read: resourceNotebooksLocationRead, + Update: resourceNotebooksLocationUpdate, + Delete: resourceNotebooksLocationDelete, + + Importer: &schema.ResourceImporter{ + State: resourceNotebooksLocationImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(4 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Minute), + Delete: schema.DefaultTimeout(4 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Description: `Name of the Location resource.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "self_link": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceNotebooksLocationCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + nameProp, err := expandNotebooksLocationName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Location: %#v", obj) + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Location: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Location: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = notebooksOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating Location", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + return fmt.Errorf("Error waiting to create Location: %s", err) + } + + if err := d.Set("name", flattenNotebooksLocationName(opRes["name"], d, config)); err != nil { + return err + } + + // This may have caused the ID to update - update it if so. + id, err = replaceVars(d, config, "projects/{{project}}/locations/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating Location %q: %#v", d.Id(), res) + + return resourceNotebooksLocationRead(d, meta) +} + +func resourceNotebooksLocationRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Location: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "GET", billingProject, url, userAgent, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("NotebooksLocation %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Location: %s", err) + } + + if err := d.Set("name", flattenNotebooksLocationName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading Location: %s", err) + } + if err := d.Set("self_link", ConvertSelfLinkToV1(res["selfLink"].(string))); err != nil { + return fmt.Errorf("Error reading Location: %s", err) + } + + return nil +} + +func resourceNotebooksLocationUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Location: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + nameProp, err := expandNotebooksLocationName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Location %q: %#v", d.Id(), obj) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "PUT", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Location %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Location %q: %#v", d.Id(), res) + } + + err = notebooksOperationWaitTime( + config, res, project, "Updating Location", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + + return resourceNotebooksLocationRead(d, meta) +} + +func resourceNotebooksLocationDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Location: %s", err) + } + billingProject = project + + url, err := replaceVars(d, config, "{{NotebooksBasePath}}projects/{{project}}/locations/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Location %q", d.Id()) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "DELETE", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Location") + } + + err = notebooksOperationWaitTime( + config, res, project, "Deleting Location", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting Location %q: %#v", d.Id(), res) + return nil +} + +func resourceNotebooksLocationImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/locations/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenNotebooksLocationName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + return NameFromSelfLinkStateFunc(v) +} + +func expandNotebooksLocationName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google/resource_notebooks_location_sweeper_test.go b/google/resource_notebooks_location_sweeper_test.go new file mode 100644 index 00000000000..6b9d5ec86d6 --- /dev/null +++ b/google/resource_notebooks_location_sweeper_test.go @@ -0,0 +1,124 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func init() { + resource.AddTestSweepers("NotebooksLocation", &resource.Sweeper{ + Name: "NotebooksLocation", + F: testSweepNotebooksLocation, + }) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepNotebooksLocation(region string) error { + resourceName := "NotebooksLocation" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := getTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://notebooks.googleapis.com/v1/projects/{{project}}/locations", "?")[0] + listUrl, err := replaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := sendRequest(config, "GET", config.Project, listUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["items"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + if obj["name"] == nil { + log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName) + return nil + } + + name := GetResourceNameFromSelfLink(obj["name"].(string)) + // Skip resources that shouldn't be sweeped + if !isSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://notebooks.googleapis.com/v1/projects/{{project}}/locations/{{name}}" + deleteUrl, err := replaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/website/docs/r/notebooks_environment.html.markdown b/website/docs/r/notebooks_environment.html.markdown index 638b747ffda..67a44e9ab24 100644 --- a/website/docs/r/notebooks_environment.html.markdown +++ b/website/docs/r/notebooks_environment.html.markdown @@ -24,8 +24,6 @@ description: |- A Cloud AI Platform Notebook environment. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. To get more information about Environment, see: diff --git a/website/docs/r/notebooks_instance.html.markdown b/website/docs/r/notebooks_instance.html.markdown index e21f0efd795..6fe913a507c 100644 --- a/website/docs/r/notebooks_instance.html.markdown +++ b/website/docs/r/notebooks_instance.html.markdown @@ -29,8 +29,6 @@ A Cloud AI Platform Notebook instance. in this resource do not properly detect drift. These fields will also not appear in state once imported. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. To get more information about Instance, see: @@ -48,7 +46,6 @@ To get more information about Instance, see: ```hcl resource "google_notebooks_instance" "instance" { - provider = google-beta name = "notebooks-instance" location = "us-west1-a" machine_type = "e2-medium" @@ -68,7 +65,6 @@ resource "google_notebooks_instance" "instance" { ```hcl resource "google_notebooks_instance" "instance" { - provider = google-beta name = "notebooks-instance" location = "us-west1-a" machine_type = "e2-medium" @@ -92,7 +88,6 @@ resource "google_notebooks_instance" "instance" { ```hcl resource "google_notebooks_instance" "instance" { - provider = google-beta name = "notebooks-instance" location = "us-west1-a" machine_type = "n1-standard-1" // can't be e2 because of accelerator @@ -118,7 +113,6 @@ resource "google_notebooks_instance" "instance" { ```hcl resource "google_notebooks_instance" "instance" { - provider = google-beta name = "notebooks-instance" location = "us-central1-a" machine_type = "e2-medium" diff --git a/website/docs/r/notebooks_instance_iam.html.markdown b/website/docs/r/notebooks_instance_iam.html.markdown index 88b8b4fc530..a00ade30b40 100644 --- a/website/docs/r/notebooks_instance_iam.html.markdown +++ b/website/docs/r/notebooks_instance_iam.html.markdown @@ -31,8 +31,6 @@ Three different resources help you manage your IAM policy for Cloud AI Notebooks ~> **Note:** `google_notebooks_instance_iam_binding` resources **can be** used in conjunction with `google_notebooks_instance_iam_member` resources **only if** they do not grant privilege to the same role. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. ## google\_notebooks\_instance\_iam\_policy diff --git a/website/docs/r/notebooks_location.html.markdown b/website/docs/r/notebooks_location.html.markdown index 8290e8ce6ee..d58377564c2 100644 --- a/website/docs/r/notebooks_location.html.markdown +++ b/website/docs/r/notebooks_location.html.markdown @@ -24,8 +24,6 @@ description: |- Represents a Location resource. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. ## Argument Reference