Skip to content
Draft
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/redis/Instance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,11 @@ properties:
Optional. The KMS key reference that you want to use to encrypt the data at rest for this Redis
instance. If this is provided, CMEK is enabled.
immutable: true
- 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_key_value}.
immutable: true
ignore_read: true
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package redis_test

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

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
)

func TestAccRedisInstance_update(t *testing.T) {
Expand Down Expand Up @@ -407,3 +412,209 @@ resource "google_redis_instance" "test" {
}
`, name)
}

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

name := fmt.Sprintf("tf-test-%d", acctest.RandInt(t))

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckRedisInstanceDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccRedisInstance_deletionprotection(name, "us-central1", true),
},
{
ResourceName: "google_redis_instance.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"labels", "terraform_labels", "deletion_protection"},
},
{
Config: testAccRedisInstance_deletionprotection(name, "us-west2", true),
ExpectError: regexp.MustCompile("deletion_protection"),
},
{
Config: testAccRedisInstance_deletionprotection(name, "us-central1", false),
},
},
})
}

func testAccRedisInstance_deletionprotection(name string, region string, deletionProtection bool) string {
return fmt.Sprintf(`
resource "google_redis_instance" "test" {
name = "%s"
region = "%s"
display_name = "tf-test-instance"
memory_size_gb = 1
deletion_protection = %t

labels = {
my_key = "my_val"
other_key = "other_val"
}
redis_configs = {
maxmemory-policy = "allkeys-lru"
notify-keyspace-events = "KEA"
}
redis_version = "REDIS_4_0"
}
`, name, region, deletionProtection)
}

func TestAccRedisInstance_tags(t *testing.T) {
t.Parallel()
tagKey := acctest.BootstrapSharedTestOrganizationTagKey(t, "redis-instance-tagkey", map[string]interface{}{})
context := map[string]interface{}{
"random_suffix": acctest.RandString(t, 10),
"org": envvar.GetTestOrgFromEnv(t),
"tagKey": tagKey,
"tagValue": acctest.BootstrapSharedTestOrganizationTagValue(t, "redis-instance-tagvalue", tagKey),
}
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckRedisInstanceDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccRedisInstanceTags(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("google_redis_instance.test", "tags.%"),
checkRedisInstanceTags(t),
),
},
{
ResourceName: "google_redis_instance.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"tags"},
},
},
})
}

func checkRedisInstanceTags(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_redis_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. Get the tag bindings from the Redis Instance.
parts := strings.Split(rs.Primary.ID, "/")
if len(parts) != 6 {
return fmt.Errorf("invalid resource ID format: %s", rs.Primary.ID)
}
project := parts[1]
location := parts[3]
instance_id := parts[5]

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

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: %v", 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)
}

// 4. Perform the comparison.
foundMatch := false
for _, binding := range tagBindings {
bindingMap, ok := binding.(map[string]interface{})
if !ok {
continue
}
if bindingMap["tagValue"] == fullTagValueName {
foundMatch = true
break
}
}

if !foundMatch {
return fmt.Errorf("expected tag value %s (from namespaced %q) not found in tag bindings for resource %s. Bindings: %v", fullTagValueName, configuredTagValueNamespacedName, rs.Primary.ID, tagBindings)
}

t.Logf("Successfully found matching tag binding for %s with tagValue %s", rs.Primary.ID, fullTagValueName)
}

return nil
}
}

func testAccRedisInstanceTags(context map[string]interface{}) string {
return acctest.Nprintf(`
resource "google_redis_instance" "test" {
name = "tf-test-instance-%{random_suffix}"
memory_size_gb = 5
tags = {
"%{org}/%{tagKey}" = "%{tagValue}"
}
}`, context)
}