diff --git a/internal/services/redis/redis_cache_resource.go b/internal/services/redis/redis_cache_resource.go index 6276925aead0..7574a1bab2b8 100644 --- a/internal/services/redis/redis_cache_resource.go +++ b/internal/services/redis/redis_cache_resource.go @@ -11,6 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2021-06-01/redis" "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-helpers/resourcemanager/zones" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -295,6 +296,8 @@ func resourceRedisCache() *pluginsdk.Resource { ValidateFunc: validation.IntBetween(1, 3), }, + "identity": commonschema.SystemAssignedUserAssignedIdentityOptional(), + "replicas_per_primary": { Type: pluginsdk.TypeInt, Optional: true, @@ -370,6 +373,11 @@ func resourceRedisCacheCreate(d *pluginsdk.ResourceData, meta interface{}) error publicNetworkAccess = redis.PublicNetworkAccessDisabled } + redisIdentity, err := expandRedisIdentity(d.Get("identity").([]interface{})) + if err != nil { + return fmt.Errorf(`expanding "identity": %v`, err) + } + parameters := redis.CreateParameters{ Location: utils.String(location), CreateProperties: &redis.CreateProperties{ @@ -383,7 +391,8 @@ func resourceRedisCacheCreate(d *pluginsdk.ResourceData, meta interface{}) error RedisConfiguration: redisConfiguration, PublicNetworkAccess: publicNetworkAccess, }, - Tags: expandedTags, + Identity: redisIdentity, + Tags: expandedTags, } if v, ok := d.GetOk("shard_count"); ok { @@ -564,6 +573,26 @@ func resourceRedisCacheUpdate(d *pluginsdk.ResourceData, meta interface{}) error return fmt.Errorf("waiting for Redis Cache %q (Resource Group %q) to become available: %+v", id.RediName, id.ResourceGroup, err) } + // identity cannot be updated with sku,publicNetworkAccess,redisVersion etc. + if d.HasChange("identity") { + redisIdentity, err := expandRedisIdentity(d.Get("identity").([]interface{})) + if err != nil { + return fmt.Errorf(`expanding "identity": %v`, err) + } + + identityParameter := redis.UpdateParameters{ + Identity: redisIdentity, + } + if _, err := client.Update(ctx, id.ResourceGroup, id.RediName, identityParameter); err != nil { + return fmt.Errorf("updating Redis Cache identity %q identity (Resource Group %q): %+v", id.RediName, id.ResourceGroup, err) + } + + log.Printf("[DEBUG] Waiting for Redis Cache %q (Resource Group %q) to become available", id.RediName, id.ResourceGroup) + if _, err = stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for Redis Cache %q (Resource Group %q) to become available: %+v", id.RediName, id.ResourceGroup, err) + } + } + patchSchedule := expandRedisPatchSchedule(d) if patchSchedule == nil || len(*patchSchedule.ScheduleEntries.ScheduleEntries) == 0 { @@ -614,6 +643,14 @@ func resourceRedisCacheRead(d *pluginsdk.ResourceData, meta interface{}) error { } } + redisIdentity, err := flattenRedisIdentity(resp.Identity) + if err != nil { + return fmt.Errorf("flattening `identity`: %+v", err) + } + if err := d.Set("identity", redisIdentity); err != nil { + return fmt.Errorf("setting `identity`: %+v", err) + } + d.Set("name", id.RediName) d.Set("resource_group_name", id.ResourceGroup) d.Set("location", location.NormalizeNilable(resp.Location)) @@ -989,3 +1026,47 @@ func flattenRedisPatchSchedules(schedule redis.PatchSchedule) []interface{} { func getRedisConnectionString(redisHostName string, sslPort int32, accessKey string, enableSslPort bool) string { return fmt.Sprintf("%s:%d,password=%s,ssl=%t,abortConnect=False", redisHostName, sslPort, accessKey, enableSslPort) } + +func expandRedisIdentity(input []interface{}) (*redis.ManagedServiceIdentity, error) { + expanded, err := identity.ExpandSystemAndUserAssignedMap(input) + if err != nil { + return nil, err + } + + out := redis.ManagedServiceIdentity{ + Type: redis.ManagedServiceIdentityType(expanded.Type), + } + if expanded.Type == identity.TypeUserAssigned || expanded.Type == identity.TypeSystemAssignedUserAssigned { + out.UserAssignedIdentities = make(map[string]*redis.UserAssignedIdentity) + for k := range expanded.IdentityIds { + out.UserAssignedIdentities[k] = &redis.UserAssignedIdentity{ + // intentionally empty + } + } + } + return &out, nil +} + +func flattenRedisIdentity(input *redis.ManagedServiceIdentity) (*[]interface{}, error) { + var transform *identity.SystemAndUserAssignedMap + + if input != nil { + transform = &identity.SystemAndUserAssignedMap{ + Type: identity.Type(input.Type), + IdentityIds: make(map[string]identity.UserAssignedIdentityDetails), + } + if input.PrincipalID != nil { + transform.PrincipalId = input.PrincipalID.String() + } + if input.TenantID != nil { + transform.TenantId = input.TenantID.String() + } + for k, v := range input.UserAssignedIdentities { + transform.IdentityIds[k] = identity.UserAssignedIdentityDetails{ + ClientId: utils.String(v.ClientID.String()), + PrincipalId: utils.String(v.PrincipalID.String()), + } + } + } + return identity.FlattenSystemAndUserAssignedMap(transform) +} diff --git a/internal/services/redis/redis_cache_resource_test.go b/internal/services/redis/redis_cache_resource_test.go index 4e56353518c9..411212a21910 100644 --- a/internal/services/redis/redis_cache_resource_test.go +++ b/internal/services/redis/redis_cache_resource_test.go @@ -474,6 +474,28 @@ func TestAccRedisCache_redisConfiguration(t *testing.T) { }) } +func TestAccRedisCache_identity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_redis_cache", "test") + r := RedisCacheResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.systemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.userAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (t RedisCacheResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := parse.CacheID(state.ID) if err != nil { @@ -1211,6 +1233,80 @@ resource "azurerm_redis_cache" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, maxMemoryPolicy) } +func (RedisCacheResource) systemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_redis_cache" "test" { + name = "acctestRedis-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + capacity = 1 + family = "C" + sku_name = "Standard" + enable_non_ssl_port = false + redis_configuration { + } + + identity { + type = "SystemAssigned" + } + + tags = { + environment = "production" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) + +} + +func (RedisCacheResource) userAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_redis_cache" "test" { + name = "acctestRedis-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + capacity = 1 + family = "C" + sku_name = "Standard" + enable_non_ssl_port = false + redis_configuration { + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } + + tags = { + environment = "production" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) +} + func testCheckSSLInConnectionString(resourceName string, propertyName string, requireSSL bool) acceptance.TestCheckFunc { return func(s *acceptance.State) error { // Ensure we have enough information in state to look up in API diff --git a/website/docs/r/redis_cache.html.markdown b/website/docs/r/redis_cache.html.markdown index 3cf629c40d11..24954decec16 100644 --- a/website/docs/r/redis_cache.html.markdown +++ b/website/docs/r/redis_cache.html.markdown @@ -59,6 +59,8 @@ The following arguments are supported: * `enable_non_ssl_port` - (Optional) Enable the non-SSL port (6379) - disabled by default. +* `identity` - (Optional) An `identity` block as defined below. + * `minimum_tls_version` - (Optional) The minimum TLS version. Defaults to `1.0`. * `patch_schedule` - (Optional) A list of `patch_schedule` blocks as defined below. @@ -91,6 +93,16 @@ The following arguments are supported: --- +An `identity` block supports the following: + +* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this Batch Account. Possible values are `SystemAssigned`, `UserAssigned`, `SystemAssigned, UserAssigned` (to enable both). + +* `identity_ids` - (Optional) A list of User Assigned Managed Identity IDs to be assigned to this Batch Account. + +~> **NOTE:** This is required when `type` is set to `UserAssigned` or `SystemAssigned, UserAssigned`. + +--- + A `redis_configuration` block supports the following: * `aof_backup_enabled` - (Optional) Enable or disable AOF persistence for this Redis Cache.