diff --git a/azurerm/internal/services/compute/client/client.go b/azurerm/internal/services/compute/client/client.go index bc1cecefcb0e..80130b9d6e09 100644 --- a/azurerm/internal/services/compute/client/client.go +++ b/azurerm/internal/services/compute/client/client.go @@ -8,6 +8,7 @@ import ( type Client struct { AvailabilitySetsClient *compute.AvailabilitySetsClient + DedicatedHostsClient *compute.DedicatedHostsClient DedicatedHostGroupsClient *compute.DedicatedHostGroupsClient DisksClient *compute.DisksClient DiskEncryptionSetsClient *compute.DiskEncryptionSetsClient @@ -32,6 +33,9 @@ func NewClient(o *common.ClientOptions) *Client { availabilitySetsClient := compute.NewAvailabilitySetsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&availabilitySetsClient.Client, o.ResourceManagerAuthorizer) + dedicatedHostsClient := compute.NewDedicatedHostsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&dedicatedHostsClient.Client, o.ResourceManagerAuthorizer) + dedicatedHostGroupsClient := compute.NewDedicatedHostGroupsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&dedicatedHostGroupsClient.Client, o.ResourceManagerAuthorizer) @@ -88,6 +92,7 @@ func NewClient(o *common.ClientOptions) *Client { return &Client{ AvailabilitySetsClient: &availabilitySetsClient, + DedicatedHostsClient: &dedicatedHostsClient, DedicatedHostGroupsClient: &dedicatedHostGroupsClient, DisksClient: &disksClient, DiskEncryptionSetsClient: &diskEncryptionSetsClient, diff --git a/azurerm/internal/services/compute/data_source_dedicated_host.go b/azurerm/internal/services/compute/data_source_dedicated_host.go new file mode 100644 index 000000000000..34a048b4e5b0 --- /dev/null +++ b/azurerm/internal/services/compute/data_source_dedicated_host.go @@ -0,0 +1,72 @@ +package compute + +import ( + "fmt" + "time" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmDedicatedHost() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmDedicatedHostRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateDedicatedHostName(), + }, + + "dedicated_host_group_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateDedicatedHostGroupName(), + }, + + "resource_group_name": azure.SchemaResourceGroupNameForDataSource(), + + "location": azure.SchemaLocationForDataSource(), + + "tags": tags.SchemaDataSource(), + }, + } +} + +func dataSourceArmDedicatedHostRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DedicatedHostsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + resourceGroupName := d.Get("resource_group_name").(string) + hostGroupName := d.Get("dedicated_host_group_name").(string) + + resp, err := client.Get(ctx, resourceGroupName, hostGroupName, name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error: Dedicated Host %q (Host Group Name %q / Resource Group %q) was not found", name, hostGroupName, resourceGroupName) + } + return fmt.Errorf("Error reading Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", name, hostGroupName, resourceGroupName, err) + } + + d.SetId(*resp.ID) + d.Set("name", name) + d.Set("resource_group_name", resourceGroupName) + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + d.Set("dedicated_host_group_name", hostGroupName) + + return tags.FlattenAndSet(d, resp.Tags) +} diff --git a/azurerm/internal/services/compute/parse/dedicated_host.go b/azurerm/internal/services/compute/parse/dedicated_host.go new file mode 100644 index 000000000000..23acd2ff569d --- /dev/null +++ b/azurerm/internal/services/compute/parse/dedicated_host.go @@ -0,0 +1,38 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type DedicatedHostId struct { + ResourceGroup string + HostGroup string + Name string +} + +func DedicatedHostID(input string) (*DedicatedHostId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse Dedicated Host ID %q: %+v", input, err) + } + + server := DedicatedHostId{ + ResourceGroup: id.ResourceGroup, + } + + if server.HostGroup, err = id.PopSegment("hostGroups"); err != nil { + return nil, err + } + + if server.Name, err = id.PopSegment("hosts"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &server, nil +} diff --git a/azurerm/internal/services/compute/parse/dedicated_host_group.go b/azurerm/internal/services/compute/parse/dedicated_host_group.go new file mode 100644 index 000000000000..d4b847cfc39b --- /dev/null +++ b/azurerm/internal/services/compute/parse/dedicated_host_group.go @@ -0,0 +1,33 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type DedicatedHostGroupId struct { + ResourceGroup string + Name string +} + +func DedicatedHostGroupID(input string) (*DedicatedHostGroupId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse Dedicated Host Group ID %q: %+v", input, err) + } + + server := DedicatedHostGroupId{ + ResourceGroup: id.ResourceGroup, + } + + if server.Name, err = id.PopSegment("hostGroups"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &server, nil +} diff --git a/azurerm/internal/services/compute/parse/dedicated_host_group_test.go b/azurerm/internal/services/compute/parse/dedicated_host_group_test.go new file mode 100644 index 000000000000..d02416904e8d --- /dev/null +++ b/azurerm/internal/services/compute/parse/dedicated_host_group_test.go @@ -0,0 +1,75 @@ +package parse + +import ( + "testing" +) + +func TestDedicatedHostGroupID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *DedicatedHostGroupId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Error: true, + }, + { + Name: "Missing Host Group Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/", + Error: true, + }, + { + Name: "Host Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/group1", + Error: false, + Expect: &DedicatedHostGroupId{ + ResourceGroup: "resGroup1", + Name: "group1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/HostGroups/group1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := DedicatedHostGroupID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/compute/parse/dedicated_host_test.go b/azurerm/internal/services/compute/parse/dedicated_host_test.go new file mode 100644 index 000000000000..3747e4a498a8 --- /dev/null +++ b/azurerm/internal/services/compute/parse/dedicated_host_test.go @@ -0,0 +1,90 @@ +package parse + +import ( + "testing" +) + +func TestDedicatedHostID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *DedicatedHostId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Error: true, + }, + { + Name: "Missing Host Group Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/", + Error: true, + }, + { + Name: "Host Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/group1/", + Error: true, + }, + { + Name: "Missing Host Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/group1/hosts/", + Error: true, + }, + { + Name: "Host ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/group1/hosts/host1", + Error: false, + Expect: &DedicatedHostId{ + ResourceGroup: "resGroup1", + HostGroup: "group1", + Name: "host1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Compute/hostGroups/group1/Hosts/host1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := DedicatedHostID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + + if actual.HostGroup != v.Expect.HostGroup { + t.Fatalf("Expected %q but got %q for HostGroup", v.Expect.HostGroup, actual.HostGroup) + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/compute/registration.go b/azurerm/internal/services/compute/registration.go index 263de4f7f94a..dd275ba5c2bc 100644 --- a/azurerm/internal/services/compute/registration.go +++ b/azurerm/internal/services/compute/registration.go @@ -16,6 +16,7 @@ func (r Registration) Name() string { func (r Registration) SupportedDataSources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azurerm_availability_set": dataSourceArmAvailabilitySet(), + "azurerm_dedicated_host": dataSourceArmDedicatedHost(), "azurerm_dedicated_host_group": dataSourceArmDedicatedHostGroup(), "azurerm_disk_encryption_set": dataSourceArmDiskEncryptionSet(), "azurerm_managed_disk": dataSourceArmManagedDisk(), @@ -34,6 +35,7 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { func (r Registration) SupportedResources() map[string]*schema.Resource { resources := map[string]*schema.Resource{ "azurerm_availability_set": resourceArmAvailabilitySet(), + "azurerm_dedicated_host": resourceArmDedicatedHost(), "azurerm_dedicated_host_group": resourceArmDedicatedHostGroup(), "azurerm_disk_encryption_set": resourceArmDiskEncryptionSet(), "azurerm_image": resourceArmImage(), diff --git a/azurerm/internal/services/compute/resource_arm_dedicated_host.go b/azurerm/internal/services/compute/resource_arm_dedicated_host.go new file mode 100644 index 000000000000..dfa334e9fa22 --- /dev/null +++ b/azurerm/internal/services/compute/resource_arm_dedicated_host.go @@ -0,0 +1,303 @@ +package compute + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/validate" + + "github.com/hashicorp/go-azure-helpers/response" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute" + "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" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmDedicatedHost() *schema.Resource { + return &schema.Resource{ + Create: resourceArmDedicatedHostCreate, + Read: resourceArmDedicatedHostRead, + Update: resourceArmDedicatedHostUpdate, + Delete: resourceArmDedicatedHostDelete, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.DedicatedHostID(id) + return err + }), + + 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), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateDedicatedHostName(), + }, + + "location": azure.SchemaLocation(), + + "dedicated_host_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.DedicatedHostGroupID, + }, + + "sku_name": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "DSv3-Type1", + "ESv3-Type1", + "FSv2-Type2", + }, false), + }, + + "platform_fault_domain": { + Type: schema.TypeInt, + ForceNew: true, + Required: true, + }, + + "auto_replace_on_failure": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "license_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.DedicatedHostLicenseTypesNone), + string(compute.DedicatedHostLicenseTypesWindowsServerHybrid), + string(compute.DedicatedHostLicenseTypesWindowsServerPerpetual), + }, false), + Default: string(compute.DedicatedHostLicenseTypesNone), + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmDedicatedHostCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DedicatedHostsClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + dedicatedHostGroupId, err := parse.DedicatedHostGroupID(d.Get("dedicated_host_group_id").(string)) + if err != nil { + return err + } + + resourceGroupName := dedicatedHostGroupId.ResourceGroup + hostGroupName := dedicatedHostGroupId.Name + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroupName, hostGroupName, name, "") + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for present of existing Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", name, hostGroupName, resourceGroupName, err) + } + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_dedicated_host", *existing.ID) + } + } + + parameters := compute.DedicatedHost{ + Location: utils.String(azure.NormalizeLocation(d.Get("location").(string))), + DedicatedHostProperties: &compute.DedicatedHostProperties{ + AutoReplaceOnFailure: utils.Bool(d.Get("auto_replace_on_failure").(bool)), + LicenseType: compute.DedicatedHostLicenseTypes(d.Get("license_type").(string)), + PlatformFaultDomain: utils.Int32(int32(d.Get("platform_fault_domain").(int))), + }, + Sku: &compute.Sku{ + Name: utils.String(d.Get("sku_name").(string)), + }, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + future, err := client.CreateOrUpdate(ctx, resourceGroupName, hostGroupName, name, parameters) + if err != nil { + return fmt.Errorf("Error creating Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", name, hostGroupName, resourceGroupName, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for creation of Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", name, hostGroupName, resourceGroupName, err) + } + + resp, err := client.Get(ctx, resourceGroupName, hostGroupName, name, "") + if err != nil { + return fmt.Errorf("Error retrieving Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", name, hostGroupName, resourceGroupName, err) + } + if resp.ID == nil { + return fmt.Errorf("Cannot read ID for Dedicated Host %q (Host Group Name %q / Resource Group %q)", name, hostGroupName, resourceGroupName) + } + d.SetId(*resp.ID) + + return resourceArmDedicatedHostRead(d, meta) +} + +func resourceArmDedicatedHostRead(d *schema.ResourceData, meta interface{}) error { + groupsClient := meta.(*clients.Client).Compute.DedicatedHostGroupsClient + hostsClient := meta.(*clients.Client).Compute.DedicatedHostsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.DedicatedHostID(d.Id()) + if err != nil { + return err + } + + group, err := groupsClient.Get(ctx, id.ResourceGroup, id.HostGroup) + if err != nil { + if utils.ResponseWasNotFound(group.Response) { + log.Printf("[INFO] Parent Dedicated Host Group %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving Dedicated Host Group %q (Resource Group %q): %+v", id.HostGroup, id.ResourceGroup, err) + } + + resp, err := hostsClient.Get(ctx, id.ResourceGroup, id.HostGroup, id.Name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Dedicated Host %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", id.Name, id.HostGroup, id.ResourceGroup, err) + } + + d.Set("name", resp.Name) + d.Set("dedicated_host_group_id", group.ID) + + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + d.Set("sku_name", resp.Sku.Name) + if props := resp.DedicatedHostProperties; props != nil { + d.Set("auto_replace_on_failure", props.AutoReplaceOnFailure) + d.Set("license_type", props.LicenseType) + + platformFaultDomain := 0 + if props.PlatformFaultDomain != nil { + platformFaultDomain = int(*props.PlatformFaultDomain) + } + d.Set("platform_fault_domain", platformFaultDomain) + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceArmDedicatedHostUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DedicatedHostsClient + ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.DedicatedHostID(d.Id()) + if err != nil { + return err + } + + parameters := compute.DedicatedHostUpdate{ + DedicatedHostProperties: &compute.DedicatedHostProperties{ + AutoReplaceOnFailure: utils.Bool(d.Get("auto_replace_on_failure").(bool)), + LicenseType: compute.DedicatedHostLicenseTypes(d.Get("license_type").(string)), + }, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + future, err := client.Update(ctx, id.ResourceGroup, id.HostGroup, id.Name, parameters) + if err != nil { + return fmt.Errorf("Error updating Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", id.Name, id.HostGroup, id.ResourceGroup, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for update of Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", id.Name, id.HostGroup, id.ResourceGroup, err) + } + + return resourceArmDedicatedHostRead(d, meta) +} + +func resourceArmDedicatedHostDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.DedicatedHostsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.DedicatedHostID(d.Id()) + if err != nil { + return err + } + + future, err := client.Delete(ctx, id.ResourceGroup, id.HostGroup, id.Name) + if err != nil { + return fmt.Errorf("Error deleting Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", id.Name, id.HostGroup, id.ResourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("Error waiting for deleting Dedicated Host %q (Host Group Name %q / Resource Group %q): %+v", id.Name, id.HostGroup, id.ResourceGroup, err) + } + } + + // API has bug, which appears to be eventually consistent. Tracked by this issue: https://github.com/Azure/azure-rest-api-specs/issues/8137 + log.Printf("[DEBUG] Waiting for Dedicated Host %q (Host Group Name %q / Resource Group %q) to disappear", id.Name, id.HostGroup, id.ResourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Exists"}, + Target: []string{"NotFound"}, + Refresh: dedicatedHostDeletedRefreshFunc(ctx, client, id), + MinTimeout: 10 * time.Second, + ContinuousTargetOccurence: 10, + } + + if features.SupportsCustomTimeouts() { + stateConf.Timeout = d.Timeout(schema.TimeoutDelete) + } else { + stateConf.Timeout = 10 * time.Minute + } + + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Dedicated Host %q (Host Group Name %q / Resource Group %q) to become available: %+v", id.Name, id.HostGroup, id.ResourceGroup, err) + } + + return nil +} + +func dedicatedHostDeletedRefreshFunc(ctx context.Context, client *compute.DedicatedHostsClient, id *parse.DedicatedHostId) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.Get(ctx, id.ResourceGroup, id.HostGroup, id.Name, "") + if err != nil { + if utils.ResponseWasNotFound(res.Response) { + return "NotFound", "NotFound", nil + } + + return nil, "", fmt.Errorf("Error polling to check if the Dedicated Host has been deleted: %+v", err) + } + + return res, "Exists", nil + } +} diff --git a/azurerm/internal/services/compute/resource_arm_dedicated_host_group.go b/azurerm/internal/services/compute/resource_arm_dedicated_host_group.go index 161bf07a0bb4..9c12d070cfd3 100644 --- a/azurerm/internal/services/compute/resource_arm_dedicated_host_group.go +++ b/azurerm/internal/services/compute/resource_arm_dedicated_host_group.go @@ -3,7 +3,6 @@ package compute import ( "fmt" "log" - "regexp" "time" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" @@ -43,7 +42,7 @@ func resourceArmDedicatedHostGroup() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[^_\W][\w-.]{0,78}[\w]$`), ""), + ValidateFunc: validateDedicatedHostGroupName(), }, "location": azure.SchemaLocation(), diff --git a/azurerm/internal/services/compute/tests/data_source_dedicated_host_test.go b/azurerm/internal/services/compute/tests/data_source_dedicated_host_test.go new file mode 100644 index 000000000000..a896d2df5fbd --- /dev/null +++ b/azurerm/internal/services/compute/tests/data_source_dedicated_host_test.go @@ -0,0 +1,41 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccDataSourceAzureRMDedicatedHost_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceDedicatedHost_basic(data), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(data.ResourceName, "location"), + resource.TestCheckResourceAttrSet(data.ResourceName, "tags.%"), + ), + }, + }, + }) +} + +func testAccDataSourceDedicatedHost_basic(data acceptance.TestData) string { + config := testAccAzureRMDedicatedHost_basic(data) + return fmt.Sprintf(` +%s + +data "azurerm_dedicated_host" "test" { + name = azurerm_dedicated_host.test.name + dedicated_host_group_name = azurerm_dedicated_host_group.test.name + resource_group_name = azurerm_dedicated_host_group.test.resource_group_name +} +`, config) +} diff --git a/azurerm/internal/services/compute/tests/resource_arm_dedicated_host_test.go b/azurerm/internal/services/compute/tests/resource_arm_dedicated_host_test.go new file mode 100644 index 000000000000..b3ab2b154960 --- /dev/null +++ b/azurerm/internal/services/compute/tests/resource_arm_dedicated_host_test.go @@ -0,0 +1,328 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" + + "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/features" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMDedicatedHost_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDedicatedHost_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDedicatedHost_autoReplaceOnFailure(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + // Enabled + Config: testAccAzureRMDedicatedHost_autoReplaceOnFailure(data, true), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + // Disabled + Config: testAccAzureRMDedicatedHost_autoReplaceOnFailure(data, false), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + // Enabled + Config: testAccAzureRMDedicatedHost_autoReplaceOnFailure(data, true), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDedicatedHost_licenseType(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDedicatedHost_licenceType(data, "None"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDedicatedHost_licenceType(data, "Windows_Server_Hybrid"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDedicatedHost_licenceType(data, "Windows_Server_Perpetual"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDedicatedHost_licenceType(data, "None"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDedicatedHost_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDedicatedHost_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDedicatedHost_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDedicatedHost_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMDedicatedHost_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMDedicatedHost_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + data := acceptance.BuildTestData(t, "azurerm_dedicated_host", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMDedicatedHost_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMDedicatedHostExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMDedicatedHost_requiresImport), + }, + }) +} + +func testCheckAzureRMDedicatedHostExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Dedicated Host not found: %s", resourceName) + } + + id, err := parse.DedicatedHostID(rs.Primary.ID) + if err != nil { + return err + } + + client := acceptance.AzureProvider.Meta().(*clients.Client).Compute.DedicatedHostsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + if resp, err := client.Get(ctx, id.ResourceGroup, id.HostGroup, id.Name, ""); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Dedicated Host %q (Host Group Name %q / Resource Group %q) does not exist", id.Name, id.HostGroup, id.ResourceGroup) + } + return fmt.Errorf("Bad: Get on Compute.DedicatedHostsClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMDedicatedHostDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Compute.DedicatedHostsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_dedicated_host" { + continue + } + + id, err := parse.DedicatedHostID(rs.Primary.ID) + if err != nil { + return err + } + + if resp, err := client.Get(ctx, id.ResourceGroup, id.HostGroup, id.Name, ""); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Get on Compute.DedicatedHostsClient: %+v", err) + } + } + + return nil + } + + return nil +} + +func testAccAzureRMDedicatedHost_basic(data acceptance.TestData) string { + template := testAccAzureRMDedicatedHost_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_dedicated_host" "test" { + name = "acctest-DH-%d" + location = azurerm_resource_group.test.location + dedicated_host_group_id = azurerm_dedicated_host_group.test.id + sku_name = "DSv3-Type1" + platform_fault_domain = 1 +} +`, template, data.RandomInteger) +} + +func testAccAzureRMDedicatedHost_autoReplaceOnFailure(data acceptance.TestData, replace bool) string { + template := testAccAzureRMDedicatedHost_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_dedicated_host" "test" { + name = "acctest-DH-%d" + location = azurerm_resource_group.test.location + dedicated_host_group_id = azurerm_dedicated_host_group.test.id + sku_name = "DSv3-Type1" + platform_fault_domain = 1 + auto_replace_on_failure = %t +} +`, template, data.RandomInteger, replace) +} + +func testAccAzureRMDedicatedHost_licenceType(data acceptance.TestData, licenseType string) string { + template := testAccAzureRMDedicatedHost_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_dedicated_host" "test" { + name = "acctest-DH-%d" + location = azurerm_resource_group.test.location + dedicated_host_group_id = azurerm_dedicated_host_group.test.id + sku_name = "DSv3-Type1" + platform_fault_domain = 1 + license_type = %q +} +`, template, data.RandomInteger, licenseType) +} + +func testAccAzureRMDedicatedHost_complete(data acceptance.TestData) string { + template := testAccAzureRMDedicatedHost_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_dedicated_host" "test" { + name = "acctest-DH-%d" + location = azurerm_resource_group.test.location + dedicated_host_group_id = azurerm_dedicated_host_group.test.id + sku_name = "DSv3-Type1" + platform_fault_domain = 1 + license_type = "Windows_Server_Hybrid" + auto_replace_on_failure = false +} +`, template, data.RandomInteger) +} + +func testAccAzureRMDedicatedHost_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMDedicatedHost_basic(data) + return fmt.Sprintf(` +%s +resource "azurerm_dedicated_host" "import" { + name = azurerm_dedicated_host.test.name + location = azurerm_dedicated_host.test.location + dedicated_host_group_id = azurerm_dedicated_host.test.dedicated_host_group_id + sku_name = azurerm_dedicated_host.test.sku_name + platform_fault_domain = azurerm_dedicated_host.test.platform_fault_domain +} +`, template) +} + +func testAccAzureRMDedicatedHost_template(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-compute-%d" + location = "%s" +} + +resource "azurerm_dedicated_host_group" "test" { + name = "acctest-DHG-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + platform_fault_domain_count = 2 +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/azurerm/internal/services/compute/validate/dedicated_host.go b/azurerm/internal/services/compute/validate/dedicated_host.go new file mode 100644 index 000000000000..148e77a6dffa --- /dev/null +++ b/azurerm/internal/services/compute/validate/dedicated_host.go @@ -0,0 +1,22 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" +) + +func DedicatedHostID(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 + } + + if _, err := parse.DedicatedHostID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/azurerm/internal/services/compute/validate/dedicated_host_group.go b/azurerm/internal/services/compute/validate/dedicated_host_group.go new file mode 100644 index 000000000000..b5964eb82c03 --- /dev/null +++ b/azurerm/internal/services/compute/validate/dedicated_host_group.go @@ -0,0 +1,22 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" +) + +func DedicatedHostGroupID(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 + } + + if _, err := parse.DedicatedHostGroupID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/azurerm/internal/services/compute/validation.go b/azurerm/internal/services/compute/validation.go index 287c285c3395..2f2e56e9b4db 100644 --- a/azurerm/internal/services/compute/validation.go +++ b/azurerm/internal/services/compute/validation.go @@ -4,6 +4,8 @@ import ( "fmt" "regexp" "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" ) func ValidateLinuxName(i interface{}, k string) (warnings []string, errors []error) { @@ -103,3 +105,11 @@ func validateDiskSizeGB(v interface{}, _ string) (warnings []string, errors []er } return warnings, errors } + +func validateDedicatedHostGroupName() func(i interface{}, k string) (warnings []string, errors []error) { + return validation.StringMatch(regexp.MustCompile(`^[^_\W][\w-.]{0,78}[\w]$`), "") +} + +func validateDedicatedHostName() func(i interface{}, k string) (warnings []string, errors []error) { + return validateDedicatedHostGroupName() +} diff --git a/website/azurerm.erb b/website/azurerm.erb index b485474d72e3..45745f5158f3 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -170,6 +170,10 @@ azurerm_data_lake_store +