diff --git a/azurerm/internal/services/compute/data_source_shared_image.go b/azurerm/internal/services/compute/data_source_shared_image.go index 633c5e6d46db..a2049ed56a37 100644 --- a/azurerm/internal/services/compute/data_source_shared_image.go +++ b/azurerm/internal/services/compute/data_source_shared_image.go @@ -44,6 +44,11 @@ func dataSourceArmSharedImage() *schema.Resource { Computed: true, }, + "hyper_v_generation": { + Type: schema.TypeString, + Computed: true, + }, + "identifier": { Type: schema.TypeList, Computed: true, @@ -120,6 +125,7 @@ func dataSourceArmSharedImageRead(d *schema.ResourceData, meta interface{}) erro d.Set("description", props.Description) d.Set("eula", props.Eula) d.Set("os_type", string(props.OsType)) + d.Set("hyper_v_generation", string(props.HyperVGeneration)) d.Set("privacy_statement_uri", props.PrivacyStatementURI) d.Set("release_note_uri", props.ReleaseNoteURI) diff --git a/azurerm/internal/services/compute/resource_arm_shared_image.go b/azurerm/internal/services/compute/resource_arm_shared_image.go index 0182c1be8e64..c48c88b66900 100644 --- a/azurerm/internal/services/compute/resource_arm_shared_image.go +++ b/azurerm/internal/services/compute/resource_arm_shared_image.go @@ -1,12 +1,13 @@ package compute import ( + "context" "fmt" "log" "time" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute" - "github.com/hashicorp/go-azure-helpers/response" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "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/azure" @@ -64,6 +65,17 @@ func resourceArmSharedImage() *schema.Resource { }, false), }, + "hyper_v_generation": { + Type: schema.TypeString, + Optional: true, + Default: string(compute.HyperVGenerationTypesV1), + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.V1), + string(compute.V2), + }, false), + }, + "identifier": { Type: schema.TypeList, Required: true, @@ -123,6 +135,7 @@ func resourceArmSharedImageCreateUpdate(d *schema.ResourceData, meta interface{} resourceGroup := d.Get("resource_group_name").(string) location := azure.NormalizeLocation(d.Get("location").(string)) description := d.Get("description").(string) + hyperVGeneration := d.Get("hyper_v_generation").(string) eula := d.Get("eula").(string) privacyStatementUri := d.Get("privacy_statement_uri").(string) @@ -156,6 +169,7 @@ func resourceArmSharedImageCreateUpdate(d *schema.ResourceData, meta interface{} ReleaseNoteURI: utils.String(releaseNoteURI), OsType: compute.OperatingSystemTypes(osType), OsState: compute.Generalized, + HyperVGeneration: compute.HyperVGeneration(hyperVGeneration), }, Tags: tags.Expand(t), } @@ -219,6 +233,7 @@ func resourceArmSharedImageRead(d *schema.ResourceData, meta interface{}) error d.Set("description", props.Description) d.Set("eula", props.Eula) d.Set("os_type", string(props.OsType)) + d.Set("hyper_v_generation", string(props.HyperVGeneration)) d.Set("privacy_statement_uri", props.PrivacyStatementURI) d.Set("release_note_uri", props.ReleaseNoteURI) @@ -247,23 +262,50 @@ func resourceArmSharedImageDelete(d *schema.ResourceData, meta interface{}) erro future, err := client.Delete(ctx, resourceGroup, galleryName, name) if err != nil { - // deleted outside of Terraform - if response.WasNotFound(future.Response()) { - return nil - } - - return fmt.Errorf("Error deleting Shared Image %q (Gallery %q / Resource Group %q): %+v", name, galleryName, resourceGroup, err) + return fmt.Errorf("deleting Shared Image %q (Gallery %q / Resource Group %q): %+v", name, galleryName, resourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - if !response.WasNotFound(future.Response()) { - return fmt.Errorf("Error waiting for the deletion of Shared Image %q (Gallery %q / Resource Group %q): %+v", name, galleryName, resourceGroup, err) - } + return fmt.Errorf("failed to wait for deleting Shared Image %q (Gallery %q / Resource Group %q): %+v", name, galleryName, resourceGroup, err) + } + + log.Printf("[DEBUG] Waiting for Shared Image %q (Gallery %q / Resource Group %q) to be eventually deleted", name, galleryName, resourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Exists"}, + Target: []string{"NotFound"}, + Refresh: sharedImageDeleteStateRefreshFunc(ctx, client, resourceGroup, name, galleryName), + MinTimeout: 10 * time.Second, + ContinuousTargetOccurence: 10, + Timeout: d.Timeout(schema.TimeoutDelete), + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("failed to wait for Shared Image %q (Gallery %q / Resource Group %q) to be deleted: %+v", name, galleryName, resourceGroup, err) } return nil } +func sharedImageDeleteStateRefreshFunc(ctx context.Context, client *compute.GalleryImagesClient, resourceGroupName string, imageName string, galleryName string) resource.StateRefreshFunc { + // The resource Shared Image depends on the resource Shared Image Gallery. + // Although the delete API returns 404 which means the Shared Image resource has been deleted. + // Then it tries to immediately delete Shared Image Gallery but it still throws error `Can not delete resource before nested resources are deleted.` + // In this case we're going to try triggering the Deletion again, in-case it didn't work prior to this attempt. + // For more details, see related Bug: https://github.com/Azure/azure-sdk-for-go/issues/8314 + return func() (interface{}, string, error) { + res, err := client.Get(ctx, resourceGroupName, galleryName, imageName) + if err != nil { + if utils.ResponseWasNotFound(res.Response) { + return "NotFound", "NotFound", nil + } + + return nil, "", fmt.Errorf("failed to poll to check if the Shared Image has been deleted: %+v", err) + } + + return res, "Exists", nil + } +} + func expandGalleryImageIdentifier(d *schema.ResourceData) *compute.GalleryImageIdentifier { vs := d.Get("identifier").([]interface{}) v := vs[0].(map[string]interface{}) diff --git a/azurerm/internal/services/compute/tests/data_source_shared_image_test.go b/azurerm/internal/services/compute/tests/data_source_shared_image_test.go index 2057013f644e..7b0d00e03322 100644 --- a/azurerm/internal/services/compute/tests/data_source_shared_image_test.go +++ b/azurerm/internal/services/compute/tests/data_source_shared_image_test.go @@ -16,7 +16,7 @@ func TestAccDataSourceAzureRMSharedImage_basic(t *testing.T) { CheckDestroy: testCheckAzureRMSharedImageDestroy, Steps: []resource.TestStep{ { - Config: testAccDataSourceSharedImage_basic(data), + Config: testAccDataSourceSharedImage_basic(data, ""), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "0"), ), @@ -25,6 +25,24 @@ func TestAccDataSourceAzureRMSharedImage_basic(t *testing.T) { }) } +func TestAccDataSourceAzureRMSharedImage_basic_hyperVGeneration_V2(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_shared_image", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMSharedImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceSharedImage_basic(data, "V2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(data.ResourceName, "hyper_v_generation", "V2"), + ), + }, + }, + }) +} + func TestAccDataSourceAzureRMSharedImage_complete(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_shared_image", "test") resource.ParallelTest(t, resource.TestCase{ @@ -33,17 +51,18 @@ func TestAccDataSourceAzureRMSharedImage_complete(t *testing.T) { CheckDestroy: testCheckAzureRMSharedImageDestroy, Steps: []resource.TestStep{ { - Config: testAccDataSourceSharedImage_complete(data), + Config: testAccDataSourceSharedImage_complete(data, "V1"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(data.ResourceName, "hyper_v_generation", "V1"), ), }, }, }) } -func testAccDataSourceSharedImage_basic(data acceptance.TestData) string { - template := testAccAzureRMSharedImage_basic(data) +func testAccDataSourceSharedImage_basic(data acceptance.TestData, hyperVGen string) string { + template := testAccAzureRMSharedImage_basic(data, hyperVGen) return fmt.Sprintf(` %s @@ -55,8 +74,8 @@ data "azurerm_shared_image" "test" { `, template) } -func testAccDataSourceSharedImage_complete(data acceptance.TestData) string { - template := testAccAzureRMSharedImage_complete(data) +func testAccDataSourceSharedImage_complete(data acceptance.TestData, hyperVGen string) string { + template := testAccAzureRMSharedImage_complete(data, hyperVGen) return fmt.Sprintf(` %s diff --git a/azurerm/internal/services/compute/tests/resource_arm_shared_image_test.go b/azurerm/internal/services/compute/tests/resource_arm_shared_image_test.go index 07dbc88691fa..4a9ce648b9f4 100644 --- a/azurerm/internal/services/compute/tests/resource_arm_shared_image_test.go +++ b/azurerm/internal/services/compute/tests/resource_arm_shared_image_test.go @@ -20,7 +20,7 @@ func TestAccAzureRMSharedImage_basic(t *testing.T) { CheckDestroy: testCheckAzureRMSharedImageDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMSharedImage_basic(data), + Config: testAccAzureRMSharedImage_basic(data, ""), Check: resource.ComposeTestCheckFunc( testCheckAzureRMSharedImageExists(data.ResourceName), resource.TestCheckResourceAttr(data.ResourceName, "description", ""), @@ -30,6 +30,27 @@ func TestAccAzureRMSharedImage_basic(t *testing.T) { }, }) } + +func TestAccAzureRMSharedImage_basic_hyperVGeneration_V2(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_shared_image", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMSharedImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMSharedImage_basic(data, "V2"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSharedImageExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "description", ""), + resource.TestCheckResourceAttr(data.ResourceName, "hyper_v_generation", "V2"), + ), + }, + data.ImportStep(), + }, + }) +} + func TestAccAzureRMSharedImage_requiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_shared_image", "test") @@ -39,7 +60,7 @@ func TestAccAzureRMSharedImage_requiresImport(t *testing.T) { CheckDestroy: testCheckAzureRMSharedImageDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMSharedImage_basic(data), + Config: testAccAzureRMSharedImage_basic(data, ""), Check: resource.ComposeTestCheckFunc( testCheckAzureRMSharedImageExists(data.ResourceName), resource.TestCheckResourceAttr(data.ResourceName, "description", ""), @@ -58,10 +79,11 @@ func TestAccAzureRMSharedImage_complete(t *testing.T) { CheckDestroy: testCheckAzureRMSharedImageDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMSharedImage_complete(data), + Config: testAccAzureRMSharedImage_complete(data, "V1"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMSharedImageExists(data.ResourceName), resource.TestCheckResourceAttr(data.ResourceName, "os_type", "Linux"), + resource.TestCheckResourceAttr(data.ResourceName, "hyper_v_generation", "V1"), resource.TestCheckResourceAttr(data.ResourceName, "description", "Wubba lubba dub dub"), resource.TestCheckResourceAttr(data.ResourceName, "eula", "Do you agree there's infinite Rick's and Infinite Morty's?"), resource.TestCheckResourceAttr(data.ResourceName, "privacy_statement_uri", "https://council.of.ricks/privacy-statement"), @@ -133,12 +155,16 @@ func testCheckAzureRMSharedImageExists(resourceName string) resource.TestCheckFu } } -func testAccAzureRMSharedImage_basic(data acceptance.TestData) string { +func testAccAzureRMSharedImage_basic(data acceptance.TestData, hyperVGen string) string { return fmt.Sprintf(` provider "azurerm" { features {} } +variable "hyper_v_generation" { + default = "%s" +} + resource "azurerm_resource_group" "test" { name = "acctestRG-%d" location = "%s" @@ -156,6 +182,7 @@ resource "azurerm_shared_image" "test" { resource_group_name = azurerm_resource_group.test.name location = azurerm_resource_group.test.location os_type = "Linux" + hyper_v_generation = var.hyper_v_generation != "" ? var.hyper_v_generation : null identifier { publisher = "AccTesPublisher%d" @@ -163,11 +190,11 @@ resource "azurerm_shared_image" "test" { sku = "AccTesSku%d" } } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +`, hyperVGen, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) } func testAccAzureRMSharedImage_requiresImport(data acceptance.TestData) string { - template := testAccAzureRMSharedImage_basic(data) + template := testAccAzureRMSharedImage_basic(data, "") return fmt.Sprintf(` %s @@ -187,7 +214,7 @@ resource "azurerm_shared_image" "import" { `, template, data.RandomInteger, data.RandomInteger, data.RandomInteger) } -func testAccAzureRMSharedImage_complete(data acceptance.TestData) string { +func testAccAzureRMSharedImage_complete(data acceptance.TestData, hyperVGen string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -210,6 +237,7 @@ resource "azurerm_shared_image" "test" { resource_group_name = azurerm_resource_group.test.name location = azurerm_resource_group.test.location os_type = "Linux" + hyper_v_generation = "%s" description = "Wubba lubba dub dub" eula = "Do you agree there's infinite Rick's and Infinite Morty's?" privacy_statement_uri = "https://council.of.ricks/privacy-statement" @@ -221,5 +249,5 @@ resource "azurerm_shared_image" "test" { sku = "AccTesSku%d" } } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, hyperVGen, data.RandomInteger, data.RandomInteger, data.RandomInteger) } diff --git a/website/docs/d/shared_image.html.markdown b/website/docs/d/shared_image.html.markdown index c3d4aa8efc3d..b1a6f05cda85 100644 --- a/website/docs/d/shared_image.html.markdown +++ b/website/docs/d/shared_image.html.markdown @@ -47,6 +47,8 @@ The following attributes are exported: * `os_type` - The type of Operating System present in this Shared Image. +* `hyper_v_generation` - The generation of HyperV that the Virtual Machine used to create the Shared Image is based on. + * `privacy_statement_uri` - The URI containing the Privacy Statement for this Shared Image. * `release_note_uri` - The URI containing the Release Notes for this Shared Image. diff --git a/website/docs/r/shared_image.html.markdown b/website/docs/r/shared_image.html.markdown index a6903b52e300..cc637f7ff9b0 100644 --- a/website/docs/r/shared_image.html.markdown +++ b/website/docs/r/shared_image.html.markdown @@ -68,6 +68,8 @@ The following arguments are supported: * `eula` - (Optional) The End User Licence Agreement for the Shared Image. +* `hyper_v_generation` - (Optional) The generation of HyperV that the Virtual Machine used to create the Shared Image is based on. Possible values are `V1` and `V2`. Defaults to `V1`. Changing this forces a new resource to be created. + * `privacy_statement_uri` - (Optional) The URI containing the Privacy Statement associated with this Shared Image. * `release_note_uri` - (Optional) The URI containing the Release Notes associated with this Shared Image.