diff --git a/internal/services/elasticsan/elastic_san_volume_snapshot_data_source.go b/internal/services/elasticsan/elastic_san_volume_snapshot_data_source.go new file mode 100644 index 000000000000..d767183e452a --- /dev/null +++ b/internal/services/elasticsan/elastic_san_volume_snapshot_data_source.go @@ -0,0 +1,120 @@ +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-sdk/resource-manager/elasticsan/2023-01-01/snapshots" + "github.com/hashicorp/go-azure-sdk/resource-manager/elasticsan/2023-01-01/volumes" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/elasticsan/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type ElasticSANVolumeSnapshotDataSource struct{} + +var _ sdk.DataSource = ElasticSANVolumeSnapshotDataSource{} + +type ElasticSANVolumeSnapshotDataSourceModel struct { + Name string `tfschema:"name"` + SourceId string `tfschema:"source_id"` + SourceVolumeSizeInGiB int64 `tfschema:"source_volume_size_in_gib"` + VolumeGroupId string `tfschema:"volume_group_id"` + VolumeName string `tfschema:"volume_name"` +} + +func (r ElasticSANVolumeSnapshotDataSource) ResourceType() string { + return "azurerm_elastic_san_volume_snapshot" +} + +func (r ElasticSANVolumeSnapshotDataSource) ModelObject() interface{} { + return &ElasticSANVolumeSnapshotDataSourceModel{} +} + +func (r ElasticSANVolumeSnapshotDataSource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.ElasticSanSnapshotName, + }, + + "volume_group_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: snapshots.ValidateVolumeGroupID, + }, + } +} + +func (r ElasticSANVolumeSnapshotDataSource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "source_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "source_volume_size_in_gib": { + Computed: true, + Type: pluginsdk.TypeInt, + }, + + "volume_name": { + Computed: true, + Type: pluginsdk.TypeString, + }, + } +} + +func (r ElasticSANVolumeSnapshotDataSource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ElasticSan.Snapshots + + var state ElasticSANVolumeSnapshotDataSourceModel + if err := metadata.Decode(&state); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + volumeGroupId, err := snapshots.ParseVolumeGroupID(state.VolumeGroupId) + if err != nil { + return err + } + + id := snapshots.NewSnapshotID(volumeGroupId.SubscriptionId, volumeGroupId.ResourceGroupName, volumeGroupId.ElasticSanName, volumeGroupId.VolumeGroupName, state.Name) + + resp, err := client.VolumeSnapshotsGet(ctx, id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return fmt.Errorf("%s does not exist", id) + } + + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + state.VolumeGroupId = volumeGroupId.ID() + state.Name = id.SnapshotName + if model := resp.Model; model != nil { + // these properties are not pointer so we don't need to check for nil + state.SourceVolumeSizeInGiB = pointer.From(model.Properties.SourceVolumeSizeGiB) + state.VolumeName = pointer.From(model.Properties.VolumeName) + + // only ElasticSAN Volumes are supported for now + volumeId, err := volumes.ParseVolumeIDInsensitively(model.Properties.CreationData.SourceId) + if err != nil { + return err + } + + state.SourceId = volumeId.ID() + } + metadata.SetID(id) + + return metadata.Encode(&state) + }, + } +} diff --git a/internal/services/elasticsan/elastic_san_volume_snapshot_data_source_test.go b/internal/services/elasticsan/elastic_san_volume_snapshot_data_source_test.go new file mode 100644 index 000000000000..438eb7bbacf7 --- /dev/null +++ b/internal/services/elasticsan/elastic_san_volume_snapshot_data_source_test.go @@ -0,0 +1,169 @@ +package elasticsan_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/go-azure-sdk/resource-manager/elasticsan/2023-01-01/snapshots" + "github.com/hashicorp/go-azure-sdk/resource-manager/elasticsan/2023-01-01/volumes" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "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" +) + +type ElasticSANVolumeSnapshotDataSource struct{} + +// https://github.com/hashicorp/terraform-provider-azurerm/pull/25372#issuecomment-2022105240 +// Elastic SAN Volume Snapshot is context-based and should not be regarded as the infrastructure managed by Terraform +// so we only onboard this as a data source instead of a resource. The acctest creates the snapshot as a test step +func TestAccElasticSANVolumeSnapshotDataSource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_elastic_san_volume_snapshot", "test") + d := ElasticSANVolumeSnapshotDataSource{} + + data.DataSourceTestInSequence(t, []acceptance.TestStep{ + { + Config: d.snapshotSource(data), + Check: acceptance.ComposeTestCheckFunc( + data.CheckWithClientForResource(func(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) error { + if _, ok := ctx.Deadline(); !ok { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 30*time.Minute) + defer cancel() + } + + volumeId, err := volumes.ParseVolumeID(state.ID) + if err != nil { + return err + } + + id := snapshots.NewSnapshotID(volumeId.SubscriptionId, volumeId.ResourceGroupName, volumeId.ElasticSanName, volumeId.VolumeGroupName, data.RandomString) + + snapshot := snapshots.Snapshot{ + Properties: snapshots.SnapshotProperties{ + CreationData: snapshots.SnapshotCreationData{ + SourceId: volumeId.ID(), + }, + }, + } + + client := clients.ElasticSan.Snapshots + if err = client.VolumeSnapshotsCreateThenPoll(ctx, id, snapshot); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + return nil + }, "azurerm_elastic_san_volume.test"), + ), + }, + { + Config: d.snapshotRestore(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("source_id").IsNotEmpty(), + check.That(data.ResourceName).Key("source_volume_size_in_gib").IsNotEmpty(), + check.That(data.ResourceName).Key("volume_name").IsNotEmpty(), + ), + }, + { + Config: d.snapshotSource(data), + Check: acceptance.ComposeTestCheckFunc( + data.CheckWithClientForResource(func(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) error { + if _, ok := ctx.Deadline(); !ok { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 30*time.Minute) + defer cancel() + } + + volumeId, err := volumes.ParseVolumeID(state.ID) + if err != nil { + return err + } + + id := snapshots.NewSnapshotID(volumeId.SubscriptionId, volumeId.ResourceGroupName, volumeId.ElasticSanName, volumeId.VolumeGroupName, data.RandomString) + + client := clients.ElasticSan.Snapshots + if err = client.VolumeSnapshotsDeleteThenPoll(ctx, id); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + return nil + }, "azurerm_elastic_san_volume.test"), + ), + }, + }) +} + +func (d ElasticSANVolumeSnapshotDataSource) snapshotSource(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestrg-esvg-%[2]d" + location = "%[1]s" +} + +resource "azurerm_elastic_san" "test" { + name = "acctestes-%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + base_size_in_tib = 1 + sku { + name = "Premium_LRS" + } +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-%[3]s" + elastic_san_id = azurerm_elastic_san.test.id +} + +resource "azurerm_elastic_san_volume" "test" { + name = "acctestesv-%[3]s" + volume_group_id = azurerm_elastic_san_volume_group.test.id + size_in_gib = 1 +} +`, data.Locations.Primary, data.RandomInteger, data.RandomString) +} + +func (d ElasticSANVolumeSnapshotDataSource) snapshotRestore(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestrg-esvg-%[2]d" + location = "%[1]s" +} + +resource "azurerm_elastic_san" "test" { + name = "acctestes-%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + base_size_in_tib = 1 + sku { + name = "Premium_LRS" + } +} + +resource "azurerm_elastic_san_volume_group" "test" { + name = "acctestesvg-%[3]s" + elastic_san_id = azurerm_elastic_san.test.id +} + +resource "azurerm_elastic_san_volume" "test" { + name = "acctestesv-%[3]s" + volume_group_id = azurerm_elastic_san_volume_group.test.id + size_in_gib = 1 +} + +data "azurerm_elastic_san_volume_snapshot" "test" { + name = "%[3]s" + volume_group_id = azurerm_elastic_san_volume_group.test.id +} +`, data.Locations.Primary, data.RandomInteger, data.RandomString) +} diff --git a/internal/services/elasticsan/registration.go b/internal/services/elasticsan/registration.go index 2b0e463cbb78..c339956a221b 100644 --- a/internal/services/elasticsan/registration.go +++ b/internal/services/elasticsan/registration.go @@ -21,6 +21,7 @@ func (Registration) DataSources() []sdk.DataSource { return []sdk.DataSource{ ElasticSANDataSource{}, ElasticSANVolumeGroupDataSource{}, + ElasticSANVolumeSnapshotDataSource{}, } } diff --git a/internal/services/elasticsan/validate/elastic_san_snapshot_name.go b/internal/services/elasticsan/validate/elastic_san_snapshot_name.go new file mode 100644 index 000000000000..6dba5d805105 --- /dev/null +++ b/internal/services/elasticsan/validate/elastic_san_snapshot_name.go @@ -0,0 +1,24 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func ElasticSanSnapshotName(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_snapshot_name_test.go b/internal/services/elasticsan/validate/elastic_san_snapshot_name_test.go new file mode 100644 index 000000000000..0da31827523a --- /dev/null +++ b/internal/services/elasticsan/validate/elastic_san_snapshot_name_test.go @@ -0,0 +1,104 @@ +package validate + +import "testing" + +func TestElasticSanSnapshotName(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, + }, + { + // cannot 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 := ElasticSanSnapshotName(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/d/elastic_san_volume_snapshot.html.markdown b/website/docs/d/elastic_san_volume_snapshot.html.markdown new file mode 100644 index 000000000000..5b7de8e16efc --- /dev/null +++ b/website/docs/d/elastic_san_volume_snapshot.html.markdown @@ -0,0 +1,60 @@ +--- +subcategory: "Elastic SAN" +layout: "azurerm" +page_title: "Azure Resource Manager: Data Source: azurerm_elastic_san_volume_snapshot" +description: |- + Gets information about an existing Elastic SAN Volume Snapshot. +--- + +# Data Source: azurerm_elastic_san_volume_snapshot + +Use this data source to access information about an existing Elastic SAN Volume Snapshot. + +## Example Usage + +```hcl +data "azurerm_elastic_san" "example" { + name = "existing" + resource_group_name = "existing" +} + +data "azurerm_elastic_san_volume_group" "example" { + name = "existing" + elastic_san_id = data.azurerm_elastic_san.example.id +} + +data "azurerm_elastic_san_volume_snapshot" "example" { + name = "existing" + volume_group_id = data.azurerm_elastic_san_volume_group.example.id +} + +output "id" { + value = data.azurerm_elastic_san_volume_snapshot.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - The name of the Elastic SAN Volume Snapshot. + +* `volume_group_id` - The Elastic SAN Volume Group ID within which the Elastic SAN Volume Snapshot exists. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Elastic SAN Volume Snapshot. + +* `source_id` - The resource ID from which the Snapshot is created. + +* `source_volume_size_in_gib` - The size of source volume. + +* `volume_name` - The source volume name of the Snapshot. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the Elastic SAN Volume Snapshot.