diff --git a/internal/services/desktopvirtualization/registration.go b/internal/services/desktopvirtualization/registration.go index 66030f1a429a..5562374d35b8 100644 --- a/internal/services/desktopvirtualization/registration.go +++ b/internal/services/desktopvirtualization/registration.go @@ -33,6 +33,7 @@ func (r Registration) WebsiteCategories() []string { func (r Registration) DataSources() []sdk.DataSource { return []sdk.DataSource{ DesktopVirtualizationWorkspaceDataSource{}, + DesktopVirtualizationApplicationGroupDataSource{}, } } diff --git a/internal/services/desktopvirtualization/validate/application_group_name.go b/internal/services/desktopvirtualization/validate/application_group_name.go new file mode 100644 index 000000000000..0ce063510070 --- /dev/null +++ b/internal/services/desktopvirtualization/validate/application_group_name.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "fmt" + "regexp" + "strings" +) + +func ApplicationGroupName(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string but it wasn't", k)) + return + } + + // The value must not be empty. + if strings.TrimSpace(v) == "" { + errors = append(errors, fmt.Errorf("%q must not be empty", k)) + return + } + + const minLength = 3 + const maxLength = 64 + + // Application Group name can be 3-64 characters in length + if len(v) > maxLength || len(v) < minLength { + errors = append(errors, fmt.Errorf("%q must be between %d-%d characters, got %d", k, minLength, maxLength, len(v))) + } + + if matched := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).Match([]byte(v)); !matched { + errors = append(errors, fmt.Errorf("%q may only contain alphanumeric characters, dots, dashes and underscores", k)) + } + + if matched := regexp.MustCompile(`^[a-zA-Z0-9]`).Match([]byte(v)); !matched { + errors = append(errors, fmt.Errorf("%q must begin with an alphanumeric character", k)) + } + + if matched := regexp.MustCompile(`\w$`).Match([]byte(v)); !matched { + errors = append(errors, fmt.Errorf("%q must end with an alphanumeric character or underscore", k)) + } + + return warnings, errors +} diff --git a/internal/services/desktopvirtualization/validate/application_group_name_test.go b/internal/services/desktopvirtualization/validate/application_group_name_test.go new file mode 100644 index 000000000000..c58bd394f505 --- /dev/null +++ b/internal/services/desktopvirtualization/validate/application_group_name_test.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/desktopvirtualization/validate" +) + +func TestApplicationGroupName(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + { + // empty + Input: "", + Valid: false, + }, + { + // basic example + Input: "hello", + Valid: true, + }, + { + // 63 chars + Input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk", + Valid: true, + }, + { + // 64 chars + Input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl", + Valid: true, + }, + { + // 65 chars + Input: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm", + Valid: false, + }, + { + // may contain alphanumerics, dots, dashes and underscores + Input: "hello_world7.goodbye-world4", + Valid: true, + }, + { + // must begin with an alphanumeric + Input: "_hello", + Valid: false, + }, + { + // can't end with a period + Input: "hello.", + Valid: false, + }, + { + // can't end with a dash + Input: "hello-", + Valid: false, + }, + { + // can end with an underscore + Input: "hello_", + Valid: true, + }, + { + // can't contain an exclamation mark + Input: "hello!", + Valid: false, + }, + { + // can start with a number + Input: "0abc", + Valid: true, + }, + { + // can contain only numbers + Input: "12345", + Valid: true, + }, + { + // can start with upper case letter + Input: "Test", + Valid: true, + }, + { + // can end with upper case letter + Input: "TEST", + Valid: true, + }, + } + + for _, tc := range cases { + _, errs := validate.ApplicationGroupName(tc.Input, "name") + valid := len(errs) == 0 + + if valid != tc.Valid { + t.Fatalf("expected %s to be %t, got %t", tc.Input, tc.Valid, valid) + } + } +} diff --git a/internal/services/desktopvirtualization/virtual_desktop_application_group_data_source.go b/internal/services/desktopvirtualization/virtual_desktop_application_group_data_source.go new file mode 100644 index 000000000000..7b6de8877f37 --- /dev/null +++ b/internal/services/desktopvirtualization/virtual_desktop_application_group_data_source.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package desktopvirtualization + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/location" + "github.com/hashicorp/go-azure-sdk/resource-manager/desktopvirtualization/2022-02-10-preview/applicationgroup" + "github.com/hashicorp/go-azure-sdk/resource-manager/desktopvirtualization/2022-02-10-preview/hostpool" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/desktopvirtualization/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type DesktopVirtualizationApplicationGroupDataSource struct{} + +type DesktopVirtualizationApplicationGroupModel struct { + ApplicationGroupName string `tfschema:"name"` + ResourceGroupName string `tfschema:"resource_group_name"` + Location string `tfschema:"location"` + ApplicationGroupType string `tfschema:"type"` + HostPoolId string `tfschema:"host_pool_id"` + WorkspaceId string `tfschema:"workspace_id"` + FriendlyName string `tfschema:"friendly_name"` + Description string `tfschema:"description"` + Tags map[string]string `tfschema:"tags"` +} + +var _ sdk.DataSource = DesktopVirtualizationApplicationGroupDataSource{} + +func (r DesktopVirtualizationApplicationGroupDataSource) ModelObject() interface{} { + return &DesktopVirtualizationApplicationGroupModel{} +} + +func (r DesktopVirtualizationApplicationGroupDataSource) ResourceType() string { + return "azurerm_virtual_desktop_application_group" +} + +func (r DesktopVirtualizationApplicationGroupDataSource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.ApplicationGroupName, + }, + + "resource_group_name": commonschema.ResourceGroupNameForDataSource(), + } +} + +func (r DesktopVirtualizationApplicationGroupDataSource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "location": commonschema.LocationComputed(), + + "type": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "host_pool_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "workspace_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "friendly_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "tags": commonschema.TagsDataSource(), + } +} + +func (r DesktopVirtualizationApplicationGroupDataSource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.DesktopVirtualization.ApplicationGroupsClient + subscriptionId := metadata.Client.Account.SubscriptionId + + var state DesktopVirtualizationApplicationGroupModel + if err := metadata.Decode(&state); err != nil { + return err + } + + id := applicationgroup.NewApplicationGroupID(subscriptionId, state.ResourceGroupName, state.ApplicationGroupName) + + resp, err := client.Get(ctx, id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return fmt.Errorf("%s was not found", id) + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + model := resp.Model + if model == nil { + return fmt.Errorf("retrieving %s: model was nil", id) + } + + state.ApplicationGroupName = id.ApplicationGroupName + state.ResourceGroupName = id.ResourceGroupName + state.Location = location.NormalizeNilable(model.Location) + state.Tags = pointer.From(model.Tags) + state.ApplicationGroupType = string(model.Properties.ApplicationGroupType) + + hostPoolId, err := hostpool.ParseHostPoolIDInsensitively(model.Properties.HostPoolArmPath) + if err != nil { + return fmt.Errorf("parsing Host Pool ID %q: %+v", model.Properties.HostPoolArmPath, err) + } + state.HostPoolId = hostPoolId.ID() + + if model.Properties.WorkspaceArmPath != nil { + state.WorkspaceId = pointer.From(model.Properties.WorkspaceArmPath) + } + + if model.Properties.FriendlyName != nil { + state.FriendlyName = pointer.From(model.Properties.FriendlyName) + } + + if model.Properties.Description != nil { + state.Description = pointer.From(model.Properties.Description) + } + + metadata.SetID(id) + + return metadata.Encode(&state) + }, + } +} diff --git a/internal/services/desktopvirtualization/virtual_desktop_application_group_data_source_test.go b/internal/services/desktopvirtualization/virtual_desktop_application_group_data_source_test.go new file mode 100644 index 000000000000..54e6e93b10fe --- /dev/null +++ b/internal/services/desktopvirtualization/virtual_desktop_application_group_data_source_test.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package desktopvirtualization_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/location" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" +) + +type DesktopVirtualizationApplicationGroupDataSource struct{} + +func TestAccDesktopVirtualizationApplicationGroupDataSource_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_virtual_desktop_application_group", "test") + d := DesktopVirtualizationApplicationGroupDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: d.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("name").IsNotEmpty(), + check.That(data.ResourceName).Key("resource_group_name").IsNotEmpty(), + check.That(data.ResourceName).Key("location").HasValue(location.Normalize(data.Locations.Secondary)), + check.That(data.ResourceName).Key("type").HasValue("RemoteApp"), + check.That(data.ResourceName).Key("host_pool_id").IsNotEmpty(), + check.That(data.ResourceName).Key("friendly_name").HasValue("TestAppGroup"), + check.That(data.ResourceName).Key("description").HasValue("Acceptance Test: An application group"), + check.That(data.ResourceName).Key("tags.Purpose").HasValue("Acceptance-Testing"), + check.That(data.ResourceName).Key("tags.%").HasValue("1"), + ), + }, + }) +} + +func (DesktopVirtualizationApplicationGroupDataSource) complete(data acceptance.TestData) string { + template := VirtualDesktopApplicationResource{}.complete(data) + return fmt.Sprintf(` +%s + +data "azurerm_virtual_desktop_application_group" "test" { + name = azurerm_virtual_desktop_application_group.test.name + resource_group_name = azurerm_virtual_desktop_application_group.test.resource_group_name +} +`, template) +} diff --git a/internal/services/desktopvirtualization/virtual_desktop_application_group_resource.go b/internal/services/desktopvirtualization/virtual_desktop_application_group_resource.go index 21f4ffba3f63..e8ed9adfe669 100644 --- a/internal/services/desktopvirtualization/virtual_desktop_application_group_resource.go +++ b/internal/services/desktopvirtualization/virtual_desktop_application_group_resource.go @@ -6,7 +6,6 @@ package desktopvirtualization import ( "fmt" "log" - "regexp" "time" "github.com/hashicorp/go-azure-helpers/lang/response" @@ -21,6 +20,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/services/desktopvirtualization/migration" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/desktopvirtualization/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" @@ -55,16 +55,10 @@ func resourceVirtualDesktopApplicationGroup() *pluginsdk.Resource { 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 group name must be 1 - 260 characters long, contain only letters, numbers and hyphens.", - ), - ), + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ApplicationGroupName, }, "location": commonschema.Location(), diff --git a/website/docs/d/virtual_desktop_application_group.html.markdown b/website/docs/d/virtual_desktop_application_group.html.markdown new file mode 100644 index 000000000000..7b083c41863c --- /dev/null +++ b/website/docs/d/virtual_desktop_application_group.html.markdown @@ -0,0 +1,58 @@ +--- +subcategory: "Desktop Virtualization" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_virtual_desktop_application_group" +description: |- + Gets information about an existing Application Group. +--- + +# Data Source: azurerm_virtual_desktop_application_group + +Use this data source to access information about an existing Application Group. + +## Example Usage + +```hcl +data "azurerm_virtual_desktop_application_group" "example" { + name = "existing" + resource_group_name = "existing" +} + +output "id" { + value = data.azurerm_virtual_desktop_application_group.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name of this Application Group. + +* `resource_group_name` - (Required) The name of the Resource Group where the Application Group exists. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Application Group. + +* `description` - The description of the Application Group. + +* `friendly_name` - The friendly name of the Application Group. + +* `host_pool_id` - The Virtual Desktop Host Pool ID the Application Group is associated to. + +* `location` - The Azure Region where the Application Group exists. + +* `tags` - A mapping of tags assigned to the Application Group. + +* `type` - The type of Application Group (`RemoteApp` or `Desktop`). + +* `workspace_id` - The Virtual Desktop Workspace ID the Application Group is associated to. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the Application Group.