From d359eb8557bdb3ad003a5d9413220a8cede69514 Mon Sep 17 00:00:00 2001 From: Kautik Khokhar Date: Mon, 6 Jan 2025 12:17:50 +0530 Subject: [PATCH 1/3] Add storage_project_management_hub resource. --- .../storage/ProjectManagementHub.yaml | 165 +++++++++++++++ .../constants/storage_management_hub.go.tmpl | 3 + .../management_hub_custom_create.go.tmpl | 0 .../storage_project_management_hub.go.tmpl | 76 +++++++ .../pre_create/storage_management_hub.go.tmpl | 3 + ...rce_storage_project_management_hub_test.go | 189 ++++++++++++++++++ 6 files changed, 436 insertions(+) create mode 100644 mmv1/products/storage/ProjectManagementHub.yaml create mode 100644 mmv1/templates/terraform/constants/storage_management_hub.go.tmpl create mode 100644 mmv1/templates/terraform/custom_create/management_hub_custom_create.go.tmpl create mode 100644 mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl create mode 100644 mmv1/templates/terraform/pre_create/storage_management_hub.go.tmpl create mode 100644 mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go diff --git a/mmv1/products/storage/ProjectManagementHub.yaml b/mmv1/products/storage/ProjectManagementHub.yaml new file mode 100644 index 000000000000..00b4e80eae49 --- /dev/null +++ b/mmv1/products/storage/ProjectManagementHub.yaml @@ -0,0 +1,165 @@ +# Copyright 2024 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# API resource name +name: 'ProjectManagementHub' +kind: 'storage#managementhub' +# Resource description for the provider documentation. +description: | + RESOURCE_DESCRIPTION +references: + guides: + # Link to quickstart in the API's Guides section. For example: + # 'Create and connect to a database': 'https://cloud.google.com/alloydb/docs/quickstart/create-and-connect' + 'QUICKSTART_TITLE': 'QUICKSTART_URL' + # Link to the REST API reference for the resource. For example, + # https://cloud.google.com/alloydb/docs/reference/rest/v1/projects.locations.backups + api: 'API_REFERENCE_URL' +# Marks the resource as beta-only. Ensure a beta version block is present in +# provider.yaml. +# min_version: beta + +# URL for the resource's standard Get method. https://google.aip.dev/131 +# Terraform field names enclosed in double curly braces are replaced with +# the field values from the resource at runtime. +self_link: 'projects/{{name}}/locations/global/managementHub' + +# URL for the resource's standard Create method, including query parameters. +# https://google.aip.dev/133 +# Terraform field names enclosed in double curly braces are replaced with +# the field values from the resource at runtime. +custom_code: + pre_create: templates/terraform/constants/storage_management_hub.go.tmpl + pre_delete: templates/terraform/constants/storage_management_hub.go.tmpl + pre_read: templates/terraform/constants/storage_management_hub.go.tmpl + pre_update: templates/terraform/constants/storage_management_hub.go.tmpl +# Overrides the URL for the resource's standard Update method. (If unset, the +# self_link URL is used by default.) https://google.aip.dev/134 +# Terraform field names enclosed in double curly braces are replaced with +# the field values from the resource at runtime. +# update_url: 'projects/{{project}}/locations/{{location}}/resourcenames/{{name}}' +# The HTTP verb used to update a resource. Allowed values: :POST, :PUT, :PATCH. Default: :PUT. +update_verb: 'PATCH' +# If true, the resource sets an `updateMask` query parameter listing modified +# fields when updating the resource. If false, it does not. +update_mask: true + +create_url: 'projects/{{name}}/locations/global/managementHub?updateMask=editionConfig,filter' +create_verb: 'PATCH' + +exclude_delete: true + +import_format: + - 'projects/{{name}}/locations/global/managementHub' + +# If true, code for handling long-running operations is generated along with +# the resource. If false, that code is not generated. +autogen_async: false + +properties: + # Fields go here + - name: 'name' + type: String + required: true + immutable: true + url_param_only: true + description: | + NAME_DESCRIPTION + - name: 'editionConfig' + type: String + required: false + default_from_api: true + - name: 'updateTime' + type: String + output: true + - name: 'filter' + type: NestedObject + description: + properties: + - name: excludedCloudStorageBuckets + type: NestedObject + required: false + conflicts: + - 'filter.0.included_cloud_storage_buckets' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: cloudStorageBuckets + required: true + type: Array + item_type: + type: NestedObject + properties: + - name: bucketId + type: String + - name: bucketIdRegex + type: String + - name: includedCloudStorageBuckets + type: NestedObject + required: false + conflicts: + - 'filter.0.excluded_cloud_storage_buckets' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: cloudStorageBuckets + required: true + type: Array + item_type: + type: NestedObject + properties: + - name: bucketId + type: String + required: false + - name: bucketIdRegex + type: String + required: false + - name: excludedCloudStorageLocations + type: NestedObject + required: false + conflicts: + - 'filter.0.included_cloud_storage_locations' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: locations + type: Array + required: true + item_type: + type: String + - name: includedCloudStorageLocations + type: NestedObject + required: false + conflicts: + - 'filter.0.excluded_cloud_storage_locations' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: locations + type: Array + required: true + item_type: + type: String diff --git a/mmv1/templates/terraform/constants/storage_management_hub.go.tmpl b/mmv1/templates/terraform/constants/storage_management_hub.go.tmpl new file mode 100644 index 000000000000..77bd4fd26df9 --- /dev/null +++ b/mmv1/templates/terraform/constants/storage_management_hub.go.tmpl @@ -0,0 +1,3 @@ + +url = strings.ReplaceAll(url, "storage/v1", "v2") + diff --git a/mmv1/templates/terraform/custom_create/management_hub_custom_create.go.tmpl b/mmv1/templates/terraform/custom_create/management_hub_custom_create.go.tmpl new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl b/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl new file mode 100644 index 000000000000..ff093a945529 --- /dev/null +++ b/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl @@ -0,0 +1,76 @@ + + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + editionConfigProp, err := expandStorageManagementHubEditionConfig(d.Get("edition_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("edition_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(editionConfigProp)) && (ok || !reflect.DeepEqual(v, editionConfigProp)) { + obj["editionConfig"] = editionConfigProp + } + filterProp, err := expandStorageManagementHubFilter(d.Get("filter"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("filter"); !tpgresource.IsEmptyValue(reflect.ValueOf(filterProp)) && (ok || !reflect.DeepEqual(v, filterProp)) { + obj["filter"] = filterProp + } + + url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}StorageBasePath{{"}}"}}projects/{{"{{"}}name{{"}}"}}/locations/global/managementHub") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new ManagementHub: %#v", obj) + billingProject := "" + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + updateMask := []string{} + + if d.HasChange("edition_config") { + updateMask = append(updateMask, "editionConfig") + } + + if d.HasChange("filter") { + updateMask = append(updateMask, "filter") + } + // updateMask is a URL parameter but not present in the schema, so ReplaceVars + // won't set it + url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + + url = strings.ReplaceAll(url, "storage/v1", "v2") + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + }) + if err != nil { + return fmt.Errorf("Error creating ManagementHub: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "projects/{{"{{"}}name{{"}}"}}/locations/global/managementHub") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating ManagementHub %q: %#v", d.Id(), res) + + return resourceStorageManagementHubRead(d, meta) diff --git a/mmv1/templates/terraform/pre_create/storage_management_hub.go.tmpl b/mmv1/templates/terraform/pre_create/storage_management_hub.go.tmpl new file mode 100644 index 000000000000..77bd4fd26df9 --- /dev/null +++ b/mmv1/templates/terraform/pre_create/storage_management_hub.go.tmpl @@ -0,0 +1,3 @@ + +url = strings.ReplaceAll(url, "storage/v1", "v2") + diff --git a/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go b/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go new file mode 100644 index 000000000000..0cc01621bce9 --- /dev/null +++ b/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go @@ -0,0 +1,189 @@ +package storage_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/envvar" + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccStorageProjectManagementHub_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": envvar.GetTestProjectFromEnv(), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageProjectManagementHub_basic(context), + }, + { + ResourceName: "google_storage_project_management_hub.project_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageProjectManagementHub_update_with_filter(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.0.bucket_id", "random-test1"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.1.bucket_id_regex", "random-test-*"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.2.bucket_id", "random-test2"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.3.bucket_id_regex", "random-test2-*"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.included_cloud_storage_locations.0.locations.0", "us-east-1"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.included_cloud_storage_locations.0.locations.1", "us-east-2"), + ), + }, + { + ResourceName: "google_storage_project_management_hub.project_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageProjectManagementHub_update_with_filter2(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.0.bucket_id", "random-test1"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.1.bucket_id_regex", "random-test-*"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.2.bucket_id", "random-test2"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.3.bucket_id_regex", "random-test2-*"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.excluded_cloud_storage_locations.0.locations.0", "us-east-1"), + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "filter.0.excluded_cloud_storage_locations.0.locations.1", "us-east-2"), + ), + }, + { + ResourceName: "google_storage_project_management_hub.project_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageProjectManagementHub_update_mode_disable(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "edition_config", "DISABLED"), + ), + }, + { + ResourceName: "google_storage_project_management_hub.project_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageProjectManagementHub_update_mode_inherit(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_project_management_hub.project_management_hub", "edition_config", "INHERIT"), + ), + }, + { + ResourceName: "google_storage_project_management_hub.project_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + }, + }) +} + +func testAccStorageProjectManagementHub_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_project_management_hub" "project_management_hub" { + name = "%{project}" + edition_config = "STANDARD" +} +`, context) +} + +func testAccStorageProjectManagementHub_update_with_filter(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_project_management_hub" "project_management_hub" { + name = "%{project}" + edition_config = "STANDARD" + filter { + excluded_cloud_storage_buckets{ + cloud_storage_buckets { + bucket_id = "random-test1" + } + cloud_storage_buckets { + bucket_id_regex = "random-test-*" + } + cloud_storage_buckets { + bucket_id = "random-test2" + } + cloud_storage_buckets { + bucket_id_regex = "random-test2-*" + } + } + included_cloud_storage_locations{ + locations = ["us-east-1", "us-east-2"] + } + } +} +`, context) +} + +func testAccStorageProjectManagementHub_update_with_filter2(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_project_management_hub" "project_management_hub" { + name = "%{project}" + edition_config = "STANDARD" + filter { + included_cloud_storage_buckets{ + cloud_storage_buckets { + bucket_id = "random-test1" + } + cloud_storage_buckets { + bucket_id_regex = "random-test-*" + } + cloud_storage_buckets { + bucket_id = "random-test2" + } + cloud_storage_buckets { + bucket_id_regex = "random-test2-*" + } + } + excluded_cloud_storage_locations{ + locations = ["us-east-1", "us-east-2"] + } + } +} +`, context) +} + +func testAccStorageProjectManagementHub_update_mode_disable(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_project_management_hub" "project_management_hub" { + name = "%{project}" + edition_config = "DISABLED" +} +`, context) +} + +func testAccStorageProjectManagementHub_update_mode_inherit(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_project_management_hub" "project_management_hub" { + name = "%{project}" + edition_config = "INHERIT" +} +`, context) +} From 7fbab20e1130ab3c58d95eadc800fff872da1513 Mon Sep 17 00:00:00 2001 From: Kautik Khokhar Date: Mon, 6 Jan 2025 12:53:00 +0530 Subject: [PATCH 2/3] Fixes import error --- .../storage/ProjectManagementHub.yaml | 82 ++++++++++++++++--- .../storage_project_management_hub.go.tmpl | 18 ++-- ...rce_storage_project_management_hub_test.go | 2 +- 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/mmv1/products/storage/ProjectManagementHub.yaml b/mmv1/products/storage/ProjectManagementHub.yaml index 00b4e80eae49..051941e78c74 100644 --- a/mmv1/products/storage/ProjectManagementHub.yaml +++ b/mmv1/products/storage/ProjectManagementHub.yaml @@ -17,15 +17,21 @@ name: 'ProjectManagementHub' kind: 'storage#managementhub' # Resource description for the provider documentation. description: | - RESOURCE_DESCRIPTION -references: - guides: - # Link to quickstart in the API's Guides section. For example: - # 'Create and connect to a database': 'https://cloud.google.com/alloydb/docs/quickstart/create-and-connect' - 'QUICKSTART_TITLE': 'QUICKSTART_URL' - # Link to the REST API reference for the resource. For example, - # https://cloud.google.com/alloydb/docs/reference/rest/v1/projects.locations.backups - api: 'API_REFERENCE_URL' + The Project Management Hub resource represents GCS Management Hub operating on + individual GCP project. Management Hub is a singleton resource and individual + instance exists on each GCP project. + + Management Hub is for Storage Admins to manage GCP storage assets at scale for + performance, cost, security & compliance. + +docs: + warning: | + Management Hub is a singleton resource which cannot be created or + deleted. A single instance of Management Hub exist for each GCP Project. + Terraform does not create or destroy this resource. Terraform resource + creation for this resource is simply an update operation on existing + resource with specified properties. Terraform deletion won't have any effect + on this resource rather it will only remove it from the state file. # Marks the resource as beta-only. Ensure a beta version block is present in # provider.yaml. # min_version: beta @@ -44,6 +50,7 @@ custom_code: pre_delete: templates/terraform/constants/storage_management_hub.go.tmpl pre_read: templates/terraform/constants/storage_management_hub.go.tmpl pre_update: templates/terraform/constants/storage_management_hub.go.tmpl + custom_create: templates/terraform/custom_create/storage_project_management_hub.go.tmpl # Overrides the URL for the resource's standard Update method. (If unset, the # self_link URL is used by default.) https://google.aip.dev/134 # Terraform field names enclosed in double curly braces are replaced with @@ -75,21 +82,32 @@ properties: immutable: true url_param_only: true description: | - NAME_DESCRIPTION + Identifier of the GCP project. For GCP project, this field can be project + name or project number. - name: 'editionConfig' type: String required: false default_from_api: true + description: | + Edition configuration of the Management Hub resource. Valid values are + INHERIT, DISABLED and STANDARD. - name: 'updateTime' type: String output: true + description: | + The time at which the Management Hub resource is last updated. - name: 'filter' type: NestedObject - description: + description: | + Filter over location and bucket using include or exclude semantics. + Resources that match the include or exclude filter are exclusively + included or excluded from the Management Hub plan. properties: - name: excludedCloudStorageBuckets type: NestedObject required: false + description: | + Buckets to exclude from the Management Hub plan. conflicts: - 'filter.0.included_cloud_storage_buckets' at_least_one_of: @@ -106,11 +124,21 @@ properties: properties: - name: bucketId type: String + description: | + Id of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. - name: bucketIdRegex type: String + description: | + ID regex of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. - name: includedCloudStorageBuckets type: NestedObject required: false + description: | + Buckets to include in the Management Hub plan. conflicts: - 'filter.0.excluded_cloud_storage_buckets' at_least_one_of: @@ -128,12 +156,22 @@ properties: - name: bucketId type: String required: false + description: | + Id of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. - name: bucketIdRegex type: String required: false + description: | + ID regex of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. - name: excludedCloudStorageLocations type: NestedObject required: false + description: | + Locations to exclude from the Management Hub plan. conflicts: - 'filter.0.included_cloud_storage_locations' at_least_one_of: @@ -145,11 +183,15 @@ properties: - name: locations type: Array required: true + description: | + List of locations. item_type: type: String - name: includedCloudStorageLocations type: NestedObject required: false + description: | + Locations to include in the Management Hub plan. conflicts: - 'filter.0.excluded_cloud_storage_locations' at_least_one_of: @@ -161,5 +203,23 @@ properties: - name: locations type: Array required: true + description: | + List of locations. item_type: type: String + - name: 'effectiveManagementHubEdition' + output: true + description: | + The Management Hub edition that is effective for the resource. + type: NestedObject + properties: + - name: managementHub + type: String + output: true + description: | + The Management Hub resource that is applied for the target resource. + - name: managementHubEdition + type: String + output: true + description: | + The `ManagementHub` edition that is applicable for the resource. diff --git a/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl b/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl index ff093a945529..f8ebcc7d71d2 100644 --- a/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl +++ b/mmv1/templates/terraform/custom_create/storage_project_management_hub.go.tmpl @@ -5,13 +5,13 @@ } obj := make(map[string]interface{}) - editionConfigProp, err := expandStorageManagementHubEditionConfig(d.Get("edition_config"), d, config) + editionConfigProp, err := expandStorageProjectManagementHubEditionConfig(d.Get("edition_config"), d, config) if err != nil { return err } else if v, ok := d.GetOkExists("edition_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(editionConfigProp)) && (ok || !reflect.DeepEqual(v, editionConfigProp)) { obj["editionConfig"] = editionConfigProp } - filterProp, err := expandStorageManagementHubFilter(d.Get("filter"), d, config) + filterProp, err := expandStorageProjectManagementHubFilter(d.Get("filter"), d, config) if err != nil { return err } else if v, ok := d.GetOkExists("filter"); !tpgresource.IsEmptyValue(reflect.ValueOf(filterProp)) && (ok || !reflect.DeepEqual(v, filterProp)) { @@ -23,7 +23,7 @@ return err } - log.Printf("[DEBUG] Creating new ManagementHub: %#v", obj) + log.Printf("[DEBUG] Patching ManagementHub: %#v", obj) billingProject := "" // err == nil indicates that the billing_project value was found @@ -32,15 +32,11 @@ } headers := make(http.Header) - updateMask := []string{} + updateMask := []string{"filter"} if d.HasChange("edition_config") { updateMask = append(updateMask, "editionConfig") } - - if d.HasChange("filter") { - updateMask = append(updateMask, "filter") - } // updateMask is a URL parameter but not present in the schema, so ReplaceVars // won't set it url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) @@ -61,7 +57,7 @@ Headers: headers, }) if err != nil { - return fmt.Errorf("Error creating ManagementHub: %s", err) + return fmt.Errorf("Error patching ManagementHub: %s", err) } // Store the ID now @@ -71,6 +67,6 @@ } d.SetId(id) - log.Printf("[DEBUG] Finished creating ManagementHub %q: %#v", d.Id(), res) + log.Printf("[DEBUG] Finished patching ManagementHub %q: %#v", d.Id(), res) - return resourceStorageManagementHubRead(d, meta) + return resourceStorageProjectManagementHubRead(d, meta) diff --git a/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go b/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go index 0cc01621bce9..48fcf9c6c665 100644 --- a/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go +++ b/mmv1/third_party/terraform/services/storage/resource_storage_project_management_hub_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-provider-google/envvar" "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" ) func TestAccStorageProjectManagementHub_update(t *testing.T) { From d098b03f4f5a5708fff969a986c0c3b27b4977ab Mon Sep 17 00:00:00 2001 From: Kautik Khokhar Date: Fri, 10 Jan 2025 12:28:38 +0530 Subject: [PATCH 3/3] Add storage_folder_management_hub resource. --- .../products/storage/FolderManagementHub.yaml | 222 +++++++++++++++ .../storage_folder_management_hub.go.tmpl | 72 +++++ ...urce_storage_folder_management_hub_test.go | 253 ++++++++++++++++++ 3 files changed, 547 insertions(+) create mode 100644 mmv1/products/storage/FolderManagementHub.yaml create mode 100644 mmv1/templates/terraform/custom_create/storage_folder_management_hub.go.tmpl create mode 100644 mmv1/third_party/terraform/services/storage/resource_storage_folder_management_hub_test.go diff --git a/mmv1/products/storage/FolderManagementHub.yaml b/mmv1/products/storage/FolderManagementHub.yaml new file mode 100644 index 000000000000..832f547b1208 --- /dev/null +++ b/mmv1/products/storage/FolderManagementHub.yaml @@ -0,0 +1,222 @@ +# Copyright 2024 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# API resource name +name: 'FolderManagementHub' +kind: 'storage#managementhub' +# Resource description for the provider documentation. +description: | + The Folder Management Hub resource represents GCS Management Hub operating on + individual GCP Folder. Management Hub is a singleton resource and individual + instance exists on each GCP project. + + Management Hub is for Storage Admins to manage GCP storage assets at scale for + performance, cost, security & compliance. + +docs: + warning: | + Management Hub is a singleton resource which cannot be created or + deleted. A single instance of Management Hub exist for each GCP Project. + Terraform does not create or destroy this resource. Terraform resource + creation for this resource is simply an update operation on existing + resource with specified properties. Terraform deletion won't have any effect + on this resource rather it will only remove it from the state file. +# Marks the resource as beta-only. Ensure a beta version block is present in +# provider.yaml. +# min_version: beta + +# URL for the resource's standard Get method. https://google.aip.dev/131 +# Terraform field names enclosed in double curly braces are replaced with +# the field values from the resource at runtime. +self_link: 'folders/{{name}}/locations/global/managementHub' + +# URL for the resource's standard Create method, including query parameters. +# https://google.aip.dev/133 +# Terraform field names enclosed in double curly braces are replaced with +# the field values from the resource at runtime. +custom_code: + pre_delete: templates/terraform/constants/storage_management_hub.go.tmpl + pre_read: templates/terraform/constants/storage_management_hub.go.tmpl + pre_update: templates/terraform/constants/storage_management_hub.go.tmpl + custom_create: templates/terraform/custom_create/storage_folder_management_hub.go.tmpl +# Overrides the URL for the resource's standard Update method. (If unset, the +# self_link URL is used by default.) https://google.aip.dev/134 +# Terraform field names enclosed in double curly braces are replaced with +# the field values from the resource at runtime. +# update_url: 'projects/{{project}}/locations/{{location}}/resourcenames/{{name}}' +# The HTTP verb used to update a resource. Allowed values: :POST, :PUT, :PATCH. Default: :PUT. +update_verb: 'PATCH' +# If true, the resource sets an `updateMask` query parameter listing modified +# fields when updating the resource. If false, it does not. +update_mask: true + +create_url: 'folders/{{name}}/locations/global/managementHub?updateMask=editionConfig,filter' +create_verb: 'PATCH' + +exclude_delete: true + +import_format: + - 'folders/{{name}}/locations/global/managementHub' + +# If true, code for handling long-running operations is generated along with +# the resource. If false, that code is not generated. +autogen_async: false + +properties: + # Fields go here + - name: 'name' + type: String + required: true + immutable: true + url_param_only: true + description: | + Identifier of the GCP folder. For GCP folder, It should be folder number. + - name: 'editionConfig' + type: String + required: true + description: | + Edition configuration of the Management Hub resource. Valid values are + INHERIT, DISABLED and STANDARD. + - name: 'updateTime' + type: String + output: true + description: | + The time at which the Management Hub resource is last updated. + - name: 'filter' + type: NestedObject + description: | + Filter over location and bucket using include or exclude semantics. + Resources that match the include or exclude filter are exclusively + included or excluded from the Management Hub plan. + properties: + - name: excludedCloudStorageBuckets + type: NestedObject + required: false + description: | + Buckets to exclude from the Management Hub plan. + conflicts: + - 'filter.0.included_cloud_storage_buckets' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: cloudStorageBuckets + required: true + type: Array + item_type: + type: NestedObject + properties: + - name: bucketId + type: String + description: | + Id of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. + - name: bucketIdRegex + type: String + description: | + ID regex of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. + - name: includedCloudStorageBuckets + type: NestedObject + required: false + description: | + Buckets to include in the Management Hub plan. + conflicts: + - 'filter.0.excluded_cloud_storage_buckets' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: cloudStorageBuckets + required: true + type: Array + item_type: + type: NestedObject + properties: + - name: bucketId + type: String + required: false + description: | + Id of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. + - name: bucketIdRegex + type: String + required: false + description: | + ID regex of the bucket. + Only one of the bucket_id and bucket_id_regex should be + specified. + - name: excludedCloudStorageLocations + type: NestedObject + required: false + description: | + Locations to exclude from the Management Hub plan. + conflicts: + - 'filter.0.included_cloud_storage_locations' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: locations + type: Array + required: true + description: | + List of locations. + item_type: + type: String + - name: includedCloudStorageLocations + type: NestedObject + required: false + description: | + Locations to include in the Management Hub plan. + conflicts: + - 'filter.0.excluded_cloud_storage_locations' + at_least_one_of: + - 'filter.0.included_cloud_storage_buckets' + - 'filter.0.excluded_cloud_storage_buckets' + - 'filter.0.included_cloud_storage_locations' + - 'filter.0.excluded_cloud_storage_locations' + properties: + - name: locations + type: Array + required: true + description: | + List of locations. + item_type: + type: String + - name: 'effectiveManagementHubEdition' + output: true + description: | + The Management Hub edition that is effective for the resource. + type: NestedObject + properties: + - name: managementHub + type: String + output: true + description: | + The Management Hub resource that is applied for the target resource. + - name: managementHubEdition + type: String + output: true + description: | + The `ManagementHub` edition that is applicable for the resource. diff --git a/mmv1/templates/terraform/custom_create/storage_folder_management_hub.go.tmpl b/mmv1/templates/terraform/custom_create/storage_folder_management_hub.go.tmpl new file mode 100644 index 000000000000..8652c00bf8a1 --- /dev/null +++ b/mmv1/templates/terraform/custom_create/storage_folder_management_hub.go.tmpl @@ -0,0 +1,72 @@ + + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + editionConfigProp, err := expandStorageFolderManagementHubEditionConfig(d.Get("edition_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("edition_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(editionConfigProp)) && (ok || !reflect.DeepEqual(v, editionConfigProp)) { + obj["editionConfig"] = editionConfigProp + } + filterProp, err := expandStorageFolderManagementHubFilter(d.Get("filter"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("filter"); !tpgresource.IsEmptyValue(reflect.ValueOf(filterProp)) && (ok || !reflect.DeepEqual(v, filterProp)) { + obj["filter"] = filterProp + } + + url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}StorageBasePath{{"}}"}}folders/{{"{{"}}name{{"}}"}}/locations/global/managementHub") + if err != nil { + return err + } + + log.Printf("[DEBUG] Patching ManagementHub: %#v", obj) + billingProject := "" + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + updateMask := []string{"filter"} + + if d.HasChange("edition_config") { + updateMask = append(updateMask, "editionConfig") + } + // updateMask is a URL parameter but not present in the schema, so ReplaceVars + // won't set it + url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + + url = strings.ReplaceAll(url, "storage/v1", "v2") + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + }) + if err != nil { + return fmt.Errorf("Error patching ManagementHub: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "folders/{{"{{"}}name{{"}}"}}/locations/global/managementHub") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished patching ManagementHub %q: %#v", d.Id(), res) + + return resourceStorageFolderManagementHubRead(d, meta) diff --git a/mmv1/third_party/terraform/services/storage/resource_storage_folder_management_hub_test.go b/mmv1/third_party/terraform/services/storage/resource_storage_folder_management_hub_test.go new file mode 100644 index 000000000000..5fab48f4377d --- /dev/null +++ b/mmv1/third_party/terraform/services/storage/resource_storage_folder_management_hub_test.go @@ -0,0 +1,253 @@ +package storage_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccStorageFolderManagementHub_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": envvar.GetTestProjectFromEnv(), + "org_id": envvar.GetTestOrgFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + Steps: []resource.TestStep{ + { + Config: testAccStorageFolderManagementHub_basic(context), + }, + { + ResourceName: "google_storage_folder_management_hub.folder_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageFolderManagementHub_update_with_filter(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.0.bucket_id", "random-test1"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.1.bucket_id_regex", "random-test-*"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.2.bucket_id", "random-test2"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.excluded_cloud_storage_buckets.0.cloud_storage_buckets.3.bucket_id_regex", "random-test2-*"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.included_cloud_storage_locations.0.locations.0", "us-east-1"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.included_cloud_storage_locations.0.locations.1", "us-east-2"), + ), + }, + { + ResourceName: "google_storage_folder_management_hub.folder_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageFolderManagementHub_update_with_filter2(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.0.bucket_id", "random-test1"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.1.bucket_id_regex", "random-test-*"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.2.bucket_id", "random-test2"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.included_cloud_storage_buckets.0.cloud_storage_buckets.3.bucket_id_regex", "random-test2-*"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.excluded_cloud_storage_locations.0.locations.0", "us-east-1"), + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "filter.0.excluded_cloud_storage_locations.0.locations.1", "us-east-2"), + ), + }, + { + ResourceName: "google_storage_folder_management_hub.folder_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageFolderManagementHub_update_mode_disable(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "edition_config", "DISABLED"), + ), + }, + { + ResourceName: "google_storage_folder_management_hub.folder_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + { + Config: testAccStorageFolderManagementHub_update_mode_inherit(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_storage_folder_management_hub.folder_management_hub", "edition_config", "INHERIT"), + ), + }, + { + ResourceName: "google_storage_folder_management_hub.folder_management_hub", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name"}, + }, + }, + }) +} + +func testAccStorageFolderManagementHub_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" + deletion_protection=false +} + +resource "time_sleep" "wait_120_seconds" { + depends_on = [google_folder.folder] + create_duration = "120s" +} + +resource "google_storage_folder_management_hub" "folder_management_hub" { + name = google_folder.folder.folder_id + edition_config = "STANDARD" + depends_on = [time_sleep.wait_120_seconds] +} +`, context) +} + +func testAccStorageFolderManagementHub_update_with_filter(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" + deletion_protection=false +} + +resource "time_sleep" "wait_120_seconds" { + depends_on = [google_folder.folder] + create_duration = "120s" +} + +resource "google_storage_folder_management_hub" "folder_management_hub" { + name = google_folder.folder.folder_id + edition_config = "STANDARD" + filter { + excluded_cloud_storage_buckets{ + cloud_storage_buckets { + bucket_id = "random-test1" + } + cloud_storage_buckets { + bucket_id_regex = "random-test-*" + } + cloud_storage_buckets { + bucket_id = "random-test2" + } + cloud_storage_buckets { + bucket_id_regex = "random-test2-*" + } + } + included_cloud_storage_locations{ + locations = ["us-east-1", "us-east-2"] + } + } + depends_on = [time_sleep.wait_120_seconds] +} +`, context) +} + +func testAccStorageFolderManagementHub_update_with_filter2(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" + deletion_protection=false +} + +resource "time_sleep" "wait_120_seconds" { + depends_on = [google_folder.folder] + create_duration = "120s" +} + +resource "google_storage_folder_management_hub" "folder_management_hub" { + name = google_folder.folder.folder_id + edition_config = "STANDARD" + filter { + included_cloud_storage_buckets{ + cloud_storage_buckets { + bucket_id = "random-test1" + } + cloud_storage_buckets { + bucket_id_regex = "random-test-*" + } + cloud_storage_buckets { + bucket_id = "random-test2" + } + cloud_storage_buckets { + bucket_id_regex = "random-test2-*" + } + } + excluded_cloud_storage_locations{ + locations = ["us-east-1", "us-east-2"] + } + } + depends_on = [time_sleep.wait_120_seconds] +} +`, context) +} + +func testAccStorageFolderManagementHub_update_mode_disable(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" + deletion_protection=false +} + +resource "time_sleep" "wait_120_seconds" { + depends_on = [google_folder.folder] + create_duration = "120s" +} + +resource "google_storage_folder_management_hub" "folder_management_hub" { + name = google_folder.folder.folder_id + edition_config = "DISABLED" + depends_on = [time_sleep.wait_120_seconds] +} +`, context) +} + +func testAccStorageFolderManagementHub_update_mode_inherit(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" + deletion_protection=false +} + +resource "time_sleep" "wait_120_seconds" { + depends_on = [google_folder.folder] + create_duration = "120s" +} + +resource "google_storage_folder_management_hub" "folder_management_hub" { + name = google_folder.folder.folder_id + edition_config = "INHERIT" + depends_on = [time_sleep.wait_120_seconds] +} +`, context) +}