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 resource azurerm_storage_sync_cloud_endpoint #8540

Merged
merged 20 commits into from Dec 9, 2020
5 changes: 5 additions & 0 deletions azurerm/internal/services/storage/client/client.go
Expand Up @@ -27,6 +27,7 @@ type Client struct {
ManagementPoliciesClient storage.ManagementPoliciesClient
BlobServicesClient storage.BlobServicesClient
CachesClient *storagecache.CachesClient
CloudEndpointsClient storagesync.CloudEndpointsClient
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
StorageTargetsClient *storagecache.StorageTargetsClient
SyncServiceClient *storagesync.ServicesClient
StoragesyncGroupClient storagesync.SyncGroupsClient
Expand All @@ -52,6 +53,9 @@ func NewClient(options *common.ClientOptions) *Client {
cachesClient := storagecache.NewCachesClientWithBaseURI(options.ResourceManagerEndpoint, options.SubscriptionId)
options.ConfigureClient(&cachesClient.Client, options.ResourceManagerAuthorizer)

cloudEndpointsClient := storagesync.NewCloudEndpointsClientWithBaseURI(options.ResourceManagerEndpoint, options.SubscriptionId)
options.ConfigureClient(&cloudEndpointsClient.Client, options.ResourceManagerAuthorizer)

storageTargetsClient := storagecache.NewStorageTargetsClientWithBaseURI(options.ResourceManagerEndpoint, options.SubscriptionId)
options.ConfigureClient(&storageTargetsClient.Client, options.ResourceManagerAuthorizer)

Expand All @@ -69,6 +73,7 @@ func NewClient(options *common.ClientOptions) *Client {
ManagementPoliciesClient: managementPoliciesClient,
BlobServicesClient: blobServicesClient,
CachesClient: &cachesClient,
CloudEndpointsClient: cloudEndpointsClient,
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
SubscriptionId: options.SubscriptionId,
StorageTargetsClient: &storageTargetsClient,
SyncServiceClient: &syncServiceClient,
Expand Down
36 changes: 36 additions & 0 deletions azurerm/internal/services/storage/parsers/id.go
Expand Up @@ -21,6 +21,13 @@ type StorageSyncGroupId struct {
ResourceGroup string
}

type CloudEndpointId struct {
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
Name string
StorageSyncName string
StorageSyncGroup string
ResourceGroup string
}

func ParseAccountID(input string) (*AccountID, error) {
id, err := azure.ParseAzureResourceID(input)
if err != nil {
Expand Down Expand Up @@ -88,3 +95,32 @@ func StorageSyncGroupID(input string) (*StorageSyncGroupId, error) {

return &storageSyncGroup, nil
}

func CloudEndpointID(input string) (*CloudEndpointId, error) {
id, err := azure.ParseAzureResourceID(input)
if err != nil {
return nil, err
}

cloudEndpoint := CloudEndpointId{
ResourceGroup: id.ResourceGroup,
}

if cloudEndpoint.StorageSyncName, err = id.PopSegment("storageSyncServices"); err != nil {
return nil, err
}

if cloudEndpoint.StorageSyncGroup, err = id.PopSegment("syncGroups"); err != nil {
return nil, err
}

if cloudEndpoint.Name, err = id.PopSegment("cloudEndpoints"); err != nil {
return nil, err
}

if err := id.ValidateNoEmptySegments(input); err != nil {
return nil, err
}

return &cloudEndpoint, nil
}
93 changes: 93 additions & 0 deletions azurerm/internal/services/storage/parsers/id_test.go
Expand Up @@ -218,3 +218,96 @@ func TestParseStorageSyncGroupID(t *testing.T) {
}
}
}

func TestCloudEndpointID(t *testing.T) {
testData := []struct {
Name string
Input string
Expected *CloudEndpointId
}{
{
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 Sync Group",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.StorageSync/storageSyncServices/sync1",
Expected: nil,
},
{
Name: "Missing Sync Group Value",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.StorageSync/storageSyncServices/sync1/syncGroups/",
Expected: nil,
},
{
Name: "Missing Cloud Endpoint",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.StorageSync/storageSyncServices/sync1/syncGroups/group1",
Expected: nil,
},
{
Name: "Missing Cloud Endpoint Value",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.StorageSync/storageSyncServices/sync1/syncGroups/group1/cloudEndpoints",
Expected: nil,
},
{
Name: "Sync Group Id",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.StorageSync/storageSyncServices/sync1/syncGroups/group1/cloudEndpoints/cloudEndpoint1",
Expected: &CloudEndpointId{
Name: "cloudEndpoint1",
StorageSyncGroup: "group1",
StorageSyncName: "sync1",
ResourceGroup: "resGroup1",
},
},
{
Name: "Wrong Casing",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.StorageSync/storageSyncServices/sync1/syncGroups/group1/CloudEndpoints/cloudEndpoints1",
Expected: nil,
},
}

for _, v := range testData {
t.Logf("[DEBUG] Testing %q", v.Name)

actual, err := CloudEndpointID(v.Input)
if err != nil {
if v.Expected == nil {
continue
}

t.Fatalf("Expected a value but got an error: %s", err)
}

if actual.Name != v.Expected.Name {
t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name)
}

if actual.StorageSyncGroup != v.Expected.StorageSyncGroup {
t.Fatalf("Expected %q but got %q for Storage Sync Group", v.Expected.Name, actual.Name)
}

if actual.StorageSyncName != v.Expected.StorageSyncName {
t.Fatalf("Expected %q but got %q for Storage Sync Name", v.Expected.Name, actual.Name)
}

if actual.ResourceGroup != v.Expected.ResourceGroup {
t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup)
}
}
}
1 change: 1 addition & 0 deletions azurerm/internal/services/storage/registration.go
Expand Up @@ -50,6 +50,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource {
"azurerm_storage_table": resourceArmStorageTable(),
"azurerm_storage_table_entity": resourceArmStorageTableEntity(),
"azurerm_storage_sync": resourceArmStorageSync(),
"azurerm_storage_sync_cloud_endpoint": resourceArmCloudEndpoint(),
"azurerm_storage_sync_group": resourceArmStorageSyncGroup(),
}
}
@@ -0,0 +1,180 @@
package storage

