diff --git a/internal/services/hpccache/hpc_cache_resource.go b/internal/services/hpccache/hpc_cache_resource.go index 9decc488bee7..0f2d82efd73e 100644 --- a/internal/services/hpccache/hpc_cache_resource.go +++ b/internal/services/hpccache/hpc_cache_resource.go @@ -1,6 +1,7 @@ package hpccache import ( + "context" "fmt" "log" "regexp" @@ -8,10 +9,17 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/services/storagecache/mgmt/2021-09-01/storagecache" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/hpccache/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/client" + keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" + keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" + msiparse "github.com/hashicorp/terraform-provider-azurerm/internal/services/msi/parse" + resourcesClient "github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/client" "github.com/hashicorp/terraform-provider-azurerm/internal/tags" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" @@ -43,6 +51,8 @@ func resourceHPCCache() *pluginsdk.Resource { func resourceHPCCacheCreateOrUpdate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).HPCCache.CachesClient + keyVaultsClient := meta.(*clients.Client).KeyVault + resourcesClient := meta.(*clients.Client).Resource subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -108,6 +118,11 @@ func resourceHPCCacheCreateOrUpdate(d *pluginsdk.ResourceData, meta interface{}) directorySetting := expandStorageCacheDirectorySettings(d) + identity, err := expandStorageCacheIdentity(d.Get("identity").([]interface{})) + if err != nil { + return fmt.Errorf("expanding `identity`: %+v", err) + } + cache := &storagecache.Cache{ Name: utils.String(name), Location: utils.String(location), @@ -123,18 +138,71 @@ func resourceHPCCacheCreateOrUpdate(d *pluginsdk.ResourceData, meta interface{}) Sku: &storagecache.CacheSku{ Name: utils.String(skuName), }, - Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + Identity: identity, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + if !d.IsNewResource() { + oldKeyVaultKeyId, newKeyVaultKeyId := d.GetChange("key_vault_key_id") + if (oldKeyVaultKeyId.(string) != "" && newKeyVaultKeyId.(string) == "") || (oldKeyVaultKeyId.(string) == "" && newKeyVaultKeyId.(string) != "") { + return fmt.Errorf("`key_vault_key_id` can not be added or removed after HPC Cache is created") + } + } + + requireAdditionalUpdate := false + if v, ok := d.GetOk("key_vault_key_id"); ok { + autoKeyRotationEnabled := d.Get("automatically_rotate_key_to_latest_enabled").(bool) + if !d.IsNewResource() && d.HasChange("key_vault_key_id") && autoKeyRotationEnabled { + // It is by design that `automatically_rotate_key_to_latest_enabled` changes to `false` when `key_vault_key_id` is changed, needs to do an additional update to set it back + requireAdditionalUpdate = true + } + + keyVaultKeyId := v.(string) + keyVaultDetails, err := storageCacheRetrieveKeyVault(ctx, keyVaultsClient, resourcesClient, keyVaultKeyId) + if err != nil { + return fmt.Errorf("validating Key Vault Key %q for HPC Cache: %+v", keyVaultKeyId, err) + } + if azure.NormalizeLocation(keyVaultDetails.location) != azure.NormalizeLocation(location) { + return fmt.Errorf("validating Key Vault %q (Resource Group %q) for HPC Cache: Key Vault must be in the same region as HPC Cache!", keyVaultDetails.keyVaultName, keyVaultDetails.resourceGroupName) + } + if !keyVaultDetails.softDeleteEnabled { + return fmt.Errorf("validating Key Vault %q (Resource Group %q) for HPC Cache: Soft Delete must be enabled but it isn't!", keyVaultDetails.keyVaultName, keyVaultDetails.resourceGroupName) + } + if !keyVaultDetails.purgeProtectionEnabled { + return fmt.Errorf("validating Key Vault %q (Resource Group %q) for HPC Cache: Purge Protection must be enabled but it isn't!", keyVaultDetails.keyVaultName, keyVaultDetails.resourceGroupName) + } + + cache.CacheProperties.EncryptionSettings = &storagecache.CacheEncryptionSettings{ + KeyEncryptionKey: &storagecache.KeyVaultKeyReference{ + KeyURL: utils.String(keyVaultKeyId), + SourceVault: &storagecache.KeyVaultKeyReferenceSourceVault{ + ID: utils.String(keyVaultDetails.keyVaultId), + }, + }, + RotationToLatestKeyVersionEnabled: utils.Bool(autoKeyRotationEnabled), + } } future, err := client.CreateOrUpdate(ctx, resourceGroup, name, cache) if err != nil { - return fmt.Errorf("creating HPC Cache %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("creating/updating HPC Cache %q (Resource Group %q): %+v", name, resourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { return fmt.Errorf("waiting for HPC Cache %q (Resource Group %q) to finish provisioning: %+v", name, resourceGroup, err) } + if requireAdditionalUpdate { + future, err := client.CreateOrUpdate(ctx, resourceGroup, name, cache) + if err != nil { + return fmt.Errorf("Updating HPC Cache %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for updating of HPC Cache %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + // If any directory setting is set, we'll further check either the `usernameDownloaded` (for LDAP/Flat File), or the `domainJoined` (for AD) in response to ensure the configuration is correct, and the cache is functional. // There are situations that the LRO succeeded, whilst ends up with a non-functional cache (e.g. providing some invalid flat file setting). if directorySetting != nil { @@ -251,6 +319,28 @@ func resourceHPCCacheRead(d *pluginsdk.ResourceData, meta interface{}) error { d.Set("sku_name", sku.Name) } + identity, err := flattenStorageCacheIdentity(resp.Identity) + if err != nil { + return err + } + if err := d.Set("identity", identity); err != nil { + return fmt.Errorf("setting `identity`: %+v", err) + } + + keyVaultKeyId := "" + autoKeyRotationEnabled := false + if props := resp.EncryptionSettings; props != nil { + if props.KeyEncryptionKey != nil && props.KeyEncryptionKey.KeyURL != nil { + keyVaultKeyId = *props.KeyEncryptionKey.KeyURL + } + + if props.RotationToLatestKeyVersionEnabled != nil { + autoKeyRotationEnabled = *props.RotationToLatestKeyVersionEnabled + } + } + d.Set("key_vault_key_id", keyVaultKeyId) + d.Set("automatically_rotate_key_to_latest_enabled", autoKeyRotationEnabled) + return tags.FlattenAndSet(d, resp.Tags) } @@ -624,6 +714,109 @@ func expandStorageCacheDirectoryLdapBind(input []interface{}) *storagecache.Cach } } +func expandStorageCacheIdentity(input []interface{}) (*storagecache.CacheIdentity, error) { + config, err := identity.ExpandUserAssignedMap(input) + if err != nil { + return nil, err + } + + identity := storagecache.CacheIdentity{ + Type: storagecache.CacheIdentityType(config.Type), + } + + if len(config.IdentityIds) != 0 { + identityIds := make(map[string]*storagecache.CacheIdentityUserAssignedIdentitiesValue, len(config.IdentityIds)) + for id := range config.IdentityIds { + identityIds[id] = &storagecache.CacheIdentityUserAssignedIdentitiesValue{} + } + identity.UserAssignedIdentities = identityIds + } + + return &identity, nil +} + +func flattenStorageCacheIdentity(input *storagecache.CacheIdentity) (*[]interface{}, error) { + var config *identity.UserAssignedMap + + if input != nil { + identityIds := map[string]identity.UserAssignedIdentityDetails{} + for id := range input.UserAssignedIdentities { + parsedId, err := msiparse.UserAssignedIdentityIDInsensitively(id) + if err != nil { + return nil, err + } + identityIds[parsedId.ID()] = identity.UserAssignedIdentityDetails{} + } + + config = &identity.UserAssignedMap{ + Type: identity.Type(string(input.Type)), + IdentityIds: identityIds, + } + } + + return identity.FlattenUserAssignedMap(config) +} + +type storageCacheKeyVault struct { + keyVaultId string + resourceGroupName string + keyVaultName string + location string + purgeProtectionEnabled bool + softDeleteEnabled bool +} + +func storageCacheRetrieveKeyVault(ctx context.Context, keyVaultsClient *client.Client, resourcesClient *resourcesClient.Client, id string) (*storageCacheKeyVault, error) { + keyVaultKeyId, err := keyVaultParse.ParseNestedItemID(id) + if err != nil { + return nil, err + } + keyVaultID, err := keyVaultsClient.KeyVaultIDFromBaseUrl(ctx, resourcesClient, keyVaultKeyId.KeyVaultBaseUrl) + if err != nil { + return nil, fmt.Errorf("retrieving the Resource ID the Key Vault at URL %q: %s", keyVaultKeyId.KeyVaultBaseUrl, err) + } + if keyVaultID == nil { + return nil, fmt.Errorf("Unable to determine the Resource ID for the Key Vault at URL %q", keyVaultKeyId.KeyVaultBaseUrl) + } + + parsedKeyVaultID, err := keyVaultParse.VaultID(*keyVaultID) + if err != nil { + return nil, err + } + + resp, err := keyVaultsClient.VaultsClient.Get(ctx, parsedKeyVaultID.ResourceGroup, parsedKeyVaultID.Name) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", *parsedKeyVaultID, err) + } + + purgeProtectionEnabled := false + softDeleteEnabled := false + + if props := resp.Properties; props != nil { + if props.EnableSoftDelete != nil { + softDeleteEnabled = *props.EnableSoftDelete + } + + if props.EnablePurgeProtection != nil { + purgeProtectionEnabled = *props.EnablePurgeProtection + } + } + + location := "" + if resp.Location != nil { + location = *resp.Location + } + + return &storageCacheKeyVault{ + keyVaultId: *keyVaultID, + resourceGroupName: parsedKeyVaultID.ResourceGroup, + keyVaultName: parsedKeyVaultID.Name, + location: location, + purgeProtectionEnabled: purgeProtectionEnabled, + softDeleteEnabled: softDeleteEnabled, + }, nil +} + func resourceHPCCacheSchema() map[string]*pluginsdk.Schema { return map[string]*pluginsdk.Schema{ "name": { @@ -923,6 +1116,21 @@ func resourceHPCCacheSchema() map[string]*pluginsdk.Schema { Elem: &pluginsdk.Schema{Type: pluginsdk.TypeString}, }, + "identity": commonschema.UserAssignedIdentityOptionalForceNew(), + + "key_vault_key_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: keyVaultValidate.NestedItemId, + RequiredWith: []string{"identity"}, + }, + + "automatically_rotate_key_to_latest_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + RequiredWith: []string{"key_vault_key_id"}, + }, + "tags": tags.Schema(), } } diff --git a/internal/services/hpccache/hpc_cache_resource_test.go b/internal/services/hpccache/hpc_cache_resource_test.go index d4ee7fea4c42..96b836d2222b 100644 --- a/internal/services/hpccache/hpc_cache_resource_test.go +++ b/internal/services/hpccache/hpc_cache_resource_test.go @@ -337,6 +337,50 @@ func TestAccHPCCache_directoryFlatFile(t *testing.T) { }) } +func TestAccHPCCache_customerManagedKeyWithAutoKeyRotationEnabled(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_hpc_cache", "test") + r := HPCCacheResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.customerManagedKeyWithAutoKeyRotationEnabled(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.customerManagedKeyWithAutoKeyRotationEnabledUpdateKey(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccHPCCache_customerManagedKeyUpdateAutoKeyRotation(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_hpc_cache", "test") + r := HPCCacheResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.customerManagedKeyWithDefaultAutoKeyRotation(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.customerManagedKeyWithAutoKeyRotationEnabled(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (HPCCacheResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := parse.CacheID(state.ID) if err != nil { @@ -854,6 +898,194 @@ resource "azurerm_network_interface" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger) } +func (r HPCCacheResource) customerManagedKeyWithDefaultAutoKeyRotation(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_hpc_cache" "test" { + name = "acctest-HPCC-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + cache_size_in_gb = 3072 + subnet_id = azurerm_subnet.test.id + sku_name = "Standard_2G" + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + key_vault_key_id = azurerm_key_vault_key.test.id +} +`, r.customerManagedKeyTemplate(data), data.RandomInteger) +} + +func (r HPCCacheResource) customerManagedKeyWithAutoKeyRotationEnabled(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_hpc_cache" "test" { + name = "acctest-HPCC-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + cache_size_in_gb = 3072 + subnet_id = azurerm_subnet.test.id + sku_name = "Standard_2G" + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + key_vault_key_id = azurerm_key_vault_key.test.id + automatically_rotate_key_to_latest_enabled = true +} +`, r.customerManagedKeyTemplate(data), data.RandomInteger) +} + +func (r HPCCacheResource) customerManagedKeyWithAutoKeyRotationEnabledUpdateKey(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_hpc_cache" "test" { + name = "acctest-HPCC-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + cache_size_in_gb = 3072 + subnet_id = azurerm_subnet.test.id + sku_name = "Standard_2G" + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + key_vault_key_id = azurerm_key_vault_key.test2.id + automatically_rotate_key_to_latest_enabled = true +} +`, r.customerManagedKeyTemplate(data), data.RandomInteger) +} + +func (HPCCacheResource) customerManagedKeyTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + recover_soft_deleted_key_vaults = false + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-storage-%[2]d" + location = "%[1]s" +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv-%[3]s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + purge_protection_enabled = true + soft_delete_retention_days = 7 + enabled_for_disk_encryption = true +} + +resource "azurerm_key_vault_access_policy" "service-principal" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Create", + "Delete", + "Get", + "Purge", + "Update", + ] +} + +resource "azurerm_key_vault_key" "test" { + name = "examplekey" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + + key_opts = [ + "decrypt", + "encrypt", + "sign", + "unwrapKey", + "verify", + "wrapKey", + ] + + depends_on = [azurerm_key_vault_access_policy.service-principal] +} + +resource "azurerm_key_vault_key" "test2" { + name = "examplekey2" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + + key_opts = [ + "decrypt", + "encrypt", + "sign", + "unwrapKey", + "verify", + "wrapKey", + ] + + depends_on = [azurerm_key_vault_access_policy.service-principal] +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acct-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_key_vault_access_policy" "service-principal2" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + + key_permissions = [ + "Get", + "UnwrapKey", + "WrapKey", + ] +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-VN-%[2]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "acctestsub-%[2]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] +} +`, data.Locations.Primary, data.RandomInteger, data.RandomString) +} + func (HPCCacheResource) template(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/website/docs/r/hpc_cache.html.markdown b/website/docs/r/hpc_cache.html.markdown index ce8670ad90aa..f43746586e52 100644 --- a/website/docs/r/hpc_cache.html.markdown +++ b/website/docs/r/hpc_cache.html.markdown @@ -83,7 +83,13 @@ The following arguments are supported: * `directory_ldap` - (Optional) A `directory_ldap` block as defined below. ~> **Note:** Only one of `directory_active_directory`, `directory_flat_file` and `directory_ldap` can be set. - + +* `identity` - (Optional) An `identity` block as defined below. Changing this forces a new resource to be created. + +* `key_vault_key_id` - (Optional) The ID of the Key Vault Key which should be used to encrypt the data in this HPC Cache. + +* `automatically_rotate_key_to_latest_enabled` - (Optional) Specifies whether the HPC Cache automatically rotates Encryption Key to the latest version. Defaults to `false`. + * `tags` - (Optional) A mapping of tags to assign to the HPC Cache. --- @@ -172,6 +178,14 @@ A `dns` block contains the following: * `search_domain` - (Optional) The DNS search domain for the HPC Cache. +--- + +An `identity` block supports the following: + +* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this HPC Cache. Only possible value is `UserAssigned`. + +* `identity_ids` - (Required) Specifies a list of User Assigned Managed Identity IDs to be assigned to this HPC Cache. + ## Attributes Reference The following attributes are exported: