diff --git a/internal/services/compute/disk_sas_token_resource.go b/internal/services/compute/disk_sas_token_resource.go new file mode 100644 index 000000000000..967da4ed1ed5 --- /dev/null +++ b/internal/services/compute/disk_sas_token_resource.go @@ -0,0 +1,169 @@ +package compute + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-11-01/compute" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/compute/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/compute/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" +) + +func resourceManagedDiskSasToken() *pluginsdk.Resource { + + return &pluginsdk.Resource{ + Create: resourceManagedDiskSasTokenCreate, + Read: resourceManagedDiskSasTokenRead, + Delete: resourceManagedDiskSasTokenDelete, + + Timeouts: &pluginsdk.ResourceTimeout{ + Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + Delete: pluginsdk.DefaultTimeout(30 * time.Minute), + }, + + Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { + _, err := parse.ManagedDiskID(id) + return err + }), + + Schema: map[string]*pluginsdk.Schema{ + "managed_disk_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ManagedDiskID, + }, + + // unable to provide upper value of 4294967295 as it's not comptabile with 32-bit (overflow errors) + "duration_in_seconds": { + Type: pluginsdk.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(30), + }, + + "access_level": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.AccessLevelRead), + string(compute.AccessLevelWrite), + }, false), + }, + + "sas_url": { + Type: pluginsdk.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } + +} + +func resourceManagedDiskSasTokenCreate(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DisksClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + log.Printf("[INFO] preparing arguments for AzureRM Disk Export.") + durationInSeconds := int32(d.Get("duration_in_seconds").(int)) + access := compute.AccessLevel(d.Get("access_level").(string)) + + diskId, err := parse.ManagedDiskID(d.Get("managed_disk_id").(string)) + if err != nil { + return err + } + + grantAccessData := compute.GrantAccessData{ + Access: access, + DurationInSeconds: &durationInSeconds, + } + + resp, err := client.Get(ctx, diskId.ResourceGroup, diskId.DiskName) + if err != nil { + return fmt.Errorf("error retrieving Disk %s: %+v", *diskId, err) + } + + // checking whether disk export SAS URL is active already before creating. If yes, we raise an error + if resp.DiskState == "ActiveSAS" { + return fmt.Errorf("active SAS Token for Disk Export already exists, cannot create another one %s: %+v", *diskId, err) + } + + future, err := client.GrantAccess(ctx, diskId.ResourceGroup, diskId.DiskName, grantAccessData) + if err != nil { + return fmt.Errorf("granting access to %s: %+v", *diskId, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for access to be granted to %s: %+v", *diskId, err) + } + read, err := future.Result(*client) + if err != nil { + return fmt.Errorf("retrieving SAS Token for Disk Access %s: %+v", *diskId, err) + } + if read.AccessSAS == nil { + return fmt.Errorf("retrieving SAS Token for Disk Access %s: SAS was nil", *diskId) + } + + d.SetId(diskId.ID()) + sasToken := *read.AccessSAS + d.Set("sas_url", sasToken) + + return resourceManagedDiskSasTokenRead(d, meta) + +} + +func resourceManagedDiskSasTokenRead(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DisksClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + diskId, err := parse.ManagedDiskID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, diskId.ResourceGroup, diskId.DiskName) + if err != nil { + // checking whether disk export SAS URL is active post creation. If no, we raise an error + if resp.DiskState != "ActiveSAS" { + log.Printf("[INFO] Disk SAS token %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("making Read request on Azure Disk Export for SAS Token %s (resource group %s): %s", diskId.DiskName, diskId.ResourceGroup, err) + } + + d.SetId(diskId.ID()) + + return nil +} + +func resourceManagedDiskSasTokenDelete(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DisksClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ManagedDiskID(d.Id()) + if err != nil { + return err + } + + future, err := client.RevokeAccess(ctx, id.ResourceGroup, id.DiskName) + if err != nil { + return fmt.Errorf("revoking access to %s: %+v", *id, err) + } + + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for revocation of access to %s: %+v", *id, err) + } + + return nil +} diff --git a/internal/services/compute/disk_sas_token_resource_test.go b/internal/services/compute/disk_sas_token_resource_test.go new file mode 100644 index 000000000000..1fb5a2cbcfe9 --- /dev/null +++ b/internal/services/compute/disk_sas_token_resource_test.go @@ -0,0 +1,76 @@ +package compute_test + +import ( + "context" + "fmt" + "testing" + + "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/services/compute/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type ManagedDiskSASTokenResource struct{} + +func TestAccManagedDiskSASToken_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_managed_disk_sas_token", "test") + r := ManagedDiskSASTokenResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func (t ManagedDiskSASTokenResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.ManagedDiskID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.Compute.DisksClient.Get(ctx, id.ResourceGroup, id.DiskName) + if err != nil { + return nil, fmt.Errorf("retrieving Compute Disk Export status %q", id.String()) + } + + if resp.DiskState != "ActiveSAS" { + return nil, fmt.Errorf("Disk SAS token %s (resource group %s): %s", id.DiskName, id.ResourceGroup, err) + } + + return utils.Bool(resp.ID != nil), nil +} + +func (r ManagedDiskSASTokenResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-revokedisk-%d" + location = "%s" +} + +resource "azurerm_managed_disk" "test" { + name = "acctestsads%s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = "1" +} + +resource "azurerm_managed_disk_sas_token" "test" { + managed_disk_id = azurerm_managed_disk.test.id + duration_in_seconds = 300 + access_level = "Read" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/internal/services/compute/registration.go b/internal/services/compute/registration.go index da17055db751..32741bcef1ed 100644 --- a/internal/services/compute/registration.go +++ b/internal/services/compute/registration.go @@ -67,6 +67,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { "azurerm_windows_virtual_machine": resourceWindowsVirtualMachine(), "azurerm_windows_virtual_machine_scale_set": resourceWindowsVirtualMachineScaleSet(), "azurerm_ssh_public_key": resourceSshPublicKey(), + "azurerm_managed_disk_sas_token": resourceManagedDiskSasToken(), } return resources diff --git a/website/docs/r/disk_sas_token.html.markdown b/website/docs/r/disk_sas_token.html.markdown new file mode 100644 index 000000000000..6c005399f09f --- /dev/null +++ b/website/docs/r/disk_sas_token.html.markdown @@ -0,0 +1,78 @@ +--- +subcategory: "Compute" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_managed_disk_sas_token" +description: |- + Manages a Disk SAS Token. +--- + +# azurerm_managed_disk_sas_token + +Manages a Disk SAS Token. + +Use this resource to obtain a Shared Access Signature (SAS Token) for an existing Managed Disk. + +Shared access signatures allow fine-grained, ephemeral access control to various aspects of Managed Disk similar to blob/storage account container. + +With the help of this resource, data from the disk can be copied from managed disk to a storage blob or to some other system without the need of azcopy. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "test" { + name = "testrg" + location = "West Europe" +} + +resource "azurerm_managed_disk" "test" { + name = "tst-disk-export" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = "1" +} + +resource "azurerm_managed_disk_sas_token" "test" { + managed_disk_id = azurerm_managed_disk.test.id + duration_in_seconds = 300 + access_level = "Read" +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `managed_disk_id` - (Required) The ID of an existing Managed Disk which should be exported. Changing this forces a new resource to be created. + +* `duration_in_seconds` - (Required) The duration for which the export should be allowed. Should be between 30 & 4294967295 seconds. + +* `access_level` - (Required) The level of access required on the disk. Supported are Read, Write. + +Refer to the [SAS creation reference from Azure](https://docs.microsoft.com/en-us/rest/api/compute/disks/grant-access) +for additional details on the fields above. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Disk Export resource. + +* `sas_url` - The computed Shared Access Signature (SAS) of the Managed Disk. + +## 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 the Disk. +* `read` - (Defaults to 5 minutes) Used when retrieving the Disk. +* `delete` - (Defaults to 30 minutes) Used when deleting the Disk. + +## Import + +Disk SAS Token can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_managed_disk_sas_token.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/microsoft.compute/disks/manageddisk1 +``` \ No newline at end of file