Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new datasource azurerm_elastic_san_volume_snapshot #26439

Merged
merged 6 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some validation here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

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)
},
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions internal/services/elasticsan/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func (Registration) DataSources() []sdk.DataSource {
return []sdk.DataSource{
ElasticSANDataSource{},
ElasticSANVolumeGroupDataSource{},
ElasticSANVolumeSnapshotDataSource{},
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading