diff --git a/internal/services/elasticsan/elastic_san_resource.go b/internal/services/elasticsan/elastic_san_resource.go index 2a6808ae6618..148fb20a9146 100644 --- a/internal/services/elasticsan/elastic_san_resource.go +++ b/internal/services/elasticsan/elastic_san_resource.go @@ -20,6 +20,7 @@ import ( var _ sdk.Resource = ElasticSANResource{} var _ sdk.ResourceWithUpdate = ElasticSANResource{} +var _ sdk.ResourceWithCustomizeDiff = ElasticSANResource{} type ElasticSANResource struct{} diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource.go b/internal/services/elasticsan/elastic_san_volume_group_resource.go new file mode 100644 index 000000000000..c41868254379 --- /dev/null +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -0,0 +1,467 @@ +package elasticsan + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" + "github.com/hashicorp/go-azure-sdk/resource-manager/elasticsan/2023-01-01/volumegroups" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/elasticsan/validate" + keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" + keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +var _ sdk.Resource = ElasticSANVolumeGroupResource{} +var _ sdk.ResourceWithUpdate = ElasticSANVolumeGroupResource{} +var _ sdk.ResourceWithCustomizeDiff = ElasticSANVolumeGroupResource{} + +type ElasticSANVolumeGroupResource struct{} + +func (r ElasticSANVolumeGroupResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return volumegroups.ValidateVolumeGroupID +} + +func (r ElasticSANVolumeGroupResource) ResourceType() string { + return "azurerm_elastic_san_volume_group" +} + +func (r ElasticSANVolumeGroupResource) ModelObject() interface{} { + return &ElasticSANVolumeGroupResourceModel{} +} + +type ElasticSANVolumeGroupResourceModel struct { + SanId string `tfschema:"elastic_san_id"` + EncryptionType string `tfschema:"encryption_type"` + Encryption []ElasticSANVolumeGroupResourceEncryptionModel `tfschema:"encryption"` + Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` + Name string `tfschema:"name"` + NetworkRule []ElasticSANVolumeGroupResourceNetworkRuleModel `tfschema:"network_rule"` + ProtocolType string `tfschema:"protocol_type"` +} + +type ElasticSANVolumeGroupResourceEncryptionModel struct { + CurrentVersionedKeyExpirationTimestamp string `tfschema:"current_versioned_key_expiration_timestamp"` + CurrentVersionedKeyId string `tfschema:"current_versioned_key_id"` + UserAssignedIdentityId string `tfschema:"user_assigned_identity_id"` + KeyVaultKeyId string `tfschema:"key_vault_key_id"` + LastKeyRotationTimestamp string `tfschema:"last_key_rotation_timestamp"` +} + +type ElasticSANVolumeGroupResourceNetworkRuleModel struct { + Action string `tfschema:"action"` + SubnetId string `tfschema:"subnet_id"` +} + +func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ElasticSanVolumeGroupName, + }, + + "elastic_san_id": commonschema.ResourceIDReferenceRequiredForceNew(volumegroups.ElasticSanId{}), + + "encryption_type": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(volumegroups.PossibleValuesForEncryptionType(), false), + Default: string(volumegroups.EncryptionTypeEncryptionAtRestWithPlatformKey), + }, + + "encryption": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "key_vault_key_id": { + Required: true, + Type: pluginsdk.TypeString, + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + }, + "user_assigned_identity_id": { + Optional: true, + Type: pluginsdk.TypeString, + ValidateFunc: commonids.ValidateUserAssignedIdentityID, + }, + "current_versioned_key_expiration_timestamp": { + Computed: true, + Type: pluginsdk.TypeString, + }, + "current_versioned_key_id": { + Computed: true, + Type: pluginsdk.TypeString, + }, + "last_key_rotation_timestamp": { + Computed: true, + Type: pluginsdk.TypeString, + }, + }, + }, + }, + + "network_rule": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "subnet_id": { + Required: true, + Type: pluginsdk.TypeString, + ValidateFunc: commonids.ValidateSubnetID, + }, + "action": { + Optional: true, + Type: pluginsdk.TypeString, + Default: string(volumegroups.ActionAllow), + ValidateFunc: validation.StringInSlice(volumegroups.PossibleValuesForAction(), false), + }, + }, + }, + }, + + "protocol_type": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + // None is not a valid value and service team will consider removing it in future versions. + string(volumegroups.StorageTargetTypeIscsi), + }, false), + Default: string(volumegroups.StorageTargetTypeIscsi), + }, + + "identity": commonschema.SystemOrUserAssignedIdentityOptional(), + } +} + +func (k ElasticSANVolumeGroupResource) CustomizeDiff() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var config ElasticSANVolumeGroupResourceModel + if err := metadata.DecodeDiff(&config); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + if len(config.Encryption) > 0 && config.EncryptionType != string(volumegroups.EncryptionTypeEncryptionAtRestWithCustomerManagedKey) { + return fmt.Errorf("encryption can only be set if encryption_type is EncryptionAtRestWithCustomerManagedKey") + } + + if len(config.Encryption) == 0 && config.EncryptionType == string(volumegroups.EncryptionTypeEncryptionAtRestWithCustomerManagedKey) { + return fmt.Errorf("encryption must be set if encryption_type is EncryptionAtRestWithCustomerManagedKey") + } + + return nil + }, + } +} + +func (r ElasticSANVolumeGroupResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r ElasticSANVolumeGroupResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ElasticSan.VolumeGroups + + var config ElasticSANVolumeGroupResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + subscriptionId := metadata.Client.Account.SubscriptionId + + elasticSanId, err := volumegroups.ParseElasticSanID(config.SanId) + if err != nil { + return err + } + + id := volumegroups.NewVolumeGroupID(subscriptionId, elasticSanId.ResourceGroupName, elasticSanId.ElasticSanName, config.Name) + + existing, err := client.Get(ctx, id) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for the presence of an existing %s: %+v", id, err) + } + } + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + expandedIdentity, err := identity.ExpandSystemOrUserAssignedMapFromModel(config.Identity) + if err != nil { + return fmt.Errorf("expanding identity: %+v", err) + } + + encryption, err := ExpandVolumeGroupEncryption(config.Encryption) + if err != nil { + return fmt.Errorf("expanding encryption: %+v", err) + } + + payload := volumegroups.VolumeGroup{ + Identity: expandedIdentity, + Properties: &volumegroups.VolumeGroupProperties{ + Encryption: pointer.To(volumegroups.EncryptionType(config.EncryptionType)), + EncryptionProperties: encryption, + NetworkAcls: ExpandVolumeGroupNetworkRules(config.NetworkRule), + ProtocolType: pointer.To(volumegroups.StorageTargetType(config.ProtocolType)), + }, + } + + if err := client.CreateThenPoll(ctx, id, payload); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r ElasticSANVolumeGroupResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ElasticSan.VolumeGroups + schema := ElasticSANVolumeGroupResourceModel{} + + id, err := volumegroups.ParseVolumeGroupID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + elasticSanId := volumegroups.NewElasticSanID(id.SubscriptionId, id.ResourceGroupName, id.ElasticSanName) + + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(*id) + } + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + if model := resp.Model; model != nil { + schema.SanId = elasticSanId.ID() + schema.Name = id.VolumeGroupName + + flattenedIdentity, err := identity.FlattenSystemOrUserAssignedMapToModel(model.Identity) + if err != nil { + return fmt.Errorf("flattening identity: %+v", err) + } + schema.Identity = *flattenedIdentity + + if model.Properties != nil { + schema.EncryptionType = string(pointer.From(model.Properties.Encryption)) + schema.NetworkRule = FlattenVolumeGroupNetworkRules(model.Properties.NetworkAcls) + + if model.Properties.ProtocolType != nil { + schema.ProtocolType = string(pointer.From(model.Properties.ProtocolType)) + } + + schema.Encryption, err = FlattenVolumeGroupEncryption(model.Properties.EncryptionProperties) + if err != nil { + return fmt.Errorf("flattening encryption: %+v", err) + } + } + } + + return metadata.Encode(&schema) + }, + } +} + +func (r ElasticSANVolumeGroupResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ElasticSan.VolumeGroups + + id, err := volumegroups.ParseVolumeGroupID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + if err := client.DeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + + return nil + }, + } +} + +func (r ElasticSANVolumeGroupResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ElasticSan.VolumeGroups + + id, err := volumegroups.ParseVolumeGroupID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var config ElasticSANVolumeGroupResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + payload := volumegroups.VolumeGroupUpdate{ + Properties: &volumegroups.VolumeGroupUpdateProperties{}, + } + + if metadata.ResourceData.HasChange("encryption_type") { + payload.Properties.Encryption = pointer.To(volumegroups.EncryptionType(config.EncryptionType)) + } + + if metadata.ResourceData.HasChange("encryption") { + encryption, err := ExpandVolumeGroupEncryption(config.Encryption) + if err != nil { + return fmt.Errorf("expanding encryption: %+v", err) + } + + payload.Properties.EncryptionProperties = encryption + } + + if metadata.ResourceData.HasChange("identity") { + expandedIdentity, err := identity.ExpandSystemOrUserAssignedMapFromModel(config.Identity) + if err != nil { + return fmt.Errorf("expanding identity: %+v", err) + } + + payload.Identity = expandedIdentity + } + + if metadata.ResourceData.HasChange("protocol_type") { + payload.Properties.ProtocolType = pointer.To(volumegroups.StorageTargetType(config.ProtocolType)) + } + + if metadata.ResourceData.HasChange("network_rule") { + payload.Properties.NetworkAcls = ExpandVolumeGroupNetworkRules(config.NetworkRule) + } + + if err := client.UpdateThenPoll(ctx, *id, payload); err != nil { + return fmt.Errorf("updating %s: %+v", *id, err) + } + + return nil + }, + } +} + +func ExpandVolumeGroupEncryption(input []ElasticSANVolumeGroupResourceEncryptionModel) (*volumegroups.EncryptionProperties, error) { + if len(input) == 0 { + return nil, nil + } + + nestedItemId, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(input[0].KeyVaultKeyId) + if err != nil { + return nil, err + } + + result := volumegroups.EncryptionProperties{ + KeyVaultProperties: &volumegroups.KeyVaultProperties{ + KeyName: pointer.To(nestedItemId.Name), + KeyVersion: pointer.To(nestedItemId.Version), + KeyVaultUri: pointer.To(nestedItemId.KeyVaultBaseUrl), + }, + } + + if input[0].UserAssignedIdentityId != "" { + result.Identity = &volumegroups.EncryptionIdentity{ + UserAssignedIdentity: pointer.To(input[0].UserAssignedIdentityId), + } + } + + return &result, nil +} + +func FlattenVolumeGroupEncryption(input *volumegroups.EncryptionProperties) ([]ElasticSANVolumeGroupResourceEncryptionModel, error) { + if input == nil { + return []ElasticSANVolumeGroupResourceEncryptionModel{}, nil + } + + var keyVaultKeyId, currentVersionedKeyExpirationTimestamp, currentVersionedKeyId, lastKeyRotationTimestamp string + if kv := input.KeyVaultProperties; kv != nil { + id, err := keyVaultParse.NewNestedItemID(pointer.From(kv.KeyVaultUri), keyVaultParse.NestedItemTypeKey, pointer.From(kv.KeyName), pointer.From(kv.KeyVersion)) + if err != nil { + return nil, fmt.Errorf("parsing Encryption Key Vault Key ID: %+v", err) + } + + keyVaultKeyId = id.ID() + + currentVersionedKeyExpirationTimestamp = pointer.From(input.KeyVaultProperties.CurrentVersionedKeyExpirationTimestamp) + currentVersionedKeyId = pointer.From(input.KeyVaultProperties.CurrentVersionedKeyIdentifier) + lastKeyRotationTimestamp = pointer.From(input.KeyVaultProperties.LastKeyRotationTimestamp) + } + + var userAssignedIdentityId string + if input.Identity != nil && input.Identity.UserAssignedIdentity != nil { + id, err := commonids.ParseUserAssignedIdentityIDInsensitively(*input.Identity.UserAssignedIdentity) + if err != nil { + return nil, fmt.Errorf("parsing Encryption User Assigned Identity ID: %+v", err) + } + + userAssignedIdentityId = id.ID() + } + + return []ElasticSANVolumeGroupResourceEncryptionModel{ + { + KeyVaultKeyId: keyVaultKeyId, + UserAssignedIdentityId: userAssignedIdentityId, + CurrentVersionedKeyExpirationTimestamp: currentVersionedKeyExpirationTimestamp, + CurrentVersionedKeyId: currentVersionedKeyId, + LastKeyRotationTimestamp: lastKeyRotationTimestamp, + }, + }, nil +} + +func ExpandVolumeGroupNetworkRules(input []ElasticSANVolumeGroupResourceNetworkRuleModel) *volumegroups.NetworkRuleSet { + // if return nil, the Network Rules will not be removed during update + if len(input) == 0 { + return &volumegroups.NetworkRuleSet{ + VirtualNetworkRules: &[]volumegroups.VirtualNetworkRule{}, + } + } + + var networkRules []volumegroups.VirtualNetworkRule + for _, rule := range input { + networkRules = append(networkRules, volumegroups.VirtualNetworkRule{ + Id: rule.SubnetId, + Action: pointer.To(volumegroups.Action(rule.Action)), + }) + } + + return &volumegroups.NetworkRuleSet{ + VirtualNetworkRules: &networkRules, + } +} + +func FlattenVolumeGroupNetworkRules(input *volumegroups.NetworkRuleSet) []ElasticSANVolumeGroupResourceNetworkRuleModel { + if input == nil || input.VirtualNetworkRules == nil { + return []ElasticSANVolumeGroupResourceNetworkRuleModel{} + } + + networkRules := make([]ElasticSANVolumeGroupResourceNetworkRuleModel, 0) + for _, rule := range *input.VirtualNetworkRules { + networkRules = append(networkRules, ElasticSANVolumeGroupResourceNetworkRuleModel{ + SubnetId: rule.Id, + Action: string(pointer.From(rule.Action)), + }) + } + + return networkRules +} diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go new file mode 100644 index 000000000000..fb86573d6987 --- /dev/null +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -0,0 +1,739 @@ +package elasticsan_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/go-azure-sdk/resource-manager/elasticsan/2023-01-01/volumegroups" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type ElasticSANVolumeGroupTestResource struct{} + +func TestAccElasticSANVolumeGroup_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccElasticSANVolumeGroup_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccElasticSANVolumeGroup_encryptionWithSystemAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.encryptionWithSystemAssignedIdentityRoleAssignment(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.encryptionWithSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccElasticSANVolumeGroup_updateIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + 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 TestAccElasticSANVolumeGroup_wrongEncryptionConfig(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.wrongEncryptionConfig(data), + ExpectError: regexp.MustCompile("encryption can only be set if encryption_type is EncryptionAtRestWithCustomerManagedKey"), + }, + }) +} + +func TestAccElasticSANVolumeGroup_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccElasticSANVolumeGroup_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") + r := ElasticSANVolumeGroupTestResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r ElasticSANVolumeGroupTestResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := volumegroups.ParseVolumeGroupID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.ElasticSan.VolumeGroups.Get(ctx, *id) + if err != nil { + return nil, fmt.Errorf("reading %s: %+v", *id, err) + } + + return utils.Bool(resp.Model != nil), nil +} + +func (r ElasticSANVolumeGroupTestResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_elastic_san_volume_group" "import" { + name = azurerm_elastic_san_volume_group.test.name + elastic_san_id = azurerm_elastic_san.test.id +} +`, r.basic(data)) +} + +func (r ElasticSANVolumeGroupTestResource) userAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest-uai-${var.random_integer}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) systemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + + identity { + type = "SystemAssigned" + } +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) encryptionWithSystemAssignedIdentityRoleAssignment(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "test" { + name = "acctestvg${var.random_string}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = true + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "systemAssignedIdentity" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_elastic_san_volume_group.test.identity[0].principal_id + + key_permissions = ["Get", "UnwrapKey", "WrapKey"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_access_policy" "client" { + 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 = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvk${var.random_string}" + 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.systemAssignedIdentity, azurerm_key_vault_access_policy.client] +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + + identity { + type = "SystemAssigned" + } +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) encryptionWithSystemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "test" { + name = "acctestvg${var.random_string}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = true + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "systemAssignedIdentity" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_elastic_san_volume_group.test.identity[0].principal_id + + key_permissions = ["Get", "UnwrapKey", "WrapKey"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_access_policy" "client" { + 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 = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvk${var.random_string}" + 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.client] +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + encryption_type = "EncryptionAtRestWithCustomerManagedKey" + + encryption { + key_vault_key_id = azurerm_key_vault_key.test.id + } + + identity { + type = "SystemAssigned" + } +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) wrongEncryptionConfig(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest-uai-${var.random_integer}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-${var.random_integer}" + 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 = "test-subnet-${var.random_integer}" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + service_endpoints = ["Microsoft.Storage.Global"] + +} + +resource "azurerm_key_vault" "test" { + name = "acctestvg${var.random_string}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = true + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "userAssignedIdentity" { + 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"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_access_policy" "client" { + 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 = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvk${var.random_string}" + 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.userAssignedIdentity, azurerm_key_vault_access_policy.client] +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + encryption_type = "EncryptionAtRestWithPlatformKey" + + encryption { + key_vault_key_id = azurerm_key_vault_key.test.versionless_id + user_assigned_identity_id = azurerm_user_assigned_identity.test.id + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } + + network_rule { + subnet_id = azurerm_subnet.test.id + action = "Allow" + } +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest-uai-${var.random_integer}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-${var.random_integer}" + 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 = "test-subnet-${var.random_integer}" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + service_endpoints = ["Microsoft.Storage.Global"] + +} + +resource "azurerm_key_vault" "test" { + name = "acctestvg${var.random_string}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = true + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "userAssignedIdentity" { + 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"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_access_policy" "client" { + 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 = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvk${var.random_string}" + 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.userAssignedIdentity, azurerm_key_vault_access_policy.client] +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + encryption_type = "EncryptionAtRestWithCustomerManagedKey" + + encryption { + key_vault_key_id = azurerm_key_vault_key.test.versionless_id + user_assigned_identity_id = azurerm_user_assigned_identity.test.id + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } + + network_rule { + subnet_id = azurerm_subnet.test.id + action = "Allow" + } +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest-uai-${var.random_integer}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-${var.random_integer}" + 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 = "test-subnet-${var.random_integer}" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + service_endpoints = ["Microsoft.Storage.Global"] + +} + +resource "azurerm_subnet" "test2" { + name = "test-subnet2-${var.random_integer}" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + service_endpoints = ["Microsoft.Storage.Global"] + +} + +resource "azurerm_key_vault" "test" { + name = "acctestvg${var.random_string}" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = true + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "userAssignedIdentity" { + 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"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_access_policy" "client" { + 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 = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvk${var.random_string}" + 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.client] +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id + encryption_type = "EncryptionAtRestWithCustomerManagedKey" + protocol_type = "Iscsi" + + encryption { + key_vault_key_id = azurerm_key_vault_key.test.id + user_assigned_identity_id = azurerm_user_assigned_identity.test.id + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } + + network_rule { + subnet_id = azurerm_subnet.test.id + action = "Allow" + } + + network_rule { + subnet_id = azurerm_subnet.test2.id + } + +} +`, r.template(data)) +} + +func (r ElasticSANVolumeGroupTestResource) template(data acceptance.TestData) string { + // some of the features are supported in limited regions, see https://learn.microsoft.com/azure/storage/elastic-san/elastic-san-networking-concepts#regional-availability + volumeGroupTestLocation := "westus2" + return fmt.Sprintf(` +variable "primary_location" { + default = %q +} +variable "random_integer" { + default = %d +} +variable "random_string" { + default = %q +} + +resource "azurerm_resource_group" "test" { + name = "acctestrg-esvg-${var.random_integer}" + location = var.primary_location +} + +resource "azurerm_elastic_san" "test" { + name = "acctestes-${var.random_string}" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + base_size_in_tib = 1 + sku { + name = "Premium_LRS" + } +} +`, volumeGroupTestLocation, data.RandomInteger, data.RandomString) +} diff --git a/internal/services/elasticsan/registration.go b/internal/services/elasticsan/registration.go index 1eaae0d1df10..120f713cf61a 100644 --- a/internal/services/elasticsan/registration.go +++ b/internal/services/elasticsan/registration.go @@ -21,6 +21,7 @@ func (Registration) DataSources() []sdk.DataSource { func (Registration) Resources() []sdk.Resource { return []sdk.Resource{ ElasticSANResource{}, + ElasticSANVolumeGroupResource{}, } } diff --git a/internal/services/elasticsan/validate/elastic_san_volume_group_name.go b/internal/services/elasticsan/validate/elastic_san_volume_group_name.go new file mode 100644 index 000000000000..4af482038188 --- /dev/null +++ b/internal/services/elasticsan/validate/elastic_san_volume_group_name.go @@ -0,0 +1,24 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func ElasticSanVolumeGroupName(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string but it wasn't!", k)) + return + } + + if matched := regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{1,61}[a-z0-9]$`).Match([]byte(v)); !matched { + errors = append(errors, fmt.Errorf("%q must be between 3 and 63 characters. It can contain only lowercase letters, numbers, underscores (_) and hyphens (-). It must start and end with a lowercase letter or number", k)) + } + + if matched := regexp.MustCompile(`[_-][_-]`).Match([]byte(v)); matched { + errors = append(errors, fmt.Errorf("%q must have hyphens and underscores be surrounded by alphanumeric character", k)) + } + + return warnings, errors +} diff --git a/internal/services/elasticsan/validate/elastic_san_volume_group_name_test.go b/internal/services/elasticsan/validate/elastic_san_volume_group_name_test.go new file mode 100644 index 000000000000..627347841d2e --- /dev/null +++ b/internal/services/elasticsan/validate/elastic_san_volume_group_name_test.go @@ -0,0 +1,104 @@ +package validate + +import "testing" + +func TestElasticSanVolumeGroupName(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // basic example + input: "hello", + expected: true, + }, + { + // 2 chars + input: "ab", + expected: false, + }, + { + // 3 chars + input: "abc", + expected: true, + }, + { + // 63 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk", + expected: true, + }, + { + // 64 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl", + expected: false, + }, + { + // may contain alphanumerics, dashes and underscores + input: "hello_world7-goodbye", + expected: true, + }, + { + // must begin with an alphanumeric + input: "_hello", + expected: false, + }, + { + // can't end with a dash + input: "hello-", + expected: false, + }, + { + // can end with an underscore + input: "hello_", + expected: false, + }, + { + // cannot have consecutive underscore + input: "hello__world", + expected: false, + }, + { + // cannot have consecutive dash + input: "hello--world", + expected: false, + }, + { + // cannot have consecutive underscore or dash + input: "hello-_world", + expected: false, + }, + { + // can't contain an exclamation mark + input: "hello!", + expected: false, + }, + { + // start with a number + input: "0abc", + expected: true, + }, + { + // contain only numbers + input: "12345", + expected: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := ElasticSanVolumeGroupName(v.input, "name") + actual := len(errors) == 0 + if v.expected != actual { + if len(errors) > 0 { + t.Logf("[DEBUG] Errors: %v", errors) + } + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown new file mode 100644 index 000000000000..54cf60a9e613 --- /dev/null +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -0,0 +1,211 @@ +--- +subcategory: "Elastic SAN" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_elastic_san_volume_group" +description: |- + Manages an Elastic SAN Volume Group resource. +--- + +# azurerm_elastic_san_volume_group + +Manages an Elastic SAN Volume Group resource. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "West Europe" +} + +resource "azurerm_elastic_san" "example" { + name = "examplees-es" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + base_size_in_tib = 1 + sku { + name = "Premium_LRS" + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_user_assigned_identity" "example" { + name = "example-uai" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_virtual_network" "example" { + name = "example-vnet" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_subnet" "example" { + name = "example-subnet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.0.1.0/24"] + service_endpoints = ["Microsoft.Storage.Global"] + +} + +resource "azurerm_key_vault" "example" { + name = "examplekv" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = true + sku_name = "standard" +} + +resource "azurerm_key_vault_access_policy" "userAssignedIdentity" { + key_vault_id = azurerm_key_vault.example.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_user_assigned_identity.example.principal_id + + key_permissions = ["Get", "UnwrapKey", "WrapKey"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_access_policy" "client" { + key_vault_id = azurerm_key_vault.example.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_key" "example" { + name = "example-kvk" + key_vault_id = azurerm_key_vault.example.id + key_type = "RSA" + key_size = 2048 + + key_opts = [ + "decrypt", + "encrypt", + "sign", + "unwrapKey", + "verify", + "wrapKey", + ] + + depends_on = [azurerm_key_vault_access_policy.userAssignedIdentity, azurerm_key_vault_access_policy.client] +} + +resource "azurerm_elastic_san_volume_group" "example" { + name = "example-esvg" + elastic_san_id = azurerm_elastic_san.example.id + encryption_type = "EncryptionAtRestWithCustomerManagedKey" + + encryption { + key_vault_key_id = azurerm_key_vault_key.example.versionless_id + user_assigned_identity_id = azurerm_user_assigned_identity.example.id + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.example.id] + } + + network_rule { + subnet_id = azurerm_subnet.example.id + action = "Allow" + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of this Elastic SAN Volume Group. Changing this forces a new resource to be created. + +* `elastic_san_id` - (Required) Specifies the Elastic SAN ID within which this Elastic SAN Volume Group should exist. Changing this forces a new resource to be created. + +* `encryption_type` - (Optional) Specifies the type of the key used to encrypt the data of the disk. Possible values are `EncryptionAtRestWithCustomerManagedKey` and `EncryptionAtRestWithPlatformKey`. Defaults to `EncryptionAtRestWithPlatformKey`. + +* `encryption` - (Optional) An `encryption` block as defined below. + +-> **NOTE:** The `encryption` block can only be set when `encryption_type` is set to `EncryptionAtRestWithCustomerManagedKey`. + +* `identity` - (Optional) An `identity` block as defined below. Specifies the Managed Identity which should be assigned to this Elastic SAN Volume Group. + +* `network_rule` - (Optional) One or more `network_rule` blocks as defined below. + +* `protocol_type` - (Optional) Specifies the type of the storage target. The only possible value is `Iscsi`. Defaults to `Iscsi`. + +--- + +An `encryption` block supports the following arguments: + +* `key_vault_key_id` - (Required) The Key Vault key URI for Customer Managed Key encryption, which can be either a full URI or a versionless URI. + +* `user_assigned_identity_id` - (Optional) The ID of the User Assigned Identity used by this Elastic SAN Volume Group. + +--- + +An `identity` block supports the following arguments: + +* `type` - (Required) Specifies the type of Managed Identity that should be assigned to this Elastic SAN Volume Group. Possible values are `SystemAssigned` and `UserAssigned`. + +* `identity_ids` - (Optional) A list of the User Assigned Identity IDs that should be assigned to this Elastic SAN Volume Group. + +--- + +A `network_rule` block supports the following arguments: + +* `subnet_id` - (Required) The ID of the Subnet which should be allowed to access this Elastic SAN Volume Group. + +* `action` - (Optional) The action to take when the Subnet attempts to access this Elastic SAN Volume Group. The only possible value is `Allow`. Defaults to `Allow`. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Elastic SAN Volume Group. + +* `encryption` - An `encryption` block as defined below. + +* `identity` - An `identity` block as defined below. + +--- + +An `encryption` block exports the following arguments: + +* `current_versioned_key_expiration_timestamp` - The timestamp of the expiration time for the current version of the customer managed key. + +* `current_versioned_key_id` - The ID of the current versioned Key Vault Key in use. + +* `last_key_rotation_timestamp` - The timestamp of the last rotation of the Key Vault Key. + +--- + +An `identity` block exports the following arguments: + +* `principal_id` - The Principal ID associated with the Managed Service Identity assigned to this Elastic SAN Volume Group. + +* `tenant_id` - The Tenant ID associated with this Managed Service Identity assigned to this Elastic SAN Volume Group. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating this Elastic SAN Volume Group. +* `delete` - (Defaults to 30 minutes) Used when deleting this Elastic SAN Volume Group. +* `read` - (Defaults to 5 minutes) Used when retrieving this Elastic SAN Volume Group. +* `update` - (Defaults to 30 minutes) Used when updating this Elastic SAN Volume Group. + +## Import + +An existing Elastic SAN Volume Group can be imported into Terraform using the `resource id`, e.g. + +```shell +terraform import azurerm_elastic_san_volume_group.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ElasticSan/elasticSans/esan1/volumeGroups/vg1 +```