import (
"fmt"
"log"
"os"
"time"

"github.com/Azure/azure-sdk-for-go/services/storagesync/mgmt/2020-03-01/storagesync"
"github.com/hashicorp/go-azure-helpers/response"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"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/storage/parsers"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/storage/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 resourceArmCloudEndpoint() *schema.Resource {
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
return &schema.Resource{
Create: resourceArmCloudEndpointCreate,
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
Read: resourceArmCloudEndpointRead,
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
Delete: resourceArmCloudEndpointDelete,
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved

Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error {
_, err := parsers.CloudEndpointID(id)
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
return err
}),

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(45 * time.Minute),
Read: schema.DefaultTimeout(5 * time.Minute),
Delete: schema.DefaultTimeout(45 * time.Minute),
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.StorageSyncName,
},

"storage_sync_group_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.StorageSyncGroupId,
},

"file_share_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: ValidateArmStorageShareName,
},

"storage_account_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.StorageAccountID,
},
katbyte marked this conversation as resolved.
Show resolved Hide resolved
},
}
}

func resourceArmCloudEndpointCreate(d *schema.ResourceData, meta interface{}) error {
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
client := meta.(*clients.Client).Storage.CloudEndpointsClient
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()

name := d.Get("name").(string)
storagesyncGroupId, _ := parsers.StorageSyncGroupID(d.Get("storage_sync_group_id").(string))
Copy link
Member

Choose a reason for hiding this comment

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

we should be catching/raising this error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we have a validation in the field storage_sync_group_id, could we assume that there'll not be errors here?


existing, err := client.Get(ctx, storagesyncGroupId.ResourceGroup, storagesyncGroupId.StorageSyncName, storagesyncGroupId.Name, name)
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("checking for present of existing Cloud Endpoint %q (Storage Sync Group %q / Storage Sync Name %q / Resource Group %q): %+v", name, storagesyncGroupId.Name, storagesyncGroupId.StorageSyncName, storagesyncGroupId.ResourceGroup, err)
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
}
}
if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_storage_sync_cloud_endpoint", *existing.ID)
}

