diff --git a/azurerm/internal/services/datashare/client/client.go b/azurerm/internal/services/datashare/client/client.go index 1292d3cfd94e..09bbc336a08c 100644 --- a/azurerm/internal/services/datashare/client/client.go +++ b/azurerm/internal/services/datashare/client/client.go @@ -6,14 +6,24 @@ import ( ) type Client struct { - AccountClient *datashare.AccountsClient + AccountClient *datashare.AccountsClient + SharesClient *datashare.SharesClient + SynchronizationClient *datashare.SynchronizationSettingsClient } func NewClient(o *common.ClientOptions) *Client { accountClient := datashare.NewAccountsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&accountClient.Client, o.ResourceManagerAuthorizer) + sharesClient := datashare.NewSharesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&sharesClient.Client, o.ResourceManagerAuthorizer) + + synchronizationSettingsClient := datashare.NewSynchronizationSettingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&synchronizationSettingsClient.Client, o.ResourceManagerAuthorizer) + return &Client{ - AccountClient: &accountClient, + AccountClient: &accountClient, + SharesClient: &sharesClient, + SynchronizationClient: &synchronizationSettingsClient, } } diff --git a/azurerm/internal/services/datashare/data_source_data_share.go b/azurerm/internal/services/datashare/data_source_data_share.go new file mode 100644 index 000000000000..860491b8424a --- /dev/null +++ b/azurerm/internal/services/datashare/data_source_data_share.go @@ -0,0 +1,139 @@ +package datashare + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/datashare/mgmt/2019-11-01/datashare" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/datashare/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/datashare/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceDataShare() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmDataShareRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.DatashareName(), + }, + + "account_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.DatashareAccountID, + }, + + "kind": { + Type: schema.TypeString, + Computed: true, + }, + + "description": { + Type: schema.TypeString, + Computed: true, + }, + + "snapshot_schedule": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + + "recurrence": { + Type: schema.TypeString, + Computed: true, + }, + + "start_time": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "terms": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceArmDataShareRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DataShare.SharesClient + syncClient := meta.(*clients.Client).DataShare.SynchronizationClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + accountID := d.Get("account_id").(string) + accountId, err := parse.DataShareAccountID(accountID) + if err != nil { + return err + } + + resp, err := client.Get(ctx, accountId.ResourceGroup, accountId.Name, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] DataShare %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("retrieving DataShare %q (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("reading DataShare %q (Resource Group %q / accountName %q): ID is empty", name, accountId.ResourceGroup, accountId.Name) + } + + d.SetId(*resp.ID) + d.Set("name", name) + d.Set("account_id", accountID) + if props := resp.ShareProperties; props != nil { + d.Set("kind", props.ShareKind) + d.Set("description", props.Description) + d.Set("terms", props.Terms) + } + + if syncIterator, err := syncClient.ListByShareComplete(ctx, accountId.ResourceGroup, accountId.Name, name, ""); syncIterator.NotDone() { + if err != nil { + return fmt.Errorf("listing DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + if syncName := syncIterator.Value().(datashare.ScheduledSynchronizationSetting).Name; syncName != nil && *syncName != "" { + syncResp, err := syncClient.Get(ctx, accountId.ResourceGroup, accountId.Name, name, *syncName) + if err != nil { + return fmt.Errorf("reading DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + if schedule := syncResp.Value.(datashare.ScheduledSynchronizationSetting); schedule.ID != nil && *schedule.ID != "" { + if err := d.Set("snapshot_schedule", flattenAzureRmDataShareSnapshotSchedule(&schedule)); err != nil { + return fmt.Errorf("setting `snapshot_schedule`: %+v", err) + } + } + } + if err := syncIterator.NextWithContext(ctx); err != nil { + return fmt.Errorf("listing DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + if syncIterator.NotDone() { + return fmt.Errorf("more than one DataShare %q snapshot schedule (Resource Group %q / accountName %q) is returned", name, accountId.ResourceGroup, accountId.Name) + } + } + + return nil +} diff --git a/azurerm/internal/services/datashare/parse/data_share.go b/azurerm/internal/services/datashare/parse/data_share.go index 83bc7c3404a1..a9247a7b0da5 100644 --- a/azurerm/internal/services/datashare/parse/data_share.go +++ b/azurerm/internal/services/datashare/parse/data_share.go @@ -11,6 +11,12 @@ type DataShareAccountId struct { Name string } +type DataShareId struct { + ResourceGroup string + AccountName string + Name string +} + func DataShareAccountID(input string) (*DataShareAccountId, error) { id, err := azure.ParseAzureResourceID(input) if err != nil { @@ -29,3 +35,25 @@ func DataShareAccountID(input string) (*DataShareAccountId, error) { return &dataShareAccount, nil } + +func DataShareID(input string) (*DataShareId, error) { + var id, err = azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("unable to parse DataShare ID %q: %+v", input, err) + } + + DataShare := DataShareId{ + ResourceGroup: id.ResourceGroup, + } + if DataShare.AccountName, err = id.PopSegment("accounts"); err != nil { + return nil, err + } + if DataShare.Name, err = id.PopSegment("shares"); err != nil { + return nil, err + } + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &DataShare, nil +} diff --git a/azurerm/internal/services/datashare/parse/data_share_test.go b/azurerm/internal/services/datashare/parse/data_share_test.go index 3c14595b2d91..fe1d4f3deca5 100644 --- a/azurerm/internal/services/datashare/parse/data_share_test.go +++ b/azurerm/internal/services/datashare/parse/data_share_test.go @@ -70,3 +70,85 @@ func TestDataShareAccountID(t *testing.T) { } } } + +func TestDataShareID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *DataShareId + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "Missing Account Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DataShare/accounts/", + Expected: nil, + }, + { + Name: "Missing Share", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DataShare/accounts/account1/", + Expected: nil, + }, + { + Name: "Missing Share Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DataShare/accounts/account1/shares/", + Expected: nil, + }, + { + Name: "Data Share ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DataShare/accounts/account1/shares/share1", + Expected: &DataShareId{ + Name: "share1", + AccountName: "account1", + ResourceGroup: "resGroup1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DataShare/accounts/account1/Shares/share1", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.Name) + + actual, err := DataShareID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.AccountName != v.Expected.AccountName { + t.Fatalf("Expected %q but got %q for account name", v.Expected.AccountName, actual.AccountName) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/azurerm/internal/services/datashare/registration.go b/azurerm/internal/services/datashare/registration.go index 0ddb1dd85d4e..87cff3e1cbaa 100644 --- a/azurerm/internal/services/datashare/registration.go +++ b/azurerm/internal/services/datashare/registration.go @@ -20,6 +20,7 @@ func (r Registration) WebsiteCategories() []string { func (r Registration) SupportedDataSources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azurerm_data_share_account": dataSourceDataShareAccount(), + "azurerm_data_share": dataSourceDataShare(), } } @@ -27,5 +28,6 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azurerm_data_share_account": resourceArmDataShareAccount(), + "azurerm_data_share": resourceArmDataShare(), } } diff --git a/azurerm/internal/services/datashare/resource_arm_data_share.go b/azurerm/internal/services/datashare/resource_arm_data_share.go new file mode 100644 index 000000000000..2359f58c9108 --- /dev/null +++ b/azurerm/internal/services/datashare/resource_arm_data_share.go @@ -0,0 +1,316 @@ +package datashare + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/datashare/mgmt/2019-11-01/datashare" + "github.com/Azure/go-autorest/autorest/date" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/datashare/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/datashare/validate" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmDataShare() *schema.Resource { + return &schema.Resource{ + Create: resourceArmDataShareCreateUpdate, + Read: resourceArmDataShareRead, + Update: resourceArmDataShareCreateUpdate, + Delete: resourceArmDataShareDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.DataShareID(id) + return err + }), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.DatashareName(), + }, + + "account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.DatashareAccountID, + }, + + "kind": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(datashare.CopyBased), + string(datashare.InPlace), + }, false), + }, + + "description": { + Type: schema.TypeString, + Optional: true, + }, + + "snapshot_schedule": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.DataShareSyncName(), + }, + + "recurrence": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(datashare.Day), + string(datashare.Hour), + }, false), + }, + + "start_time": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsRFC3339Time, + DiffSuppressFunc: suppress.RFC3339Time, + }, + }, + }, + }, + + "terms": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} +func resourceArmDataShareCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DataShare.SharesClient + syncClient := meta.(*clients.Client).DataShare.SynchronizationClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + accountId, err := parse.DataShareAccountID(d.Get("account_id").(string)) + if err != nil { + return err + } + + if d.IsNewResource() { + existing, err := client.Get(ctx, accountId.ResourceGroup, accountId.Name, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for present of existing DataShare %q (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_data_share", *existing.ID) + } + } + + share := datashare.Share{ + ShareProperties: &datashare.ShareProperties{ + ShareKind: datashare.ShareKind(d.Get("kind").(string)), + Description: utils.String(d.Get("description").(string)), + Terms: utils.String(d.Get("terms").(string)), + }, + } + + if _, err := client.Create(ctx, accountId.ResourceGroup, accountId.Name, name, share); err != nil { + return fmt.Errorf("creating DataShare %q (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + + resp, err := client.Get(ctx, accountId.ResourceGroup, accountId.Name, name) + if err != nil { + return fmt.Errorf("retrieving DataShare %q (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("reading DataShare %q (Resource Group %q / accountName %q): ID is empty", name, accountId.ResourceGroup, accountId.Name) + } + + d.SetId(*resp.ID) + + if d.HasChange("snapshot_schedule") { + // only one dependent sync setting is allowed in one data share + o, _ := d.GetChange("snapshot_schedule") + if origins := o.([]interface{}); len(origins) > 0 { + origin := origins[0].(map[string]interface{}) + if originName, ok := origin["name"].(string); ok && originName != "" { + syncFuture, err := syncClient.Delete(ctx, accountId.ResourceGroup, accountId.Name, name, originName) + if err != nil { + return fmt.Errorf("deleting DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + if err = syncFuture.WaitForCompletionRef(ctx, syncClient.Client); err != nil { + return fmt.Errorf("waiting for DataShare %q snapshot schedule (Resource Group %q / accountName %q) to be deleted: %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + } + } + } + + if snapshotSchedule := expandAzureRmDataShareSnapshotSchedule(d.Get("snapshot_schedule").([]interface{})); snapshotSchedule != nil { + if _, err := syncClient.Create(ctx, accountId.ResourceGroup, accountId.Name, name, d.Get("snapshot_schedule.0.name").(string), snapshotSchedule); err != nil { + return fmt.Errorf("creating DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", name, accountId.ResourceGroup, accountId.Name, err) + } + } + + return resourceArmDataShareRead(d, meta) +} + +func resourceArmDataShareRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DataShare.SharesClient + accountClient := meta.(*clients.Client).DataShare.AccountClient + syncClient := meta.(*clients.Client).DataShare.SynchronizationClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.DataShareID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.AccountName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] DataShare %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("retrieving DataShare %q (Resource Group %q / accountName %q): %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + + accountResp, err := accountClient.Get(ctx, id.ResourceGroup, id.AccountName) + if err != nil { + return fmt.Errorf("retrieving DataShare Account %q (Resource Group %q): %+v", id.AccountName, id.ResourceGroup, err) + } + if accountResp.ID == nil || *accountResp.ID == "" { + return fmt.Errorf("reading DataShare Account %q (Resource Group %q): ID is empty", id.AccountName, id.ResourceGroup) + } + + d.Set("name", id.Name) + d.Set("account_id", accountResp.ID) + + if props := resp.ShareProperties; props != nil { + d.Set("kind", props.ShareKind) + d.Set("description", props.Description) + d.Set("terms", props.Terms) + } + + if syncIterator, err := syncClient.ListByShareComplete(ctx, id.ResourceGroup, id.AccountName, id.Name, ""); syncIterator.NotDone() { + if err != nil { + return fmt.Errorf("listing DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + if syncName := syncIterator.Value().(datashare.ScheduledSynchronizationSetting).Name; syncName != nil && *syncName != "" { + syncResp, err := syncClient.Get(ctx, id.ResourceGroup, id.AccountName, id.Name, *syncName) + if err != nil { + return fmt.Errorf("reading DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + if schedule := syncResp.Value.(datashare.ScheduledSynchronizationSetting); schedule.ID != nil && *schedule.ID != "" { + if err := d.Set("snapshot_schedule", flattenAzureRmDataShareSnapshotSchedule(&schedule)); err != nil { + return fmt.Errorf("setting `snapshot_schedule`: %+v", err) + } + } + } + if err := syncIterator.NextWithContext(ctx); err != nil { + return fmt.Errorf("listing DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + if syncIterator.NotDone() { + return fmt.Errorf("more than one DataShare %q snapshot schedule (Resource Group %q / accountName %q) is returned", id.Name, id.ResourceGroup, id.AccountName) + } + } + + return nil +} + +func resourceArmDataShareDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DataShare.SharesClient + syncClient := meta.(*clients.Client).DataShare.SynchronizationClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.DataShareID(d.Id()) + if err != nil { + return err + } + + // sync setting will not automatically be deleted after the data share is deleted + if _, ok := d.GetOk("snapshot_schedule"); ok { + syncFuture, err := syncClient.Delete(ctx, id.ResourceGroup, id.AccountName, id.Name, d.Get("snapshot_schedule.0.name").(string)) + if err != nil { + return fmt.Errorf("deleting DataShare %q snapshot schedule (Resource Group %q / accountName %q): %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + if err = syncFuture.WaitForCompletionRef(ctx, syncClient.Client); err != nil { + return fmt.Errorf("waiting for DataShare %q snapshot schedule (Resource Group %q / accountName %q) to be deleted: %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + } + + future, err := client.Delete(ctx, id.ResourceGroup, id.AccountName, id.Name) + if err != nil { + return fmt.Errorf("deleting DataShare %q (Resource Group %q / accountName %q): %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for DataShare %q (Resource Group %q / accountName %q) to be deleted: %+v", id.Name, id.ResourceGroup, id.AccountName, err) + } + + return nil +} + +func expandAzureRmDataShareSnapshotSchedule(input []interface{}) *datashare.ScheduledSynchronizationSetting { + if len(input) == 0 { + return nil + } + + snapshotSchedule := input[0].(map[string]interface{}) + + startTime, _ := time.Parse(time.RFC3339, snapshotSchedule["start_time"].(string)) + + return &datashare.ScheduledSynchronizationSetting{ + Kind: datashare.KindBasicSynchronizationSettingKindScheduleBased, + ScheduledSynchronizationSettingProperties: &datashare.ScheduledSynchronizationSettingProperties{ + RecurrenceInterval: datashare.RecurrenceInterval(snapshotSchedule["recurrence"].(string)), + SynchronizationTime: &date.Time{Time: startTime}, + }, + } +} + +func flattenAzureRmDataShareSnapshotSchedule(sync *datashare.ScheduledSynchronizationSetting) []interface{} { + if sync == nil { + return []interface{}{} + } + + var startTime string + if sync.SynchronizationTime != nil && !sync.SynchronizationTime.IsZero() { + startTime = sync.SynchronizationTime.Format(time.RFC3339) + } + + return []interface{}{ + map[string]interface{}{ + "name": sync.Name, + "recurrence": string(sync.RecurrenceInterval), + "start_time": startTime, + }, + } +} diff --git a/azurerm/internal/services/datashare/tests/data_source_data_share_test.go b/azurerm/internal/services/datashare/tests/data_source_data_share_test.go new file mode 100644 index 000000000000..5444560e6cc9 --- /dev/null +++ b/azurerm/internal/services/datashare/tests/data_source_data_share_test.go @@ -0,0 +1,75 @@ +package tests + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" +) + +func TestAccDataSourceAzureRMDataShare_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_data_share", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceDataShare_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + resource.TestCheckResourceAttrSet(data.ResourceName, "account_id"), + resource.TestCheckResourceAttrSet(data.ResourceName, "kind"), + ), + }, + }, + }) +} + +func TestAccDataSourceAzureRMDataShare_snapshotSchedule(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_data_share", "test") + startTime := time.Now().Add(time.Hour * 7).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAzureRMDataShare_snapshotSchedule(data, startTime), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + resource.TestCheckResourceAttrSet(data.ResourceName, "snapshot_schedule.0.name"), + resource.TestCheckResourceAttrSet(data.ResourceName, "snapshot_schedule.0.recurrence"), + resource.TestCheckResourceAttrSet(data.ResourceName, "snapshot_schedule.0.start_time"), + ), + }, + }, + }) +} + +func testAccDataSourceDataShare_basic(data acceptance.TestData) string { + config := testAccAzureRMDataShare_basic(data) + return fmt.Sprintf(` +%s + +data "azurerm_data_share" "test" { + name = azurerm_data_share.test.name + account_id = azurerm_data_share_account.test.id +} +`, config) +} + +func testAccDataSourceAzureRMDataShare_snapshotSchedule(data acceptance.TestData, startTime string) string { + config := testAccAzureRMDataShare_snapshotSchedule(data, startTime) + return fmt.Sprintf(` +%s + +data "azurerm_data_share" "test" { + name = azurerm_data_share.test.name + account_id = azurerm_data_share_account.test.id +} +`, config) +} diff --git a/azurerm/internal/services/datashare/tests/resource_arm_data_share_test.go b/azurerm/internal/services/datashare/tests/resource_arm_data_share_test.go new file mode 100644 index 000000000000..6829c4fc8c48 --- /dev/null +++ b/azurerm/internal/services/datashare/tests/resource_arm_data_share_test.go @@ -0,0 +1,308 @@ +package tests + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/datashare/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMDataShare_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_data_share", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDataShare_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDataShare_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_data_share", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDataShare_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMDataShare_requiresImport), + }, + }) +} + +func TestAccAzureRMDataShare_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_data_share", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDataShare_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDataShare_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_data_share", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDataShare_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDataShare_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDataShare_update(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDataShare_snapshotSchedule(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_data_share", "test") + startTime := time.Now().Add(time.Hour * 7).Format(time.RFC3339) + startTime2 := time.Now().Add(time.Hour * 8).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDataShareDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDataShare_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDataShare_snapshotSchedule(data, startTime), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDataShare_snapshotScheduleUpdated(data, startTime2), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDataShare_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDataShareExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func testCheckAzureRMDataShareExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).DataShare.SharesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("dataShare not found: %s", resourceName) + } + id, err := parse.DataShareID(rs.Primary.ID) + if err != nil { + return err + } + if resp, err := client.Get(ctx, id.ResourceGroup, id.AccountName, id.Name); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("bad: data_share share %q does not exist", id.Name) + } + return fmt.Errorf("bad: Get on DataShareClient: %+v", err) + } + return nil + } +} + +func testCheckAzureRMDataShareDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).DataShare.SharesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_data_share" { + continue + } + id, err := parse.DataShareID(rs.Primary.ID) + if err != nil { + return err + } + if resp, err := client.Get(ctx, id.ResourceGroup, id.AccountName, id.Name); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("bad: Get on data_share.shareClient: %+v", err) + } + } + return nil + } + return nil +} +func testAccAzureRMDataShare_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctest-datashare-%d" + location = "%s" +} + +resource "azurerm_data_share_account" "test" { + name = "acctest-dsa-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + identity { + type = "SystemAssigned" + } + + tags = { + env = "Test" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func testAccAzureRMDataShare_basic(data acceptance.TestData) string { + template := testAccAzureRMDataShare_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_data_share" "test" { + name = "acctest_ds_%d" + account_id = azurerm_data_share_account.test.id + kind = "CopyBased" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMDataShare_requiresImport(data acceptance.TestData) string { + config := testAccAzureRMDataShare_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_data_share" "import" { + name = azurerm_data_share.test.name + account_id = azurerm_data_share_account.test.id + kind = azurerm_data_share.test.kind +} +`, config) +} + +func testAccAzureRMDataShare_complete(data acceptance.TestData) string { + template := testAccAzureRMDataShare_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_data_share" "test" { + name = "acctest_ds_%d" + account_id = azurerm_data_share_account.test.id + kind = "CopyBased" + description = "share desc" + terms = "share terms" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMDataShare_update(data acceptance.TestData) string { + template := testAccAzureRMDataShare_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_data_share" "test" { + name = "acctest_ds_%d" + account_id = azurerm_data_share_account.test.id + kind = "CopyBased" + description = "share desc 2" + terms = "share terms 2" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMDataShare_snapshotSchedule(data acceptance.TestData, startTime string) string { + template := testAccAzureRMDataShare_template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_data_share" "test" { + name = "acctest_ds_%[2]d" + account_id = azurerm_data_share_account.test.id + kind = "CopyBased" + + snapshot_schedule { + name = "acctest-ss-%[2]d" + recurrence = "Day" + start_time = "%[3]s" + } +} +`, template, data.RandomInteger, startTime) +} + +func testAccAzureRMDataShare_snapshotScheduleUpdated(data acceptance.TestData, startTime string) string { + template := testAccAzureRMDataShare_template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_data_share" "test" { + name = "acctest_ds_%[2]d" + account_id = azurerm_data_share_account.test.id + kind = "CopyBased" + + snapshot_schedule { + name = "acctest-ss2-%[2]d" + recurrence = "Hour" + start_time = "%[3]s" + } +} +`, template, data.RandomInteger, startTime) +} diff --git a/azurerm/internal/services/datashare/validate/data_share.go b/azurerm/internal/services/datashare/validate/data_share.go index 4bd6971dfe22..c1a393af2b1b 100644 --- a/azurerm/internal/services/datashare/validate/data_share.go +++ b/azurerm/internal/services/datashare/validate/data_share.go @@ -1,10 +1,12 @@ package validate import ( + "fmt" "regexp" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/datashare/parse" ) func DataShareAccountName() schema.SchemaValidateFunc { @@ -12,3 +14,29 @@ func DataShareAccountName() schema.SchemaValidateFunc { regexp.MustCompile(`^[^<>%&:\\?/#*$^();,.\|+={}\[\]!~@]{3,90}$`), `Data share account name should have length of 3 - 90, and cannot contain <>%&:\?/#*$^();,.|+={}[]!~@.`, ) } + +func DatashareName() schema.SchemaValidateFunc { + return validation.StringMatch( + regexp.MustCompile(`^\w{2,90}$`), `DataShare name can only contain alphanumeric characters and _, and must be between 2 and 90 characters long.`, + ) +} + +func DatashareAccountID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return warnings, errors + } + + if _, err := parse.DataShareAccountID(v); err != nil { + errors = append(errors, fmt.Errorf("can not parse %q as a Datashare account id: %v", k, err)) + } + + return warnings, errors +} + +func DataShareSyncName() schema.SchemaValidateFunc { + return validation.StringMatch( + regexp.MustCompile(`^[^&%#/]{1,90}$`), `Data share snapshot schedule name should have length of 1 - 90, and cannot contain &%#/`, + ) +} diff --git a/azurerm/internal/services/datashare/validate/data_share_test.go b/azurerm/internal/services/datashare/validate/data_share_test.go index 7e3d706bef34..9542d7bd9401 100644 --- a/azurerm/internal/services/datashare/validate/data_share_test.go +++ b/azurerm/internal/services/datashare/validate/data_share_test.go @@ -55,3 +55,81 @@ func TestDataShareAccountName(t *testing.T) { }) } } + +func TestDatashareName(t *testing.T) { + tests := []struct { + name string + input string + valid bool + }{ + { + name: "invalid character", + input: "9()", + valid: false, + }, + { + name: "less character", + input: "a", + valid: false, + }, + { + name: "invalid character2", + input: "adgeFG-98", + valid: false, + }, + { + name: "valid", + input: "dfakF88u7_", + valid: true, + }, + } + var validationFunction = DatashareName() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := validationFunction(tt.input, "") + valid := err == nil + if valid != tt.valid { + t.Errorf("expected valid status %t but got %t for input %s", tt.valid, valid, tt.input) + } + }) + } +} + +func TestDatashareSyncName(t *testing.T) { + tests := []struct { + name string + input string + valid bool + }{ + { + name: "valid characters", + input: "*() _-$@!", + valid: true, + }, + { + name: "Empty", + input: "", + valid: false, + }, + { + name: "invalid characters", + input: "&^*", + valid: false, + }, + { + name: "invalid characters", + input: "dfwe%", + valid: false, + }, + } + var validationFunction = DataShareSyncName() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := validationFunction(tt.input, "") + valid := err == nil + if valid != tt.valid { + t.Errorf("expected valid status %t but got %t for input %s", tt.valid, valid, tt.input) + } + }) + } +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 0ce801bf38ad..a418353c0224 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -186,6 +186,10 @@ azurerm_data_lake_store +
  • + azurerm_data_share +
  • +
  • azurerm_data_share_account
  • @@ -1426,6 +1430,9 @@
  • Data Share Resources