From 55bb2e8cd94e4e4fc3311b29158610d59ffce2b1 Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:27:40 +0000 Subject: [PATCH 1/9] new resource elastic_san_volume_group --- .../elastic_san_volume_group_resource.go | 444 +++++++++++++ .../elastic_san_volume_group_resource_test.go | 621 ++++++++++++++++++ internal/services/elasticsan/registration.go | 1 + .../validate/elastic_san_volumn_group_name.go | 24 + .../elastic_san_volumn_group_name_test.go | 104 +++ .../r/elastic_san_volume_group.html.markdown | 190 ++++++ 6 files changed, 1384 insertions(+) create mode 100644 internal/services/elasticsan/elastic_san_volume_group_resource.go create mode 100644 internal/services/elasticsan/elastic_san_volume_group_resource_test.go create mode 100644 internal/services/elasticsan/validate/elastic_san_volumn_group_name.go create mode 100644 internal/services/elasticsan/validate/elastic_san_volumn_group_name_test.go create mode 100644 website/docs/r/elastic_san_volume_group.html.markdown 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..ef739cde7ea9 --- /dev/null +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -0,0 +1,444 @@ +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{} + +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 { + ElasticSanId 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.ElasticSanVolumnGroupName, + }, + + "elastic_san_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: volumegroups.ValidateElasticSanID, + }, + + "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{ + "user_assigned_identity_id": { + Optional: true, + Type: pluginsdk.TypeString, + ValidateFunc: commonids.ValidateUserAssignedIdentityID, + }, + "key_vault_key_id": { + Required: true, + Type: pluginsdk.TypeString, + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + }, + "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 exposed + string(volumegroups.StorageTargetTypeIscsi), + }, false), + Default: string(volumegroups.StorageTargetTypeIscsi), + }, + + "identity": commonschema.SystemOrUserAssignedIdentityOptional(), + } +} + +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.ElasticSanId) + 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.ElasticSanId = 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 && *model.Properties.ProtocolType != volumegroups.StorageTargetTypeNone { + 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 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() + } + + 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: pointer.From(input.KeyVaultProperties.CurrentVersionedKeyExpirationTimestamp), + CurrentVersionedKeyId: pointer.From(input.KeyVaultProperties.CurrentVersionedKeyIdentifier), + LastKeyRotationTimestamp: pointer.From(input.KeyVaultProperties.LastKeyRotationTimestamp), + }, + }, nil +} + +func ExpandVolumeGroupNetworkRules(input []ElasticSANVolumeGroupResourceNetworkRuleModel) *volumegroups.NetworkRuleSet { + 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 { + return []ElasticSANVolumeGroupResourceNetworkRuleModel{} + } + + var networkRules []ElasticSANVolumeGroupResourceNetworkRuleModel + 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..a84be44b478f --- /dev/null +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -0,0 +1,621 @@ +package elasticsan_test + +import ( + "context" + "fmt" + "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_identity(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_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" "userAssignedIdentity" { + 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.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.id + } + + identity { + type = "SystemAssigned" + } +} +`, 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.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" + 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/en-us/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-${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_volumn_group_name.go b/internal/services/elasticsan/validate/elastic_san_volumn_group_name.go new file mode 100644 index 000000000000..66c6bc542187 --- /dev/null +++ b/internal/services/elasticsan/validate/elastic_san_volumn_group_name.go @@ -0,0 +1,24 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func ElasticSanVolumnGroupName(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_volumn_group_name_test.go b/internal/services/elasticsan/validate/elastic_san_volumn_group_name_test.go new file mode 100644 index 000000000000..ff3db2028815 --- /dev/null +++ b/internal/services/elasticsan/validate/elastic_san_volumn_group_name_test.go @@ -0,0 +1,104 @@ +package validate + +import "testing" + +func TestElasticSanVolumnGroupName(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 := ElasticSanVolumnGroupName(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..271f9104f477 --- /dev/null +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -0,0 +1,190 @@ +--- +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 + +provider "azurerm" { + features {} +} + +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_role_assignment" "client" { + scope = azurerm_resource_group.example.id + role_definition_name = "Key Vault Crypto Officer" + principal_id = data.azurerm_client_config.current.object_id +} + +resource "azurerm_role_assignment" "userAssignedIdentity" { + scope = azurerm_resource_group.example.id + role_definition_name = "Key Vault Crypto Service Encryption User" + principal_id = azurerm_user_assigned_identity.example.principal_id +} + +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_role_assignment.userAssignedIdentity, azurerm_role_assignment.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) Type of encryption. 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) The protocol type of the Elastic SAN Volume Group. The only possible value is `Iscsi`. Defaults to `Iscsi`. + +--- + +An `encryption` block supports the following arguments: + +* `identity` - (Optional) An `identity` block as defined below. +* `key_vault_key_id` - (Optional) + +--- + +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. + +--- + +An `encryption` block exports the following arguments: + +* `current_versioned_key_expiration_timestamp` - Current Versioned Key Expiration Timestamp. + +* `current_versioned_key_id` - Current Versioned Key ID. + +* `last_key_rotation_timestamp` - Last Key Rotation Timestamp. + +--- + +An `identity` block exports the following arguments: + +* `principal_id` - The Principal ID for the System-Assigned Managed Identity assigned to this Elastic SAN Volume Group. + +* `tenant_id` - The Tenant ID for the System-Assigned Managed 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 +``` From e52435a4f65e1ad5edd091d4467e75ed2cde742a Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Tue, 12 Dec 2023 02:57:27 +0000 Subject: [PATCH 2/9] fix --- .../elasticsan/elastic_san_resource.go | 1 + .../elastic_san_volume_group_resource.go | 19 +++ .../elastic_san_volume_group_resource_test.go | 123 +++++++++++++++++- .../r/elastic_san_volume_group.html.markdown | 45 +++++-- 4 files changed, 170 insertions(+), 18 deletions(-) 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 index ef739cde7ea9..9b77bbffbe20 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -21,6 +21,7 @@ import ( var _ sdk.Resource = ElasticSANVolumeGroupResource{} var _ sdk.ResourceWithUpdate = ElasticSANVolumeGroupResource{} +var _ sdk.ResourceWithCustomizeDiff = ElasticSANVolumeGroupResource{} type ElasticSANVolumeGroupResource struct{} @@ -149,6 +150,24 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema } } +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") + } + + return nil + }, + } +} + func (r ElasticSANVolumeGroupResource) Attributes() map[string]*pluginsdk.Schema { return map[string]*pluginsdk.Schema{} } diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go index a84be44b478f..cf463bac0ec0 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -3,6 +3,7 @@ package elasticsan_test import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/go-azure-sdk/resource-manager/elasticsan/2023-01-01/volumegroups" @@ -96,6 +97,18 @@ func TestAccElasticSANVolumeGroup_identity(t *testing.T) { }) } +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{} @@ -318,7 +331,7 @@ resource "azurerm_key_vault" "test" { sku_name = "standard" } -resource "azurerm_key_vault_access_policy" "userAssignedIdentity" { +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 @@ -351,7 +364,7 @@ resource "azurerm_key_vault_key" "test" { "wrapKey", ] - depends_on = [azurerm_key_vault_access_policy.userAssignedIdentity, azurerm_key_vault_access_policy.client] + depends_on = [azurerm_key_vault_access_policy.client] } resource "azurerm_elastic_san_volume_group" "test" { @@ -370,6 +383,108 @@ resource "azurerm_elastic_san_volume_group" "test" { `, 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 @@ -557,7 +672,7 @@ resource "azurerm_key_vault_key" "test" { "wrapKey", ] - depends_on = [azurerm_key_vault_access_policy.userAssignedIdentity, azurerm_key_vault_access_policy.client] + depends_on = [azurerm_key_vault_access_policy.client] } resource "azurerm_elastic_san_volume_group" "test" { @@ -604,7 +719,7 @@ variable "random_string" { } resource "azurerm_resource_group" "test" { - name = "acctestrg-${var.random_integer}" + name = "acctestrg-esvg-${var.random_integer}" location = var.primary_location } diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index 271f9104f477..3e6d924aeaa7 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -13,9 +13,19 @@ Manages an Elastic SAN Volume Group resource. ## Example Usage ```hcl +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "West Europe" +} -provider "azurerm" { - features {} +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" {} @@ -53,16 +63,22 @@ resource "azurerm_key_vault" "example" { sku_name = "standard" } -resource "azurerm_role_assignment" "client" { - scope = azurerm_resource_group.example.id - role_definition_name = "Key Vault Crypto Officer" - principal_id = data.azurerm_client_config.current.object_id +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_role_assignment" "userAssignedIdentity" { - scope = azurerm_resource_group.example.id - role_definition_name = "Key Vault Crypto Service Encryption User" - principal_id = azurerm_user_assigned_identity.example.principal_id +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" { @@ -80,7 +96,7 @@ resource "azurerm_key_vault_key" "example" { "wrapKey", ] - depends_on = [azurerm_role_assignment.userAssignedIdentity, azurerm_role_assignment.client] + depends_on = [azurerm_key_vault_access_policy.userAssignedIdentity, azurerm_key_vault_access_policy.client] } resource "azurerm_elastic_san_volume_group" "example" { @@ -129,8 +145,9 @@ The following arguments are supported: An `encryption` block supports the following arguments: -* `identity` - (Optional) An `identity` block as defined below. -* `key_vault_key_id` - (Optional) +* `identity` - (Optional) An `identity` block as defined below. + +* `key_vault_key_id` - (Optional) The Key Vault key URI for Customer Managed Key encryption, which can be either a full URI or a versionless URI. --- @@ -158,7 +175,7 @@ In addition to the Arguments listed above - the following Attributes are exporte An `encryption` block exports the following arguments: -* `current_versioned_key_expiration_timestamp` - Current Versioned Key Expiration Timestamp. +* `current_versioned_key_expiration_timestamp` - Current Versioned Key Expiration Timestamp of the Elastic SAN Volume Group. * `current_versioned_key_id` - Current Versioned Key ID. From bc529bcf917dfbb1cf854b5067d1aac93bf0929c Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Thu, 21 Dec 2023 08:40:12 +0000 Subject: [PATCH 3/9] fix --- .../elastic_san_volume_group_resource.go | 21 +++++++++--------- .../r/elastic_san_volume_group.html.markdown | 22 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource.go b/internal/services/elasticsan/elastic_san_volume_group_resource.go index 9b77bbffbe20..510030a2ef36 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -38,7 +38,7 @@ func (r ElasticSANVolumeGroupResource) ModelObject() interface{} { } type ElasticSANVolumeGroupResourceModel struct { - ElasticSanId string `tfschema:"elastic_san_id"` + SanId string `tfschema:"san_id"` EncryptionType string `tfschema:"encryption_type"` Encryption []ElasticSANVolumeGroupResourceEncryptionModel `tfschema:"encryption"` Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` @@ -69,7 +69,7 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema ValidateFunc: validate.ElasticSanVolumnGroupName, }, - "elastic_san_id": { + "san_id": { Type: pluginsdk.TypeString, Required: true, ForceNew: true, @@ -89,16 +89,16 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema MaxItems: 1, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ - "user_assigned_identity_id": { - Optional: true, - Type: pluginsdk.TypeString, - ValidateFunc: commonids.ValidateUserAssignedIdentityID, - }, "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, @@ -185,7 +185,7 @@ func (r ElasticSANVolumeGroupResource) Create() sdk.ResourceFunc { subscriptionId := metadata.Client.Account.SubscriptionId - elasticSanId, err := volumegroups.ParseElasticSanID(config.ElasticSanId) + elasticSanId, err := volumegroups.ParseElasticSanID(config.SanId) if err != nil { return err } @@ -255,7 +255,7 @@ func (r ElasticSANVolumeGroupResource) Read() sdk.ResourceFunc { } if model := resp.Model; model != nil { - schema.ElasticSanId = elasticSanId.ID() + schema.SanId = elasticSanId.ID() schema.Name = id.VolumeGroupName flattenedIdentity, err := identity.FlattenSystemOrUserAssignedMapToModel(model.Identity) @@ -427,6 +427,7 @@ func FlattenVolumeGroupEncryption(input *volumegroups.EncryptionProperties) ([]E } 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{}, @@ -447,7 +448,7 @@ func ExpandVolumeGroupNetworkRules(input []ElasticSANVolumeGroupResourceNetworkR } func FlattenVolumeGroupNetworkRules(input *volumegroups.NetworkRuleSet) []ElasticSANVolumeGroupResourceNetworkRuleModel { - if input == nil { + if input == nil || input.VirtualNetworkRules == nil { return []ElasticSANVolumeGroupResourceNetworkRuleModel{} } diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index 3e6d924aeaa7..dba61be97c01 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -129,7 +129,7 @@ The following arguments are supported: * `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) Type of encryption. Possible values are `EncryptionAtRestWithCustomerManagedKey` and `EncryptionAtRestWithPlatformKey`. Defaults to `EncryptionAtRestWithPlatformKey`. +* `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. @@ -139,15 +139,15 @@ The following arguments are supported: * `network_rule` - (Optional) One or more `network_rule` blocks as defined below. -* `protocol_type` - (Optional) The protocol type of the Elastic SAN Volume Group. The only possible value is `Iscsi`. Defaults to `Iscsi`. +* `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: -* `identity` - (Optional) An `identity` block as defined below. +* `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. -* `key_vault_key_id` - (Optional) 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. --- @@ -171,23 +171,27 @@ In addition to the Arguments listed above - the following Attributes are exporte * `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` - Current Versioned Key Expiration Timestamp of the Elastic SAN Volume Group. +* `current_versioned_key_expiration_timestamp` - The timestamp of the expiration time for the current version of the customer managed key. -* `current_versioned_key_id` - Current Versioned Key ID. +* `current_versioned_key_id` - The ID of the current versioned Key Vault Key in use. -* `last_key_rotation_timestamp` - Last Key Rotation Timestamp. +* `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 for the System-Assigned Managed Identity assigned to this Elastic SAN Volume Group. +* `principal_id` - The Principal ID associated with the Managed Service Identity assigned to this Elastic SAN Volume Group. -* `tenant_id` - The Tenant ID for the System-Assigned Managed 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 From 744940d503335eeac1bf5fdc680d84e44a1f1b2b Mon Sep 17 00:00:00 2001 From: Tao <104055472+teowa@users.noreply.github.com> Date: Thu, 21 Dec 2023 20:26:25 +0800 Subject: [PATCH 4/9] fix --- .../elastic_san_volume_group_resource_test.go | 28 +++++++++---------- .../r/elastic_san_volume_group.html.markdown | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go index cf463bac0ec0..936a94c90294 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -183,8 +183,8 @@ provider "azurerm" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + san_id = azurerm_elastic_san.test.id } `, r.template(data)) } @@ -194,8 +194,8 @@ func (r ElasticSANVolumeGroupTestResource) requiresImport(data acceptance.TestDa %s resource "azurerm_elastic_san_volume_group" "import" { - name = azurerm_elastic_san_volume_group.test.name - elastic_san_id = azurerm_elastic_san.test.id + name = azurerm_elastic_san_volume_group.test.name + san_id = azurerm_elastic_san.test.id } `, r.basic(data)) } @@ -215,8 +215,8 @@ resource "azurerm_user_assigned_identity" "test" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + san_id = azurerm_elastic_san.test.id identity { type = "UserAssigned" identity_ids = [azurerm_user_assigned_identity.test.id] @@ -234,8 +234,8 @@ provider "azurerm" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + san_id = azurerm_elastic_san.test.id identity { type = "SystemAssigned" } @@ -301,8 +301,8 @@ resource "azurerm_key_vault_key" "test" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + san_id = azurerm_elastic_san.test.id identity { type = "SystemAssigned" } @@ -369,7 +369,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" encryption { @@ -464,7 +464,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithPlatformKey" encryption { @@ -566,7 +566,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" encryption { @@ -677,7 +677,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - elastic_san_id = azurerm_elastic_san.test.id + san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" protocol_type = "Iscsi" diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index dba61be97c01..a31a49412801 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -101,7 +101,7 @@ resource "azurerm_key_vault_key" "example" { resource "azurerm_elastic_san_volume_group" "example" { name = "example-esvg" - elastic_san_id = azurerm_elastic_san.example.id + san_id = azurerm_elastic_san.example.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" encryption { @@ -127,7 +127,7 @@ 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. +* `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`. From c21af09db1e8269ce26d6e82931ad7e1f15e2133 Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Tue, 26 Dec 2023 10:29:14 +0000 Subject: [PATCH 5/9] fix --- .../elasticsan/elastic_san_volume_group_resource.go | 11 +++-------- .../elastic_san_volume_group_resource_test.go | 4 ++++ website/docs/r/elastic_san_volume_group.html.markdown | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource.go b/internal/services/elasticsan/elastic_san_volume_group_resource.go index 510030a2ef36..e6f00715e135 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -69,12 +69,7 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema ValidateFunc: validate.ElasticSanVolumnGroupName, }, - "san_id": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: volumegroups.ValidateElasticSanID, - }, + "san_id": commonschema.ResourceIDReferenceRequiredForceNew(volumegroups.ElasticSanId{}), "encryption_type": { Type: pluginsdk.TypeString, @@ -140,8 +135,8 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ - // None is not exposed string(volumegroups.StorageTargetTypeIscsi), + string(volumegroups.StorageTargetTypeNone), }, false), Default: string(volumegroups.StorageTargetTypeIscsi), }, @@ -268,7 +263,7 @@ func (r ElasticSANVolumeGroupResource) Read() sdk.ResourceFunc { schema.EncryptionType = string(pointer.From(model.Properties.Encryption)) schema.NetworkRule = FlattenVolumeGroupNetworkRules(model.Properties.NetworkAcls) - if model.Properties.ProtocolType != nil && *model.Properties.ProtocolType != volumegroups.StorageTargetTypeNone { + if model.Properties.ProtocolType != nil { schema.ProtocolType = string(pointer.From(model.Properties.ProtocolType)) } diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go index 936a94c90294..fb1d9d5d2d55 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -217,6 +217,7 @@ resource "azurerm_user_assigned_identity" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" san_id = azurerm_elastic_san.test.id + identity { type = "UserAssigned" identity_ids = [azurerm_user_assigned_identity.test.id] @@ -236,6 +237,7 @@ provider "azurerm" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" san_id = azurerm_elastic_san.test.id + identity { type = "SystemAssigned" } @@ -303,6 +305,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" san_id = azurerm_elastic_san.test.id + identity { type = "SystemAssigned" } @@ -568,6 +571,7 @@ resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" + protocol_type = "None" encryption { key_vault_key_id = azurerm_key_vault_key.test.versionless_id diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index a31a49412801..6088957cb4da 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -139,7 +139,7 @@ The following arguments are supported: * `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`. +* `protocol_type` - (Optional) Specifies the type of the storage target. The only possible values are `Iscsi` and `None`. Defaults to `Iscsi`. --- From 9c6a41844c2a307e3b124de613e01fc66c864ef4 Mon Sep 17 00:00:00 2001 From: Tao <104055472+teowa@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:15:16 +0800 Subject: [PATCH 6/9] remove None --- .../services/elasticsan/elastic_san_volume_group_resource.go | 2 +- .../elasticsan/elastic_san_volume_group_resource_test.go | 1 - website/docs/r/elastic_san_volume_group.html.markdown | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource.go b/internal/services/elasticsan/elastic_san_volume_group_resource.go index e6f00715e135..c8cf374e721e 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -135,8 +135,8 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ + // None is not exposed, Iscsi is only allowed protocolType to connect to volume group string(volumegroups.StorageTargetTypeIscsi), - string(volumegroups.StorageTargetTypeNone), }, false), Default: string(volumegroups.StorageTargetTypeIscsi), }, diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go index fb1d9d5d2d55..aa3827120085 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -571,7 +571,6 @@ resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" - protocol_type = "None" encryption { key_vault_key_id = azurerm_key_vault_key.test.versionless_id diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index 6088957cb4da..bef74ed12758 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -139,7 +139,7 @@ The following arguments are supported: * `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 values are `Iscsi` and `None`. Defaults to `Iscsi`. +* `protocol_type` - (Optional) Specifies the type of the storage target. The only possible values are `Iscsi`. Defaults to `Iscsi`. --- From d47b3f76e2bb860ce732c2692355bf820e18c685 Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Thu, 28 Dec 2023 03:09:24 +0000 Subject: [PATCH 7/9] fix --- website/docs/r/elastic_san_volume_group.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index bef74ed12758..a31a49412801 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -139,7 +139,7 @@ The following arguments are supported: * `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 values are `Iscsi`. Defaults to `Iscsi`. +* `protocol_type` - (Optional) Specifies the type of the storage target. The only possible value is `Iscsi`. Defaults to `Iscsi`. --- From 2fe88ea0cc4c50ce519d4b5f98c1b324160bbbec Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Thu, 28 Dec 2023 03:10:38 +0000 Subject: [PATCH 8/9] fix --- .../services/elasticsan/elastic_san_volume_group_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource.go b/internal/services/elasticsan/elastic_san_volume_group_resource.go index c8cf374e721e..dd7ab8a745e2 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -135,7 +135,7 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ - // None is not exposed, Iscsi is only allowed protocolType to connect to volume group + // None is not a valid value and service team will consider removing it in future versions. string(volumegroups.StorageTargetTypeIscsi), }, false), Default: string(volumegroups.StorageTargetTypeIscsi), From e0c103810341729576185cef28a81fc3b7d16432 Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:28:30 +0000 Subject: [PATCH 9/9] fix --- .../elastic_san_volume_group_resource.go | 24 +++++++++----- .../elastic_san_volume_group_resource_test.go | 32 +++++++++---------- ...me.go => elastic_san_volume_group_name.go} | 2 +- ... => elastic_san_volume_group_name_test.go} | 4 +-- .../r/elastic_san_volume_group.html.markdown | 6 ++-- 5 files changed, 38 insertions(+), 30 deletions(-) rename internal/services/elasticsan/validate/{elastic_san_volumn_group_name.go => elastic_san_volume_group_name.go} (92%) rename internal/services/elasticsan/validate/{elastic_san_volumn_group_name_test.go => elastic_san_volume_group_name_test.go} (94%) diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource.go b/internal/services/elasticsan/elastic_san_volume_group_resource.go index dd7ab8a745e2..c41868254379 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource.go @@ -38,7 +38,7 @@ func (r ElasticSANVolumeGroupResource) ModelObject() interface{} { } type ElasticSANVolumeGroupResourceModel struct { - SanId string `tfschema:"san_id"` + SanId string `tfschema:"elastic_san_id"` EncryptionType string `tfschema:"encryption_type"` Encryption []ElasticSANVolumeGroupResourceEncryptionModel `tfschema:"encryption"` Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` @@ -66,10 +66,10 @@ func (r ElasticSANVolumeGroupResource) Arguments() map[string]*pluginsdk.Schema Type: pluginsdk.TypeString, Required: true, ForceNew: true, - ValidateFunc: validate.ElasticSanVolumnGroupName, + ValidateFunc: validate.ElasticSanVolumeGroupName, }, - "san_id": commonschema.ResourceIDReferenceRequiredForceNew(volumegroups.ElasticSanId{}), + "elastic_san_id": commonschema.ResourceIDReferenceRequiredForceNew(volumegroups.ElasticSanId{}), "encryption_type": { Type: pluginsdk.TypeString, @@ -158,6 +158,10 @@ func (k ElasticSANVolumeGroupResource) CustomizeDiff() sdk.ResourceFunc { 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 }, } @@ -390,7 +394,7 @@ func FlattenVolumeGroupEncryption(input *volumegroups.EncryptionProperties) ([]E return []ElasticSANVolumeGroupResourceEncryptionModel{}, nil } - var keyVaultKeyId string + 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 { @@ -398,6 +402,10 @@ func FlattenVolumeGroupEncryption(input *volumegroups.EncryptionProperties) ([]E } keyVaultKeyId = id.ID() + + currentVersionedKeyExpirationTimestamp = pointer.From(input.KeyVaultProperties.CurrentVersionedKeyExpirationTimestamp) + currentVersionedKeyId = pointer.From(input.KeyVaultProperties.CurrentVersionedKeyIdentifier) + lastKeyRotationTimestamp = pointer.From(input.KeyVaultProperties.LastKeyRotationTimestamp) } var userAssignedIdentityId string @@ -414,9 +422,9 @@ func FlattenVolumeGroupEncryption(input *volumegroups.EncryptionProperties) ([]E { KeyVaultKeyId: keyVaultKeyId, UserAssignedIdentityId: userAssignedIdentityId, - CurrentVersionedKeyExpirationTimestamp: pointer.From(input.KeyVaultProperties.CurrentVersionedKeyExpirationTimestamp), - CurrentVersionedKeyId: pointer.From(input.KeyVaultProperties.CurrentVersionedKeyIdentifier), - LastKeyRotationTimestamp: pointer.From(input.KeyVaultProperties.LastKeyRotationTimestamp), + CurrentVersionedKeyExpirationTimestamp: currentVersionedKeyExpirationTimestamp, + CurrentVersionedKeyId: currentVersionedKeyId, + LastKeyRotationTimestamp: lastKeyRotationTimestamp, }, }, nil } @@ -447,7 +455,7 @@ func FlattenVolumeGroupNetworkRules(input *volumegroups.NetworkRuleSet) []Elasti return []ElasticSANVolumeGroupResourceNetworkRuleModel{} } - var networkRules []ElasticSANVolumeGroupResourceNetworkRuleModel + networkRules := make([]ElasticSANVolumeGroupResourceNetworkRuleModel, 0) for _, rule := range *input.VirtualNetworkRules { networkRules = append(networkRules, ElasticSANVolumeGroupResourceNetworkRuleModel{ SubnetId: rule.Id, diff --git a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go index aa3827120085..fb86573d6987 100644 --- a/internal/services/elasticsan/elastic_san_volume_group_resource_test.go +++ b/internal/services/elasticsan/elastic_san_volume_group_resource_test.go @@ -68,7 +68,7 @@ func TestAccElasticSANVolumeGroup_encryptionWithSystemAssignedIdentity(t *testin }) } -func TestAccElasticSANVolumeGroup_identity(t *testing.T) { +func TestAccElasticSANVolumeGroup_updateIdentity(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_elastic_san_volume_group", "test") r := ElasticSANVolumeGroupTestResource{} @@ -183,8 +183,8 @@ provider "azurerm" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id } `, r.template(data)) } @@ -194,8 +194,8 @@ func (r ElasticSANVolumeGroupTestResource) requiresImport(data acceptance.TestDa %s resource "azurerm_elastic_san_volume_group" "import" { - name = azurerm_elastic_san_volume_group.test.name - san_id = azurerm_elastic_san.test.id + name = azurerm_elastic_san_volume_group.test.name + elastic_san_id = azurerm_elastic_san.test.id } `, r.basic(data)) } @@ -215,8 +215,8 @@ resource "azurerm_user_assigned_identity" "test" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id identity { type = "UserAssigned" @@ -235,8 +235,8 @@ provider "azurerm" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id identity { type = "SystemAssigned" @@ -303,8 +303,8 @@ resource "azurerm_key_vault_key" "test" { } resource "azurerm_elastic_san_volume_group" "test" { - name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + name = "acctestesvg-${var.random_string}" + elastic_san_id = azurerm_elastic_san.test.id identity { type = "SystemAssigned" @@ -372,7 +372,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + elastic_san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" encryption { @@ -467,7 +467,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + elastic_san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithPlatformKey" encryption { @@ -569,7 +569,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + elastic_san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" encryption { @@ -680,7 +680,7 @@ resource "azurerm_key_vault_key" "test" { resource "azurerm_elastic_san_volume_group" "test" { name = "acctestesvg-${var.random_string}" - san_id = azurerm_elastic_san.test.id + elastic_san_id = azurerm_elastic_san.test.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" protocol_type = "Iscsi" @@ -708,7 +708,7 @@ resource "azurerm_elastic_san_volume_group" "test" { } func (r ElasticSANVolumeGroupTestResource) template(data acceptance.TestData) string { - // some of the features are supported in limited regions, see https://learn.microsoft.com/en-us/azure/storage/elastic-san/elastic-san-networking-concepts#regional-availability + // 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" { diff --git a/internal/services/elasticsan/validate/elastic_san_volumn_group_name.go b/internal/services/elasticsan/validate/elastic_san_volume_group_name.go similarity index 92% rename from internal/services/elasticsan/validate/elastic_san_volumn_group_name.go rename to internal/services/elasticsan/validate/elastic_san_volume_group_name.go index 66c6bc542187..4af482038188 100644 --- a/internal/services/elasticsan/validate/elastic_san_volumn_group_name.go +++ b/internal/services/elasticsan/validate/elastic_san_volume_group_name.go @@ -5,7 +5,7 @@ import ( "regexp" ) -func ElasticSanVolumnGroupName(i interface{}, k string) (warnings []string, errors []error) { +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)) diff --git a/internal/services/elasticsan/validate/elastic_san_volumn_group_name_test.go b/internal/services/elasticsan/validate/elastic_san_volume_group_name_test.go similarity index 94% rename from internal/services/elasticsan/validate/elastic_san_volumn_group_name_test.go rename to internal/services/elasticsan/validate/elastic_san_volume_group_name_test.go index ff3db2028815..627347841d2e 100644 --- a/internal/services/elasticsan/validate/elastic_san_volumn_group_name_test.go +++ b/internal/services/elasticsan/validate/elastic_san_volume_group_name_test.go @@ -2,7 +2,7 @@ package validate import "testing" -func TestElasticSanVolumnGroupName(t *testing.T) { +func TestElasticSanVolumeGroupName(t *testing.T) { testData := []struct { input string expected bool @@ -92,7 +92,7 @@ func TestElasticSanVolumnGroupName(t *testing.T) { for _, v := range testData { t.Logf("[DEBUG] Testing %q..", v.input) - _, errors := ElasticSanVolumnGroupName(v.input, "name") + _, errors := ElasticSanVolumeGroupName(v.input, "name") actual := len(errors) == 0 if v.expected != actual { if len(errors) > 0 { diff --git a/website/docs/r/elastic_san_volume_group.html.markdown b/website/docs/r/elastic_san_volume_group.html.markdown index a31a49412801..54cf60a9e613 100644 --- a/website/docs/r/elastic_san_volume_group.html.markdown +++ b/website/docs/r/elastic_san_volume_group.html.markdown @@ -101,7 +101,7 @@ resource "azurerm_key_vault_key" "example" { resource "azurerm_elastic_san_volume_group" "example" { name = "example-esvg" - san_id = azurerm_elastic_san.example.id + elastic_san_id = azurerm_elastic_san.example.id encryption_type = "EncryptionAtRestWithCustomerManagedKey" encryption { @@ -127,13 +127,13 @@ 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. -* `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. +* `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`. +-> **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.