From 5518782e4f43ff080a40839e25dc573911e9ab0e Mon Sep 17 00:00:00 2001 From: Xingheng Lin Date: Sat, 11 Jan 2025 03:54:32 +0800 Subject: [PATCH] release v1.0.9 add fix --- docs/data-sources/admin_permissions.md | 51 ++++ docs/data-sources/image_version.md | 78 +++++++ docs/index.md | 2 +- docs/resources/admin_role.md | 2 +- docs/resources/delivery_group.md | 8 +- docs/resources/desktop_icon.md | 10 + docs/resources/gac_settings.md | 24 +- docs/resources/image_version.md | 2 + docs/resources/machine_catalog.md | 56 ++++- docs/resources/zone.md | 7 +- go.mod | 22 +- go.sum | 60 +++-- .../resource_locations_utils.go | 18 ++ .../admin_permissions_data_source.go | 119 ++++++++++ .../admin_permissions_data_source_model.go | 78 +++++++ .../daas/admin_role/admin_role_resource.go | 24 +- .../admin_role/admin_role_resource_model.go | 2 +- .../daas/application/application_resource.go | 4 +- .../delivery_group/delivery_group_resource.go | 81 ++++--- .../delivery_group_resource_model.go | 15 +- .../delivery_group/delivery_group_utils.go | 51 ++-- .../image_definition_data_source.go | 4 +- .../image_definition_resource.go | 4 +- .../image_version_data_source.go | 99 ++++++++ .../image_version_data_source_model.go | 146 ++++++++++++ .../image_version_resource.go | 8 +- .../image_version_resource_model.go | 215 +++++++++++------ .../machine_catalog_mcs_pvs_utils.go | 217 +++++++++++------- .../machine_catalog_resource.go | 134 ++++++++++- .../daas/machine_catalog/machine_config.go | 141 +++++++++--- internal/daas/zone/zone_resource.go | 4 - internal/daas/zone/zone_resource_model.go | 3 +- .../citrix_admin_permissions/data-source.tf | 17 ++ .../citrix_image_version/data-source.tf | 11 + .../citrix_machine_catalog/resource.tf | 44 ++++ .../resources/citrix_zone/resource.tf | 2 +- internal/provider/provider.go | 2 + .../admin_permissions_data_source_test.go | 42 ++++ .../test/image_version_data_source_test.go | 97 ++++++++ internal/util/common.go | 35 ++- internal/util/image.go | 58 ++++- scripts/onboarding-helper/README.md | 2 +- .../terraform-onboarding.ps1 | 8 +- scripts/onboarding-helper/terraform.tf | 2 +- settings.cloud.example.json | 7 + settings.onprem.example.json | 7 + templates/index.md.tmpl | 2 +- templates/resources/desktop_icon.md.tmpl | 49 ++++ 48 files changed, 1736 insertions(+), 338 deletions(-) create mode 100644 docs/data-sources/admin_permissions.md create mode 100644 docs/data-sources/image_version.md create mode 100644 internal/daas/admin_role/admin_permissions_data_source.go create mode 100644 internal/daas/admin_role/admin_permissions_data_source_model.go create mode 100644 internal/daas/image_definition/image_version_data_source.go create mode 100644 internal/daas/image_definition/image_version_data_source_model.go create mode 100644 internal/examples/data-sources/citrix_admin_permissions/data-source.tf create mode 100644 internal/examples/data-sources/citrix_image_version/data-source.tf create mode 100644 internal/test/admin_permissions_data_source_test.go create mode 100644 internal/test/image_version_data_source_test.go create mode 100644 templates/resources/desktop_icon.md.tmpl diff --git a/docs/data-sources/admin_permissions.md b/docs/data-sources/admin_permissions.md new file mode 100644 index 0000000..d51d443 --- /dev/null +++ b/docs/data-sources/admin_permissions.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_admin_permissions Data Source - citrix" +subcategory: "CVAD" +description: |- + Data source for a list of administrator permissions. +--- + +# citrix_admin_permissions (Data Source) + +Data source for a list of administrator permissions. + +## Example Usage + +```terraform +# Get all predefined admin permissions +data "citrix_admin_permissions" "all-permissions" { } + +# The permissions can then be viewed via: +# terraform apply +# terraform state show data.citrix_admin_permissions.all-permissions + +# Example using the data source to create a new custom role +data "citrix_admin_role" "desktop_group_admin_role" { + name = "Desktop Group Custom Admin Role" + description = "Role for managing delivery groups" + permissions = [ + // all permissions from the data source that start with "DesktopGroup_" + for permission in data.citrix_admin_permissions.all-permissions.permissions : + permission.id if startswith(permission.id, "DesktopGroup_") + ] +} +``` + + +## Schema + +### Read-Only + +- `permissions` (Attributes List) The list of administrator permissions. (see [below for nested schema](#nestedatt--permissions)) + + +### Nested Schema for `permissions` + +Read-Only: + +- `description` (String) Description of the admin permission. +- `group_id` (String) ID of the group to which the permission belongs. +- `group_name` (String) Name of the group to which the permission belongs. +- `id` (String) ID of the admin permission. +- `name` (String) Name of the admin permission. \ No newline at end of file diff --git a/docs/data-sources/image_version.md b/docs/data-sources/image_version.md new file mode 100644 index 0000000..ad41fc6 --- /dev/null +++ b/docs/data-sources/image_version.md @@ -0,0 +1,78 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_image_version Data Source - citrix" +subcategory: "CVAD" +description: |- + Data source an image version. Note that this feature is in Tech Preview. +--- + +# citrix_image_version (Data Source) + +Data source an image version. **Note that this feature is in Tech Preview.** + +## Example Usage + +```terraform +# Define Image Version data source by id +data "citrix_image_version" "example_image_version_by_id" { + id = "00000000-0000-0000-0000-000000000000" + image_definition = "00000000-0000-0000-0000-000000000000" +} + +# Define Image Version data source by version number +data "citrix_image_version" "example_image_version_by_version_number" { + version_number = 1 + image_definition = "00000000-0000-0000-0000-000000000000" +} +``` + + +## Schema + +### Required + +- `image_definition` (String) Id of the image definition to associate this image version with. + +### Optional + +- `id` (String) The id of the image version. +- `version_number` (Number) The version number of the image version. + +### Read-Only + +- `azure_image_specs` (Attributes) Image configuration for Azure image version. (see [below for nested schema](#nestedatt--azure_image_specs)) +- `description` (String) Description of the image version. +- `hypervisor` (String) Id of the hypervisor to use for creating this image version. +- `hypervisor_resource_pool` (String) Id of the hypervisor resource pool to use for creating this image version. +- `os_type` (String) The OS type of the image version. +- `session_support` (String) Session support for the image version. + + +### Nested Schema for `azure_image_specs` + +Read-Only: + +- `disk_encryption_set` (Attributes) The configuration for Disk Encryption Set (DES). The DES must be in the same subscription and region as your resources. If your master image is encrypted with a DES, use the same DES when creating this machine catalog. When using a DES, if you later disable the key with which the corresponding DES is associated in Azure, you can no longer power on the machines in this catalog or add machines to it. (see [below for nested schema](#nestedatt--azure_image_specs--disk_encryption_set)) +- `license_type` (String) Windows license type used to provision virtual machines in Azure at the base compute rate. License types include: `Windows_Client` and `Windows_Server`. +- `machine_profile` (Attributes) The name of the virtual machine or template spec that will be used to identify the default value for the tags, virtual machine size, boot diagnostics, host cache property of OS disk, accelerated networking and availability zone. (see [below for nested schema](#nestedatt--azure_image_specs--machine_profile)) +- `service_offering` (String) The Azure VM Sku to use when creating machines. +- `storage_type` (String) Storage account type used for provisioned virtual machine disks on Azure. Storage types include: `Standard_LRS`, `StandardSSD_LRS` and `Premium_LRS`. + + +### Nested Schema for `azure_image_specs.disk_encryption_set` + +Read-Only: + +- `disk_encryption_set_name` (String) The name of the disk encryption set. +- `disk_encryption_set_resource_group` (String) The name of the resource group in which the disk encryption set resides. + + + +### Nested Schema for `azure_image_specs.machine_profile` + +Read-Only: + +- `machine_profile_resource_group` (String) The name of the resource group where the machine profile VM or template spec is located. +- `machine_profile_template_spec_name` (String) The name of the machine profile template spec. +- `machine_profile_template_spec_version` (String) The version of the machine profile template spec. +- `machine_profile_vm_name` (String) The name of the machine profile virtual machine. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 9f86cce..02cfa0f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,7 @@ Please refer to [Citrix Tech Zone](https://community.citrix.com/tech-zone/automa - [Citrix policies](https://community.citrix.com/tech-zone/automation/cvad-terraform-policies/) ### Demo video -[![alt text](../images/techzone-youtube-thumbnail.png)](https://www.youtube.com/watch?v=c33sMLaCVjY) +[![alt text](https://raw.githubusercontent.com/citrix/terraform-provider-citrix/refs/heads/main/images/techzone-youtube-thumbnail.png)](https://www.youtube.com/watch?v=c33sMLaCVjY) https://www.youtube.com/watch?v=c33sMLaCVjY ### (On-Premises Only) Enable Web Studio diff --git a/docs/resources/admin_role.md b/docs/resources/admin_role.md index 205f59d..3d8ff31 100644 --- a/docs/resources/admin_role.md +++ b/docs/resources/admin_role.md @@ -40,7 +40,7 @@ resource "citrix_admin_role" "cloud_example_role" { - `name` (String) Name of the admin role. - `permissions` (Set of String) Permissions to be associated with the admin role. --> **Note** To get a list of supported permissions, please refer to [Admin Predefined Permissions for Cloud](https://developer-docs.citrix.com/en-us/citrix-daas-service-apis/citrix-daas-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions) and [Admin Predefined Permissions for On-Premise](https://developer-docs.citrix.com/en-us/citrix-virtual-apps-desktops/citrix-cvad-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions). +-> **Note** To get a list of supported permissions, please refer to [Admin Predefined Permissions for Cloud](https://developer-docs.citrix.com/en-us/citrix-daas-service-apis/citrix-daas-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions) and [Admin Predefined Permissions for On-Premise](https://developer-docs.citrix.com/en-us/citrix-virtual-apps-desktops/citrix-cvad-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions), or use the `citrix_admin_permissions` data source. ### Optional diff --git a/docs/resources/delivery_group.md b/docs/resources/delivery_group.md index 17f7ebf..b3f1fd6 100644 --- a/docs/resources/delivery_group.md +++ b/docs/resources/delivery_group.md @@ -214,7 +214,9 @@ resource "citrix_delivery_group" "example-delivery-group" { - `metadata` (Attributes List) Metadata for the Delivery Group. (see [below for nested schema](#nestedatt--metadata)) - `minimum_functional_level` (String) Specifies the minimum functional level for the VDA machines in the delivery group. Defaults to `L7_20`. - `reboot_schedules` (Attributes List) The reboot schedule for the delivery group. (see [below for nested schema](#nestedatt--reboot_schedules)) -- `restricted_access_users` (Attributes) Restrict access to this Delivery Group by specifying users and groups in the allow and block list. If no value is specified, all authenticated users will have access to this Delivery Group. To give access to unauthenticated users, use the `allow_anonymous_access` property. (see [below for nested schema](#nestedatt--restricted_access_users)) +- `restricted_access_users` (Attributes) Restrict access to this Delivery Group by specifying users and groups in the allow and block list. To give access to unauthenticated users, use the `allow_anonymous_access` property. + +~> **Please Note** If `restricted_access_users` attribute is omitted or set to `null`, all authenticated users will have access to this Delivery Group. If attribute is specified as an empty object i.e. `{}`, then no user will have access to the delivery group because `allow_list` and `block_list` will be set as empty sets by default. (see [below for nested schema](#nestedatt--restricted_access_users)) - `scopes` (Set of String) The IDs of the scopes for the delivery group to be a part of. - `session_support` (String) The session support for the delivery group. Can only be set to `SingleSession` or `MultiSession`. Specify only if you want to create a Delivery Group without any `associated_machine_catalogs`. Ensure session support is same as that of the prospective Machine Catalogs you will associate this Delivery Group with. - `sharing_kind` (String) The sharing kind for the delivery group. Can only be set to `Shared` or `Private`. Specify only if you want to create a Delivery Group wthout any `associated_machine_catalogs`. @@ -447,7 +449,9 @@ Optional: ~> **Please Note** Session roaming should be set to `false` for Remote PC Delivery Group. - `enabled` (Boolean) Specify whether to enable the delivery of this desktop. Default is `true`. - `restrict_to_tag` (String) Restrict session launch to machines with tag specified in GUID. -- `restricted_access_users` (Attributes) Restrict access to this Desktop by specifying users and groups in the allow and block list. If no value is specified, all users that have access to this Delivery Group will have access to the Desktop. +- `restricted_access_users` (Attributes) Restrict access to this Desktop by specifying users and groups in the allow and block list. + +~> **Please Note** If `restricted_access_users` attribute is omitted or set to `null`, all authenticated users will have access to this Desktop. If attribute is specified as an empty object i.e. `{}`, then no user will have access to the desktop because `allow_list` and `block_list` will be set as empty sets by default. ~> **Please Note** For Remote PC Delivery Groups desktops, `restricted_access_users` has to be set. (see [below for nested schema](#nestedatt--desktops--restricted_access_users)) diff --git a/docs/resources/desktop_icon.md b/docs/resources/desktop_icon.md index 5d71d07..bff11ff 100644 --- a/docs/resources/desktop_icon.md +++ b/docs/resources/desktop_icon.md @@ -38,4 +38,14 @@ Import is supported using the following syntax: ```shell # Desktop icon can be imported by specifying the ID terraform import citrix_desktop_icon.example-desktop-icon 1 +``` + +## Generating Raw Data of an icon +To generate in Linux/Mac terminal use the command: +``` +base64 fileName.ico +``` +To generate in Powershell: +``` +[System.Convert]::ToBase64String(Get-Content fileName.ico -Encoding Byte) ``` \ No newline at end of file diff --git a/docs/resources/gac_settings.md b/docs/resources/gac_settings.md index 20bd27a..0cdb373 100644 --- a/docs/resources/gac_settings.md +++ b/docs/resources/gac_settings.md @@ -309,7 +309,7 @@ Optional: - `value_string` (String) String value (if any) associated with the setting. -### Nested Schema for `app_settings.linux.settings.value_string` +### Nested Schema for `app_settings.linux.settings.auto_launch_protocols_from_origins` Required: @@ -321,7 +321,7 @@ Optional: -### Nested Schema for `app_settings.linux.settings.value_string` +### Nested Schema for `app_settings.linux.settings.extension_install_allow_list` Required: @@ -331,7 +331,7 @@ Required: -### Nested Schema for `app_settings.linux.settings.value_string` +### Nested Schema for `app_settings.linux.settings.managed_bookmarks` Required: @@ -367,7 +367,7 @@ Optional: - `value_string` (String) String value (if any) associated with the setting. -### Nested Schema for `app_settings.macos.settings.value_string` +### Nested Schema for `app_settings.macos.settings.auto_launch_protocols_from_origins` Required: @@ -379,7 +379,7 @@ Optional: -### Nested Schema for `app_settings.macos.settings.value_string` +### Nested Schema for `app_settings.macos.settings.enterprise_browser_sso` Required: @@ -388,7 +388,7 @@ Required: -### Nested Schema for `app_settings.macos.settings.value_string` +### Nested Schema for `app_settings.macos.settings.extension_install_allow_list` Required: @@ -398,7 +398,7 @@ Required: -### Nested Schema for `app_settings.macos.settings.value_string` +### Nested Schema for `app_settings.macos.settings.managed_bookmarks` Required: @@ -435,7 +435,7 @@ Optional: - `value_string` (String) String value (if any) associated with the setting. -### Nested Schema for `app_settings.windows.settings.value_string` +### Nested Schema for `app_settings.windows.settings.auto_launch_protocols_from_origins` Required: @@ -447,7 +447,7 @@ Optional: -### Nested Schema for `app_settings.windows.settings.value_string` +### Nested Schema for `app_settings.windows.settings.enterprise_browser_sso` Required: @@ -456,7 +456,7 @@ Required: -### Nested Schema for `app_settings.windows.settings.value_string` +### Nested Schema for `app_settings.windows.settings.extension_install_allow_list` Required: @@ -466,7 +466,7 @@ Required: -### Nested Schema for `app_settings.windows.settings.value_string` +### Nested Schema for `app_settings.windows.settings.local_app_allow_list` Required: @@ -476,7 +476,7 @@ Required: -### Nested Schema for `app_settings.windows.settings.value_string` +### Nested Schema for `app_settings.windows.settings.managed_bookmarks` Required: diff --git a/docs/resources/image_version.md b/docs/resources/image_version.md index b308aca..e7609a9 100644 --- a/docs/resources/image_version.md +++ b/docs/resources/image_version.md @@ -72,6 +72,8 @@ resource "citrix_image_version" "example_azure_image_version" { ### Read-Only - `id` (String) The id of the image version. +- `os_type` (String) The OS type of the image version. +- `session_support` (String) Session support for the image version. - `version_number` (Number) The version number of the image version. diff --git a/docs/resources/machine_catalog.md b/docs/resources/machine_catalog.md index 6a5f317..dc20e4e 100644 --- a/docs/resources/machine_catalog.md +++ b/docs/resources/machine_catalog.md @@ -72,6 +72,50 @@ resource "citrix_machine_catalog" "example-azure-mtsession" { } } +resource "citrix_machine_catalog" "example_azure_prepared_image_mtsession" { + name = "example_azure_prepared_image_mtsession" + description = "Example multi-session catalog on Azure hypervisor with Prepared Image" + zone = "" + allocation_type = "Random" + session_support = "MultiSession" + provisioning_type = "MCS" + provisioning_scheme = { + hypervisor = citrix_azure_hypervisor.example-azure-hypervisor.id + hypervisor_resource_pool = citrix_azure_hypervisor_resource_pool.example-azure-hypervisor-resource-pool.id + identity_type = "ActiveDirectory" + machine_domain_identity = { + domain = "" + domain_ou = "" + service_account = "" + service_account_password = "" + } + azure_machine_config = { + storage_type = "Standard_LRS" + use_managed_disks = true + service_offering = "Standard_D2_v2" + prepared_image = { + image_definition = citrix_image_definition.example_image_definition.id + image_version = citrix_image_version.example_azure_image_version.id + } + writeback_cache = { + wbc_disk_storage_type = "pd-standard" + persist_wbc = true + persist_os_disk = true + persist_vm = true + writeback_cache_disk_size_gb = 127 + writeback_cache_memory_size_mb = 256 + storage_cost_saving = true + } + } + availability_zones = ["1","2"] + number_of_total_machines = 1 + machine_account_creation_rules ={ + naming_scheme = "az-multi-##" + naming_scheme_type ="Numeric" + } + } +} + resource "citrix_machine_catalog" "example-aws-mtsession" { name = "example-aws-mtsession" description = "Example multi-session catalog on AWS hypervisor" @@ -588,6 +632,7 @@ Optional: - `license_type` (String) Windows license type used to provision virtual machines in Azure at the base compute rate. License types include: `Windows_Client` and `Windows_Server`. - `machine_profile` (Attributes) The name of the virtual machine or template spec that will be used to identify the default value for the tags, virtual machine size, boot diagnostics, host cache property of OS disk, accelerated networking and availability zone.
Required when provisioning_type is set to PVSStreaming or when identity_type is set to `AzureAD` (see [below for nested schema](#nestedatt--provisioning_scheme--azure_machine_config--machine_profile)) - `master_image_note` (String) The note for the master image. +- `prepared_image` (Attributes) Specifying the prepared master image to be used for machine catalog. (see [below for nested schema](#nestedatt--provisioning_scheme--azure_machine_config--prepared_image)) - `use_azure_compute_gallery` (Attributes) Use this to place prepared image in Azure Compute Gallery. Required when `storage_type = Azure_Ephemeral_OS_Disk`. (see [below for nested schema](#nestedatt--provisioning_scheme--azure_machine_config--use_azure_compute_gallery)) - `use_managed_disks` (Boolean) Indicate whether to use Azure managed disks for the provisioned virtual machine. - `vda_resource_group` (String) Designated resource group where the VDA VMs will be located on Azure. @@ -609,7 +654,7 @@ Optional: - `storage_account` (String) The Azure Storage Account where the image VHD for creating machines is located. Only applicable to Azure VHD image blob. -### Nested Schema for `provisioning_scheme.azure_machine_config.azure_master_image.storage_account` +### Nested Schema for `provisioning_scheme.azure_machine_config.azure_master_image.gallery_image` Required: @@ -665,6 +710,15 @@ Optional: - `machine_profile_vm_name` (String) The name of the machine profile virtual machine. + +### Nested Schema for `provisioning_scheme.azure_machine_config.prepared_image` + +Required: + +- `image_definition` (String) ID of the image definition. +- `image_version` (String) ID of the image version. + + ### Nested Schema for `provisioning_scheme.azure_machine_config.use_azure_compute_gallery` diff --git a/docs/resources/zone.md b/docs/resources/zone.md index 319b2ce..1cb1944 100644 --- a/docs/resources/zone.md +++ b/docs/resources/zone.md @@ -4,11 +4,14 @@ page_title: "citrix_zone Resource - citrix" subcategory: "CVAD" description: |- Manages a zone. + ~> Please Note For Citrix Cloud Customer, Citrix Cloud Resource Location permissions are required to manage DaaS Zones. --- # citrix_zone (Resource) -Manages a zone. +Manages a zone. + +~> **Please Note** For Citrix Cloud Customer, Citrix Cloud Resource Location permissions are required to manage DaaS Zones. ## Zone For Citrix Cloud Customers @@ -33,7 +36,7 @@ resource "citrix_zone" "example-onpremises-zone" { ] } -# Exmaple for Cloud Zone +# Example for Cloud Zone resource "citrix_cloud_resource_location" "example-resource-location" { name = "example-resource-location" } diff --git a/go.mod b/go.mod index 55a7433..0ca0e0a 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,11 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-docs v0.14.1 github.com/hashicorp/terraform-plugin-framework v1.13.0 - github.com/hashicorp/terraform-plugin-framework-validators v0.15.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.16.0 github.com/hashicorp/terraform-plugin-go v0.25.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.8.0 - golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/mod v0.22.0 ) @@ -52,7 +52,7 @@ require ( github.com/hashicorp/yamux v0.1.2 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -68,17 +68,17 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.15.1 // indirect - golang.org/x/crypto v0.30.0 // indirect - golang.org/x/net v0.32.0 // indirect + github.com/zclconf/go-cty v1.16.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.28.0 // indirect + golang.org/x/tools v0.29.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect - google.golang.org/grpc v1.68.1 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/grpc v1.69.2 // indirect + google.golang.org/protobuf v1.36.2 // indirect ) // replace github.com/citrix/citrix-daas-rest-go => ../citrix-daas-rest-go diff --git a/go.sum b/go.sum index 8d363e9..44d7428 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,10 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+ github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -104,8 +108,8 @@ github.com/hashicorp/terraform-plugin-docs v0.14.1 h1:MikFi59KxrP/ewrZoaowrB9he5 github.com/hashicorp/terraform-plugin-docs v0.14.1/go.mod h1:k2NW8+t113jAus6bb5tQYQgEAX/KueE/u8X2Z45V1GM= github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= -github.com/hashicorp/terraform-plugin-framework-validators v0.15.0 h1:RXMmu7JgpFjnI1a5QjMCBb11usrW2OtAG+iOTIj5c9Y= -github.com/hashicorp/terraform-plugin-framework-validators v0.15.0/go.mod h1:Bh89/hNmqsEWug4/XWKYBwtnw3tbz5BAy1L1OgvbIaY= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.0 h1:O9QqGoYDzQT7lwTXUsZEtgabeWW96zUBh47Smn2lkFA= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.0/go.mod h1:Bh89/hNmqsEWug4/XWKYBwtnw3tbz5BAy1L1OgvbIaY= github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -143,12 +147,11 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= @@ -206,19 +209,29 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.15.1 h1:RgQYm4j2EvoBRXOPxhUvxPzRrGDo1eCOhHXuGfrj5S0= -github.com/zclconf/go-cty v1.15.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w= +github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= @@ -227,8 +240,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -245,11 +258,10 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -264,21 +276,21 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/citrixcloud/resource_locations/resource_locations_utils.go b/internal/citrixcloud/resource_locations/resource_locations_utils.go index 64aef74..49ddaea 100644 --- a/internal/citrixcloud/resource_locations/resource_locations_utils.go +++ b/internal/citrixcloud/resource_locations/resource_locations_utils.go @@ -4,6 +4,7 @@ package resource_locations import ( "context" + "fmt" resourcelocations "github.com/citrix/citrix-daas-rest-go/ccresourcelocations" citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" @@ -15,6 +16,23 @@ func GetResourceLocation(ctx context.Context, client *citrixdaasclient.CitrixDaa // Get resource location getResourceLocationRequest := client.ResourceLocationsClient.LocationsDAAS.LocationsGet(ctx, resourceLocationId) resourceLocation, httpResp, err := citrixdaasclient.ExecuteWithRetry[*resourcelocations.CitrixCloudServicesRegistryApiModelsLocationsResourceLocationModel](getResourceLocationRequest, client) + if httpResp.StatusCode == 403 { + diagnostics.AddError( + "Error reading resource location with id: "+resourceLocationId, + "Terraform user does not have the Citrix Cloud Resource Location permission. This is required to manage DaaS Zones.", + ) + return nil, err + } + if httpResp.StatusCode == 404 || resourceLocation == nil { + diagnostics.AddError( + "Error reading resource location with id: "+resourceLocationId, + "Resource Location "+resourceLocationId+" not found. Ensure the resource location has been created manually or via terraform, then try again.", + ) + if err == nil { + err = fmt.Errorf("resource Location %s not found", resourceLocationId) + } + return nil, err + } if err != nil { diagnostics.AddError( "Error reading resource location with id: "+resourceLocationId, diff --git a/internal/daas/admin_role/admin_permissions_data_source.go b/internal/daas/admin_role/admin_permissions_data_source.go new file mode 100644 index 0000000..9c84928 --- /dev/null +++ b/internal/daas/admin_role/admin_permissions_data_source.go @@ -0,0 +1,119 @@ +// Copyright © 2024. Citrix Systems, Inc. +package admin_role + +import ( + "context" + "sync" + + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +var ( + _ datasource.DataSource = &AdminPermissionsDataSource{} +) + +func NewAdminPermissionsDataSource() datasource.DataSource { + return &AdminPermissionsDataSource{} +} + +type AdminPermissionsDataSource struct { + client *citrixdaasclient.CitrixDaasClient +} + +func (d *AdminPermissionsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_admin_permissions" +} + +func (d *AdminPermissionsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = AdminPermissionsDataSourceModel{}.GetDataSourceSchema() +} + +func (d *AdminPermissionsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + d.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) +} + +func (d *AdminPermissionsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + if d.client != nil && d.client.ApiClient == nil { + resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) + return + } + + var data AdminPermissionsDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + permissions, err := getAdminPermissions(ctx, d.client, &resp.Diagnostics, true) + if err != nil { + return + } + + data = data.RefreshPropertyValues(permissions) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Cache the admin predefined permissions as they do not change +var adminPermissionsCache []citrixorchestration.PredefinedPermissionResponseModel +var cacheLoad sync.Once +var cacheLoadError error + +func getAdminPermissions(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, filterCloudRestrictedPermissions bool) ([]citrixorchestration.PredefinedPermissionResponseModel, error) { + cacheLoad.Do(func() { + getPermissionsRequest := client.ApiClient.AdminAPIsDAAS.AdminGetPredefinedPermissions(ctx) + permissions := []citrixorchestration.PredefinedPermissionResponseModel{} + continuationToken := "" + for { + getPermissionsRequest = getPermissionsRequest.ContinuationToken(continuationToken) + resp, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.PredefinedPermissionResponseModelCollection](getPermissionsRequest, client) + if err != nil { + diagnostics.AddError( + "Error reading predefined admin permissions", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + cacheLoadError = err + return + } + + permissions = append(permissions, resp.GetItems()...) + if resp.GetContinuationToken() == "" { + adminPermissionsCache = append(adminPermissionsCache, permissions...) + return + } + continuationToken = resp.GetContinuationToken() + } + }) + + if cacheLoadError != nil { + return nil, cacheLoadError + } + + if filterCloudRestrictedPermissions { + filteredPermissions := []citrixorchestration.PredefinedPermissionResponseModel{} + for _, permission := range adminPermissionsCache { + if _, ok := util.RestrictedPermissionsInCloud[permission.GetId()]; !ok { + filteredPermissions = append(filteredPermissions, permission) + } + } + return filteredPermissions, nil + } else { + return adminPermissionsCache, nil + } +} diff --git a/internal/daas/admin_role/admin_permissions_data_source_model.go b/internal/daas/admin_role/admin_permissions_data_source_model.go new file mode 100644 index 0000000..fb24f74 --- /dev/null +++ b/internal/daas/admin_role/admin_permissions_data_source_model.go @@ -0,0 +1,78 @@ +// Copyright © 2024. Citrix Systems, Inc. +package admin_role + +import ( + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AdminPermissionModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + GroupId types.String `tfsdk:"group_id"` + GroupName types.String `tfsdk:"group_name"` +} + +func (AdminPermissionModel) GetAdminPermissionModelSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the admin permission.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the admin permission.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the admin permission.", + Computed: true, + }, + "group_id": schema.StringAttribute{ + Description: "ID of the group to which the permission belongs.", + Computed: true, + }, + "group_name": schema.StringAttribute{ + Description: "Name of the group to which the permission belongs.", + Computed: true, + }, + }, + } +} + +type AdminPermissionsDataSourceModel struct { + Permissions []AdminPermissionModel `tfsdk:"permissions"` +} + +func (AdminPermissionsDataSourceModel) GetDataSourceSchema() schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + Description: "CVAD --- Data source for a list of administrator permissions.", + + Attributes: map[string]schema.Attribute{ + "permissions": schema.ListNestedAttribute{ + Description: "The list of administrator permissions.", + Computed: true, + NestedObject: AdminPermissionModel{}.GetAdminPermissionModelSchema(), + }, + }, + } +} + +func (r AdminPermissionsDataSourceModel) RefreshPropertyValues(adminPermissions []citrixorchestration.PredefinedPermissionResponseModel) AdminPermissionsDataSourceModel { + res := []AdminPermissionModel{} + for _, model := range adminPermissions { + res = append(res, AdminPermissionModel{ + Id: types.StringValue(model.GetId()), + Name: types.StringValue(model.GetName()), + Description: types.StringValue(model.GetDescription()), + GroupId: types.StringValue(model.GetGroupId()), + GroupName: types.StringValue(model.GetGroupName()), + }) + } + + r.Permissions = res + return r +} diff --git a/internal/daas/admin_role/admin_role_resource.go b/internal/daas/admin_role/admin_role_resource.go index 62c0070..2073426 100644 --- a/internal/daas/admin_role/admin_role_resource.go +++ b/internal/daas/admin_role/admin_role_resource.go @@ -281,10 +281,30 @@ func (r *adminRoleResource) ModifyPlan(ctx context.Context, req resource.ModifyP if r.client.AuthConfig.OnPremises { if !plan.CanLaunchManage.ValueBool() { - resp.Diagnostics.AddError("CanLaunchManage", "CanLaunchManage can only be set to true for On-Premise deployments. Please either set the attribute to true or remove it from the configuration and try again.") + resp.Diagnostics.AddError("can_launch_manage set to false", "can_launch_manage can only be set to true for On-Premise deployments. Please either set the attribute to true or remove it from the configuration and try again.") } if !plan.CanLaunchMonitor.ValueBool() { - resp.Diagnostics.AddError("CanLaunchMonitor", "CanLaunchMonitor can only be set to true for On-Premise deployments. Please either set the attribute to true or remove it from the configuration and try again.") + resp.Diagnostics.AddError("can_launch_monitor set to false", "can_launch_monitor can only be set to true for On-Premise deployments. Please either set the attribute to true or remove it from the configuration and try again.") + } + } + + predefinedPermissions, err := getAdminPermissions(ctx, r.client, &resp.Diagnostics, false) + if err != nil { + return + } + // convert permissions from a slice of predefined permissions to a set of strings + var predefinedPermissionsSet = make(map[string]bool) + for _, permission := range predefinedPermissions { + predefinedPermissionsSet[permission.GetId()] = true + } + + permissions := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Permissions) + for _, permission := range permissions { + if _, ok := predefinedPermissionsSet[permission]; !ok { + resp.Diagnostics.AddError("Unknown permission "+permission, "Permission "+permission+" was not found. Please remove the permission from the configuration and try again.") + } + if _, ok := util.RestrictedPermissionsInCloud[permission]; !r.client.AuthConfig.OnPremises && ok { + resp.Diagnostics.AddError("Permission not supported for cloud deployments", "Permission "+permission+" is not supported for cloud deployments. Please remove the permission from the configuration and try again.") } } } diff --git a/internal/daas/admin_role/admin_role_resource_model.go b/internal/daas/admin_role/admin_role_resource_model.go index 0bfab29..0dd10e7 100644 --- a/internal/daas/admin_role/admin_role_resource_model.go +++ b/internal/daas/admin_role/admin_role_resource_model.go @@ -97,7 +97,7 @@ func (AdminRoleModel) GetSchema() schema.Schema { "permissions": schema.SetAttribute{ ElementType: types.StringType, Description: "Permissions to be associated with the admin role. " + - "\n\n-> **Note** To get a list of supported permissions, please refer to [Admin Predefined Permissions for Cloud](https://developer-docs.citrix.com/en-us/citrix-daas-service-apis/citrix-daas-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions) and [Admin Predefined Permissions for On-Premise](https://developer-docs.citrix.com/en-us/citrix-virtual-apps-desktops/citrix-cvad-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions).", + "\n\n-> **Note** To get a list of supported permissions, please refer to [Admin Predefined Permissions for Cloud](https://developer-docs.citrix.com/en-us/citrix-daas-service-apis/citrix-daas-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions) and [Admin Predefined Permissions for On-Premise](https://developer-docs.citrix.com/en-us/citrix-virtual-apps-desktops/citrix-cvad-rest-apis/apis/#/Admin-APIs/Admin-GetPredefinedPermissions), or use the `citrix_admin_permissions` data source.", Required: true, Validators: []validator.Set{ setvalidator.SizeAtLeast(1), diff --git a/internal/daas/application/application_resource.go b/internal/daas/application/application_resource.go index da5b793..b485487 100644 --- a/internal/daas/application/application_resource.go +++ b/internal/daas/application/application_resource.go @@ -507,7 +507,7 @@ func validateDeliveryGroupsPriority(ctx context.Context, diagnostics *diag.Diagn deliveryGroupMap := map[string]bool{} isValid := true for _, dg := range dgPriority { - if !dg.Priority.IsNull() { + if !dg.Priority.IsUnknown() && !dg.Priority.IsNull() { if priorityMap[dg.Priority.ValueInt32()] { isValid = false } else { @@ -515,7 +515,7 @@ func validateDeliveryGroupsPriority(ctx context.Context, diagnostics *diag.Diagn } } - if !dg.Id.IsNull() { + if !dg.Id.IsUnknown() && !dg.Id.IsNull() { if deliveryGroupMap[dg.Id.ValueString()] { isValid = false } else { diff --git a/internal/daas/delivery_group/delivery_group_resource.go b/internal/daas/delivery_group/delivery_group_resource.go index b7b51e3..3aeaa72 100644 --- a/internal/daas/delivery_group/delivery_group_resource.go +++ b/internal/daas/delivery_group/delivery_group_resource.go @@ -638,6 +638,49 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod return } + if !plan.SharingKind.IsNull() && !plan.AssociatedMachineCatalogs.IsNull() { + associatedMachineCatalogs := util.ObjectSetToTypedArray[DeliveryGroupMachineCatalogModel](ctx, &resp.Diagnostics, plan.AssociatedMachineCatalogs) + associatedMachineCatalogProperties, err := validateAndReturnMachineCatalogSessionSupport(ctx, *r.client, &resp.Diagnostics, associatedMachineCatalogs, !create) + if err != nil || associatedMachineCatalogProperties.SessionSupport == "" { + return + } + + if plan.SharingKind.ValueString() == string(citrixorchestration.SHARINGKIND_PRIVATE) && associatedMachineCatalogProperties.AllocationType != citrixorchestration.ALLOCATIONTYPE_STATIC { + resp.Diagnostics.AddError( + fmt.Sprintf("Error %s Delivery Group", operation), + "When `sharing_kind` is `Private`, the associated machine catalogs must have `Static` allocation type.", + ) + return + } + + if plan.SharingKind.ValueString() == string(citrixorchestration.SHARINGKIND_SHARED) && associatedMachineCatalogProperties.AllocationType != citrixorchestration.ALLOCATIONTYPE_RANDOM { + resp.Diagnostics.AddError( + fmt.Sprintf("Error %s Delivery Group", operation), + "When `sharing_kind` is `Shared`, the associated machine catalogs must have `Random` allocation type.", + ) + return + } + } + + if !plan.Desktops.IsNull() { + sessionRoamingShouldBeSet := true + if !plan.SharingKind.IsNull() { + sharingKind := plan.SharingKind.ValueString() + if sharingKind == string(citrixorchestration.SHARINGKIND_PRIVATE) { + sessionRoamingShouldBeSet = false + } + } + desktops := util.ObjectListToTypedArray[DeliveryGroupDesktop](ctx, &resp.Diagnostics, plan.Desktops) + isValid, errMsg := validateSessionRoaming(desktops, sessionRoamingShouldBeSet) + if !isValid { + resp.Diagnostics.AddError( + "Error "+operation+" Delivery Group "+plan.Name.ValueString(), + "Error message: "+errMsg, + ) + return + } + } + if plan.AssociatedMachineCatalogs.IsNull() { errorSummary := fmt.Sprintf("Error %s Delivery Group", operation) feature := "Delivery Groups without associated machine catalogs" @@ -653,7 +696,6 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod "Autoscale settings can only be configured if associated machine catalogs are specified.", ) } - return } @@ -743,36 +785,15 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod return } - if !plan.Desktops.IsNull() { - sessionRoamingShouldBeSet := true - if associatedMachineCatalogProperties.AllocationType == citrixorchestration.ALLOCATIONTYPE_STATIC { - sessionRoamingShouldBeSet = false - } - - if !plan.SharingKind.IsNull() { - sharingKind := plan.SharingKind.ValueString() - if sharingKind == string(citrixorchestration.SHARINGKIND_PRIVATE) { - sessionRoamingShouldBeSet = false - } - } - + if associatedMachineCatalogProperties.AllocationType == citrixorchestration.ALLOCATIONTYPE_STATIC && !plan.Desktops.IsNull() { desktops := util.ObjectListToTypedArray[DeliveryGroupDesktop](ctx, &resp.Diagnostics, plan.Desktops) - for _, desktop := range desktops { - if desktop.EnableSessionRoaming.IsUnknown() { - continue - } else if !desktop.EnableSessionRoaming.IsNull() && !sessionRoamingShouldBeSet { - resp.Diagnostics.AddError( - "Error "+operation+" Delivery Group "+plan.Name.ValueString(), - "`enable_session_roaming` cannot be set when `sharing_kind` is `Private` or associated machine catalogs have `Static` allocation type.", - ) - return - } else if desktop.EnableSessionRoaming.IsNull() && sessionRoamingShouldBeSet { - resp.Diagnostics.AddError( - "Error "+operation+" Delivery Group "+plan.Name.ValueString(), - "`enable_session_roaming` should be set when `sharing_kind` is `Shared` or associated machine catalogs have `Random` allocation type.", - ) - return - } + isValid, errMsg := validateSessionRoaming(desktops, false) + if !isValid { + resp.Diagnostics.AddError( + "Error "+operation+" Delivery Group "+plan.Name.ValueString(), + "Error message: "+errMsg, + ) + return } } } diff --git a/internal/daas/delivery_group/delivery_group_resource_model.go b/internal/daas/delivery_group/delivery_group_resource_model.go index 38e542e..af4e4cb 100644 --- a/internal/daas/delivery_group/delivery_group_resource_model.go +++ b/internal/daas/delivery_group/delivery_group_resource_model.go @@ -674,10 +674,12 @@ func (RestrictedAccessUsers) GetSchemaForDeliveryGroup() schema.SingleNestedAttr func (RestrictedAccessUsers) getSchemaInternal(forDeliveryGroup bool) schema.SingleNestedAttribute { resource := "Delivery Group" - description := "Restrict access to this Delivery Group by specifying users and groups in the allow and block list. If no value is specified, all authenticated users will have access to this Delivery Group. To give access to unauthenticated users, use the `allow_anonymous_access` property." + description := "Restrict access to this Delivery Group by specifying users and groups in the allow and block list. To give access to unauthenticated users, use the `allow_anonymous_access` property." + + "\n\n~> **Please Note** If `restricted_access_users` attribute is omitted or set to `null`, all authenticated users will have access to this Delivery Group. If attribute is specified as an empty object i.e. `{}`, then no user will have access to the delivery group because `allow_list` and `block_list` will be set as empty sets by default." if !forDeliveryGroup { resource = "Desktop" - description = "Restrict access to this Desktop by specifying users and groups in the allow and block list. If no value is specified, all users that have access to this Delivery Group will have access to the Desktop. " + + description = "Restrict access to this Desktop by specifying users and groups in the allow and block list. " + + "\n\n~> **Please Note** If `restricted_access_users` attribute is omitted or set to `null`, all authenticated users will have access to this Desktop. If attribute is specified as an empty object i.e. `{}`, then no user will have access to the desktop because `allow_list` and `block_list` will be set as empty sets by default." + "\n\n~> **Please Note** For Remote PC Delivery Groups desktops, `restricted_access_users` has to be set." } @@ -688,29 +690,28 @@ func (RestrictedAccessUsers) getSchemaInternal(forDeliveryGroup bool) schema.Sin ElementType: types.StringType, Description: fmt.Sprintf("Users who can use this %s. \n\n-> **Note** Users must be in `DOMAIN\\UserOrGroupName` or `user@domain.com` format", resource), Optional: true, + Computed: true, + Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})), Validators: []validator.Set{ setvalidator.ValueStringsAre( validator.String( stringvalidator.RegexMatches(regexp.MustCompile(util.SamAndUpnRegex), "must be in `DOMAIN\\UserOrGroupName` or `user@domain.com` format"), ), ), - setvalidator.SizeAtLeast(1), - setvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName("block_list"), - ), }, }, "block_list": schema.SetAttribute{ ElementType: types.StringType, Description: fmt.Sprintf("Users who cannot use this %s. A block list is meaningful only when used to block users in the allow list. \n\n-> **Note** Users must be in `DOMAIN\\UserOrGroupName` or `user@domain.com` format", resource), Optional: true, + Computed: true, + Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})), Validators: []validator.Set{ setvalidator.ValueStringsAre( validator.String( stringvalidator.RegexMatches(regexp.MustCompile(util.SamAndUpnRegex), "must be in `DOMAIN\\UserOrGroupName` or `user@domain.com` format"), ), ), - setvalidator.SizeAtLeast(1), }, }, }, diff --git a/internal/daas/delivery_group/delivery_group_utils.go b/internal/daas/delivery_group/delivery_group_utils.go index 24657e4..a784f60 100644 --- a/internal/daas/delivery_group/delivery_group_utils.go +++ b/internal/daas/delivery_group/delivery_group_utils.go @@ -306,6 +306,19 @@ func validateAndReturnMachineCatalogSessionSupport(ctx context.Context, client c return associatedMachineCatalogProperties, err } +func validateSessionRoaming(desktops []DeliveryGroupDesktop, sessionRoamingShouldBeSet bool) (bool, string) { + for _, desktop := range desktops { + if desktop.EnableSessionRoaming.IsUnknown() { + continue + } else if !desktop.EnableSessionRoaming.IsNull() && !sessionRoamingShouldBeSet { + return false, "`enable_session_roaming` cannot be set when `sharing_kind` is `Private` or associated machine catalogs have `Static` allocation type." + } else if desktop.EnableSessionRoaming.IsNull() && sessionRoamingShouldBeSet { + return false, "`enable_session_roaming` should be set when `sharing_kind` is `Shared` or associated machine catalogs have `Random` allocation type." + } + } + return true, "" +} + func getDeliveryGroupAddMachinesRequest(associatedMachineCatalogs []DeliveryGroupMachineCatalogModel) []citrixorchestration.DeliveryGroupAddMachinesRequestModel { var deliveryGroupMachineCatalogsArray []citrixorchestration.DeliveryGroupAddMachinesRequestModel for _, associatedMachineCatalog := range associatedMachineCatalogs { @@ -1310,17 +1323,9 @@ func (dgDesktop DeliveryGroupDesktop) RefreshListItem(ctx context.Context, diagn includedUsers := desktop.GetIncludedUsers() excludedUsers := desktop.GetExcludedUsers() - if len(includedUsers) == 0 { - users.AllowList = types.SetNull(types.StringType) - } else { - users.AllowList = util.RefreshUsersList(ctx, diagnostics, users.AllowList, includedUsers) - } + users.AllowList = util.RefreshUsersList(ctx, diagnostics, users.AllowList, includedUsers) + users.BlockList = util.RefreshUsersList(ctx, diagnostics, users.BlockList, excludedUsers) - if len(excludedUsers) == 0 { - users.BlockList = types.SetNull(types.StringType) - } else { - users.BlockList = util.RefreshUsersList(ctx, diagnostics, users.BlockList, excludedUsers) - } usersObj := util.TypedObjectToObjectValue(ctx, diagnostics, users) dgDesktop.RestrictedAccessUsers = usersObj @@ -1714,7 +1719,7 @@ func (r DeliveryGroupResourceModel) updatePlanWithRestrictedAccessUsers(ctx cont } } - if !accessPolicy.GetIncludedUserFilterEnabled() { + if !accessPolicy.GetIncludedUserFilterEnabled() || !accessPolicy.GetExcludedUserFilterEnabled() { if attributes, err := util.ResourceAttributeMapFromObject(RestrictedAccessUsers{}); err == nil { r.RestrictedAccessUsers = types.ObjectNull(attributes) } else { @@ -1725,28 +1730,10 @@ func (r DeliveryGroupResourceModel) updatePlanWithRestrictedAccessUsers(ctx cont users := util.ObjectValueToTypedObject[RestrictedAccessUsers](ctx, diagnostics, r.RestrictedAccessUsers) - remoteIncludedUsers := accessPolicy.GetIncludedUsers() - if len(remoteIncludedUsers) == 0 { - users.AllowList = types.SetNull(types.StringType) - } else { - users.AllowList = util.RefreshUsersList(ctx, diagnostics, users.AllowList, accessPolicy.GetIncludedUsers()) - } - - if accessPolicy.GetExcludedUserFilterEnabled() { - if len(accessPolicy.GetExcludedUsers()) == 0 { - users.BlockList = types.SetNull(types.StringType) - } else { - users.BlockList = util.RefreshUsersList(ctx, diagnostics, users.BlockList, accessPolicy.GetExcludedUsers()) - } - } + users.AllowList = util.RefreshUsersList(ctx, diagnostics, users.AllowList, accessPolicy.GetIncludedUsers()) + users.BlockList = util.RefreshUsersList(ctx, diagnostics, users.BlockList, accessPolicy.GetExcludedUsers()) - if users.AllowList.IsNull() && users.BlockList.IsNull() { - if attributes, err := util.ResourceAttributeMapFromObject(users); err == nil { - r.RestrictedAccessUsers = types.ObjectNull(attributes) - } - } else { - r.RestrictedAccessUsers = util.TypedObjectToObjectValue(ctx, diagnostics, users) - } + r.RestrictedAccessUsers = util.TypedObjectToObjectValue(ctx, diagnostics, users) return r } diff --git a/internal/daas/image_definition/image_definition_data_source.go b/internal/daas/image_definition/image_definition_data_source.go index 295c5d6..283a023 100644 --- a/internal/daas/image_definition/image_definition_data_source.go +++ b/internal/daas/image_definition/image_definition_data_source.go @@ -65,12 +65,12 @@ func (d *ImageDefinitionDataSource) Read(ctx context.Context, req datasource.Rea imageDefinitionNameOrId = data.Name.ValueString() } - iamgeDefinition, err := getImageDefinition(ctx, d.client, &resp.Diagnostics, imageDefinitionNameOrId) + imageDefinition, err := GetImageDefinition(ctx, d.client, &resp.Diagnostics, imageDefinitionNameOrId) if err != nil { return } - data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, false, iamgeDefinition) + data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, false, imageDefinition) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/daas/image_definition/image_definition_resource.go b/internal/daas/image_definition/image_definition_resource.go index 6e4c8fe..ad52c42 100644 --- a/internal/daas/image_definition/image_definition_resource.go +++ b/internal/daas/image_definition/image_definition_resource.go @@ -127,7 +127,7 @@ func (r *ImageDefinitionResource) Create(ctx context.Context, req resource.Creat } } - imageDefinitionResp, err := getImageDefinition(ctx, r.client, &resp.Diagnostics, plan.Name.ValueString()) + imageDefinitionResp, err := GetImageDefinition(ctx, r.client, &resp.Diagnostics, plan.Name.ValueString()) if err != nil { return } @@ -348,7 +348,7 @@ func (r *ImageDefinitionResource) ModifyPlan(ctx context.Context, req resource.M } } -func getImageDefinition(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, imageDefinitionNameOrId string) (*citrixorchestration.ImageDefinitionResponseModel, error) { +func GetImageDefinition(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, imageDefinitionNameOrId string) (*citrixorchestration.ImageDefinitionResponseModel, error) { getImageDefinitionRequest := client.ApiClient.ImageDefinitionsAPIsDAAS.ImageDefinitionsGetImageDefinition(ctx, imageDefinitionNameOrId) imageDefinitionResource, httpResp, err := citrixdaasclient.AddRequestData(getImageDefinitionRequest, client).Execute() if err != nil { diff --git a/internal/daas/image_definition/image_version_data_source.go b/internal/daas/image_definition/image_version_data_source.go new file mode 100644 index 0000000..872c5e1 --- /dev/null +++ b/internal/daas/image_definition/image_version_data_source.go @@ -0,0 +1,99 @@ +// Copyright © 2024. Citrix Systems, Inc. +package image_definition + +import ( + "context" + + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +var ( + _ datasource.DataSource = &ImageVersionDataSource{} +) + +func NewImageVersionDataSource() datasource.DataSource { + return &ImageVersionDataSource{} +} + +type ImageVersionDataSource struct { + client *citrixdaasclient.CitrixDaasClient +} + +func (d *ImageVersionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_image_version" +} + +func (d *ImageVersionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = ImageVersionModel{}.GetDataSourceSchema() +} + +func (d *ImageVersionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + d.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) +} + +func (d *ImageVersionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + if d.client != nil && d.client.ApiClient == nil { + resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) + return + } + + util.CheckProductVersion(d.client, &resp.Diagnostics, 121, 118, 7, 41, "Error reading Image Version data source", "Image Version data source") + + var data ImageVersionModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Read the data from the API + useImageVersionId := true + var imageVersionId string + var imageVersionNumber int32 + + imageDefinitionId := data.ImageDefinition.ValueString() + + if !data.Id.IsNull() { + imageVersionId = data.Id.ValueString() + } + if !data.VersionNumber.IsNull() { + useImageVersionId = false + imageVersionNumber = data.VersionNumber.ValueInt32() + } + + var imageVersion *citrixorchestration.ImageVersionResponseModel + var err error + if useImageVersionId { + imageVersion, err = GetImageVersion(ctx, d.client, &resp.Diagnostics, imageDefinitionId, imageVersionId) + if err != nil { + return + } + } else { + imageVersions, err := getImageVersions(ctx, &resp.Diagnostics, d.client, imageDefinitionId) + if err != nil { + return + } + for _, imageVersionInRemote := range imageVersions { + if imageVersionInRemote.GetNumber() == imageVersionNumber { + imageVersion = &imageVersionInRemote + break + } + } + } + + data = data.RefreshDataSourcePropertyValues(ctx, &resp.Diagnostics, imageVersion) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/daas/image_definition/image_version_data_source_model.go b/internal/daas/image_definition/image_version_data_source_model.go new file mode 100644 index 0000000..790f527 --- /dev/null +++ b/internal/daas/image_definition/image_version_data_source_model.go @@ -0,0 +1,146 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package image_definition + +import ( + "context" + "fmt" + "regexp" + + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AzureImageSpecsDataSourceModel struct { + // Required Attributes + ServiceOffering types.String `tfsdk:"service_offering"` + LicenseType types.String `tfsdk:"license_type"` + StorageType types.String `tfsdk:"storage_type"` + + // Optional Attributes + MachineProfile types.Object `tfsdk:"machine_profile"` + DiskEncryptionSet types.Object `tfsdk:"disk_encryption_set"` +} + +func (AzureImageSpecsDataSourceModel) GetDataSourceSchema() schema.SingleNestedAttribute { + return schema.SingleNestedAttribute{ + Description: "Image configuration for Azure image version.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "service_offering": schema.StringAttribute{ + Description: "The Azure VM Sku to use when creating machines.", + Computed: true, + }, + "license_type": schema.StringAttribute{ + Description: "Windows license type used to provision virtual machines in Azure at the base compute rate. License types include: `Windows_Client` and `Windows_Server`.", + Computed: true, + }, + "storage_type": schema.StringAttribute{ + Description: "Storage account type used for provisioned virtual machine disks on Azure. Storage types include: `Standard_LRS`, `StandardSSD_LRS` and `Premium_LRS`.", + Computed: true, + }, + "machine_profile": util.AzureMachineProfileModel{}.GetDataSourceSchema(), + "disk_encryption_set": util.AzureDiskEncryptionSetModel{}.GetDataSourceSchema(), + }, + } +} + +func (AzureImageSpecsDataSourceModel) GetDataSourceAttributes() map[string]schema.Attribute { + return AzureImageSpecsDataSourceModel{}.GetDataSourceSchema().Attributes +} + +func (ImageVersionModel) GetDataSourceSchema() schema.Schema { + return schema.Schema{ + Description: "CVAD --- Data source an image version. **Note that this feature is in Tech Preview.**", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The id of the image version.", + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + stringvalidator.ExactlyOneOf(path.MatchRoot("version_number")), + }, + }, + "image_definition": schema.StringAttribute{ + Description: "Id of the image definition to associate this image version with.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + }, + }, + "version_number": schema.Int32Attribute{ + Description: "The version number of the image version.", + Optional: true, + }, + "hypervisor": schema.StringAttribute{ + Description: "Id of the hypervisor to use for creating this image version.", + Computed: true, + }, + "hypervisor_resource_pool": schema.StringAttribute{ + Description: "Id of the hypervisor resource pool to use for creating this image version.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the image version.", + Computed: true, + }, + "azure_image_specs": AzureImageSpecsDataSourceModel{}.GetDataSourceSchema(), + "session_support": schema.StringAttribute{ + Description: "Session support for the image version.", + Computed: true, + }, + "os_type": schema.StringAttribute{ + Description: "The OS type of the image version.", + Computed: true, + }, + }, + } +} + +func (ImageVersionModel) GetDataSourceAttributes() map[string]schema.Attribute { + return ImageVersionModel{}.GetDataSourceSchema().Attributes +} + +func (r ImageVersionModel) RefreshDataSourcePropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, imageVersion *citrixorchestration.ImageVersionResponseModel) ImageVersionModel { + r, imageSpecs, specConfigured := r.RefreshImageVersionBaseProperties(ctx, diagnostics, imageVersion) + if specConfigured { + return r + } + + imageContext := imageSpecs.GetContext() + switch imageContext.GetPluginFactoryName() { + case util.AZURERM_FACTORY_NAME: + imageScheme := imageContext.GetImageScheme() + azureImageSpecs := AzureImageSpecsDataSourceModel{} + + azureImageSpecs.ServiceOffering = parseAzureImageVersionServiceOffering(imageScheme.GetServiceOffering()) + + licenseType, storageType, des, err := parseAzureImageCustomProperties(ctx, diagnostics, false, imageScheme.GetCustomProperties(), azureImageSpecs.DiskEncryptionSet) + if err != nil { + return r + } + azureImageSpecs.LicenseType = licenseType + azureImageSpecs.StorageType = storageType + azureImageSpecs.DiskEncryptionSet = des + + updatedMachineProfile, err := refreshAzureImageVersionMachineProfile(ctx, diagnostics, false, imageScheme) + if err == nil { + azureImageSpecs.MachineProfile = updatedMachineProfile + } + + r.AzureImageSpecs = util.DataSourceTypedObjectToObjectValue(ctx, diagnostics, azureImageSpecs) + default: + diagnostics.AddError( + "Error refreshing Image Version data source", + fmt.Sprintf("Hypervisor connection type %s is not supported", imageContext.GetPluginFactoryName()), + ) + } + + return r +} diff --git a/internal/daas/image_definition/image_version_resource.go b/internal/daas/image_definition/image_version_resource.go index 9081a70..f79903f 100644 --- a/internal/daas/image_definition/image_version_resource.go +++ b/internal/daas/image_definition/image_version_resource.go @@ -200,7 +200,7 @@ func (r *ImageVersionResource) Create(ctx context.Context, req resource.CreateRe return } - imageVersion, err = getImageVersion(ctx, r.client, &resp.Diagnostics, plan.ImageDefinition.ValueString(), imageVersion.GetId()) + imageVersion, err = GetImageVersion(ctx, r.client, &resp.Diagnostics, plan.ImageDefinition.ValueString(), imageVersion.GetId()) if err != nil { return } @@ -286,7 +286,7 @@ func (r *ImageVersionResource) Update(ctx context.Context, req resource.UpdateRe return } - imageVersion, err = getImageVersion(ctx, r.client, &resp.Diagnostics, plan.ImageDefinition.ValueString(), imageVersion.GetId()) + imageVersion, err = GetImageVersion(ctx, r.client, &resp.Diagnostics, plan.ImageDefinition.ValueString(), imageVersion.GetId()) if err != nil { return } @@ -397,7 +397,7 @@ func (r *ImageVersionResource) ModifyPlan(ctx context.Context, req resource.Modi return } - imageDefinition, err := getImageDefinition(ctx, r.client, &resp.Diagnostics, plan.ImageDefinition.ValueString()) + imageDefinition, err := GetImageDefinition(ctx, r.client, &resp.Diagnostics, plan.ImageDefinition.ValueString()) if err != nil { return } @@ -486,7 +486,7 @@ func readImageVersion(ctx context.Context, client *citrixdaasclient.CitrixDaasCl return imageVersionResource, err } -func getImageVersion(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, imageDefinitionId string, imageVersionId string) (*citrixorchestration.ImageVersionResponseModel, error) { +func GetImageVersion(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, imageDefinitionId string, imageVersionId string) (*citrixorchestration.ImageVersionResponseModel, error) { getImageVersionRequest := client.ApiClient.ImageDefinitionsAPIsDAAS.ImageDefinitionsGetImageDefinitionImageVersion(ctx, imageDefinitionId, imageVersionId) imageVersionResource, httpResp, err := citrixdaasclient.AddRequestData(getImageVersionRequest, client).Execute() if err != nil { diff --git a/internal/daas/image_definition/image_version_resource_model.go b/internal/daas/image_definition/image_version_resource_model.go index 499b94f..ac78e1b 100644 --- a/internal/daas/image_definition/image_version_resource_model.go +++ b/internal/daas/image_definition/image_version_resource_model.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -133,6 +134,8 @@ type ImageVersionModel struct { // Optional Attributes Description types.String `tfsdk:"description"` AzureImageSpecs types.Object `tfsdk:"azure_image_specs"` + SessionSupport types.String `tfsdk:"session_support"` + OsType types.String `tfsdk:"os_type"` } func (ImageVersionModel) GetSchema() schema.Schema { @@ -190,6 +193,14 @@ func (ImageVersionModel) GetSchema() schema.Schema { Default: stringdefault.StaticString(""), }, "azure_image_specs": AzureImageSpecsModel{}.GetSchema(), + "session_support": schema.StringAttribute{ + Description: "Session support for the image version.", + Computed: true, + }, + "os_type": schema.StringAttribute{ + Description: "The OS type of the image version.", + Computed: true, + }, }, } } @@ -199,84 +210,42 @@ func (ImageVersionModel) GetAttributes() map[string]schema.Attribute { } func (r ImageVersionModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, imageVersion *citrixorchestration.ImageVersionResponseModel) ImageVersionModel { - r.Id = types.StringValue(imageVersion.GetId()) - r.VersionNumber = types.Int32Value(imageVersion.GetNumber()) - imageDefinition := imageVersion.GetImageDefinition() - r.ImageDefinition = types.StringValue(imageDefinition.GetId()) - r.Description = types.StringValue(imageVersion.GetDescription()) - - var imageContext citrixorchestration.ImageVersionSpecContextResponseModel - var masterImage citrixorchestration.HypervisorResourceRefResponseModel - imageContextConfigured := false - for _, spec := range imageVersion.GetImageVersionSpecs() { - if spec.Context != nil { - context := spec.GetContext() - if context.ImageScheme == nil { - continue - } - masterImage = spec.GetMasterImage() - imageContext = spec.GetContext() - resourcePool := spec.GetResourcePool() - hypervisor := resourcePool.GetHypervisor() - r.Hypervisor = types.StringValue(hypervisor.GetId()) - r.ResourcePool = types.StringValue(resourcePool.GetId()) - imageContextConfigured = true - break - } - } - if !imageContextConfigured { - diagnostics.AddError( - "Error refreshing Image Version", - "Image Version does not have image context configured", - ) + r, imageSpecs, specConfigured := r.RefreshImageVersionBaseProperties(ctx, diagnostics, imageVersion) + if specConfigured { + return r } + imageContext := imageSpecs.GetContext() + + resourcePool := imageSpecs.GetResourcePool() + hypervisor := resourcePool.GetHypervisor() + r.Hypervisor = types.StringValue(hypervisor.GetId()) + r.ResourcePool = types.StringValue(resourcePool.GetId()) + + imageRuntimeEnvironment := imageSpecs.GetImageRuntimeEnvironment() + r.SessionSupport = types.StringValue(imageRuntimeEnvironment.GetVDASessionSupport()) + vdaOS := imageRuntimeEnvironment.GetOperatingSystem() + r.OsType = types.StringValue(vdaOS.GetType()) + + masterImage := imageSpecs.GetMasterImage() switch imageContext.GetPluginFactoryName() { case util.AZURERM_FACTORY_NAME: imageScheme := imageContext.GetImageScheme() azureImageSpecs := util.ObjectValueToTypedObject[AzureImageSpecsModel](ctx, diagnostics, r.AzureImageSpecs) - serviceOfferingXdPath := imageScheme.GetServiceOffering() - serviceOfferingSegments := strings.Split(serviceOfferingXdPath, "\\") - serviceOfferingLastIndex := len(serviceOfferingSegments) - serviceOffering := strings.TrimSuffix(serviceOfferingSegments[serviceOfferingLastIndex-1], ".serviceoffering") - azureImageSpecs.ServiceOffering = types.StringValue(serviceOffering) + azureImageSpecs.ServiceOffering = parseAzureImageVersionServiceOffering(imageScheme.GetServiceOffering()) - // Set initial values before refreshing the custom properties - azureImageSpecs.LicenseType = types.StringNull() - azureImageSpecs.StorageType = types.StringNull() - attributeMap, err := util.ResourceAttributeMapFromObject(util.AzureDiskEncryptionSetModel{}) + licenseType, storageType, des, err := parseAzureImageCustomProperties(ctx, diagnostics, true, imageScheme.GetCustomProperties(), azureImageSpecs.DiskEncryptionSet) if err != nil { - diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) return r } - azureImageSpecs.DiskEncryptionSet = types.ObjectNull(attributeMap) - - for _, customerProperty := range imageScheme.GetCustomProperties() { - if strings.EqualFold(customerProperty.GetName(), "LicenseType") && customerProperty.GetValue() != "" { - azureImageSpecs.LicenseType = types.StringValue(customerProperty.GetValue()) - } else if strings.EqualFold(customerProperty.GetName(), "StorageType") && customerProperty.GetValue() != "" { - azureImageSpecs.StorageType = types.StringValue(customerProperty.GetValue()) - } else if strings.EqualFold(customerProperty.GetName(), "DiskEncryptionSetId") && customerProperty.GetValue() != "" { - diskEncryptionSetModel := util.ObjectValueToTypedObject[util.AzureDiskEncryptionSetModel](ctx, diagnostics, azureImageSpecs.DiskEncryptionSet) - diskEncryptionSetModel = util.RefreshDiskEncryptionSetModel(diskEncryptionSetModel, customerProperty.GetValue()) - diskEncryptionSetModel.DiskEncryptionSetResourceGroup = types.StringValue(strings.ToLower(diskEncryptionSetModel.DiskEncryptionSetResourceGroup.ValueString())) - diskEncryptionSetModel.DiskEncryptionSetName = types.StringValue(strings.ToLower(diskEncryptionSetModel.DiskEncryptionSetName.ValueString())) - azureImageSpecs.DiskEncryptionSet = util.TypedObjectToObjectValue(ctx, diagnostics, diskEncryptionSetModel) - } - } + azureImageSpecs.LicenseType = licenseType + azureImageSpecs.StorageType = storageType + azureImageSpecs.DiskEncryptionSet = des - if imageScheme.MachineProfile != nil { - // Refresh machine profile - machineProfile := imageScheme.GetMachineProfile() - machineProfileModel := util.ParseAzureMachineProfileResponseToModel(machineProfile) - azureImageSpecs.MachineProfile = util.TypedObjectToObjectValue(ctx, diagnostics, machineProfileModel) - } else { - if attributesMap, err := util.ResourceAttributeMapFromObject(util.AzureMachineProfileModel{}); err == nil { - azureImageSpecs.MachineProfile = types.ObjectNull(attributesMap) - } else { - diagnostics.AddWarning("Error when creating null AzureMachineProfileModel", err.Error()) - } + updatedMachineProfile, err := refreshAzureImageVersionMachineProfile(ctx, diagnostics, true, imageScheme) + if err == nil { + azureImageSpecs.MachineProfile = updatedMachineProfile } // Refresh NetworkMapping @@ -325,3 +294,117 @@ func ParseMasterImageToAzureImageModel(ctx context.Context, diagnostics *diag.Di } return azureImageSpecs } + +func (r ImageVersionModel) RefreshImageVersionBaseProperties(ctx context.Context, diagnostics *diag.Diagnostics, imageVersion *citrixorchestration.ImageVersionResponseModel) (ImageVersionModel, citrixorchestration.ImageVersionSpecResponseModel, bool) { + r.Id = types.StringValue(imageVersion.GetId()) + r.VersionNumber = types.Int32Value(imageVersion.GetNumber()) + imageDefinition := imageVersion.GetImageDefinition() + r.ImageDefinition = types.StringValue(imageDefinition.GetId()) + r.Description = types.StringValue(imageVersion.GetDescription()) + + imageSpecs, specConfigured := identifyImageVersionSpec(diagnostics, imageVersion.GetImageVersionSpecs()) + if !specConfigured { + return r, imageSpecs, false + } + + resourcePool := imageSpecs.GetResourcePool() + hypervisor := resourcePool.GetHypervisor() + r.Hypervisor = types.StringValue(hypervisor.GetId()) + r.ResourcePool = types.StringValue(resourcePool.GetId()) + + imageRuntimeEnvironment := imageSpecs.GetImageRuntimeEnvironment() + r.SessionSupport = types.StringValue(imageRuntimeEnvironment.GetVDASessionSupport()) + vdaOS := imageRuntimeEnvironment.GetOperatingSystem() + r.OsType = types.StringValue(vdaOS.GetType()) + return r, imageSpecs, false +} + +func refreshAzureImageVersionMachineProfile(ctx context.Context, diagnostics *diag.Diagnostics, isResource bool, imageScheme citrixorchestration.ImageSchemeResponseModel) (types.Object, error) { + var machineProfileToReturn types.Object + + if imageScheme.MachineProfile != nil { + // Refresh machine profile + machineProfile := imageScheme.GetMachineProfile() + machineProfileModel := util.ParseAzureMachineProfileResponseToModel(machineProfile) + if isResource { + machineProfileToReturn = util.TypedObjectToObjectValue(ctx, diagnostics, machineProfileModel) + } else { + machineProfileToReturn = util.DataSourceTypedObjectToObjectValue(ctx, diagnostics, machineProfileModel) + } + return machineProfileToReturn, nil + } else { + var attributesMap map[string]attr.Type + var err error + if isResource { + attributesMap, err = util.ResourceAttributeMapFromObject(util.AzureMachineProfileModel{}) + } else { + attributesMap, err = util.DataSourceAttributeMapFromObject(util.AzureMachineProfileModel{}) + } + if err != nil { + diagnostics.AddWarning("Error when creating null AzureMachineProfileModel", err.Error()) + return machineProfileToReturn, err + } + machineProfileToReturn = types.ObjectNull(attributesMap) + return machineProfileToReturn, err + } +} + +func identifyImageVersionSpec(diagnostics *diag.Diagnostics, imageVersionSpecs []citrixorchestration.ImageVersionSpecResponseModel) (citrixorchestration.ImageVersionSpecResponseModel, bool) { + for _, spec := range imageVersionSpecs { + if spec.Context != nil { + context := spec.GetContext() + if context.ImageScheme == nil { + continue + } + return spec, true + } + } + diagnostics.AddError( + "Error refreshing Image Version", + "Image Version does not have image context configured", + ) + return citrixorchestration.ImageVersionSpecResponseModel{}, false +} + +func parseAzureImageVersionServiceOffering(serviceOfferingXdPath string) types.String { + serviceOfferingSegments := strings.Split(serviceOfferingXdPath, "\\") + serviceOfferingLastIndex := len(serviceOfferingSegments) + serviceOffering := strings.TrimSuffix(serviceOfferingSegments[serviceOfferingLastIndex-1], ".serviceoffering") + return types.StringValue(serviceOffering) +} + +func parseAzureImageCustomProperties(ctx context.Context, diagnostics *diag.Diagnostics, isResource bool, customProperties []citrixorchestration.NameValueStringPairModel, des types.Object) (types.String, types.String, types.Object, error) { + // Set initial values before refreshing the custom properties\ + licenseType := types.StringNull() + storageType := types.StringNull() + var attributeMap map[string]attr.Type + var err error + if isResource { + attributeMap, err = util.ResourceAttributeMapFromObject(util.AzureDiskEncryptionSetModel{}) + } else { + attributeMap, err = util.DataSourceAttributeMapFromObject(util.AzureDiskEncryptionSetModel{}) + } + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return licenseType, storageType, des, err + } + des = types.ObjectNull(attributeMap) + for _, customerProperty := range customProperties { + if strings.EqualFold(customerProperty.GetName(), "LicenseType") && customerProperty.GetValue() != "" { + licenseType = types.StringValue(customerProperty.GetValue()) + } else if strings.EqualFold(customerProperty.GetName(), "StorageType") && customerProperty.GetValue() != "" { + storageType = types.StringValue(customerProperty.GetValue()) + } else if strings.EqualFold(customerProperty.GetName(), "DiskEncryptionSetId") && customerProperty.GetValue() != "" { + diskEncryptionSetModel := util.AzureDiskEncryptionSetModel{} + desName, desResourceGroup := util.ParseDiskEncryptionSetIdToNameAndResourceGroup(customerProperty.GetValue()) + diskEncryptionSetModel.DiskEncryptionSetResourceGroup = types.StringValue(strings.ToLower(desResourceGroup)) + diskEncryptionSetModel.DiskEncryptionSetName = types.StringValue(strings.ToLower(desName)) + if isResource { + des = util.TypedObjectToObjectValue(ctx, diagnostics, diskEncryptionSetModel) + } else { + des = util.DataSourceTypedObjectToObjectValue(ctx, diagnostics, diskEncryptionSetModel) + } + } + } + return licenseType, storageType, des, nil +} diff --git a/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go b/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go index 4cedcb7..c900487 100644 --- a/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go +++ b/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go @@ -451,22 +451,33 @@ func buildProvSchemeForCatalog(ctx context.Context, client *citrixdaasclient.Cit } func setProvisioningSchemeForMcsCatalog(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, azureMachineConfigModel AzureMachineConfigModel, diagnostics *diag.Diagnostics, provisioningScheme *citrixorchestration.CreateMachineCatalogProvisioningSchemeRequestModel, hypervisor *citrixorchestration.HypervisorDetailResponseModel, hypervisorResourcePool *citrixorchestration.HypervisorResourcePoolDetailResponseModel) error { - azureMasterImageModel := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, diagnostics, azureMachineConfigModel.AzureMasterImage) - sharedSubscription := azureMasterImageModel.SharedSubscription.ValueString() - resourceGroup := azureMasterImageModel.ResourceGroup.ValueString() - masterImage := azureMasterImageModel.MasterImage.ValueString() - storageAccount := azureMasterImageModel.StorageAccount.ValueString() - container := azureMasterImageModel.Container.ValueString() - err := error(nil) - imagePath, err := util.BuildAzureMasterImagePath(ctx, client, diagnostics, azureMasterImageModel.GalleryImage, sharedSubscription, resourceGroup, storageAccount, container, masterImage, hypervisor.GetName(), hypervisorResourcePool.GetName(), "Error creating Machine Catalog") - if err != nil { - return err - } + if !azureMachineConfigModel.AzureMasterImage.IsNull() { + azureMasterImageModel := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, diagnostics, azureMachineConfigModel.AzureMasterImage) + sharedSubscription := azureMasterImageModel.SharedSubscription.ValueString() + resourceGroup := azureMasterImageModel.ResourceGroup.ValueString() + masterImage := azureMasterImageModel.MasterImage.ValueString() + storageAccount := azureMasterImageModel.StorageAccount.ValueString() + container := azureMasterImageModel.Container.ValueString() + err := error(nil) + imagePath, err := util.BuildAzureMasterImagePath(ctx, client, diagnostics, azureMasterImageModel.GalleryImage, sharedSubscription, resourceGroup, storageAccount, container, masterImage, hypervisor.GetName(), hypervisorResourcePool.GetName(), "Error creating Machine Catalog") + if err != nil { + return err + } - provisioningScheme.SetMasterImagePath(imagePath) + provisioningScheme.SetMasterImagePath(imagePath) - masterImageNote := azureMachineConfigModel.MasterImageNote.ValueString() - provisioningScheme.SetMasterImageNote(masterImageNote) + masterImageNote := azureMachineConfigModel.MasterImageNote.ValueString() + provisioningScheme.SetMasterImageNote(masterImageNote) + } else if !azureMachineConfigModel.AzurePreparedImage.IsNull() { + preparedImageConfig := util.ObjectValueToTypedObject[PreparedImageConfigModel](ctx, diagnostics, azureMachineConfigModel.AzurePreparedImage) + provisioningScheme.SetPrepareImage(true) + + var assignImageVersionToProvScheme citrixorchestration.AssignImageVersionToProvisioningSchemeRequestModel + assignImageVersionToProvScheme.SetImageDefinition(preparedImageConfig.ImageDefinition.ValueString()) + assignImageVersionToProvScheme.SetImageVersion(preparedImageConfig.ImageVersion.ValueString()) + provisioningScheme.SetAssignImageVersionToProvisioningScheme(assignImageVersionToProvScheme) + provisioningScheme.SetResourcePool(hypervisorResourcePool.GetName()) // Override with name to adapt to image version workflow + } return nil } @@ -804,8 +815,8 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas provScheme := catalog.GetProvisioningScheme() masterImage := provScheme.GetMasterImage() currentDiskImage := provScheme.GetCurrentDiskImage() - machineProfile := provScheme.GetMachineProfile() + customProps := provScheme.GetCustomProperties() provisioningSchemePlan := util.ObjectValueToTypedObject[ProvisioningSchemeModel](ctx, &resp.Diagnostics, plan.ProvisioningScheme) @@ -823,6 +834,9 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas imagePath := "" masterImageNote := "" machineProfilePath := "" + usePreparedImage := false + imageDefinition := "" + imageVersion := "" var err error var httpResp *http.Response updateCustomProperties := []citrixorchestration.NameValueStringPairModel{} @@ -837,74 +851,82 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas azureMachineConfigModel := util.ObjectValueToTypedObject[AzureMachineConfigModel](ctx, &resp.Diagnostics, provisioningSchemePlan.AzureMachineConfig) azureMachineProfile := azureMachineConfigModel.MachineProfile if !(*provisioningType == citrixorchestration.PROVISIONINGTYPE_PVS_STREAMING) { - azureMasterImageModel := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, &resp.Diagnostics, azureMachineConfigModel.AzureMasterImage) - sharedSubscription := azureMasterImageModel.SharedSubscription.ValueString() - newImage := azureMasterImageModel.MasterImage.ValueString() - resourceGroup := azureMasterImageModel.ResourceGroup.ValueString() - imageBasePath := "image.folder" - if sharedSubscription != "" { - imageBasePath = fmt.Sprintf("image.folder\\%s.sharedsubscription", sharedSubscription) - } + if !azureMachineConfigModel.AzureMasterImage.IsNull() { + azureMasterImageModel := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, &resp.Diagnostics, azureMachineConfigModel.AzureMasterImage) + sharedSubscription := azureMasterImageModel.SharedSubscription.ValueString() + newImage := azureMasterImageModel.MasterImage.ValueString() + resourceGroup := azureMasterImageModel.ResourceGroup.ValueString() + imageBasePath := "image.folder" + if sharedSubscription != "" { + imageBasePath = fmt.Sprintf("image.folder\\%s.sharedsubscription", sharedSubscription) + } - if newImage != "" { - storageAccount := azureMasterImageModel.StorageAccount.ValueString() - container := azureMasterImageModel.Container.ValueString() - if storageAccount != "" && container != "" { - queryPath := fmt.Sprintf( - "%s\\%s.resourcegroup\\%s.storageaccount\\%s.container", - imageBasePath, - resourceGroup, - storageAccount, - container) - imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, newImage, "", "") - if err != nil { - resp.Diagnostics.AddError( - "Error updating Machine Catalog", - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - fmt.Sprintf("\nFailed to resolve master image VHD %s in container %s of storage account %s, error: %s", newImage, container, storageAccount, err.Error()), - ) - return err + if newImage != "" { + storageAccount := azureMasterImageModel.StorageAccount.ValueString() + container := azureMasterImageModel.Container.ValueString() + if storageAccount != "" && container != "" { + queryPath := fmt.Sprintf( + "%s\\%s.resourcegroup\\%s.storageaccount\\%s.container", + imageBasePath, + resourceGroup, + storageAccount, + container) + imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, newImage, "", "") + if err != nil { + resp.Diagnostics.AddError( + "Error updating Machine Catalog", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + fmt.Sprintf("\nFailed to resolve master image VHD %s in container %s of storage account %s, error: %s", newImage, container, storageAccount, err.Error()), + ) + return err + } + } else { + queryPath := fmt.Sprintf( + "%s\\%s.resourcegroup", + imageBasePath, + resourceGroup) + imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, newImage, "", "") + if err != nil { + resp.Diagnostics.AddError( + "Error updating Machine Catalog", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + fmt.Sprintf("\nFailed to resolve master image Managed Disk or Snapshot %s, error: %s", newImage, err.Error()), + ) + return err + } } - } else { - queryPath := fmt.Sprintf( - "%s\\%s.resourcegroup", - imageBasePath, - resourceGroup) - imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, newImage, "", "") - if err != nil { - resp.Diagnostics.AddError( - "Error updating Machine Catalog", - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - fmt.Sprintf("\nFailed to resolve master image Managed Disk or Snapshot %s, error: %s", newImage, err.Error()), - ) - return err - } - } - } else if !azureMasterImageModel.GalleryImage.IsNull() { - azureGalleryImage := util.ObjectValueToTypedObject[util.GalleryImageModel](ctx, &resp.Diagnostics, azureMasterImageModel.GalleryImage) - gallery := azureGalleryImage.Gallery.ValueString() - definition := azureGalleryImage.Definition.ValueString() - version := azureGalleryImage.Version.ValueString() - if gallery != "" && definition != "" { - queryPath := fmt.Sprintf( - "%s\\%s.resourcegroup\\%s.gallery\\%s.imagedefinition", - imageBasePath, - resourceGroup, - gallery, - definition) - imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, version, "", "") - if err != nil { - resp.Diagnostics.AddError( - "Error updating Machine Catalog", - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - fmt.Sprintf("\nFailed to locate Azure Image Gallery image %s of version %s in gallery %s, error: %s", newImage, version, gallery, err.Error()), - ) - return err + } else if !azureMasterImageModel.GalleryImage.IsNull() { + azureGalleryImage := util.ObjectValueToTypedObject[util.GalleryImageModel](ctx, &resp.Diagnostics, azureMasterImageModel.GalleryImage) + gallery := azureGalleryImage.Gallery.ValueString() + definition := azureGalleryImage.Definition.ValueString() + version := azureGalleryImage.Version.ValueString() + if gallery != "" && definition != "" { + queryPath := fmt.Sprintf( + "%s\\%s.resourcegroup\\%s.gallery\\%s.imagedefinition", + imageBasePath, + resourceGroup, + gallery, + definition) + imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, version, "", "") + if err != nil { + resp.Diagnostics.AddError( + "Error updating Machine Catalog", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + fmt.Sprintf("\nFailed to locate Azure Image Gallery image %s of version %s in gallery %s, error: %s", newImage, version, gallery, err.Error()), + ) + return err + } } } - } - masterImageNote = azureMachineConfigModel.MasterImageNote.ValueString() + masterImageNote = azureMachineConfigModel.MasterImageNote.ValueString() + } else if !azureMachineConfigModel.AzurePreparedImage.IsNull() { + // Handle prepared image + usePreparedImage = true + preparedImageModel := util.ObjectValueToTypedObject[PreparedImageConfigModel](ctx, &resp.Diagnostics, azureMachineConfigModel.AzurePreparedImage) + imageDefinition = preparedImageModel.ImageDefinition.ValueString() + imageVersion = preparedImageModel.ImageVersion.ValueString() + } // Set reboot options if configured if !azureMachineConfigModel.ImageUpdateRebootOptions.IsNull() { @@ -1144,7 +1166,12 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas // Updating image is not supported for PVSStreaming catalog if !(*provisioningType == citrixorchestration.PROVISIONINGTYPE_PVS_STREAMING) { - if masterImage.GetXDPath() == imagePath && currentDiskImage.GetMasterImageNote() == masterImageNote { + + replicaRatio, replicaMaximum, useSharedGallery := setComputeGalleryValues(customProps) + + updateReplicaRatio, updateReplicaMaximum, updateUseSharedGallery := setComputeGalleryValues(updateCustomProperties) + + if masterImage.GetXDPath() == imagePath && currentDiskImage.GetMasterImageNote() == masterImageNote && updateReplicaRatio == replicaRatio && updateReplicaMaximum == replicaMaximum && updateUseSharedGallery == useSharedGallery { return nil } @@ -1161,11 +1188,17 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas } updateProvisioningSchemeModel.SetMinimumFunctionalLevel(*functionalLevel) - updateProvisioningSchemeModel.SetMasterImagePath(imagePath) - updateProvisioningSchemeModel.SetStoreOldImage(true) - updateProvisioningSchemeModel.SetMasterImageNote(masterImageNote) + if !usePreparedImage { + updateProvisioningSchemeModel.SetMasterImagePath(imagePath) + updateProvisioningSchemeModel.SetMasterImageNote(masterImageNote) + } else { + var assignImageVersionToProvScheme citrixorchestration.AssignImageVersionToProvisioningSchemeRequestModel + assignImageVersionToProvScheme.SetImageDefinition(imageDefinition) + assignImageVersionToProvScheme.SetImageVersion(imageVersion) + updateProvisioningSchemeModel.SetAssignImageVersionToProvisioningScheme(assignImageVersionToProvScheme) + } updateProvisioningSchemeModel.SetRebootOptions(rebootOption) if len(updateCustomProperties) > 0 { @@ -1192,6 +1225,24 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas return nil } +func setComputeGalleryValues(customProperties []citrixorchestration.NameValueStringPairModel) (string, string, string) { + replicaRatio := "" + replicaMaximum := "" + useSharedGallery := "false" + for _, customProp := range customProperties { + if customProp.GetName() == util.ReplicaRatio { + replicaRatio = customProp.GetValue() + } + if customProp.GetName() == util.ReplicaMaximum { + replicaMaximum = customProp.GetValue() + } + if customProp.GetName() == util.SharedGallery { + useSharedGallery = customProp.GetValue() + } + } + return replicaRatio, replicaMaximum, useSharedGallery +} + func (r MachineCatalogResourceModel) updateCatalogWithProvScheme(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, catalog *citrixorchestration.MachineCatalogDetailResponseModel, connectionType *citrixorchestration.HypervisorConnectionType, pluginId string, provScheme citrixorchestration.ProvisioningSchemeResponseModel, machineAdAccounts []citrixorchestration.ProvisioningSchemeMachineAccountResponseModel) MachineCatalogResourceModel { provSchemeModel := util.ObjectValueToTypedObject[ProvisioningSchemeModel](ctx, diagnostics, r.ProvisioningScheme) resourcePool := provScheme.GetResourcePool() @@ -1352,6 +1403,8 @@ func (r MachineCatalogResourceModel) updateCatalogWithProvScheme(ctx context.Con } if machineAccountCreateRules.GetOU() != "" { machineDomainIdentityModel.Ou = types.StringValue(machineAccountCreateRules.GetOU()) + } else { + machineDomainIdentityModel.Ou = types.StringNull() } provSchemeModel.MachineDomainIdentity = util.TypedObjectToObjectValue(ctx, diagnostics, machineDomainIdentityModel) diff --git a/internal/daas/machine_catalog/machine_catalog_resource.go b/internal/daas/machine_catalog/machine_catalog_resource.go index 8fe4499..d2c1f7b 100644 --- a/internal/daas/machine_catalog/machine_catalog_resource.go +++ b/internal/daas/machine_catalog/machine_catalog_resource.go @@ -12,6 +12,7 @@ import ( citrixorchestration "github.com/citrix/citrix-daas-rest-go/citrixorchestration" citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/daas/image_definition" "github.com/citrix/terraform-provider-citrix/internal/util" "github.com/hashicorp/terraform-plugin-framework/path" @@ -757,11 +758,12 @@ func (r *machineCatalogResource) ValidateConfig(ctx context.Context, req resourc azureMachineConfigModel := util.ObjectValueToTypedObject[AzureMachineConfigModel](ctx, &resp.Diagnostics, provSchemeModel.AzureMachineConfig) // Validate Azure Machine Config // Validate Azure Master Image - if !azureMachineConfigModel.AzureMasterImage.IsUnknown() && azureMachineConfigModel.AzureMasterImage.IsNull() { + if (!azureMachineConfigModel.AzureMasterImage.IsUnknown() && azureMachineConfigModel.AzureMasterImage.IsNull()) && + (!azureMachineConfigModel.AzurePreparedImage.IsUnknown() && azureMachineConfigModel.AzurePreparedImage.IsNull()) { resp.Diagnostics.AddAttributeError( - path.Root("azure_master_image"), + path.Root("azure_machine_config"), "Missing Attribute Configuration", - fmt.Sprintf("Expected azure_master_image to be configured when provisioning_type is %s.", provisioningTypeMcs), + fmt.Sprintf("Expected either `azure_master_image` or `prepared_image` to be configured when provisioning_type is %s.", provisioningTypeMcs), ) } @@ -1195,10 +1197,10 @@ func (r *machineCatalogResource) ModifyPlan(ctx context.Context, req resource.Mo return } - if !plan.ProvisioningScheme.IsNull() { + if !plan.ProvisioningScheme.IsUnknown() && !plan.ProvisioningScheme.IsNull() { provSchemePlan := util.ObjectValueToTypedObject[ProvisioningSchemeModel](ctx, &resp.Diagnostics, plan.ProvisioningScheme) - if !provSchemePlan.MachineADAccounts.IsNull() { + if !provSchemePlan.MachineADAccounts.IsUnknown() && !provSchemePlan.MachineADAccounts.IsNull() { machineAccountsInPlan := util.ObjectListToTypedArray[MachineADAccountModel](ctx, &resp.Diagnostics, provSchemePlan.MachineADAccounts) if len(machineAccountsInPlan) > 0 { @@ -1234,6 +1236,122 @@ func (r *machineCatalogResource) ModifyPlan(ctx context.Context, req resource.Mo return } } + + if !provSchemePlan.AzureMachineConfig.IsUnknown() && !provSchemePlan.AzureMachineConfig.IsNull() { + azureMachineConfig := util.ObjectValueToTypedObject[AzureMachineConfigModel](ctx, &resp.Diagnostics, provSchemePlan.AzureMachineConfig) + if !azureMachineConfig.AzurePreparedImage.IsUnknown() && !azureMachineConfig.AzurePreparedImage.IsNull() { + isPreparedImageSupported := util.CheckProductVersion(r.client, &resp.Diagnostics, 121, 118, 7, 41, "Error using Prepared Image in citrix_machine_catalog resource", "Prepared Image") + if !isPreparedImageSupported { + return + } + if !azureMachineConfig.UseManagedDisks.IsUnknown() && !azureMachineConfig.UseManagedDisks.IsNull() { + if !azureMachineConfig.UseManagedDisks.ValueBool() { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "use_managed_disks must be set to true when using prepared image.", + ) + return + } + } + azurePreparedImage := util.ObjectValueToTypedObject[PreparedImageConfigModel](ctx, &resp.Diagnostics, azureMachineConfig.AzurePreparedImage) + imageDefinition, err := image_definition.GetImageDefinition(ctx, r.client, &resp.Diagnostics, azurePreparedImage.ImageDefinition.ValueString()) + if err != nil { + return + } + + imageVersion, err := image_definition.GetImageVersion(ctx, r.client, &resp.Diagnostics, imageDefinition.GetId(), azurePreparedImage.ImageVersion.ValueString()) + if err != nil { + return + } + + imageVersionStatus := imageVersion.GetImageVersionStatus() + if imageVersionStatus != citrixorchestration.IMAGEVERSIONSTATUS_SUCCESS { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + fmt.Sprintf("Image version in state `%s` cannot be used to create machine catalog.", string(imageVersionStatus)), + ) + return + } + + imageContextConfigured := false + var imageScheme citrixorchestration.ImageSchemeResponseModel + var imageSpecs citrixorchestration.ImageVersionSpecResponseModel + for _, spec := range imageVersion.GetImageVersionSpecs() { + if spec.Context != nil { + context := spec.GetContext() + if context.ImageScheme == nil { + continue + } + imageContextConfigured = true + imageScheme = context.GetImageScheme() + imageSpecs = spec + } + } + imageRuntimeEnvironment := imageSpecs.GetImageRuntimeEnvironment() + if imageContextConfigured { + // Validate session support + if !plan.SessionSupport.IsUnknown() && imageRuntimeEnvironment.GetVDASessionSupport() != "" { + if !strings.EqualFold(plan.SessionSupport.ValueString(), imageRuntimeEnvironment.GetVDASessionSupport()) { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "session_support specified does not match the session support configured in the prepared image.", + ) + return + } + } + + // Validate machine profile + if imageScheme.MachineProfile != nil && azureMachineConfig.MachineProfile.IsNull() { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "machine_profile needs to be specified when using prepared image configrued with machine profile.", + ) + return + } + if imageScheme.MachineProfile == nil && !azureMachineConfig.MachineProfile.IsNull() && !azureMachineConfig.MachineProfile.IsUnknown() { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "machine_profile cannot be specified when using prepared image without a machine profile.", + ) + return + } + + // Validate disk encryption set + for _, customerProperty := range imageScheme.GetCustomProperties() { + if strings.EqualFold(customerProperty.GetName(), "DiskEncryptionSetId") && customerProperty.GetValue() != "" { + desName, desResourceGroup := util.ParseDiskEncryptionSetIdToNameAndResourceGroup(customerProperty.GetValue()) + if azureMachineConfig.DiskEncryptionSet.IsNull() { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "disk_encryption_set needs to be specified with the same disk encryption set configuration used for the prepared image.", + ) + return + } else if !azureMachineConfig.DiskEncryptionSet.IsUnknown() { + diskEncryptionSet := util.ObjectValueToTypedObject[util.AzureDiskEncryptionSetModel](ctx, &resp.Diagnostics, azureMachineConfig.DiskEncryptionSet) + if diskEncryptionSet.DiskEncryptionSetName.ValueString() != desName || diskEncryptionSet.DiskEncryptionSetResourceGroup.ValueString() != desResourceGroup { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "disk_encryption_set specified does not match the disk encryption set configured in the prepared image.", + ) + return + } + } + } + } + + if !provSchemePlan.HypervisorResourcePool.IsUnknown() && !provSchemePlan.HypervisorResourcePool.IsNull() { + imageVersionResourcePool := imageSpecs.GetResourcePool() + if imageVersionResourcePool.GetId() != provSchemePlan.HypervisorResourcePool.ValueString() { + resp.Diagnostics.AddError( + "Error validating azure_machine_config", + "resource pool specified in the prepared image does not match the resource pool configured in the provisioning scheme.", + ) + return + } + } + } + } + } } if req.State.Raw.IsNull() { @@ -1286,12 +1404,12 @@ func (r *machineCatalogResource) ModifyPlan(ctx context.Context, req resource.Mo machineDomainIdentityPlan := util.ObjectValueToTypedObject[MachineDomainIdentityModel](ctx, &resp.Diagnostics, provSchemePlan.MachineDomainIdentity) machineDomainIdentityState := util.ObjectValueToTypedObject[MachineDomainIdentityModel](ctx, &resp.Diagnostics, provSchemeState.MachineDomainIdentity) - if machineDomainIdentityPlan != machineDomainIdentityState && - provSchemePlan.NumTotalMachines.ValueInt64() == provSchemeState.NumTotalMachines.ValueInt64() { + if machineDomainIdentityPlan.Ou != machineDomainIdentityState.Ou && + provSchemePlan.NumTotalMachines.ValueInt64() <= provSchemeState.NumTotalMachines.ValueInt64() { resp.Diagnostics.AddError( "Error updating Machine Catalog "+state.Name.ValueString(), - "machine_domain_identity can only be updated when adding or removing machines.", + "Machine Catalog OU can only be updated when adding machines.", ) return } diff --git a/internal/daas/machine_catalog/machine_config.go b/internal/daas/machine_catalog/machine_config.go index 26af648..2281595 100644 --- a/internal/daas/machine_catalog/machine_config.go +++ b/internal/daas/machine_catalog/machine_config.go @@ -33,6 +33,7 @@ type AzureMachineConfigModel struct { /** Azure Hypervisor **/ AzureMasterImage types.Object `tfsdk:"azure_master_image"` AzurePvsConfiguration types.Object `tfsdk:"azure_pvs_config"` + AzurePreparedImage types.Object `tfsdk:"prepared_image"` // PreparedImageConfigModel MasterImageNote types.String `tfsdk:"master_image_note"` ImageUpdateRebootOptions types.Object `tfsdk:"image_update_reboot_options"` VdaResourceGroup types.String `tfsdk:"vda_resource_group"` @@ -57,11 +58,15 @@ func (AzureMachineConfigModel) GetSchema() schema.SingleNestedAttribute { }, "azure_pvs_config": AzurePvsConfigurationModel{}.GetSchema(), "azure_master_image": AzureMasterImageModel{}.GetSchema(), + "prepared_image": PreparedImageConfigModel{}.GetSchema(), "master_image_note": schema.StringAttribute{ Description: "The note for the master image.", Optional: true, Computed: true, Default: stringdefault.StaticString(""), + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("prepared_image")), + }, }, "image_update_reboot_options": ImageUpdateRebootOptionsModel{}.GetSchema(), "storage_type": schema.StringAttribute{ @@ -583,6 +588,18 @@ func (AzureMasterImageModel) GetSchema() schema.SingleNestedAttribute { }, "gallery_image": util.GalleryImageModel{}.GetSchema(), }, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplaceIf( + func(_ context.Context, req planmodifier.ObjectRequest, resp *objectplanmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = !req.StateValue.IsUnknown() && !req.StateValue.IsNull() && req.PlanValue.IsNull() + }, + "Changing machine catalog image type requires replacing the machine catalog resource.", + "Changing machine catalog image type requires replacing the machine catalog resource.", + ), + }, + Validators: []validator.Object{ + objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("prepared_image")), + }, } } @@ -616,7 +633,7 @@ func (AzurePvsConfigurationModel) GetSchema() schema.SingleNestedAttribute { }, }, Validators: []validator.Object{ - objectvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("azure_master_image")), + objectvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("azure_master_image"), path.MatchRelative().AtParent().AtName("prepared_image")), }, } } @@ -981,41 +998,62 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno // Refresh Master Image for non PVS catalogs if *provisioningType != citrixorchestration.PROVISIONINGTYPE_PVS_STREAMING { - masterImage := provScheme.GetMasterImage() - azureMasterImage := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, diagnostics, mc.AzureMasterImage) - masterImageXdPath := masterImage.GetXDPath() - if masterImageXdPath != "" { - segments := strings.Split(masterImage.GetXDPath(), "\\") - lastIndex := len(segments) - resourceTag := strings.Split(segments[lastIndex-1], ".") - resourceType := resourceTag[len(resourceTag)-1] - - if strings.EqualFold(resourceType, util.ImageVersionResourceType) { - azureMasterImage.GalleryImage, - azureMasterImage.ResourceGroup, - azureMasterImage.SharedSubscription = - util.ParseMasterImageToUpdateGalleryImageModel(ctx, diagnostics, azureMasterImage.GalleryImage, masterImage, segments, lastIndex) - - // Clear other master image details - azureMasterImage.MasterImage = types.StringNull() - azureMasterImage.StorageAccount = types.StringNull() - azureMasterImage.Container = types.StringNull() + if provScheme.CurrentImageVersion != nil { + currentImage := provScheme.GetCurrentImageVersion() + imageVersion := currentImage.GetImageVersion() + imageDefinition := imageVersion.GetImageDefinition() + preparedImageConfig := util.ObjectValueToTypedObject[PreparedImageConfigModel](ctx, diagnostics, mc.AzurePreparedImage) + preparedImageConfig.ImageDefinition = types.StringValue(imageDefinition.GetId()) + preparedImageConfig.ImageVersion = types.StringValue(imageVersion.GetId()) + mc.AzurePreparedImage = util.TypedObjectToObjectValue(ctx, diagnostics, preparedImageConfig) + + if attributesMap, err := util.ResourceAttributeMapFromObject(AzureMasterImageModel{}); err == nil { + mc.AzureMasterImage = types.ObjectNull(attributesMap) } else { - azureMasterImage.MasterImage, - azureMasterImage.ResourceGroup, - azureMasterImage.SharedSubscription, + diagnostics.AddWarning("Error when creating null AzureMasterImageModel", err.Error()) + } + } else { + masterImage := provScheme.GetMasterImage() + azureMasterImage := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, diagnostics, mc.AzureMasterImage) + masterImageXdPath := masterImage.GetXDPath() + if masterImageXdPath != "" { + segments := strings.Split(masterImage.GetXDPath(), "\\") + lastIndex := len(segments) + resourceTag := strings.Split(segments[lastIndex-1], ".") + resourceType := resourceTag[len(resourceTag)-1] + + if strings.EqualFold(resourceType, util.ImageVersionResourceType) { azureMasterImage.GalleryImage, - azureMasterImage.StorageAccount, - azureMasterImage.Container = - util.ParseMasterImageToUpdateAzureImageSpecs(ctx, diagnostics, resourceType, masterImage, segments, lastIndex) + azureMasterImage.ResourceGroup, + azureMasterImage.SharedSubscription = + util.ParseMasterImageToUpdateGalleryImageModel(ctx, diagnostics, azureMasterImage.GalleryImage, masterImage, segments, lastIndex) + + // Clear other master image details + azureMasterImage.MasterImage = types.StringNull() + azureMasterImage.StorageAccount = types.StringNull() + azureMasterImage.Container = types.StringNull() + } else { + azureMasterImage.MasterImage, + azureMasterImage.ResourceGroup, + azureMasterImage.SharedSubscription, + azureMasterImage.GalleryImage, + azureMasterImage.StorageAccount, + azureMasterImage.Container = + util.ParseMasterImageToUpdateAzureImageSpecs(ctx, diagnostics, resourceType, masterImage, segments, lastIndex) + } } - } - mc.AzureMasterImage = util.TypedObjectToObjectValue(ctx, diagnostics, azureMasterImage) + mc.AzureMasterImage = util.TypedObjectToObjectValue(ctx, diagnostics, azureMasterImage) + // Refresh Master Image Note + currentDiskImage := provScheme.GetCurrentDiskImage() + mc.MasterImageNote = types.StringValue(currentDiskImage.GetMasterImageNote()) - // Refresh Master Image Note - currentDiskImage := provScheme.GetCurrentDiskImage() - mc.MasterImageNote = types.StringValue(currentDiskImage.GetMasterImageNote()) + if attributesMap, err := util.ResourceAttributeMapFromObject(PreparedImageConfigModel{}); err == nil { + mc.AzurePreparedImage = types.ObjectNull(attributesMap) + } else { + diagnostics.AddWarning("Error when creating null PreparedImageConfigModel", err.Error()) + } + } } else { azurePvsConfiguration := util.ObjectValueToTypedObject[AzurePvsConfigurationModel](ctx, diagnostics, mc.AzurePvsConfiguration) // Set values for PVS Streaming catalogs @@ -1424,3 +1462,44 @@ func parseOnPremImagePath(catalog citrixorchestration.MachineCatalogDetailRespon return masterImage, imageSnapshot, resourcePoolPath } + +type PreparedImageConfigModel struct { + ImageDefinition types.String `tfsdk:"image_definition"` + ImageVersion types.String `tfsdk:"image_version"` +} + +func (PreparedImageConfigModel) GetSchema() schema.SingleNestedAttribute { + return schema.SingleNestedAttribute{ + Description: "Specifying the prepared master image to be used for machine catalog.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "image_definition": schema.StringAttribute{ + Description: "ID of the image definition.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + }, + }, + "image_version": schema.StringAttribute{ + Description: "ID of the image version.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + }, + }, + }, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplaceIf( + func(_ context.Context, req planmodifier.ObjectRequest, resp *objectplanmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = !req.StateValue.IsUnknown() && !req.StateValue.IsNull() && req.PlanValue.IsNull() + }, + "Changing machine catalog image type requires replacing the machine catalog resource.", + "Changing machine catalog image type requires replacing the machine catalog resource.", + ), + }, + } +} + +func (PreparedImageConfigModel) GetAttributes() map[string]schema.Attribute { + return PreparedImageConfigModel{}.GetSchema().Attributes +} diff --git a/internal/daas/zone/zone_resource.go b/internal/daas/zone/zone_resource.go index 9d0c61d..93627ef 100644 --- a/internal/daas/zone/zone_resource.go +++ b/internal/daas/zone/zone_resource.go @@ -101,10 +101,6 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r // Get resource location from remote using id resourceLocation, err := resource_locations.GetResourceLocation(ctx, r.client, &resp.Diagnostics, plan.ResourceLocationId.ValueString()) if err != nil { - resp.Diagnostics.AddError( - "Error creating Zone with Resource Location ID", - "Resource Location "+plan.ResourceLocationId.ValueString()+" not found. Ensure the resource location has been created manually or via terraform, then try again.", - ) return } diff --git a/internal/daas/zone/zone_resource_model.go b/internal/daas/zone/zone_resource_model.go index 008d7cc..081e7fb 100644 --- a/internal/daas/zone/zone_resource_model.go +++ b/internal/daas/zone/zone_resource_model.go @@ -29,7 +29,8 @@ type ZoneResourceModel struct { func (ZoneResourceModel) GetSchema() schema.Schema { return schema.Schema{ - Description: "CVAD --- Manages a zone.", + Description: "CVAD --- Manages a zone. " + + "\n\n~> **Please Note** For Citrix Cloud Customer, Citrix Cloud Resource Location permissions are required to manage DaaS Zones.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "GUID identifier of the zone.", diff --git a/internal/examples/data-sources/citrix_admin_permissions/data-source.tf b/internal/examples/data-sources/citrix_admin_permissions/data-source.tf new file mode 100644 index 0000000..0c1783f --- /dev/null +++ b/internal/examples/data-sources/citrix_admin_permissions/data-source.tf @@ -0,0 +1,17 @@ +# Get all predefined admin permissions +data "citrix_admin_permissions" "all-permissions" { } + +# The permissions can then be viewed via: +# terraform apply +# terraform state show data.citrix_admin_permissions.all-permissions + +# Example using the data source to create a new custom role +data "citrix_admin_role" "desktop_group_admin_role" { + name = "Desktop Group Custom Admin Role" + description = "Role for managing delivery groups" + permissions = [ + // all permissions from the data source that start with "DesktopGroup_" + for permission in data.citrix_admin_permissions.all-permissions.permissions : + permission.id if startswith(permission.id, "DesktopGroup_") + ] +} \ No newline at end of file diff --git a/internal/examples/data-sources/citrix_image_version/data-source.tf b/internal/examples/data-sources/citrix_image_version/data-source.tf new file mode 100644 index 0000000..902e078 --- /dev/null +++ b/internal/examples/data-sources/citrix_image_version/data-source.tf @@ -0,0 +1,11 @@ +# Define Image Version data source by id +data "citrix_image_version" "example_image_version_by_id" { + id = "00000000-0000-0000-0000-000000000000" + image_definition = "00000000-0000-0000-0000-000000000000" +} + +# Define Image Version data source by version number +data "citrix_image_version" "example_image_version_by_version_number" { + version_number = 1 + image_definition = "00000000-0000-0000-0000-000000000000" +} diff --git a/internal/examples/resources/citrix_machine_catalog/resource.tf b/internal/examples/resources/citrix_machine_catalog/resource.tf index d18d63e..f16602b 100644 --- a/internal/examples/resources/citrix_machine_catalog/resource.tf +++ b/internal/examples/resources/citrix_machine_catalog/resource.tf @@ -54,6 +54,50 @@ resource "citrix_machine_catalog" "example-azure-mtsession" { } } +resource "citrix_machine_catalog" "example_azure_prepared_image_mtsession" { + name = "example_azure_prepared_image_mtsession" + description = "Example multi-session catalog on Azure hypervisor with Prepared Image" + zone = "" + allocation_type = "Random" + session_support = "MultiSession" + provisioning_type = "MCS" + provisioning_scheme = { + hypervisor = citrix_azure_hypervisor.example-azure-hypervisor.id + hypervisor_resource_pool = citrix_azure_hypervisor_resource_pool.example-azure-hypervisor-resource-pool.id + identity_type = "ActiveDirectory" + machine_domain_identity = { + domain = "" + domain_ou = "" + service_account = "" + service_account_password = "" + } + azure_machine_config = { + storage_type = "Standard_LRS" + use_managed_disks = true + service_offering = "Standard_D2_v2" + prepared_image = { + image_definition = citrix_image_definition.example_image_definition.id + image_version = citrix_image_version.example_azure_image_version.id + } + writeback_cache = { + wbc_disk_storage_type = "pd-standard" + persist_wbc = true + persist_os_disk = true + persist_vm = true + writeback_cache_disk_size_gb = 127 + writeback_cache_memory_size_mb = 256 + storage_cost_saving = true + } + } + availability_zones = ["1","2"] + number_of_total_machines = 1 + machine_account_creation_rules ={ + naming_scheme = "az-multi-##" + naming_scheme_type ="Numeric" + } + } +} + resource "citrix_machine_catalog" "example-aws-mtsession" { name = "example-aws-mtsession" description = "Example multi-session catalog on AWS hypervisor" diff --git a/internal/examples/resources/citrix_zone/resource.tf b/internal/examples/resources/citrix_zone/resource.tf index d85c519..6fcdf46 100644 --- a/internal/examples/resources/citrix_zone/resource.tf +++ b/internal/examples/resources/citrix_zone/resource.tf @@ -10,7 +10,7 @@ resource "citrix_zone" "example-onpremises-zone" { ] } -# Exmaple for Cloud Zone +# Example for Cloud Zone resource "citrix_cloud_resource_location" "example-resource-location" { name = "example-resource-location" } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8dfa81b..47b611e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -929,6 +929,7 @@ func (p *citrixProvider) DataSources(_ context.Context) []func() datasource.Data application.NewApplicationDataSourceSource, admin_folder.NewAdminFolderDataSource, admin_role.NewAdminRoleDataSource, + admin_role.NewAdminPermissionsDataSource, admin_scope.NewAdminScopeDataSource, admin_user.NewAdminUserDataSource, machine_catalog.NewPvsDataSource, @@ -938,6 +939,7 @@ func (p *citrixProvider) DataSources(_ context.Context) []func() datasource.Data policies.NewPolicySetDataSource, storefront_server.NewStoreFrontServerDataSource, image_definition.NewImageDefinitionDataSource, + image_definition.NewImageVersionDataSource, // StoreFront DataSources stf_roaming.NewSTFRoamingServiceDataSource, // QuickCreate DataSources diff --git a/internal/test/admin_permissions_data_source_test.go b/internal/test/admin_permissions_data_source_test.go new file mode 100644 index 0000000..69dec09 --- /dev/null +++ b/internal/test/admin_permissions_data_source_test.go @@ -0,0 +1,42 @@ +// Copyright © 2024. Citrix Systems, Inc. +package test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAdminPermissionsDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + }, + Steps: []resource.TestStep{ + // Read testing + { + Config: BuildAdminPermissionsDataSource(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrWith("data.citrix_admin_permissions.test_all_permissions", "permissions.#", func(val string) error { + if val == "0" { + return fmt.Errorf("expected at least one permission") + } + return nil + }), + ), + }, + }, + }) +} + +func BuildAdminPermissionsDataSource(t *testing.T) string { + return adminPermissionsTestDataSource +} + +var ( + adminPermissionsTestDataSource = ` + data "citrix_admin_permissions" "test_all_permissions" { } + ` +) diff --git a/internal/test/image_version_data_source_test.go b/internal/test/image_version_data_source_test.go new file mode 100644 index 0000000..4294826 --- /dev/null +++ b/internal/test/image_version_data_source_test.go @@ -0,0 +1,97 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImageVersionDataSourcePreCheck(t *testing.T) { + checkTestEnvironmentVariables(t, imageVersionDataSourceTestVariables) +} + +func TestImageVersionDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestImageVersionDataSourcePreCheck(t) + }, + Steps: []resource.TestStep{ + // Test Image Version Data Source with Id + { + Config: BuildImageVersionDataSourceWithId(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "id", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_ID")), + // Verify the version number of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "version_number", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_VERSION_NUMBER")), + // Verify the image definition id of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "image_definition", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID")), + // Verify the session support of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "session_support", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_SESSION_SUPPORT")), + // Verify the os type of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "os_type", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_OS_TYPE")), + ), + }, + // Test Image Version Data Source with Version Number + { + Config: BuildImageVersionDataSourceWithVersionNumber(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "id", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_ID")), + // Verify the version number of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "version_number", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_VERSION_NUMBER")), + // Verify the image definition id of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "image_definition", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID")), + // Verify the session support of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "session_support", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_SESSION_SUPPORT")), + // Verify the os type of the image version data source + resource.TestCheckResourceAttr("data.citrix_image_version.test_image_version", "os_type", os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_OS_TYPE")), + ), + }, + }, + }) +} + +func BuildImageVersionDataSourceWithId(t *testing.T) string { + imageVersionId := os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_ID") + imageDefinitionId := os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID") + + return fmt.Sprintf(imageVersionTestDataSourceById, imageVersionId, imageDefinitionId) +} + +func BuildImageVersionDataSourceWithVersionNumber(t *testing.T) string { + imageVersionNumber := os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_VERSION_NUMBER") + imageDefinitionId := os.Getenv("TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID") + + return fmt.Sprintf(imageVersionTestDataSourceByNumber, imageVersionNumber, imageDefinitionId) +} + +var ( + imageVersionDataSourceTestVariables = []string{ + "TEST_IMAGE_VERSION_DATA_SOURCE_ID", + "TEST_IMAGE_VERSION_DATA_SOURCE_VERSION_NUMBER", + "TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID", + "TEST_IMAGE_VERSION_DATA_SOURCE_SESSION_SUPPORT", + "TEST_IMAGE_VERSION_DATA_SOURCE_OS_TYPE", + } + + imageVersionTestDataSourceById = ` +data "citrix_image_version" "test_image_version" { + id = "%s" + image_definition = "%s" +} +` + + imageVersionTestDataSourceByNumber = ` +data "citrix_image_version" "test_image_version" { + version_number = %s + image_definition = "%s" +} +` +) diff --git a/internal/util/common.go b/internal/util/common.go index 9b5c448..fed4484 100644 --- a/internal/util/common.go +++ b/internal/util/common.go @@ -157,7 +157,36 @@ const AllScopeId string = "00000000-0000-0000-0000-000000000000" // ID of the Citrix Managed Users Scope const CtxManagedScopeId string = "f71a1148-7030-467a-a6d3-4a6bcf6a6532" -// ID of the Citrix Managed Users Scope +// Restricted permissions in the cloud +var RestrictedPermissionsInCloud = map[string]bool{ + "Admin_FullControl": true, + "C365_EA_Acct": true, + "C365_EA_Broker": true, + "C365_EA_Hyp": true, + "C365_EA_Prov": true, + "Configuration_Read": true, + "Configuration_Unrestricted_Write": true, + "Controller_EditProperties": true, + "Controllers_Remove": true, + "Database_Read": true, + "Director_Admin": true, + "EA_Acct": true, + "EA_Broker": true, + "EA_Hyp": true, + "EA_Prov": true, + "EdgeService_Admin": true, + "Licensing_ChangeLicenseServer": true, + "Licensing_EditLicensingProperties": true, + "Licensing_Read ": true, + "Logging_Delete": true, + "Orchestration_RestApi": true, + "Paladin_Admin": true, + "PerformUpgrade": true, + "PVS_Admin": true, + "Trust_ServiceKeys": true, +} + +// Username for decoupled workspaces const UsernameForDecoupledWorkspaces string = "[UNDEFINED]" // Default QuickCreateService AWS Workspaces Scale Settings @@ -213,6 +242,10 @@ const MetadataImageManagementPrepPrefix = "imagemanagementprep_" const MetadataTaskDataPrefix = "taskdata_" const MetadataTaskStatePrefix = "taskstate_" +const ReplicaRatio = "SharedImageGalleryReplicaRatio" +const ReplicaMaximum = "SharedImageGalleryReplicaMaximum" +const SharedGallery = "UseSharedImageGallery" + // CC Admin User const AdminUserMonitorAccessPolicySuffix = " - Access to 'Monitor' tab only" diff --git a/internal/util/image.go b/internal/util/image.go index 414e96b..a3f6f63 100644 --- a/internal/util/image.go +++ b/internal/util/image.go @@ -13,6 +13,7 @@ import ( citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -92,15 +93,41 @@ func (AzureDiskEncryptionSetModel) GetSchema() schema.SingleNestedAttribute { } } +func (AzureDiskEncryptionSetModel) GetDataSourceSchema() dataSourceSchema.SingleNestedAttribute { + return dataSourceSchema.SingleNestedAttribute{ + Description: "The configuration for Disk Encryption Set (DES). The DES must be in the same subscription and region as your resources. If your master image is encrypted with a DES, use the same DES when creating this machine catalog. When using a DES, if you later disable the key with which the corresponding DES is associated in Azure, you can no longer power on the machines in this catalog or add machines to it.", + Computed: true, + Attributes: map[string]dataSourceSchema.Attribute{ + "disk_encryption_set_name": dataSourceSchema.StringAttribute{ + Description: "The name of the disk encryption set.", + Computed: true, + }, + "disk_encryption_set_resource_group": dataSourceSchema.StringAttribute{ + Description: "The name of the resource group in which the disk encryption set resides.", + Computed: true, + }, + }, + } +} + func (AzureDiskEncryptionSetModel) GetAttributes() map[string]schema.Attribute { return AzureDiskEncryptionSetModel{}.GetSchema().Attributes } -func RefreshDiskEncryptionSetModel(diskEncryptionSetModelToRefresh AzureDiskEncryptionSetModel, desId string) AzureDiskEncryptionSetModel { +func (AzureDiskEncryptionSetModel) GetDataSourceAttributes() map[string]dataSourceSchema.Attribute { + return AzureDiskEncryptionSetModel{}.GetDataSourceSchema().Attributes +} + +func ParseDiskEncryptionSetIdToNameAndResourceGroup(desId string) (string, string) { desArray := strings.Split(desId, "/") desName := desArray[len(desArray)-1] resourceGroupsIndex := slices.Index(desArray, "resourceGroups") resourceGroupName := desArray[resourceGroupsIndex+1] + return desName, resourceGroupName +} + +func RefreshDiskEncryptionSetModel(diskEncryptionSetModelToRefresh AzureDiskEncryptionSetModel, desId string) AzureDiskEncryptionSetModel { + desName, resourceGroupName := ParseDiskEncryptionSetIdToNameAndResourceGroup(desId) if !strings.EqualFold(diskEncryptionSetModelToRefresh.DiskEncryptionSetName.ValueString(), desName) { diskEncryptionSetModelToRefresh.DiskEncryptionSetName = types.StringValue(desName) } @@ -228,10 +255,39 @@ func (AzureMachineProfileModel) GetSchema() schema.SingleNestedAttribute { } } +func (AzureMachineProfileModel) GetDataSourceSchema() dataSourceSchema.SingleNestedAttribute { + return dataSourceSchema.SingleNestedAttribute{ + Description: "The name of the virtual machine or template spec that will be used to identify the default value for the tags, virtual machine size, boot diagnostics, host cache property of OS disk, accelerated networking and availability zone.", + Computed: true, + Attributes: map[string]dataSourceSchema.Attribute{ + "machine_profile_vm_name": dataSourceSchema.StringAttribute{ + Description: "The name of the machine profile virtual machine.", + Computed: true, + }, + "machine_profile_template_spec_name": dataSourceSchema.StringAttribute{ + Description: "The name of the machine profile template spec.", + Computed: true, + }, + "machine_profile_template_spec_version": dataSourceSchema.StringAttribute{ + Description: "The version of the machine profile template spec.", + Computed: true, + }, + "machine_profile_resource_group": dataSourceSchema.StringAttribute{ + Description: "The name of the resource group where the machine profile VM or template spec is located.", + Computed: true, + }, + }, + } +} + func (AzureMachineProfileModel) GetAttributes() map[string]schema.Attribute { return AzureMachineProfileModel{}.GetSchema().Attributes } +func (AzureMachineProfileModel) GetDataSourceAttributes() map[string]dataSourceSchema.Attribute { + return AzureMachineProfileModel{}.GetDataSourceSchema().Attributes +} + func HandleMachineProfileForAzureMcsPvsCatalog(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diag *diag.Diagnostics, hypervisorName string, resourcePoolName string, machineProfile AzureMachineProfileModel, errorTitle string) (string, error) { machineProfileResourceGroup := machineProfile.MachineProfileResourceGroup.ValueString() machineProfileVmOrTemplateSpecVersion := machineProfile.MachineProfileVmName.ValueString() diff --git a/scripts/onboarding-helper/README.md b/scripts/onboarding-helper/README.md index 438e640..38362c8 100644 --- a/scripts/onboarding-helper/README.md +++ b/scripts/onboarding-helper/README.md @@ -5,7 +5,7 @@ This automation script is designed to onboard an existing site to Terraform. It ## Environment Requirements - PowerShell version `5.0` or higher -- Citrix Provider version `1.0.7` +- Citrix Provider version `1.0.8` - For On-Premises Customers: CVAD DDC `version 2311` or newer. ## Workflow: diff --git a/scripts/onboarding-helper/terraform-onboarding.ps1 b/scripts/onboarding-helper/terraform-onboarding.ps1 index a4b105a..6cd17c8 100644 --- a/scripts/onboarding-helper/terraform-onboarding.ps1 +++ b/scripts/onboarding-helper/terraform-onboarding.ps1 @@ -1,4 +1,4 @@ - + # Copyright © 2024. Citrix Systems, Inc. All Rights Reserved. <# Currently this script is still in TechPreview @@ -694,7 +694,7 @@ function RemoveNullAndEmptyStringValues { Write-Verbose "Removing null values and empty strings from terraform output." # Remove null values and empty strings from terraform output $lines = $content -split "`r?`n" - $filteredLines = $lines | Where-Object { $_ -notmatch 'null' -and $_ -notmatch '^\s*[^=]+\s*=\s*""' } + $filteredLines = $lines | Where-Object { $_ -notmatch '.*= null.*' -and $_ -notmatch '^\s*[^=]+\s*=\s*""' } $content = $filteredLines -join "`n" Write-Verbose "Null values and empty strings removed successfully." @@ -779,7 +779,7 @@ function ExtractAndSaveApplicationIcons { } foreach ($line in $lines) { - if ($line -match 'raw_data\s*=\s*"([^"]+)"') { + if ($line -match '.*raw_data\s*=.*') { $rawDataValue = $matches[1] $iconBytes = [System.Convert]::FromBase64String($rawDataValue) $iconFileName = "$iconsFolder\app_icon_$iconCounter.ico" @@ -945,4 +945,4 @@ try { finally { # Clean up environment variables for client secret $env:CITRIX_CLIENT_SECRET = '' -} +} \ No newline at end of file diff --git a/scripts/onboarding-helper/terraform.tf b/scripts/onboarding-helper/terraform.tf index 6363d6f..ad1585e 100644 --- a/scripts/onboarding-helper/terraform.tf +++ b/scripts/onboarding-helper/terraform.tf @@ -4,7 +4,7 @@ terraform { required_providers { citrix = { source = "citrix/citrix" - version = "=1.0.7" + version = "=1.0.8" } } diff --git a/settings.cloud.example.json b/settings.cloud.example.json index edb997e..07a2568 100644 --- a/settings.cloud.example.json +++ b/settings.cloud.example.json @@ -322,6 +322,13 @@ "TEST_AZURE_IMAGE_VERSION_DES_NAME":"{image_version_disk_encryption_set_name}", "TEST_AZURE_IMAGE_VERSION_DES_RESOURCE_GROUP":"{image_version_disk_encryption_set_resource_group}", + // Image Version Data Source Go Tests + "TEST_IMAGE_VERSION_DATA_SOURCE_ID":"{image_version_id}", + "TEST_IMAGE_VERSION_DATA_SOURCE_VERSION_NUMBER":"{image_version_number}", + "TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID":"{image_version_definition_id}", + "TEST_IMAGE_VERSION_DATA_SOURCE_SESSION_SUPPORT":"{image_version_session_support}", + "TEST_IMAGE_VERSION_DATA_SOURCE_OS_TYPE":"{image_version_os_type}", + // StoreFront env variable "SF_COMPUTER_NAME" : "{storefront_computer_name}", "SF_AD_ADMIN_USERNAME" : "{storefront_admin_username}", diff --git a/settings.onprem.example.json b/settings.onprem.example.json index d8616e6..a90fd40 100644 --- a/settings.onprem.example.json +++ b/settings.onprem.example.json @@ -333,6 +333,13 @@ "TEST_AZURE_IMAGE_VERSION_DES_NAME":"{image_version_disk_encryption_set_name}", "TEST_AZURE_IMAGE_VERSION_DES_RESOURCE_GROUP":"{image_version_disk_encryption_set_resource_group}", + // Image Version Data Source Go Tests + "TEST_IMAGE_VERSION_DATA_SOURCE_ID":"{image_version_id}", + "TEST_IMAGE_VERSION_DATA_SOURCE_VERSION_NUMBER":"{image_version_number}", + "TEST_IMAGE_VERSION_DATA_SOURCE_IMAGE_DEFINITION_ID":"{image_version_definition_id}", + "TEST_IMAGE_VERSION_DATA_SOURCE_SESSION_SUPPORT":"{image_version_session_support}", + "TEST_IMAGE_VERSION_DATA_SOURCE_OS_TYPE":"{image_version_os_type}", + // StoreFront env variable "SF_COMPUTER_NAME" : "{storefront_computer_name}", "SF_AD_ADMIN_USERNAME" : "{storefront_admin_username}", diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 0ea887e..05156c4 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -35,7 +35,7 @@ Please refer to [Citrix Tech Zone](https://community.citrix.com/tech-zone/automa - [Citrix policies](https://community.citrix.com/tech-zone/automation/cvad-terraform-policies/) ### Demo video -[![alt text](../images/techzone-youtube-thumbnail.png)](https://www.youtube.com/watch?v=c33sMLaCVjY) +[![alt text](https://raw.githubusercontent.com/citrix/terraform-provider-citrix/refs/heads/main/images/techzone-youtube-thumbnail.png)](https://www.youtube.com/watch?v=c33sMLaCVjY) https://www.youtube.com/watch?v=c33sMLaCVjY ### (On-Premises Only) Enable Web Studio diff --git a/templates/resources/desktop_icon.md.tmpl b/templates/resources/desktop_icon.md.tmpl new file mode 100644 index 0000000..e220f79 --- /dev/null +++ b/templates/resources/desktop_icon.md.tmpl @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +{{ if gt (len (split .Description " --- ")) 1 -}} +subcategory: "{{ index (split .Description " --- ") 0 }}" +{{- else -}} +subcategory: "" +{{- end }} +description: |- +{{ if gt (len (split .Description " --- ")) 1 -}} +{{ index (split .Description " --- ") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +# {{.Name}} ({{.Type}}) + +{{ if gt (len (split .Description " --- ")) 1 -}} +{{ index (split .Description " --- ") 1 | trimspace }} +{{ else }} +{{ .Description | trimspace }} +{{- end }} +{{ if .HasExample -}} + +## Example Usage + +{{ tffile (printf "%s%s%s" "internal/examples/resources/" .Name "/resource.tf") }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "%s%s%s" "internal/examples/resources/" .Name "/import.sh") }} +{{- end }} + +## Generating Raw Data of an icon +To generate in Linux/Mac terminal use the command: +``` +base64 fileName.ico +``` +To generate in Powershell: +``` +[System.Convert]::ToBase64String(Get-Content fileName.ico -Encoding Byte) +``` \ No newline at end of file