Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mmv1/products/spanner/Instance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ properties:
description: |
If true, disables total CPU autoscaling for this replica and ignores
total_cpu_utilization_percent in the top-level autoscaling configuration.
- name: 'tags'
type: KeyValuePairs
description: |
A map of resource manager tags.
Resource manager tag keys and values have the same definition as resource manager tags.
Keys must be in the format tagKeys/{tag_key_id}, and values are in the format tagValues/{tag_value_id}.
immutable: true
ignore_read: true
- name: edition
type: Enum
description: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ package spanner_test

import (
"fmt"
"net/url"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
_ "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager"
_ "github.com/hashicorp/terraform-provider-google/google/services/spanner"
"github.com/hashicorp/terraform-provider-google/google/services/tags"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

// Acceptance Tests
Expand Down Expand Up @@ -39,6 +44,41 @@ func TestAccSpannerInstance_basic(t *testing.T) {
})
}

func TestAccSpannerInstance_tags(t *testing.T) {
t.Parallel()

tagKey := tags.BootstrapSharedTestOrganizationTagKey(t, "spanner-instance-tagkey", map[string]interface{}{})

context := map[string]interface{}{
"project": envvar.GetTestProjectFromEnv(),
"org": envvar.GetTestOrgFromEnv(t),
"random_suffix": acctest.RandString(t, 10),
"tagKey": tagKey,
"tagValue": tags.BootstrapSharedTestOrganizationTagValue(t, "spanner-instance-tagvalue", tagKey),
}

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckSpannerInstanceDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccSpannerInstance_tags(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("google_spanner_instance.tags", "tags.%"),
testAccCheckSpannerInstanceHasTagBindings(t),
),
},
{
ResourceName: "google_spanner_instance.tags",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"labels", "terraform_labels", "tags"},
},
},
})
}

func TestAccSpannerInstance_basicUpdateWithProviderDefaultLabels(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -1098,3 +1138,136 @@ resource "google_spanner_instance" "test" {
edition = "ENTERPRISE_PLUS"
}`, name, name)
}

func testAccSpannerInstance_tags(context map[string]interface{}) string {
return acctest.Nprintf(`
resource "google_spanner_instance" "tags" {
name = "tf-test-%{random_suffix}"
config = "regional-us-central1"
display_name = "tf-test-%{random_suffix}"

processing_units = 100
edition = "ENTERPRISE"

tags = {
"%{org}/%{tagKey}" = "%{tagValue}"
}
}
`, context)
}

func testAccCheckSpannerInstanceHasTagBindings(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_spanner_instance" {
continue
}
if strings.HasPrefix(name, "data.") {
continue
}

config := acctest.GoogleProviderConfig(t)

// 1. Get the configured tag key and value from the state.
var configuredTagValueNamespacedName string
var tagKeyNamespacedName, tagValueShortName string
for key, val := range rs.Primary.Attributes {
if strings.HasPrefix(key, "tags.") && key != "tags.%" {
tagKeyNamespacedName = strings.TrimPrefix(key, "tags.")
tagValueShortName = val
if tagValueShortName != "" {
configuredTagValueNamespacedName = fmt.Sprintf("%s/%s", tagKeyNamespacedName, tagValueShortName)
break
}
}
}

if configuredTagValueNamespacedName == "" {
return fmt.Errorf("could not find a configured tag value in the state for resource %s", rs.Primary.ID)
}

// Check if placeholders are still present.
if strings.Contains(configuredTagValueNamespacedName, "%{") {
return fmt.Errorf("tag namespaced name contains unsubstituted variables: %q. Ensure the context map in the test step is populated", configuredTagValueNamespacedName)
}

// 2. Describe the tag value using the namespaced name to get its full resource name.
safeNamespacedName := url.QueryEscape(configuredTagValueNamespacedName)
describeTagValueURL := fmt.Sprintf("https://cloudresourcemanager.googleapis.com/v3/tagValues/namespaced?name=%s", safeNamespacedName)

respDescribe, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "GET",
RawURL: describeTagValueURL,
UserAgent: config.UserAgent,
})

if err != nil {
return fmt.Errorf("error describing tag value using namespaced name %q: %v", configuredTagValueNamespacedName, err)
}

fullTagValueName, ok := respDescribe["name"].(string)
if !ok || fullTagValueName == "" {
return fmt.Errorf("tag value details (name) not found in response for namespaced name: %q, response: %v", configuredTagValueNamespacedName, respDescribe)
}

// 3. Check if bindings exist via API
parts := strings.Split(rs.Primary.ID, "/")
if len(parts) != 2 {
return fmt.Errorf("invalid resource ID format: %s", rs.Primary.ID)
}
project := parts[0]
instanceName := parts[1]

// Extract location from instance config (e.g., "regional-us-central1" -> "us-central1")
configAttr, ok := rs.Primary.Attributes["config"]
if !ok {
return fmt.Errorf("could not find config attribute for resource %s", rs.Primary.ID)
}

// configAttr can be "regional-us-central1" or "projects/my-project/instanceConfigs/regional-us-central1"
configShortName := configAttr
if parts := strings.Split(configAttr, "/"); len(parts) > 1 {
configShortName = parts[len(parts)-1]
}

location := strings.TrimPrefix(configShortName, "regional-")

parentURL := fmt.Sprintf("//spanner.googleapis.com/projects/%s/instances/%s", project, instanceName)
listBindingsURL := fmt.Sprintf("https://%s-cloudresourcemanager.googleapis.com/v3/tagBindings?parent=%s", location, url.QueryEscape(parentURL))

respList, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "GET",
RawURL: listBindingsURL,
UserAgent: config.UserAgent,
})

if err != nil {
return fmt.Errorf("error listing tag bindings for parent %s: %v", parentURL, err)
}

bindings, ok := respList["tagBindings"].([]interface{})
if !ok {
return fmt.Errorf("unexpected or missing 'tagBindings' in response for parent %s: %v", parentURL, respList)
}

found := false
for _, b := range bindings {
bindingMap, ok := b.(map[string]interface{})
if !ok {
continue
}
if bindingMap["tagValue"] == fullTagValueName {
found = true
break
}
}

if !found {
return fmt.Errorf("tag binding not found via API for parent %s with tag value %s", parentURL, fullTagValueName)
}
}
return nil
}
}
Loading