diff --git a/mmv1/products/compute/Image.yaml b/mmv1/products/compute/Image.yaml index 1be8f03deefa..fae50465cd4e 100644 --- a/mmv1/products/compute/Image.yaml +++ b/mmv1/products/compute/Image.yaml @@ -186,6 +186,26 @@ properties: description: Labels to apply to this Image. update_url: 'projects/{{project}}/global/images/{{name}}/setLabels' update_verb: 'POST' + - name: "params" + type: NestedObject + ignore_read: true + immutable: true + description: | + Additional params passed with the request, but not persisted as part of resource payload. + properties: + - name: "resourceManagerTags" + type: KeyValuePairs + description: | + Resource manager tags to be bound to the image. Tag keys and values have the + same definition as resource manager tags. Keys and values can be either in numeric format, + such as tagKeys/{tag_key_id} and tagValues/{tag_value_id} or in namespaced format such as + {org_id|projectId}/{tag_key_short_name} and {tag_value_short_name}. The field is ignored when empty. + The field is immutable and causes resource replacement when mutated. This field is only + set at create time and modifying this field after creation will trigger recreation. + To apply tags to an existing resource, see the google_tags_tag_binding resource. + api_name: "resourceManagerTags" + ignore_read: true + immutable: true - name: 'labelFingerprint' type: Fingerprint description: | diff --git a/mmv1/products/compute/StoragePool.yaml b/mmv1/products/compute/StoragePool.yaml index 71353ca799db..5cf7e70391e9 100644 --- a/mmv1/products/compute/StoragePool.yaml +++ b/mmv1/products/compute/StoragePool.yaml @@ -29,7 +29,7 @@ update_verb: "PATCH" update_mask: false autogen_async: true async: - type: 'OpAsync' + type: "OpAsync" operation: base_url: "{{op_id}}" iam_policy: @@ -45,14 +45,14 @@ examples: vars: storage_pool_name: "storage-pool-basic" ignore_read_extra: - - 'deletion_protection' + - "deletion_protection" exclude_test: true - name: "compute_storage_pool_full" primary_resource_id: "test-storage-pool-full" vars: storage_pool_name: "storage-pool-full" ignore_read_extra: - - 'deletion_protection' + - "deletion_protection" exclude_test: true parameters: - name: "zone" @@ -194,9 +194,9 @@ properties: * `hyperdisk-throughput` required: true immutable: true - custom_expand: 'templates/terraform/custom_expand/resourceref_with_validation.go.tmpl' - resource: 'StoragePoolType' - imports: 'selfLink' + custom_expand: "templates/terraform/custom_expand/resourceref_with_validation.go.tmpl" + resource: "StoragePoolType" + imports: "selfLink" - name: "status" type: NestedObject description: | @@ -279,6 +279,26 @@ properties: type: KeyValueLabels description: | Labels to apply to this storage pool. These can be later modified by the setLabels method. + - name: "params" + type: NestedObject + ignore_read: true + immutable: true + description: | + Additional params passed with the request, but not persisted as part of resource payload + properties: + - name: "resourceManagerTags" + type: KeyValuePairs + description: | + Resource manager tags to be bound to the storage pool. Tag keys and values have the + same definition as resource manager tags. Keys and values can be either in numeric format, + such as tagKeys/{tag_key_id} and tagValues/{tag_value_id} or in namespaced format such as + {org_id|projectId}/{tag_key_short_name} and {tag_value_short_name}. The field is ignored when empty. + The field is immutable and causes resource replacement when mutated. This field is only + set at create time and modifying this field after creation will trigger recreation. + To apply tags to an existing resource, see the google_tags_tag_binding resource. + api_name: "resourceManagerTags" + ignore_read: true + immutable: true virtual_fields: - name: "deletion_protection" type: Boolean diff --git a/mmv1/third_party/terraform/acctest/resource_test_utils.go b/mmv1/third_party/terraform/acctest/resource_test_utils.go index 3fb077176c18..0a6b24971108 100644 --- a/mmv1/third_party/terraform/acctest/resource_test_utils.go +++ b/mmv1/third_party/terraform/acctest/resource_test_utils.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "net/url" "os" "slices" + "strings" "testing" "time" @@ -126,6 +128,145 @@ func BuildIAMImportId(name, role, member, condition string) string { return ret } +// TagBindingCheckConfig configures a CheckTagBindings assertion. +// BuildParent must return the full resource name used as the tagBindings parent. +// If GetLocation is nil, the global tagBindings endpoint is used. +// If GetLocation is set, the location-scoped endpoint is used. +type TagBindingCheckConfig struct { + ResourceName string + ExpectedTagValueResources []string + UnexpectedTagValueResources []string + BuildParent func(rs *terraform.ResourceState) (string, error) + GetLocation func(rs *terraform.ResourceState) (string, error) +} + +// CheckTagBindings verifies that the target resource has the expected tag value +// bindings and does not have the unexpected ones. +func CheckTagBindings(t *testing.T, cfg TagBindingCheckConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + if cfg.ResourceName == "" { + return fmt.Errorf("resource name must be set for CheckTagBindings") + } + if cfg.BuildParent == nil { + return fmt.Errorf("BuildParent must be set for CheckTagBindings on resource %s", cfg.ResourceName) + } + + rs, err := getResourceState(s, cfg.ResourceName) + if err != nil { + return err + } + + expectedTagValues := make([]string, 0, len(cfg.ExpectedTagValueResources)) + for _, resourceName := range cfg.ExpectedTagValueResources { + tagValueID, err := getResourceID(s, resourceName) + if err != nil { + return err + } + expectedTagValues = append(expectedTagValues, tagValueID) + } + + unexpectedTagValues := make([]string, 0, len(cfg.UnexpectedTagValueResources)) + for _, resourceName := range cfg.UnexpectedTagValueResources { + tagValueID, err := getResourceID(s, resourceName) + if err != nil { + return err + } + unexpectedTagValues = append(unexpectedTagValues, tagValueID) + } + + parent, err := cfg.BuildParent(rs) + if err != nil { + return err + } + + config := GoogleProviderConfig(t) + basePath := config.TagsBasePath + if cfg.GetLocation != nil { + location, err := cfg.GetLocation(rs) + if err != nil { + return err + } + basePath = strings.Replace(config.TagsLocationBasePath, "{{location}}", location, 1) + } + + listBindingsURL := fmt.Sprintf("%stagBindings/?parent=%s&pageSize=300", basePath, url.QueryEscape(parent)) + resp, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: listBindingsURL, + UserAgent: config.UserAgent, + }) + if err != nil { + return fmt.Errorf("error calling tagBindings API for resource %s: %v", rs.Primary.ID, err) + } + + tagBindingsVal, exists := resp["tagBindings"] + if !exists { + tagBindingsVal = []interface{}{} + } + + tagBindings, ok := tagBindingsVal.([]interface{}) + if !ok { + return fmt.Errorf("'tagBindings' is not a slice in response for resource %s. response: %v", rs.Primary.ID, resp) + } + + foundExpected := make(map[string]bool, len(expectedTagValues)) + foundUnexpected := make(map[string]bool, len(unexpectedTagValues)) + + for _, binding := range tagBindings { + bindingMap, ok := binding.(map[string]interface{}) + if !ok { + continue + } + + tagValue, _ := bindingMap["tagValue"].(string) + for _, expectedTagValue := range expectedTagValues { + if tagValue == expectedTagValue { + foundExpected[expectedTagValue] = true + } + } + for _, unexpectedTagValue := range unexpectedTagValues { + if tagValue == unexpectedTagValue { + foundUnexpected[unexpectedTagValue] = true + } + } + } + + for _, expectedTagValue := range expectedTagValues { + if !foundExpected[expectedTagValue] { + return fmt.Errorf("expected tag value %s not found in tag bindings for resource %s. bindings: %v", expectedTagValue, rs.Primary.ID, tagBindings) + } + } + + for _, unexpectedTagValue := range unexpectedTagValues { + if foundUnexpected[unexpectedTagValue] { + return fmt.Errorf("unexpected tag value %s found in tag bindings for resource %s. bindings: %v", unexpectedTagValue, rs.Primary.ID, tagBindings) + } + } + + return nil + } +} + +func getResourceState(s *terraform.State, resourceName string) (*terraform.ResourceState, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("terraform resource not found: %s", resourceName) + } + return rs, nil +} + +func getResourceID(s *terraform.State, resourceName string) (string, error) { + rs, err := getResourceState(s, resourceName) + if err != nil { + return "", err + } + if rs.Primary.ID == "" { + return "", fmt.Errorf("terraform resource %s has no id", resourceName) + } + return rs.Primary.ID, nil +} + // testStringValue returns string values from string pointers, handling nil pointers. func testStringValue(sPtr *string) string { if sPtr == nil { diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_image_test.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_image_test.go.tmpl index 62584db726cb..3212bd2bfb89 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_image_test.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_image_test.go.tmpl @@ -5,10 +5,12 @@ import ( "testing" "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" tpgcompute "github.com/hashicorp/terraform-provider-google/google/services/compute" "github.com/hashicorp/terraform-provider-google/google/tpgresource" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" {{ if eq $.TargetVersionName `ga` }} @@ -1086,3 +1088,153 @@ resource "google_compute_image" "image" { } `, kmsRingName, kmsKeyName, suffix, suffix) } + +func TestAccComputeImage_resourceManagerTags(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_id": envvar.GetTestProjectFromEnv(), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckComputeImageDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeImage_resourceManagerTags(context), + Check: resource.ComposeTestCheckFunc( + acctest.CheckTagBindings(t, testAccComputeImageTagBindingCheckConfig( + "google_compute_image.image_with_resource_manager_tags", + []string{"google_tags_tag_value.tag_value_1"}, + nil, + )), + ), + }, + { + ResourceName: "google_compute_image.image_with_resource_manager_tags", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"params", "raw_disk"}, + }, + { + Config: testAccComputeImage_resourceManagerTagsUpdated(context), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("google_compute_image.image_with_resource_manager_tags", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeTestCheckFunc( + acctest.CheckTagBindings(t, testAccComputeImageTagBindingCheckConfig( + "google_compute_image.image_with_resource_manager_tags", + []string{"google_tags_tag_value.tag_value_2"}, + []string{"google_tags_tag_value.tag_value_1"}, + )), + ), + }, + { + ResourceName: "google_compute_image.image_with_resource_manager_tags", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"params", "raw_disk"}, + }, + }, + }) +} + +func testAccComputeImage_resourceManagerTags(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_tags_tag_key" "tag_key" { + parent = "projects/%{project_id}" + short_name = "image-tag-%{random_suffix}" + description = "Tag key for image acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_1" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-one-%{random_suffix}" + description = "First tag value for image acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_2" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-two-%{random_suffix}" + description = "Second tag value for image acceptance tests" + + # Serialize value creation for stable VCR recordings. + depends_on = [google_tags_tag_value.tag_value_1] +} + +resource "google_compute_image" "image_with_resource_manager_tags" { + name = "tf-test-image-rmt%{random_suffix}" + + raw_disk { + source = "https://storage.googleapis.com/bosh-gce-raw-stemcells/bosh-stemcell-97.98-google-kvm-ubuntu-xenial-go_agent-raw-1557960142.tar.gz" + } + + params { + resource_manager_tags = { + (google_tags_tag_key.tag_key.id) = google_tags_tag_value.tag_value_1.id + } + } +} +`, context) +} + +func testAccComputeImage_resourceManagerTagsUpdated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_tags_tag_key" "tag_key" { + parent = "projects/%{project_id}" + short_name = "image-tag-%{random_suffix}" + description = "Tag key for image acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_1" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-one-%{random_suffix}" + description = "First tag value for image acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_2" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-two-%{random_suffix}" + description = "Second tag value for image acceptance tests" + + # Serialize value creation for stable VCR recordings. + depends_on = [google_tags_tag_value.tag_value_1] +} + +resource "google_compute_image" "image_with_resource_manager_tags" { + name = "tf-test-image-rmt%{random_suffix}" + + raw_disk { + source = "https://storage.googleapis.com/bosh-gce-raw-stemcells/bosh-stemcell-97.98-google-kvm-ubuntu-xenial-go_agent-raw-1557960142.tar.gz" + } + + params { + resource_manager_tags = { + (google_tags_tag_key.tag_key.id) = google_tags_tag_value.tag_value_2.id + } + } +} +`, context) +} + +func testAccComputeImageTagBindingCheckConfig(resourceName string, expectedTagValueResources, unexpectedTagValueResources []string) acctest.TagBindingCheckConfig { + return acctest.TagBindingCheckConfig{ + ResourceName: resourceName, + ExpectedTagValueResources: expectedTagValueResources, + UnexpectedTagValueResources: unexpectedTagValueResources, + BuildParent: func(rs *terraform.ResourceState) (string, error) { + project := rs.Primary.Attributes["project"] + id := rs.Primary.Attributes["id"] + + if project == "" || id == "" { + return "", fmt.Errorf("expected project and id to be set for %s. got project=%q id=%q", resourceName, project, id) + } + + return fmt.Sprintf("//compute.googleapis.com/projects/%s/global/images/%s", project, id), nil + }, + } +} diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_storage_pool_test.go b/mmv1/third_party/terraform/services/compute/resource_compute_storage_pool_test.go index f507485e208e..b17c4d218aa9 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_storage_pool_test.go +++ b/mmv1/third_party/terraform/services/compute/resource_compute_storage_pool_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) @@ -220,3 +221,217 @@ func testAccCheckComputeStoragePoolDestroyProducer(t *testing.T) func(s *terrafo return nil } } + +func TestAccComputeStoragePool_resourceManagerTags(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_id": envvar.GetTestProjectFromEnv(), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckComputeStoragePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeStoragePool_resourceManagerTags(context), + Check: resource.ComposeTestCheckFunc( + acctest.CheckTagBindings(t, testAccComputeStoragePoolTagBindingCheckConfig( + t, + "google_compute_storage_pool.test-storage-pool-with-resource-manager-tags", + []string{"google_tags_tag_value.tag_value_1"}, + nil, + )), + ), + }, + { + ResourceName: "google_compute_storage_pool.test-storage-pool-with-resource-manager-tags", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "params", "zone"}, + }, + { + Config: testAccComputeStoragePool_resourceManagerTagsUpdated(context), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("google_compute_storage_pool.test-storage-pool-with-resource-manager-tags", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeTestCheckFunc( + acctest.CheckTagBindings(t, testAccComputeStoragePoolTagBindingCheckConfig( + t, + "google_compute_storage_pool.test-storage-pool-with-resource-manager-tags", + []string{"google_tags_tag_value.tag_value_2"}, + []string{"google_tags_tag_value.tag_value_1"}, + )), + ), + }, + { + ResourceName: "google_compute_storage_pool.test-storage-pool-with-resource-manager-tags", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "params", "zone"}, + }, + }, + }) +} + +func testAccComputeStoragePool_resourceManagerTags(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_tags_tag_key" "tag_key" { + parent = "projects/%{project_id}" + short_name = "storage-pool-tag-%{random_suffix}" + description = "Tag key for storage pool acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_1" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-one-%{random_suffix}" + description = "First tag value for storage pool acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_2" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-two-%{random_suffix}" + description = "Second tag value for storage pool acceptance tests" + + # Serialize value creation for stable VCR recordings. + depends_on = [google_tags_tag_value.tag_value_1] +} + +resource "google_compute_storage_pool" "test-storage-pool-with-resource-manager-tags" { + name = "tf-test-storage-pool-rmt%{random_suffix}" + + description = "Hyperdisk Balanced storage pool with resource manager tags" + + capacity_provisioning_type = "STANDARD" + pool_provisioned_capacity_gb = "10240" + performance_provisioning_type = "STANDARD" + pool_provisioned_iops = "10000" + pool_provisioned_throughput = "1024" + + storage_pool_type = data.google_compute_storage_pool_types.balanced.self_link + + zone = "us-central1-a" + + deletion_protection = false + + params { + resource_manager_tags = { + (google_tags_tag_key.tag_key.id) = google_tags_tag_value.tag_value_1.id + } + } +} + +data "google_project" "project" {} + +data "google_compute_storage_pool_types" "balanced" { + zone = "us-central1-a" + storage_pool_type = "hyperdisk-balanced" +} +`, context) +} + +func testAccComputeStoragePool_resourceManagerTagsUpdated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_tags_tag_key" "tag_key" { + parent = "projects/%{project_id}" + short_name = "storage-pool-tag-%{random_suffix}" + description = "Tag key for storage pool acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_1" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-one-%{random_suffix}" + description = "First tag value for storage pool acceptance tests" +} + +resource "google_tags_tag_value" "tag_value_2" { + parent = google_tags_tag_key.tag_key.id + short_name = "value-two-%{random_suffix}" + description = "Second tag value for storage pool acceptance tests" + + # Serialize value creation for stable VCR recordings. + depends_on = [google_tags_tag_value.tag_value_1] +} + +resource "google_compute_storage_pool" "test-storage-pool-with-resource-manager-tags" { + name = "tf-test-storage-pool-rmt%{random_suffix}" + + description = "Hyperdisk Balanced storage pool with resource manager tags" + + capacity_provisioning_type = "STANDARD" + pool_provisioned_capacity_gb = "10240" + performance_provisioning_type = "STANDARD" + pool_provisioned_iops = "10000" + pool_provisioned_throughput = "1024" + + storage_pool_type = data.google_compute_storage_pool_types.balanced.self_link + + zone = "us-central1-a" + + deletion_protection = false + + params { + resource_manager_tags = { + (google_tags_tag_key.tag_key.id) = google_tags_tag_value.tag_value_2.id + } + } +} + +data "google_project" "project" {} + +data "google_compute_storage_pool_types" "balanced" { + zone = "us-central1-a" + storage_pool_type = "hyperdisk-balanced" +} +`, context) +} + +func testAccComputeStoragePoolTagBindingCheckConfig(t *testing.T, resourceName string, expectedTagValueResources, unexpectedTagValueResources []string) acctest.TagBindingCheckConfig { + return acctest.TagBindingCheckConfig{ + ResourceName: resourceName, + ExpectedTagValueResources: expectedTagValueResources, + UnexpectedTagValueResources: unexpectedTagValueResources, + BuildParent: func(rs *terraform.ResourceState) (string, error) { + project := rs.Primary.Attributes["project"] + zone := rs.Primary.Attributes["zone"] + name := rs.Primary.Attributes["name"] + if project == "" || zone == "" || name == "" { + return "", fmt.Errorf("expected project, zone, and name to be set for %s. got project=%q zone=%q name=%q", resourceName, project, zone, name) + } + + config := acctest.GoogleProviderConfig(t) + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{ComputeBasePath}}projects/{{project}}/zones/{{zone}}/storagePools/{{name}}") + if err != nil { + return "", err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: url, + UserAgent: config.UserAgent, + }) + if err != nil { + return "", err + } + + poolID, ok := res["id"].(string) + if !ok || poolID == "" { + return "", fmt.Errorf("expected id to be set for %s. got id=%q", resourceName, res["id"]) + + } + + return fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/storagePools/%s", project, zone, poolID), nil + }, + GetLocation: func(rs *terraform.ResourceState) (string, error) { + zone := rs.Primary.Attributes["zone"] + if zone == "" { + return "", fmt.Errorf("expected zone to be set for %s", resourceName) + } + return zone, nil + }, + } +}