diff --git a/azurerm/internal/services/desktopvirtualization/client/client.go b/azurerm/internal/services/desktopvirtualization/client/client.go index 37f09f22c8dd..970a93b1104a 100644 --- a/azurerm/internal/services/desktopvirtualization/client/client.go +++ b/azurerm/internal/services/desktopvirtualization/client/client.go @@ -7,6 +7,7 @@ import ( type Client struct { ApplicationGroupsClient *desktopvirtualization.ApplicationGroupsClient + ApplicationsClient *desktopvirtualization.ApplicationsClient DesktopsClient *desktopvirtualization.DesktopsClient HostPoolsClient *desktopvirtualization.HostPoolsClient OperationsClient *desktopvirtualization.OperationsClient @@ -19,6 +20,9 @@ func NewClient(o *common.ClientOptions) *Client { ApplicationGroupsClient := desktopvirtualization.NewApplicationGroupsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&ApplicationGroupsClient.Client, o.ResourceManagerAuthorizer) + ApplicationsClient := desktopvirtualization.NewApplicationsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&ApplicationsClient.Client, o.ResourceManagerAuthorizer) + DesktopsClient := desktopvirtualization.NewDesktopsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&DesktopsClient.Client, o.ResourceManagerAuthorizer) @@ -36,6 +40,7 @@ func NewClient(o *common.ClientOptions) *Client { return &Client{ ApplicationGroupsClient: &ApplicationGroupsClient, + ApplicationsClient: &ApplicationsClient, DesktopsClient: &DesktopsClient, HostPoolsClient: &HostPoolsClient, OperationsClient: &OperationsClient, diff --git a/azurerm/internal/services/desktopvirtualization/parse/application.go b/azurerm/internal/services/desktopvirtualization/parse/application.go new file mode 100644 index 000000000000..408219bb691a --- /dev/null +++ b/azurerm/internal/services/desktopvirtualization/parse/application.go @@ -0,0 +1,131 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type ApplicationId struct { + SubscriptionId string + ResourceGroup string + ApplicationGroupName string + Name string +} + +func NewApplicationID(subscriptionId, resourceGroup, applicationGroupName, name string) ApplicationId { + return ApplicationId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + ApplicationGroupName: applicationGroupName, + Name: name, + } +} + +func (id ApplicationId) String() string { + segments := []string{ + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Application Group Name %q", id.ApplicationGroupName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Application", segmentsStr) +} + +func (id ApplicationId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.DesktopVirtualization/applicationGroups/%s/applications/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.ApplicationGroupName, id.Name) +} + +// ApplicationID parses a Application ID into an ApplicationId struct +func ApplicationID(input string) (*ApplicationId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := ApplicationId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.ApplicationGroupName, err = id.PopSegment("applicationGroups"); err != nil { + return nil, err + } + if resourceId.Name, err = id.PopSegment("applications"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} + +// ApplicationIDInsensitively parses an Application ID into an ApplicationId struct, insensitively +// This should only be used to parse an ID for rewriting, the ApplicationID +// method should be used instead for validation etc. +// +// Whilst this may seem strange, this enables Terraform have consistent casing +// which works around issues in Core, whilst handling broken API responses. +func ApplicationIDInsensitively(input string) (*ApplicationId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := ApplicationId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + // find the correct casing for the 'applicationGroups' segment + applicationGroupsKey := "applicationGroups" + for key := range id.Path { + if strings.EqualFold(key, applicationGroupsKey) { + applicationGroupsKey = key + break + } + } + if resourceId.ApplicationGroupName, err = id.PopSegment(applicationGroupsKey); err != nil { + return nil, err + } + + // find the correct casing for the 'applications' segment + applicationsKey := "applications" + for key := range id.Path { + if strings.EqualFold(key, applicationsKey) { + applicationsKey = key + break + } + } + if resourceId.Name, err = id.PopSegment(applicationsKey); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/azurerm/internal/services/desktopvirtualization/parse/application_test.go b/azurerm/internal/services/desktopvirtualization/parse/application_test.go new file mode 100644 index 000000000000..21ee6b41fc90 --- /dev/null +++ b/azurerm/internal/services/desktopvirtualization/parse/application_test.go @@ -0,0 +1,264 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/resourceid" +) + +var _ resourceid.Formatter = ApplicationId{} + +func TestApplicationIDFormatter(t *testing.T) { + actual := NewApplicationID("12345678-1234-9876-4563-123456789012", "resGroup1", "applicationGroup1", "application1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/application1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestApplicationID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *ApplicationId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing ApplicationGroupName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/", + Error: true, + }, + + { + // missing value for ApplicationGroupName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/application1", + Expected: &ApplicationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ApplicationGroupName: "applicationGroup1", + Name: "application1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.DESKTOPVIRTUALIZATION/APPLICATIONGROUPS/APPLICATIONGROUP1/APPLICATIONS/APPLICATION1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := ApplicationID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.ApplicationGroupName != v.Expected.ApplicationGroupName { + t.Fatalf("Expected %q but got %q for ApplicationGroupName", v.Expected.ApplicationGroupName, actual.ApplicationGroupName) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} + +func TestApplicationIDInsensitively(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *ApplicationId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing ApplicationGroupName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/", + Error: true, + }, + + { + // missing value for ApplicationGroupName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/application1", + Expected: &ApplicationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ApplicationGroupName: "applicationGroup1", + Name: "application1", + }, + }, + + { + // lower-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationgroups/applicationGroup1/applications/application1", + Expected: &ApplicationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ApplicationGroupName: "applicationGroup1", + Name: "application1", + }, + }, + + { + // upper-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/APPLICATIONGROUPS/applicationGroup1/APPLICATIONS/application1", + Expected: &ApplicationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ApplicationGroupName: "applicationGroup1", + Name: "application1", + }, + }, + + { + // mixed-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/ApPlIcAtIoNgRoUpS/applicationGroup1/ApPlIcAtIoNs/application1", + Expected: &ApplicationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ApplicationGroupName: "applicationGroup1", + Name: "application1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := ApplicationIDInsensitively(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.ApplicationGroupName != v.Expected.ApplicationGroupName { + t.Fatalf("Expected %q but got %q for ApplicationGroupName", v.Expected.ApplicationGroupName, actual.ApplicationGroupName) + } + 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/desktopvirtualization/registration.go b/azurerm/internal/services/desktopvirtualization/registration.go index 62c2a158df8c..819540f180c1 100644 --- a/azurerm/internal/services/desktopvirtualization/registration.go +++ b/azurerm/internal/services/desktopvirtualization/registration.go @@ -29,6 +29,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { "azurerm_virtual_desktop_workspace": resourceArmDesktopVirtualizationWorkspace(), "azurerm_virtual_desktop_host_pool": resourceVirtualDesktopHostPool(), "azurerm_virtual_desktop_application_group": resourceVirtualDesktopApplicationGroup(), + "azurerm_virtual_desktop_application": resourceVirtualDesktopApplication(), "azurerm_virtual_desktop_workspace_application_group_association": resourceVirtualDesktopWorkspaceApplicationGroupAssociation(), } } diff --git a/azurerm/internal/services/desktopvirtualization/resourcesid.go b/azurerm/internal/services/desktopvirtualization/resourcesid.go index 6b20621db9a9..3d49df141138 100644 --- a/azurerm/internal/services/desktopvirtualization/resourcesid.go +++ b/azurerm/internal/services/desktopvirtualization/resourcesid.go @@ -1,5 +1,6 @@ package desktopvirtualization //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=ApplicationGroup -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1 -rewrite=true +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Application -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/application1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=HostPool -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/hostPools/pool1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Workspace -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/workspaces/workspace1 diff --git a/azurerm/internal/services/desktopvirtualization/validate/application_id.go b/azurerm/internal/services/desktopvirtualization/validate/application_id.go new file mode 100644 index 000000000000..7ca66d9b4b79 --- /dev/null +++ b/azurerm/internal/services/desktopvirtualization/validate/application_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/desktopvirtualization/parse" +) + +func ApplicationID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.ApplicationID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/azurerm/internal/services/desktopvirtualization/validate/application_id_test.go b/azurerm/internal/services/desktopvirtualization/validate/application_id_test.go new file mode 100644 index 000000000000..61765d81af9d --- /dev/null +++ b/azurerm/internal/services/desktopvirtualization/validate/application_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestApplicationID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing ApplicationGroupName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/", + Valid: false, + }, + + { + // missing value for ApplicationGroupName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/applicationGroup1/applications/application1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.DESKTOPVIRTUALIZATION/APPLICATIONGROUPS/APPLICATIONGROUP1/APPLICATIONS/APPLICATION1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := ApplicationID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/azurerm/internal/services/desktopvirtualization/virtual_desktop_application_resource.go b/azurerm/internal/services/desktopvirtualization/virtual_desktop_application_resource.go new file mode 100644 index 000000000000..49a2c0e999df --- /dev/null +++ b/azurerm/internal/services/desktopvirtualization/virtual_desktop_application_resource.go @@ -0,0 +1,229 @@ +package desktopvirtualization + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/desktopvirtualization/mgmt/2020-11-02-preview/desktopvirtualization" + "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/locks" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/desktopvirtualization/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/desktopvirtualization/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/pluginsdk" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +var applicationType = "azurerm_virtual_desktop_application" + +func resourceVirtualDesktopApplication() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Create: resourceVirtualDesktopApplicationCreateUpdate, + Read: resourceVirtualDesktopApplicationRead, + Update: resourceVirtualDesktopApplicationCreateUpdate, + Delete: resourceVirtualDesktopApplicationDelete, + + Timeouts: &pluginsdk.ResourceTimeout{ + Create: pluginsdk.DefaultTimeout(60 * time.Minute), + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + Update: pluginsdk.DefaultTimeout(60 * time.Minute), + Delete: pluginsdk.DefaultTimeout(60 * time.Minute), + }, + + Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { + _, err := parse.ApplicationID(id) + return err + }), + + SchemaVersion: 0, + + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringIsNotEmpty, + validation.StringMatch( + regexp.MustCompile("^[-a-zA-Z0-9]{1,260}$"), + "Virtual desktop application name must be 1 - 260 characters long, contain only letters, numbers and hyphens.", + ), + ), + }, + + "application_group_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ApplicationGroupID, + }, + + "friendly_name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 64), + Computed: true, + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 512), + }, + + "path": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "command_line_argument_policy": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(desktopvirtualization.Allow), + string(desktopvirtualization.DoNotAllow), + string(desktopvirtualization.Require), + }, false), + }, + + "command_line_arguments": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "show_in_portal": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + + "icon_path": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + + "icon_index": { + Type: pluginsdk.TypeInt, + Optional: true, + }, + }, + } +} + +func resourceVirtualDesktopApplicationCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DesktopVirtualization.ApplicationsClient + subscriptionId := meta.(*clients.Client).Account.SubscriptionId + + log.Printf("[INFO] preparing arguments for Virtual Desktop Application creation") + + name := d.Get("name").(string) + applicationGroup, _ := parse.ApplicationGroupID(d.Get("application_group_id").(string)) + + locks.ByName(name, applicationType) + defer locks.UnlockByName(name, applicationType) + + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + resourceId := parse.NewApplicationID(subscriptionId, applicationGroup.ResourceGroup, applicationGroup.Name, name).ID() + if d.IsNewResource() { + existing, err := client.Get(ctx, applicationGroup.ResourceGroup, applicationGroup.Name, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing Virtual Desktop Application %q (Application Group %q) (Resource Group %q): %s", name, applicationGroup.Name, applicationGroup.ResourceGroup, err) + } + } + + if existing.ApplicationProperties != nil { + return tf.ImportAsExistsError("azurerm_virtual_desktop_application", resourceId) + } + } + + context := desktopvirtualization.Application{ + ApplicationProperties: &desktopvirtualization.ApplicationProperties{ + FriendlyName: utils.String(d.Get("friendly_name").(string)), + Description: utils.String(d.Get("description").(string)), + FilePath: utils.String(d.Get("path").(string)), + CommandLineSetting: desktopvirtualization.CommandLineSetting(d.Get("command_line_argument_policy").(string)), + CommandLineArguments: utils.String(d.Get("command_line_arguments").(string)), + ShowInPortal: utils.Bool(d.Get("show_in_portal").(bool)), + IconPath: utils.String(d.Get("icon_path").(string)), + IconIndex: utils.Int32(int32(d.Get("icon_index").(int))), + }, + } + + if _, err := client.CreateOrUpdate(ctx, applicationGroup.ResourceGroup, applicationGroup.Name, name, context); err != nil { + return fmt.Errorf("creating Virtual Desktop Application %q (Application Group %q) (Resource Group %q): %+v", name, applicationGroup.Name, applicationGroup.ResourceGroup, err) + } + + d.SetId(resourceId) + return resourceVirtualDesktopApplicationRead(d, meta) +} + +func resourceVirtualDesktopApplicationRead(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DesktopVirtualization.ApplicationsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ApplicationID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.ApplicationGroupName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Virtual Desktop Application %q was not found in Resource Group %q - removing from state!", id.Name, id.ResourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("retrieving Virtual Desktop Application %q (Application Group %q) (Resource Group %q): %+v", id.Name, id.ApplicationGroupName, id.ResourceGroup, err) + } + + applicationGroup := parse.ApplicationGroupId{ + SubscriptionId: id.SubscriptionId, + ResourceGroup: id.ResourceGroup, + Name: id.ApplicationGroupName, + } + + d.Set("name", id.Name) + d.Set("application_group_id", applicationGroup.ID()) + + if props := resp.ApplicationProperties; props != nil { + d.Set("friendly_name", props.FriendlyName) + d.Set("description", props.Description) + d.Set("path", props.FilePath) + d.Set("command_line_argument_policy", props.CommandLineSetting) + d.Set("command_line_arguments", props.CommandLineArguments) + d.Set("show_in_portal", props.ShowInPortal) + d.Set("icon_path", props.IconPath) + d.Set("icon_index", props.IconIndex) + } + + return nil +} + +func resourceVirtualDesktopApplicationDelete(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).DesktopVirtualization.ApplicationsClient + + id, err := parse.ApplicationID(d.Id()) + if err != nil { + return err + } + + locks.ByName(id.Name, applicationType) + defer locks.UnlockByName(id.Name, applicationType) + + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + if _, err = client.Delete(ctx, id.ResourceGroup, id.ApplicationGroupName, id.Name); err != nil { + return fmt.Errorf("deleting Virtual Desktop Application %q (Application Group %q) (Resource Group %q): %+v", id.Name, id.ApplicationGroupName, id.ResourceGroup, err) + } + + return nil +} diff --git a/azurerm/internal/services/desktopvirtualization/virtual_desktop_application_resource_test.go b/azurerm/internal/services/desktopvirtualization/virtual_desktop_application_resource_test.go new file mode 100644 index 000000000000..d8f276c98dd7 --- /dev/null +++ b/azurerm/internal/services/desktopvirtualization/virtual_desktop_application_resource_test.go @@ -0,0 +1,203 @@ +package desktopvirtualization_test + +import ( + "context" + "fmt" + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/desktopvirtualization/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/pluginsdk" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type VirtualDesktopApplicationResource struct { +} + +func TestAccVirtualDesktopApplication_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_desktop_application", "test") + r := VirtualDesktopApplicationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualDesktopApplication_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_desktop_application", "test") + r := VirtualDesktopApplicationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualDesktopApplication_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_desktop_application", "test") + r := VirtualDesktopApplicationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualDesktopApplication_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_desktop_application", "test") + r := VirtualDesktopApplicationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_virtual_desktop_application"), + }, + }) +} + +func (VirtualDesktopApplicationResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.ApplicationID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.DesktopVirtualization.ApplicationsClient.Get(ctx, id.ResourceGroup, id.ApplicationGroupName, id.Name) + if err != nil { + return nil, fmt.Errorf("retrieving Virtual Desktop Application Group %q (Resource Group: %q) does not exist", id.Name, id.ResourceGroup) + } + + return utils.Bool(resp.ApplicationProperties != nil), nil +} + +func (VirtualDesktopApplicationResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-vdesktop-%d" + location = "%s" +} + +resource "azurerm_virtual_desktop_host_pool" "test" { + name = "acctestHP" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + type = "Pooled" + load_balancer_type = "BreadthFirst" +} + +resource "azurerm_virtual_desktop_application_group" "test" { + name = "acctestAG%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + type = "RemoteApp" + host_pool_id = azurerm_virtual_desktop_host_pool.test.id +} + +resource "azurerm_virtual_desktop_application" "test" { + name = "acctestAG%d" + application_group_id = azurerm_virtual_desktop_application_group.test.id + path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + command_line_argument_policy = "DoNotAllow" +} +`, data.RandomInteger, data.Locations.Secondary, data.RandomIntOfLength(8), data.RandomIntOfLength(8)) +} + +func (VirtualDesktopApplicationResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-vdesktop-%d" + location = "%s" +} + +resource "azurerm_virtual_desktop_host_pool" "test" { + name = "acctestHP" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + validate_environment = true + description = "Acceptance Test: A host pool" + type = "Pooled" + load_balancer_type = "BreadthFirst" +} + +resource "azurerm_virtual_desktop_application_group" "test" { + name = "acctestAG%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + type = "RemoteApp" + host_pool_id = azurerm_virtual_desktop_host_pool.test.id + friendly_name = "TestAppGroup" + description = "Acceptance Test: An application group" + tags = { + Purpose = "Acceptance-Testing" + } +} + +resource "azurerm_virtual_desktop_application" "test" { + name = "acctestAG%d" + application_group_id = azurerm_virtual_desktop_application_group.test.id + friendly_name = "Google Chrome" + description = "Chromium based web browser" + path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + command_line_argument_policy = "DoNotAllow" + command_line_arguments = "--incognito" + show_in_portal = false + icon_path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + icon_index = 1 +} + +`, data.RandomInteger, data.Locations.Secondary, data.RandomIntOfLength(8), data.RandomIntOfLength(8)) +} + +func (r VirtualDesktopApplicationResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_desktop_application" "import" { + name = azurerm_virtual_desktop_application.test.name + application_group_id = azurerm_virtual_desktop_application.test.application_group_id + path = azurerm_virtual_desktop_application.test.path + command_line_argument_policy = azurerm_virtual_desktop_application.test.command_line_argument_policy + +} +`, r.basic(data)) +} diff --git a/website/docs/r/virtual_desktop_application.html.markdown b/website/docs/r/virtual_desktop_application.html.markdown new file mode 100644 index 000000000000..3f4570a80499 --- /dev/null +++ b/website/docs/r/virtual_desktop_application.html.markdown @@ -0,0 +1,114 @@ +--- +subcategory: "Desktop Virtualization" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_virtual_desktop_application" +description: |- + Manages a Virtual Desktop Application. +--- + +# virtual_desktop_application + +Manages a Virtual Desktop Application. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "rg-example-virtualdesktop" + location = "West Europe" +} + +resource "azurerm_virtual_desktop_host_pool" "pooledbreadthfirst" { + name = "pooledbreadthfirst" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + type = "Pooled" + load_balancer_type = "BreadthFirst" +} + +resource "azurerm_virtual_desktop_host_pool" "personalautomatic" { + name = "personalautomatic" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + type = "Personal" + personal_desktop_assignment_type = "Automatic" +} + +resource "azurerm_virtual_desktop_application_group" "remoteapp" { + name = "acctag" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + type = "RemoteApp" + host_pool_id = azurerm_virtual_desktop_host_pool.pooledbreadthfirst.id + friendly_name = "TestAppGroup" + description = "Acceptance Test: An application group" +} + +resource "azurerm_virtual_desktop_application" "chrome" { + name = "googlechrome" + application_group_id = azurerm_virtual_desktop_application_group.remoteapp.id + friendly_name = "Google Chrome" + description = "Chromium based web browser" + path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + command_line_argument_policy = "DoNotAllow" + command_line_arguments = "--incognito" + show_in_portal = false + icon_path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + icon_index = 0 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Virtual Desktop Application. Changing the name forces a new resource to be created. + +* `application_group_id` - (Required) Resource ID for a Virtual Desktop Application Group to associate with the + Virtual Desktop Application. Changing the ID forces a new resource to be created. + +* `friendly_name` - (Optional) Option to set a friendly name for the Virtual Desktop Application. + +* `description` - (Optional) Option to set a description for the Virtual Desktop Application. + +* `path` - (Required) The file path location of the app on the Virtual Desktop OS. + +* `command_line_argument_policy` - (Required) Specifies whether this published application can be launched with command line arguments provided by the client, command line arguments specified at publish time, or no command line arguments at all. Possible values include: `DoNotAllow`, `Allow`, `Require`. + +* `command_line_arguments` - (Optional) Command Line Arguments for Virtual Desktop Application. + +* `show_in_portal` - (Optional) Specifies whether to show the RemoteApp program in the RD Web Access server. + +* `icon_path` - (Optional) Specifies the path for an icon which will be used for this Virtual Desktop Application. + +* `icon_index` - (Optional) The index of the icon you wish to use. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Virtual Desktop Application. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 60 minutes) Used when creating the Virtual Desktop Application. +* `update` - (Defaults to 60 minutes) Used when updating the Virtual Desktop Application. +* `read` - (Defaults to 5 minutes) Used when retrieving the Virtual Desktop Application. +* `delete` - (Defaults to 60 minutes) Used when deleting the Virtual Desktop Application. + +## Import + +Virtual Desktop Application can be imported using the `resource id`, e.g. + +``` +terraform import azurerm_virtual_desktop_application.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myGroup1/providers/Microsoft.DesktopVirtualization/applicationGroups/myapplicationgroup/applications/myapplication +```