parameters := storagesync.CloudEndpointCreateParameters{
CloudEndpointCreateParametersProperties: &storagesync.CloudEndpointCreateParametersProperties{
StorageAccountResourceID: utils.String(d.Get("storage_account_id").(string)),
AzureFileShareName: utils.String(d.Get("file_share_name").(string)),
StorageAccountTenantID: utils.String(os.Getenv("ARM_TENANT_ID")),
Copy link
Member

Choose a reason for hiding this comment

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

this'll break when not using a Service Principal.. can we instead source this from meta.(*clients.Client).Account.TenantID

However in practice we should make this field optional/computed and if a user leaves this unspecified default this to their Tenant ID, to allow this to be used across Tenants

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. Previously, I have assumed that there might not be a service principal that could be cross tenant. Anyway, I'll change it back.

},
}

future, err := client.Create(ctx, storagesyncGroupId.ResourceGroup, storagesyncGroupId.StorageSyncName, storagesyncGroupId.Name, name, parameters)
if err != nil {
return fmt.Errorf("creating Cloud Endpoint %q (Storage Sync Group %q / Storage Sync %q / Resource Group %q): %+v", name, storagesyncGroupId.Name, storagesyncGroupId.StorageSyncName, storagesyncGroupId.ResourceGroup, err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("waiting for Cloud Endpoint %q to be created: %+v", name, err)
}

resp, err := client.Get(ctx, storagesyncGroupId.ResourceGroup, storagesyncGroupId.StorageSyncName, storagesyncGroupId.Name, name)

yupwei68 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("retrieving Cloud Endpoint %q (Storage Sync Group %q / Storage Sync %q / Resource Group %q): %+v", name, storagesyncGroupId.Name, storagesyncGroupId.StorageSyncName, storagesyncGroupId.ResourceGroup, err)
}

if resp.ID == nil || *resp.ID == "" {
return fmt.Errorf("reading Cloud Endpoint %q (Storage Sync Group %q / Storage Sync %q / Resource Group %q) ID is nil or empty", name, storagesyncGroupId.Name, storagesyncGroupId.StorageSyncName, storagesyncGroupId.ResourceGroup)
}

d.SetId(*resp.ID)

return resourceArmCloudEndpointRead(d, meta)
}

func resourceArmCloudEndpointRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Storage.CloudEndpointsClient
gpClient := meta.(*clients.Client).Storage.StoragesyncGroupClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parsers.CloudEndpointID(d.Id())
if err != nil {
return err
}

resp, err := client.Get(ctx, id.ResourceGroup, id.StorageSyncName, id.StorageSyncGroup, id.Name)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("[INFO] Cloud Endpoint %q does not exist - removing from state", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("reading Cloud Endpoint %q (Storage Sync Group %q / Storage Sync %q / Resource Group %q): %+v", id.Name, id.StorageSyncGroup, id.StorageSyncName, id.ResourceGroup, err)
}
d.Set("name", resp.Name)

gpResp, err := gpClient.Get(ctx, id.ResourceGroup, id.StorageSyncName, id.StorageSyncGroup)
if err != nil {
return fmt.Errorf("reading Storage Sync Group (Storage Sync Group Name %q / Storage Sync Name %q /Resource Group %q): %+v", id.StorageSyncGroup, id.StorageSyncName, id.ResourceGroup, err)
}

if gpResp.ID == nil || *gpResp.ID == "" {
return fmt.Errorf("reading Storage Sync Group %q (Resource Group %q) ID is empty or nil", id.StorageSyncGroup, id.ResourceGroup)
}

d.Set("storage_sync_group_id", gpResp.ID)
d.Set("storage_account_id", resp.StorageAccountResourceID)
d.Set("file_share_name", resp.AzureFileShareName)
yupwei68 marked this conversation as resolved.
Show resolved Hide resolved

return nil
}

func resourceArmCloudEndpointDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Storage.CloudEndpointsClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parsers.CloudEndpointID(d.Id())
if err != nil {
return err
}

future, err := client.Delete(ctx, id.ResourceGroup, id.StorageSyncName, id.StorageSyncGroup, id.Name)
if err != nil {
return fmt.Errorf("deleting Cloud Endpoint %q (Storage Sync Group %q / Storage Sync %q / Resource Group %q): %+v", id.Name, id.StorageSyncGroup, id.StorageSyncName, id.ResourceGroup, err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
if !response.WasNotFound(future.Response()) {
return fmt.Errorf("waiting for deletion of Cloud Endpoint %q (Storage Sync Group %q / Storage Sync %q / Resource Group %q): %+v", id.Name, id.StorageSyncGroup, id.StorageSyncName, id.ResourceGroup, err)
}
}

return nil
}