From f7b8e0059f3554cd0e50c7495b5289045233f3c0 Mon Sep 17 00:00:00 2001 From: "Sourav Samanta[Sourav.Samanta]" Date: Wed, 23 Oct 2024 15:48:19 -0400 Subject: [PATCH] release-v1.0.6 --- docs/data-sources/wem_configuration_set.md | 37 + docs/resources/delivery_group.md | 3 +- docs/resources/desktop_icon.md | 41 + docs/resources/policy_set.md | 46 +- docs/resources/wem_configuration_set.md | 47 ++ docs/resources/wem_directory_object.md | 49 ++ go.mod | 10 +- go.sum | 20 +- .../delivery_group/delivery_group_resource.go | 6 +- .../delivery_group_resource_model.go | 15 +- .../delivery_group/delivery_group_utils.go | 15 +- .../desktop_icon/desktop_icon_resource.go | 227 +++++ .../desktop_icon_resource_model.go | 72 ++ .../daas/machine_catalog/machine_config.go | 4 + internal/daas/policies/policy_filter_model.go | 555 +++++++++++++ internal/daas/policies/policy_set_resource.go | 773 +++++++++--------- internal/daas/policies/policy_set_resource.md | 14 +- .../policies/policy_set_resource_model.go | 364 +-------- internal/daas/policies/policy_set_utils.go | 634 ++++++++++++++ internal/daas/tags/tag_resource.go | 5 + .../data-source.tf | 9 + .../resources/citrix_desktop_icon/import.sh | 2 + .../resources/citrix_desktop_icon/resource.tf | 4 + .../resources/citrix_policy_set/resource.tf | 1 - .../citrix_wem_configuration_set/import.sh | 2 + .../citrix_wem_configuration_set/resource.tf | 4 + .../citrix_wem_directory_object/import.sh | 2 + .../citrix_wem_directory_object/resource.tf | 5 + internal/provider/provider.go | 29 + internal/test/azure_mcs_suite_test.go | 99 ++- internal/test/delivery_group_test.go | 4 +- internal/test/policy_set_resource_test.go | 29 +- .../wem_directory_object_resource_test.go | 89 ++ .../test/wem_site_service_data_source_test.go | 82 ++ .../test/wem_site_service_resource_test.go | 92 +++ internal/util/common.go | 7 +- .../also_requires_on_values_validator.go | 4 +- ...lso_requires_one_of_on_values_validator.go | 180 ++++ ...one_of_on_values_validator_example_test.go | 66 ++ .../conflicts_with_on_values_validator.go | 95 +++ ...s_with_on_values_validator_example_test.go | 58 ++ .../wem_directory_object_resource.go | 268 ++++++ .../wem_directory_object_resource_model.go | 73 ++ .../wem_directory_object_utils.go | 59 ++ .../wem_site/wem_site_service_data_source.go | 120 +++ .../wem_site_service_data_source_model.go | 55 ++ .../wem/wem_site/wem_site_service_resource.go | 240 ++++++ .../wem_site_service_resource_model.go | 66 ++ .../wem/wem_site/wem_site_service_utils.go | 66 ++ scripts/onboarding-helper/README.md | 2 +- .../terraform-onboarding.ps1 | 123 ++- scripts/onboarding-helper/terraform.tf | 2 +- settings.cloud.example.json | 11 +- 53 files changed, 4031 insertions(+), 854 deletions(-) create mode 100644 docs/data-sources/wem_configuration_set.md create mode 100644 docs/resources/desktop_icon.md create mode 100644 docs/resources/wem_configuration_set.md create mode 100644 docs/resources/wem_directory_object.md create mode 100644 internal/daas/desktop_icon/desktop_icon_resource.go create mode 100644 internal/daas/desktop_icon/desktop_icon_resource_model.go create mode 100644 internal/daas/policies/policy_filter_model.go create mode 100644 internal/daas/policies/policy_set_utils.go create mode 100644 internal/examples/data-sources/citrix_wem_configuration_set/data-source.tf create mode 100644 internal/examples/resources/citrix_desktop_icon/import.sh create mode 100644 internal/examples/resources/citrix_desktop_icon/resource.tf create mode 100644 internal/examples/resources/citrix_wem_configuration_set/import.sh create mode 100644 internal/examples/resources/citrix_wem_configuration_set/resource.tf create mode 100644 internal/examples/resources/citrix_wem_directory_object/import.sh create mode 100644 internal/examples/resources/citrix_wem_directory_object/resource.tf create mode 100644 internal/test/wem_directory_object_resource_test.go create mode 100644 internal/test/wem_site_service_data_source_test.go create mode 100644 internal/test/wem_site_service_resource_test.go create mode 100644 internal/validators/also_requires_one_of_on_values_validator.go create mode 100644 internal/validators/also_requires_one_of_on_values_validator_example_test.go create mode 100644 internal/validators/conflicts_with_on_values_validator.go create mode 100644 internal/validators/conflicts_with_on_values_validator_example_test.go create mode 100644 internal/wem/wem_machine_ad_object/wem_directory_object_resource.go create mode 100644 internal/wem/wem_machine_ad_object/wem_directory_object_resource_model.go create mode 100644 internal/wem/wem_machine_ad_object/wem_directory_object_utils.go create mode 100644 internal/wem/wem_site/wem_site_service_data_source.go create mode 100644 internal/wem/wem_site/wem_site_service_data_source_model.go create mode 100644 internal/wem/wem_site/wem_site_service_resource.go create mode 100644 internal/wem/wem_site/wem_site_service_resource_model.go create mode 100644 internal/wem/wem_site/wem_site_service_utils.go diff --git a/docs/data-sources/wem_configuration_set.md b/docs/data-sources/wem_configuration_set.md new file mode 100644 index 0000000..aa49c1a --- /dev/null +++ b/docs/data-sources/wem_configuration_set.md @@ -0,0 +1,37 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_wem_configuration_set Data Source - citrix" +subcategory: "WEM" +description: |- + Data source to get details regarding a specific configuration set. +--- + +# citrix_wem_configuration_set (Data Source) + +Data source to get details regarding a specific configuration set. + +## Example Usage + +```terraform +# Get WEM configuration set by id +data "citrix_wem_configuration_set" "test_wem_configuration_set_by_id" { + id = "1" +} + +# Get WEM configuration set by name +data "citrix_wem_configuration_set" "test_wem_configuration_set_by_name" { + name = "Default Site" +} +``` + + +## Schema + +### Optional + +- `id` (String) ID of the WEM configuration set. +- `name` (String) Name of the configuration set. + +### Read-Only + +- `description` (String) Description of the configuration set. \ No newline at end of file diff --git a/docs/resources/delivery_group.md b/docs/resources/delivery_group.md index 572d001..65ba165 100644 --- a/docs/resources/delivery_group.md +++ b/docs/resources/delivery_group.md @@ -188,7 +188,7 @@ resource "citrix_delivery_group" "example-delivery-group" { - `app_protection` (Attributes) App Protection, an add-on feature for the Citrix Workspace app, provides enhanced security for Citrix published apps and desktops. The feature provides anti-keylogging and anti-screen capture capabilities for client sessions, helping protect data from keyloggers and screen scrapers. ~> **Please Note** Before using the feature, make sure that these [requirements](https://docs.citrix.com/en-us/citrix-workspace-app/app-protection.html#system-requirements) are met. (see [below for nested schema](#nestedatt--app_protection)) -- `associated_machine_catalogs` (Attributes List) Machine catalogs from which to assign machines to the newly created delivery group. (see [below for nested schema](#nestedatt--associated_machine_catalogs)) +- `associated_machine_catalogs` (Attributes Set) Machine catalogs from which to assign machines to the newly created delivery group. (see [below for nested schema](#nestedatt--associated_machine_catalogs)) - `autoscale_settings` (Attributes) The power management settings governing the machine(s) in the delivery group. (see [below for nested schema](#nestedatt--autoscale_settings)) - `custom_access_policies` (Attributes List) Custom Access Policies for the delivery group. To manage built-in access policies use the `default_access_policies` instead. (see [below for nested schema](#nestedatt--custom_access_policies)) - `default_access_policies` (Attributes List) Manage built-in Access Policies for the delivery group. These are the Citrix Gateway Connections (via Access Gateway) and Non-Citrix Gateway Connections (not via Access Gateway) access policies. @@ -196,6 +196,7 @@ resource "citrix_delivery_group" "example-delivery-group" { ~> **Please Note** Default Access Policies can only be modified; they cannot be deleted. If using this property, both default policies have to be specified. -> **Note** Use `Citrix Gateway connections` as the name for the default policy that is Via Access Gateway and `Non-Citrix Gateway connections` as the name for the default policy that is Not Via Access Gateway. (see [below for nested schema](#nestedatt--default_access_policies)) +- `default_desktop_icon` (String) The id of the icon to be used as the default icon for the desktops in the delivery group. - `delivery_group_folder_path` (String) The path of the folder in which the delivery group is located. - `description` (String) Description of the delivery group. - `desktops` (Attributes List) A list of Desktop resources to publish on the delivery group. Only 1 desktop can be added to a Remote PC Delivery Group. (see [below for nested schema](#nestedatt--desktops)) diff --git a/docs/resources/desktop_icon.md b/docs/resources/desktop_icon.md new file mode 100644 index 0000000..54ca4c7 --- /dev/null +++ b/docs/resources/desktop_icon.md @@ -0,0 +1,41 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_desktop_icon Resource - citrix" +subcategory: "CVAD" +description: |- + Resource for managing desktop icons. +--- + +# citrix_desktop_icon (Resource) + +Resource for managing desktop icons. + +## Example Usage + +```terraform +resource "citrix_desktop_icon" "example-desktop-icon" { + raw_data = filebase64("path/to/desktopicon.ico") +} +# Use filebase64 to encode a file's content in base64 format. +``` + + +## Schema + +### Optional + +- `file_path` (String) Path to the icon file. Exactly one of `raw_data` and `file_path` is required. +- `raw_data` (String) Prepare an icon in ICO format and convert its binary raw data to base64 encoding. Use the base64 encoded string as the value of this attribute. Exactly one of `raw_data` and `file_path` is required. + +### Read-Only + +- `id` (String) GUID identifier of the desktop icon. + +## Import + +Import is supported using the following syntax: + +```shell +# Desktop icon can be imported by specifying the GUID +terraform import citrix_desktop_icon.example-desktop-icon 4cec0568-1c91-407f-a32e-cc487822d0a0 +``` \ No newline at end of file diff --git a/docs/resources/policy_set.md b/docs/resources/policy_set.md index b11573d..5af4cde 100644 --- a/docs/resources/policy_set.md +++ b/docs/resources/policy_set.md @@ -44,7 +44,6 @@ resource "citrix_policy_set" "example-policy-set" { }, ] branch_repeater_filter = { - enabled = true allowed = true }, client_ip_filters = [ @@ -140,7 +139,7 @@ Required: Optional: - `access_control_filters` (Attributes Set) Set of Access control policy filters. (see [below for nested schema](#nestedatt--policies--access_control_filters)) -- `branch_repeater_filter` (Attributes) Set of policy filters. (see [below for nested schema](#nestedatt--policies--branch_repeater_filter)) +- `branch_repeater_filter` (Attributes) Definition of branch repeater policy filter. (see [below for nested schema](#nestedatt--policies--branch_repeater_filter)) - `client_ip_filters` (Attributes Set) Set of Client ip policy filters. (see [below for nested schema](#nestedatt--policies--client_ip_filters)) - `client_name_filters` (Attributes Set) Set of Client name policy filters. (see [below for nested schema](#nestedatt--policies--client_name_filters)) - `delivery_group_filters` (Attributes Set) Set of Delivery group policy filters. (see [below for nested schema](#nestedatt--policies--delivery_group_filters)) @@ -150,12 +149,16 @@ Optional: - `tag_filters` (Attributes Set) Set of Tag policy filters. (see [below for nested schema](#nestedatt--policies--tag_filters)) - `user_filters` (Attributes Set) Set of User policy filters. (see [below for nested schema](#nestedatt--policies--user_filters)) +Read-Only: + +- `id` (String) Id of the policy. + ### Nested Schema for `policies.policy_settings` Required: -- `name` (String) Name of the policy setting name. +- `name` (String) Name of the policy setting. - `use_default` (Boolean) Indicate whether using default value for the policy setting. Optional: @@ -175,6 +178,10 @@ Required: - `enabled` (Boolean) Indicate whether the filter is being enabled. - `gateway` (String) Gateway for the policy filter. +Read-Only: + +- `id` (String) Id of the policy filter. + ### Nested Schema for `policies.branch_repeater_filter` @@ -182,7 +189,10 @@ Required: Required: - `allowed` (Boolean) Indicate the filtered policy is allowed or denied if the filter condition is met. -- `enabled` (Boolean) Indicate whether the filter is being enabled. + +Read-Only: + +- `id` (String) Id of the branch repeater policy filter. @@ -194,6 +204,10 @@ Required: - `enabled` (Boolean) Indicate whether the filter is being enabled. - `ip_address` (String) IP Address of the client to be filtered. +Read-Only: + +- `id` (String) Id of the client ip policy filter. + ### Nested Schema for `policies.client_name_filters` @@ -204,6 +218,10 @@ Required: - `client_name` (String) Name of the client to be filtered. - `enabled` (Boolean) Indicate whether the filter is being enabled. +Read-Only: + +- `id` (String) Id of the client name policy filter. + ### Nested Schema for `policies.delivery_group_filters` @@ -214,6 +232,10 @@ Required: - `delivery_group_id` (String) Id of the delivery group to be filtered. - `enabled` (Boolean) Indicate whether the filter is being enabled. +Read-Only: + +- `id` (String) Id of the delivery group policy filter. + ### Nested Schema for `policies.delivery_group_type_filters` @@ -224,6 +246,10 @@ Required: - `delivery_group_type` (String) Type of the delivery groups to be filtered. - `enabled` (Boolean) Indicate whether the filter is being enabled. +Read-Only: + +- `id` (String) Id of the delivery group type policy filter. + ### Nested Schema for `policies.ou_filters` @@ -234,6 +260,10 @@ Required: - `enabled` (Boolean) Indicate whether the filter is being enabled. - `ou` (String) Organizational Unit to be filtered. +Read-Only: + +- `id` (String) Id of the organizational unit policy filter. + ### Nested Schema for `policies.tag_filters` @@ -244,6 +274,10 @@ Required: - `enabled` (Boolean) Indicate whether the filter is being enabled. - `tag` (String) Tag to be filtered. +Read-Only: + +- `id` (String) Id of the tag policy filter. + ### Nested Schema for `policies.user_filters` @@ -254,6 +288,10 @@ Required: - `enabled` (Boolean) Indicate whether the filter is being enabled. - `sid` (String) SID of the user or user group to be filtered. +Read-Only: + +- `id` (String) Id of the user policy filter. + ## Import Import is supported using the following syntax: diff --git a/docs/resources/wem_configuration_set.md b/docs/resources/wem_configuration_set.md new file mode 100644 index 0000000..e35c3bf --- /dev/null +++ b/docs/resources/wem_configuration_set.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_wem_configuration_set Resource - citrix" +subcategory: "WEM" +description: |- + Manages configuration sets within a WEM deployment. + ~> Disclaimer This feature is supported for Citrix Cloud customers, and will be made available for On-Premises soon. +--- + +# citrix_wem_configuration_set (Resource) + +Manages configuration sets within a WEM deployment. + +~> **Disclaimer** This feature is supported for Citrix Cloud customers, and will be made available for On-Premises soon. + +## Example Usage + +```terraform +resource "citrix_wem_configuration_set" "example-config-set"{ + name = "example config set" + description = "example WEM configuration set" +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the configuration set. WEM Site Name should be unique. + +### Optional + +- `description` (String) Description of the configuration set. Default value is empty string. + +### Read-Only + +- `id` (String) Identifier of the configuration set. + +## Import + +Import is supported using the following syntax: + +```shell +# WEM Configuration Set can be imported by specifying the ID +terraform import citrix_wem_configuration_set.example-config-set 1234 +``` \ No newline at end of file diff --git a/docs/resources/wem_directory_object.md b/docs/resources/wem_directory_object.md new file mode 100644 index 0000000..d254f4f --- /dev/null +++ b/docs/resources/wem_directory_object.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_wem_directory_object Resource - citrix" +subcategory: "WEM" +description: |- + Manages machine-level AD objects within a WEM deployment. + ~> Disclaimer This feature is supported for Citrix Cloud customers, and will be made available for On-Premises soon. + ~> Warning Having more than one Directory Object with the same Catalog ID is not allowed. +--- + +# citrix_wem_directory_object (Resource) + +Manages machine-level AD objects within a WEM deployment. + +~> **Disclaimer** This feature is supported for Citrix Cloud customers, and will be made available for On-Premises soon. + +~> **Warning** Having more than one Directory Object with the same Catalog ID is not allowed. + +## Example Usage + +```terraform +resource "citrix_wem_directory_object" "example-directory-object" { + configuration_set_id = citrix_wem_configuration_set.example-config-set.id + machine_catalog_id = citrix_machine_catalog.example-machine-catalog.id + enabled = true +} +``` + + +## Schema + +### Required + +- `configuration_set_id` (Number) Identifier of the site to which the machine-level AD object belongs. +- `enabled` (Boolean) Indicates whether the machine-level AD object is enabled. +- `machine_catalog_id` (String) GUID identifier of the machine catalog. + +### Read-Only + +- `id` (String) Identifier of the directory object. + +## Import + +Import is supported using the following syntax: + +```shell +# WEM Directory Object can be imported by specifying the ID +terraform import citrix_wem_directory_object.example-directory-object 1234 +``` \ No newline at end of file diff --git a/go.mod b/go.mod index d1f65ea..6b5fabc 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.22.0 toolchain go1.23.1 require ( - github.com/citrix/citrix-daas-rest-go v1.0.6 + github.com/citrix/citrix-daas-rest-go v1.0.7 github.com/google/uuid v1.6.0 github.com/hashicorp/go-azure-helpers v0.71.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-docs v0.14.1 github.com/hashicorp/terraform-plugin-framework v1.12.0 - github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.14.0 github.com/hashicorp/terraform-plugin-go v0.24.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.8.0 @@ -29,7 +29,7 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -37,7 +37,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-plugin v1.6.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.6.4 // indirect @@ -75,7 +75,7 @@ require ( golang.org/x/text v0.19.0 // indirect golang.org/x/tools v0.26.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/go.sum b/go.sum index a14bc8c..e757f69 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/citrix/citrix-daas-rest-go v1.0.6 h1:yUHs6jWmlOB0DReJyvAoxG7oKPi2TYXAituxBbOOBLE= -github.com/citrix/citrix-daas-rest-go v1.0.6/go.mod h1:4Me0VHpyxMYfPwpU2XWV0jOE2Jdz8MHNpge3MLD5B2E= +github.com/citrix/citrix-daas-rest-go v1.0.7 h1:KMflWuN6dR5ZqFVWiksqFRtZD5IDrVoq6U2CtPH/5Dk= +github.com/citrix/citrix-daas-rest-go v1.0.7/go.mod h1:4Me0VHpyxMYfPwpU2XWV0jOE2Jdz8MHNpge3MLD5B2E= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= @@ -37,8 +37,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -81,8 +81,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= -github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -102,8 +102,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.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ= github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE= -github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E= -github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo= +github.com/hashicorp/terraform-plugin-framework-validators v0.14.0 h1:3PCn9iyzdVOgHYOBmncpSSOxjQhCTYmc+PGvbdlqSaI= +github.com/hashicorp/terraform-plugin-framework-validators v0.14.0/go.mod h1:LwDKNdzxrDY/mHBrlC6aYfE2fQ3Dk3gaJD64vNiXvo4= github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -269,8 +269,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/internal/daas/delivery_group/delivery_group_resource.go b/internal/daas/delivery_group/delivery_group_resource.go index f13b674..d1ea27c 100644 --- a/internal/daas/delivery_group/delivery_group_resource.go +++ b/internal/daas/delivery_group/delivery_group_resource.go @@ -67,7 +67,7 @@ func (r *deliveryGroupResource) Create(ctx context.Context, req resource.CreateR } // Get machine catalogs and verify all of them have the same session support - associatedMachineCatalogs := util.ObjectListToTypedArray[DeliveryGroupMachineCatalogModel](ctx, &resp.Diagnostics, plan.AssociatedMachineCatalogs) + associatedMachineCatalogs := util.ObjectSetToTypedArray[DeliveryGroupMachineCatalogModel](ctx, &resp.Diagnostics, plan.AssociatedMachineCatalogs) associatedMachineCatalogProperties, err := validateAndReturnMachineCatalogSessionSupport(ctx, *r.client, &resp.Diagnostics, associatedMachineCatalogs, true) if err != nil { @@ -642,7 +642,7 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod return } - associatedMachineCatalogs := util.ObjectListToTypedArray[DeliveryGroupMachineCatalogModel](ctx, &resp.Diagnostics, plan.AssociatedMachineCatalogs) + 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 @@ -655,7 +655,6 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod "Error "+operation+" Delivery Group "+plan.Name.ValueString(), "Error message: "+errMsg, ) - return } @@ -664,7 +663,6 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod "Error "+operation+" Delivery Group "+plan.Name.ValueString(), "Autoscale settings can only be configured if associated machine catalogs are power managed.", ) - return } diff --git a/internal/daas/delivery_group/delivery_group_resource_model.go b/internal/daas/delivery_group/delivery_group_resource_model.go index 358ab04..afc5732 100644 --- a/internal/daas/delivery_group/delivery_group_resource_model.go +++ b/internal/daas/delivery_group/delivery_group_resource_model.go @@ -912,7 +912,7 @@ type DeliveryGroupResourceModel struct { RestrictedAccessUsers types.Object `tfsdk:"restricted_access_users"` AllowAnonymousAccess types.Bool `tfsdk:"allow_anonymous_access"` Desktops types.List `tfsdk:"desktops"` // List[DeliveryGroupDesktop] - AssociatedMachineCatalogs types.List `tfsdk:"associated_machine_catalogs"` // List[DeliveryGroupMachineCatalogModel] + AssociatedMachineCatalogs types.Set `tfsdk:"associated_machine_catalogs"` // List[DeliveryGroupMachineCatalogModel] AutoscaleSettings types.Object `tfsdk:"autoscale_settings"` // DeliveryGroupPowerManagementSettings RebootSchedules types.List `tfsdk:"reboot_schedules"` // List[DeliveryGroupRebootSchedule] TotalMachines types.Int64 `tfsdk:"total_machines"` @@ -930,6 +930,7 @@ type DeliveryGroupResourceModel struct { Tenants types.Set `tfsdk:"tenants"` // Set[String] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairmodel] Tags types.Set `tfsdk:"tags"` // Set[string] + DefaultDesktopIcon types.String `tfsdk:"default_desktop_icon"` } func (DeliveryGroupResourceModel) GetSchema() schema.Schema { @@ -1003,7 +1004,7 @@ func (DeliveryGroupResourceModel) GetSchema() schema.Schema { listvalidator.SizeAtLeast(1), }, }, - "associated_machine_catalogs": schema.ListNestedAttribute{ + "associated_machine_catalogs": schema.SetNestedAttribute{ Description: "Machine catalogs from which to assign machines to the newly created delivery group.", Optional: true, NestedObject: DeliveryGroupMachineCatalogModel{}.GetSchema(), @@ -1134,6 +1135,12 @@ func (DeliveryGroupResourceModel) GetSchema() schema.Schema { ), }, }, + "default_desktop_icon": schema.StringAttribute{ + Description: "The id of the icon to be used as the default icon for the desktops in the delivery group.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("1"), + }, }, } } @@ -1161,7 +1168,7 @@ func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, d r.MinimumFunctionalLevel = types.StringValue(string(minimumFunctionalLevel)) parentList := []string{} - associatedCatalogs := util.ObjectListToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, r.AssociatedMachineCatalogs) + associatedCatalogs := util.ObjectSetToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, r.AssociatedMachineCatalogs) for _, machineCatalog := range associatedCatalogs { parentList = append(parentList, machineCatalog.MachineCatalog.ValueString()) } @@ -1212,6 +1219,8 @@ func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, d r.DeliveryGroupFolderPath = types.StringNull() } + r.DefaultDesktopIcon = types.StringValue(deliveryGroup.GetDefaultDesktopIconId()) + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, deliveryGroup.GetTenants()) effectiveMetadata := util.GetEffectiveMetadata(util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, diagnostics, r.Metadata), deliveryGroup.GetMetadata()) diff --git a/internal/daas/delivery_group/delivery_group_utils.go b/internal/daas/delivery_group/delivery_group_utils.go index 1770386..e1c7271 100644 --- a/internal/daas/delivery_group/delivery_group_utils.go +++ b/internal/daas/delivery_group/delivery_group_utils.go @@ -353,7 +353,7 @@ func addRemoveMachinesFromDeliveryGroup(ctx context.Context, client *citrixdaasc existingAssociatedMachineCatalogsMap := createExistingCatalogsAndMachinesMap(deliveryGroupMachines) requestedAssociatedMachineCatalogsMap := map[string]bool{} - for _, associatedMachineCatalog := range util.ObjectListToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, plan.AssociatedMachineCatalogs) { + for _, associatedMachineCatalog := range util.ObjectSetToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, plan.AssociatedMachineCatalogs) { requestedAssociatedMachineCatalogsMap[associatedMachineCatalog.MachineCatalog.ValueString()] = true @@ -653,9 +653,10 @@ func getRequestModelForDeliveryGroupCreate(ctx context.Context, diagnostics *dia body.SetName(plan.Name.ValueString()) body.SetDescription(plan.Description.ValueString()) body.SetRebootSchedules(deliveryGroupRebootScheduleArray) + body.SetDefaultDesktopIcon(plan.DefaultDesktopIcon.ValueString()) if !plan.AssociatedMachineCatalogs.IsNull() && len(plan.AssociatedMachineCatalogs.Elements()) > 0 { - deliveryGroupMachineCatalogsArray := getDeliveryGroupAddMachinesRequest(util.ObjectListToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, plan.AssociatedMachineCatalogs)) + deliveryGroupMachineCatalogsArray := getDeliveryGroupAddMachinesRequest(util.ObjectSetToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, plan.AssociatedMachineCatalogs)) body.SetMachineCatalogs(deliveryGroupMachineCatalogsArray) } @@ -716,6 +717,9 @@ func getRequestModelForDeliveryGroupCreate(ctx context.Context, diagnostics *dia body.SetReuseMachinesWithoutShutdownInOutage(plan.MakeResourcesAvailableInLHC.ValueBool()) } + // Set the default value to false to handle cases where the API incorrectly returns auto scale enabled as true, even when auto scale settings are not provided. + body.SetAutoScaleEnabled(false) + if !plan.AutoscaleSettings.IsNull() { autoscale := util.ObjectValueToTypedObject[DeliveryGroupPowerManagementSettings](ctx, diagnostics, plan.AutoscaleSettings) body.SetAutoScaleEnabled(autoscale.AutoscaleEnabled.ValueBool()) @@ -912,6 +916,7 @@ func getRequestModelForDeliveryGroupUpdate(ctx context.Context, diagnostics *dia editDeliveryGroupRequestBody.SetDesktops(deliveryGroupDesktopsArray) editDeliveryGroupRequestBody.SetRebootSchedules(deliveryGroupRebootScheduleArray) editDeliveryGroupRequestBody.SetAdvancedAccessPolicy(advancedAccessPolicies) + editDeliveryGroupRequestBody.SetDefaultDesktopIcon(plan.DefaultDesktopIcon.ValueString()) if !plan.Scopes.IsNull() { plannedScopes := util.StringSetToStringArray(ctx, diagnostics, plan.Scopes) @@ -1081,7 +1086,7 @@ func parseDeliveryGroupRebootScheduleToClientModel(ctx context.Context, diags *d rebootScheduleRequest.SetRestrictToTag(rebootSchedule.RestrictToTag.ValueString()) } - rebootScheduleRequest.SetIgnoreMaintenanceMode(true) + rebootScheduleRequest.SetIgnoreMaintenanceMode(rebootSchedule.IgnoreMaintenanceMode.ValueBool()) rebootScheduleRequest.SetEnabled(rebootSchedule.RebootScheduleEnabled.ValueBool()) rebootScheduleRequest.SetFrequency(getFrequencyActionValue(rebootSchedule.Frequency.ValueString())) rebootScheduleRequest.SetFrequencyFactor(int32(rebootSchedule.FrequencyFactor.ValueInt64())) @@ -1448,7 +1453,7 @@ func (r DeliveryGroupResourceModel) updatePlanWithAssociatedCatalogs(ctx context associatedMachineCatalogs = append(associatedMachineCatalogs, deliveryGroupMachineCatalogModel) } - r.AssociatedMachineCatalogs = util.TypedArrayToObjectList[DeliveryGroupMachineCatalogModel](ctx, diags, associatedMachineCatalogs) + r.AssociatedMachineCatalogs = util.TypedArrayToObjectSet[DeliveryGroupMachineCatalogModel](ctx, diags, associatedMachineCatalogs) return r } @@ -1647,7 +1652,7 @@ func (r DeliveryGroupResourceModel) updatePlanWithRestrictedAccessUsers(ctx cont } func (r DeliveryGroupResourceModel) updatePlanWithAutoscaleSettings(ctx context.Context, diags *diag.Diagnostics, deliveryGroup *citrixorchestration.DeliveryGroupDetailResponseModel, dgPowerTimeSchemes *citrixorchestration.PowerTimeSchemeResponseModelCollection) DeliveryGroupResourceModel { - if r.AutoscaleSettings.IsNull() { + if !deliveryGroup.GetAutoScaleEnabled() && r.AutoscaleSettings.IsNull() { return r } diff --git a/internal/daas/desktop_icon/desktop_icon_resource.go b/internal/daas/desktop_icon/desktop_icon_resource.go new file mode 100644 index 0000000..40cb316 --- /dev/null +++ b/internal/daas/desktop_icon/desktop_icon_resource.go @@ -0,0 +1,227 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package desktop_icon + +import ( + "context" + "encoding/base64" + "net/http" + "os" + "strconv" + "strings" + + "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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &desktopIconResource{} + _ resource.ResourceWithConfigure = &desktopIconResource{} + _ resource.ResourceWithImportState = &desktopIconResource{} + _ resource.ResourceWithValidateConfig = &desktopIconResource{} + _ resource.ResourceWithModifyPlan = &desktopIconResource{} +) + +// NewDesktopIconResource is a helper function to simplify the provider implementation. +func NewDesktopIconResource() resource.Resource { + return &desktopIconResource{} +} + +// desktopIconResource is the resource implementation. +type desktopIconResource struct { + client *citrixdaasclient.CitrixDaasClient +} + +// Metadata returns the data source type name. +func (r *desktopIconResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_desktop_icon" +} + +// Configure adds the provider configured client to the data source. +func (r *desktopIconResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) +} + +// Schema returns the resource schema. +func (r *desktopIconResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = DesktopIconResourceModel{}.GetSchema() +} + +func (r *desktopIconResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan DesktopIconResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + var createDesktopIconRequest citrixorchestration.AddIconRequestModel + if !plan.RawData.IsNull() { + createDesktopIconRequest.SetRawData(plan.RawData.ValueString()) + } else { + bytes, err := os.ReadFile(plan.FilePath.ValueString()) + if err != nil { + if os.IsPermission(err) { + resp.Diagnostics.AddError( + "Error reading icon file", + "Permission denied to read icon file: "+plan.FilePath.ValueString()+ + "\nError message: "+err.Error(), + ) + return + } + resp.Diagnostics.AddError( + "Error reading file", + err.Error(), + ) + return + } + base64String := base64.StdEncoding.EncodeToString(bytes) + createDesktopIconRequest.SetRawData(base64String) + } + + // Set default icon format to 32x32x24 png format + createDesktopIconRequest.SetIconFormat("image/png;32x32x24") + + // Create new desktop icon + addDesktopIconRequest := r.client.ApiClient.IconsAPIsDAAS.IconsAddIcon(ctx) + addDesktopIconRequest = addDesktopIconRequest.AddIconRequestModel(createDesktopIconRequest) + + desktopIcon, httpResp, err := citrixdaasclient.AddRequestData(addDesktopIconRequest, r.client).Execute() + if err != nil { + resp.Diagnostics.AddError( + "Error creating Desktop Icon", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + // Map response body to schema and populate Computed attribute values + plan = plan.RefreshPropertyValues(desktopIcon) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +func (r *desktopIconResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from state + var state DesktopIconResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + desktopIconId := state.Id.ValueString() + // Get refreshed desktop icon properties from Orchestration + desktopIcon, err := readDesktopIcon(ctx, r.client, resp, desktopIconId) + if err != nil { + return + } + + state = state.RefreshPropertyValues(desktopIcon) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +func (r *desktopIconResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + resp.Diagnostics.AddError("Unsupported Operation", "Update is not supported for this resource") +} + +func (r *desktopIconResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from state + var state DesktopIconResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + desktopIconId, err := strconv.ParseInt(state.Id.ValueString(), 10, 32) + if err != nil { + resp.Diagnostics.AddError("Error deleting Icon", "Invalid Icon Id") + return + } + + deleteDesktopIconRequest := r.client.ApiClient.IconsAPIsDAAS.IconsRemoveIcon(ctx, int32(desktopIconId)) + httpResp, err := citrixdaasclient.AddRequestData(deleteDesktopIconRequest, r.client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + resp.Diagnostics.AddError( + "Error deleting Desktop Icon "+state.Id.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } +} + +func (r *desktopIconResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to id attribute + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func readDesktopIcon(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, desktopIconId string) (*citrixorchestration.IconResponseModel, error) { + getDesktopIconRequest := client.ApiClient.IconsAPIsDAAS.IconsGetIcon(ctx, desktopIconId) + desktopIcon, _, err := util.ReadResource[*citrixorchestration.IconResponseModel](getDesktopIconRequest, ctx, client, resp, "Desktop Icon", desktopIconId) + return desktopIcon, err +} + +func (r *desktopIconResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + var data DesktopIconResourceModel + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if !data.FilePath.IsNull() && !strings.HasSuffix(strings.ToLower(data.FilePath.ValueString()), ".ico") { + resp.Diagnostics.AddError( + "Invalid file format", + "Only `.ico` icon file format is supported", + ) + return + } + + schemaType, configValuesForSchema := util.GetConfigValuesForSchema(ctx, &resp.Diagnostics, &data) + tflog.Debug(ctx, "Validate Config - "+schemaType, configValuesForSchema) +} + +func (r *desktopIconResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + if r.client != nil && r.client.ApiClient == nil { + resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) + return + } +} diff --git a/internal/daas/desktop_icon/desktop_icon_resource_model.go b/internal/daas/desktop_icon/desktop_icon_resource_model.go new file mode 100644 index 0000000..fcca2df --- /dev/null +++ b/internal/daas/desktop_icon/desktop_icon_resource_model.go @@ -0,0 +1,72 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package desktop_icon + +import ( + citrixorchestration "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// DesktopIconResourceModel maps the resource schema data. +type DesktopIconResourceModel struct { + Id types.String `tfsdk:"id"` + RawData types.String `tfsdk:"raw_data"` + FilePath types.String `tfsdk:"file_path"` +} + +func (DesktopIconResourceModel) GetSchema() schema.Schema { + return schema.Schema{ + Description: "CVAD --- Resource for managing desktop icons.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "GUID identifier of the desktop icon.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "raw_data": schema.StringAttribute{ + Description: "Prepare an icon in ICO format and convert its binary raw data to base64 encoding. Use the base64 encoded string as the value of this attribute. Exactly one of `raw_data` and `file_path` is required.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf( + path.MatchRoot("file_path"), + ), + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "file_path": schema.StringAttribute{ + Description: "Path to the icon file. Exactly one of `raw_data` and `file_path` is required.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (DesktopIconResourceModel) GetAttributes() map[string]schema.Attribute { + return DesktopIconResourceModel{}.GetSchema().Attributes +} + +func (r DesktopIconResourceModel) RefreshPropertyValues(desktop *citrixorchestration.IconResponseModel) DesktopIconResourceModel { + // Overwrite desktop folder with refreshed state + r.Id = types.StringValue(desktop.GetId()) + if r.FilePath.IsNull() { + r.RawData = types.StringValue(desktop.GetRawData()) + } + return r +} diff --git a/internal/daas/machine_catalog/machine_config.go b/internal/daas/machine_catalog/machine_config.go index fe41758..6b79f94 100644 --- a/internal/daas/machine_catalog/machine_config.go +++ b/internal/daas/machine_catalog/machine_config.go @@ -1259,6 +1259,10 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno if !isUseEphemeralOsDiskSet { mc.StorageType = types.StringValue(stringPair.GetValue()) } + case "StorageAccountType": + if !isUseEphemeralOsDiskSet { + mc.StorageType = types.StringValue(stringPair.GetValue()) + } case "UseManagedDisks": mc.UseManagedDisks = util.StringToTypeBool(stringPair.GetValue()) case "ResourceGroups": diff --git a/internal/daas/policies/policy_filter_model.go b/internal/daas/policies/policy_filter_model.go new file mode 100644 index 0000000..ed1242b --- /dev/null +++ b/internal/daas/policies/policy_filter_model.go @@ -0,0 +1,555 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package policies + +import ( + "encoding/json" + "regexp" + + citrixorchestration "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/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ PolicyFilterInterface = AccessControlFilterModel{} + _ PolicyFilterInterface = BranchRepeaterFilterModel{} + _ PolicyFilterInterface = ClientIPFilterModel{} + _ PolicyFilterInterface = ClientNameFilterModel{} + _ PolicyFilterInterface = DeliveryGroupFilterModel{} + _ PolicyFilterInterface = DeliveryGroupTypeFilterModel{} + _ PolicyFilterInterface = OuFilterModel{} + _ PolicyFilterInterface = UserFilterModel{} + _ PolicyFilterInterface = TagFilterModel{} +) + +type PolicyFilterInterface interface { + GetId() string + GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) +} + +type AccessControlFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + Connection types.String `tfsdk:"connection"` + Condition types.String `tfsdk:"condition"` + Gateway types.String `tfsdk:"gateway"` +} + +func (AccessControlFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "connection": schema.StringAttribute{ + Description: "Gateway connection for the policy filter.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{ + "WithAccessGateway", + "WithoutAccessGateway"}...), + }, + }, + "condition": schema.StringAttribute{ + Description: "Gateway condition for the policy filter.", + Required: true, + }, + "gateway": schema.StringAttribute{ + Description: "Gateway for the policy filter.", + Required: true, + }, + }, + } +} + +func (AccessControlFilterModel) GetAttributes() map[string]schema.Attribute { + return AccessControlFilterModel{}.GetSchema().Attributes +} + +func (filter AccessControlFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter AccessControlFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("AccessControl") + + policyFilterDataClientModel := PolicyFilterGatewayDataClientModel{ + Connection: filter.Connection.ValueString(), + Condition: filter.Condition.ValueString(), + Gateway: filter.Gateway.ValueString(), + } + + policyFilterDataJson, err := json.Marshal(policyFilterDataClientModel) + if err != nil { + diagnostics.AddError( + "Error constructing Access Control Policy Filter request.", + "An unexpected error occurred: "+err.Error(), + ) + return filterRequest, err + } + filterRequest.SetFilterData(string(policyFilterDataJson)) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + return filterRequest, nil +} + +type BranchRepeaterFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` +} + +func (BranchRepeaterFilterModel) GetSchema() schema.SingleNestedAttribute { + return schema.SingleNestedAttribute{ + Description: "Definition of branch repeater policy filter.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the branch repeater policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + }, + } +} + +func (BranchRepeaterFilterModel) GetAttributes() map[string]schema.Attribute { + return BranchRepeaterFilterModel{}.GetSchema().Attributes +} + +func (filter BranchRepeaterFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter BranchRepeaterFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + branchRepeaterFilterRequest := citrixorchestration.FilterRequest{} + branchRepeaterFilterRequest.SetFilterType("BranchRepeater") + branchRepeaterFilterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + branchRepeaterFilterRequest.SetIsEnabled(true) + branchRepeaterFilterRequest.SetFilterData("") + return branchRepeaterFilterRequest, nil +} + +type ClientIPFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + IpAddress types.String `tfsdk:"ip_address"` +} + +func (ClientIPFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the client ip policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "ip_address": schema.StringAttribute{ + Description: "IP Address of the client to be filtered.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.IPv4Regex), "must be a valid IPv4 address without protocol (http:// or https://) and port number"), + }, + }, + }, + } +} + +func (ClientIPFilterModel) GetAttributes() map[string]schema.Attribute { + return ClientIPFilterModel{}.GetSchema().Attributes +} + +func (filter ClientIPFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter ClientIPFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("ClientIP") + filterRequest.SetFilterData(filter.IpAddress.ValueString()) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + + return filterRequest, nil +} + +type ClientNameFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + ClientName types.String `tfsdk:"client_name"` +} + +func (ClientNameFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the client name policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "client_name": schema.StringAttribute{ + Description: "Name of the client to be filtered.", + Required: true, + }, + }, + } +} + +func (ClientNameFilterModel) GetAttributes() map[string]schema.Attribute { + return ClientNameFilterModel{}.GetSchema().Attributes +} + +func (filter ClientNameFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter ClientNameFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("ClientName") + filterRequest.SetFilterData(filter.ClientName.ValueString()) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + + return filterRequest, nil +} + +type DeliveryGroupFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + DeliveryGroupId types.String `tfsdk:"delivery_group_id"` +} + +func (DeliveryGroupFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the delivery group policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "delivery_group_id": schema.StringAttribute{ + Description: "Id of the delivery group to be filtered.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + }, + }, + }, + } +} + +func (DeliveryGroupFilterModel) GetAttributes() map[string]schema.Attribute { + return DeliveryGroupFilterModel{}.GetSchema().Attributes +} + +func (filter DeliveryGroupFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter DeliveryGroupFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("DesktopGroup") + + policyFilterDataClientModel := PolicyFilterUuidDataClientModel{ + Uuid: filter.DeliveryGroupId.ValueString(), + Server: serverValue, + } + + policyFilterDataJson, err := json.Marshal(policyFilterDataClientModel) + if err != nil { + diagnostics.AddError( + "Error adding Access Control Policy Filter to Policy Set. ", + "An unexpected error occurred: "+err.Error(), + ) + return filterRequest, err + } + + filterRequest.SetFilterData(string(policyFilterDataJson)) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + return filterRequest, nil +} + +type DeliveryGroupTypeFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + DeliveryGroupType types.String `tfsdk:"delivery_group_type"` +} + +func (DeliveryGroupTypeFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the delivery group type policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "delivery_group_type": schema.StringAttribute{ + Description: "Type of the delivery groups to be filtered.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{ + "Private", + "PrivateApp", + "Shared", + "SharedApp"}...), + }, + }, + }, + } +} + +func (DeliveryGroupTypeFilterModel) GetAttributes() map[string]schema.Attribute { + return DeliveryGroupTypeFilterModel{}.GetSchema().Attributes +} + +func (filter DeliveryGroupTypeFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter DeliveryGroupTypeFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("DesktopKind") + + filterRequest.SetFilterData(filter.DeliveryGroupType.ValueString()) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + return filterRequest, nil +} + +type OuFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + Ou types.String `tfsdk:"ou"` +} + +func (OuFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the organizational unit policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "ou": schema.StringAttribute{ + Description: "Organizational Unit to be filtered.", + Required: true, + }, + }, + } +} + +func (OuFilterModel) GetAttributes() map[string]schema.Attribute { + return OuFilterModel{}.GetSchema().Attributes +} + +func (filter OuFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter OuFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("OU") + + filterRequest.SetFilterData(filter.Ou.ValueString()) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + return filterRequest, nil +} + +type UserFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + UserSid types.String `tfsdk:"sid"` +} + +func (UserFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the user policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "sid": schema.StringAttribute{ + Description: "SID of the user or user group to be filtered.", + Required: true, + }, + }, + } +} + +func (UserFilterModel) GetAttributes() map[string]schema.Attribute { + return UserFilterModel{}.GetSchema().Attributes +} + +func (filter UserFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter UserFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("User") + + filterRequest.SetFilterData(filter.UserSid.ValueString()) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + return filterRequest, nil +} + +type TagFilterModel struct { + Id types.String `tfsdk:"id"` + Allowed types.Bool `tfsdk:"allowed"` + Enabled types.Bool `tfsdk:"enabled"` + Tag types.String `tfsdk:"tag"` +} + +func (TagFilterModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the tag policy filter.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicate whether the filter is being enabled.", + Required: true, + }, + "allowed": schema.BoolAttribute{ + Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", + Required: true, + }, + "tag": schema.StringAttribute{ + Description: "Tag to be filtered.", + Required: true, + }, + }, + } +} + +func (TagFilterModel) GetAttributes() map[string]schema.Attribute { + return TagFilterModel{}.GetSchema().Attributes +} + +func (filter TagFilterModel) GetId() string { + return filter.Id.ValueString() +} + +func (filter TagFilterModel) GetFilterRequest(diagnostics *diag.Diagnostics, serverValue string) (citrixorchestration.FilterRequest, error) { + filterRequest := citrixorchestration.FilterRequest{} + filterRequest.SetFilterType("DesktopTag") + + policyFilterDataClientModel := PolicyFilterUuidDataClientModel{ + Uuid: filter.Tag.ValueString(), + Server: serverValue, + } + + policyFilterDataJson, err := json.Marshal(policyFilterDataClientModel) + if err != nil { + diagnostics.AddError( + "Error adding Access Control Policy Filter to Policy Set. ", + "An unexpected error occurred: "+err.Error(), + ) + return filterRequest, err + } + + filterRequest.SetFilterData(string(policyFilterDataJson)) + filterRequest.SetIsAllowed(filter.Allowed.ValueBool()) + filterRequest.SetIsEnabled(filter.Enabled.ValueBool()) + return filterRequest, nil +} diff --git a/internal/daas/policies/policy_set_resource.go b/internal/daas/policies/policy_set_resource.go index 41981f6..1bbdd7b 100644 --- a/internal/daas/policies/policy_set_resource.go +++ b/internal/daas/policies/policy_set_resource.go @@ -4,7 +4,6 @@ package policies import ( "context" - "encoding/json" "fmt" "net/http" "slices" @@ -15,11 +14,8 @@ import ( citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/citrix/terraform-provider-citrix/internal/util" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -88,12 +84,38 @@ func (r *policySetResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } + userSettings := []string{} + getSettingDefinitionsRequest := r.client.ApiClient.GpoDAAS.GpoGetSettingDefinitions(ctx) + getSettingDefinitionsRequest = getSettingDefinitionsRequest.IsLean(true) + getSettingDefinitionsRequest = getSettingDefinitionsRequest.Limit(-1) + getSettingDefinitionsRequest = getSettingDefinitionsRequest.IsUserSetting(true) + getSettingDefinitionsRequest.Execute() + userSettingsResp, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.SettingDefinitionEnvelope](getSettingDefinitionsRequest, r.client) + if err != nil { + resp.Diagnostics.AddError( + "Unable to fetch user setting definitions", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + for _, setting := range userSettingsResp.GetItems() { + userSettings = append(userSettings, setting.GetSettingName()) + } + plannedPolicies := util.ObjectListToTypedArray[PolicyModel](ctx, &resp.Diagnostics, plan.Policies) for _, policy := range plannedPolicies { + policyContainsUserSetting := false policySettings := util.ObjectSetToTypedArray[PolicySettingModel](ctx, &resp.Diagnostics, policy.PolicySettings) - for _, setting := range policySettings { + if slices.ContainsFunc(userSettings, func(userSetting string) bool { + return strings.EqualFold(userSetting, setting.Name.ValueString()) + }) { + policyContainsUserSetting = true + } + if strings.EqualFold(setting.Value.ValueString(), "true") || strings.EqualFold(setting.Value.ValueString(), "1") || strings.EqualFold(setting.Value.ValueString(), "false") || @@ -102,6 +124,55 @@ func (r *policySetResource) ModifyPlan(ctx context.Context, req resource.ModifyP "Error "+operation+" Policy Set", "Please specify boolean policy setting value with the 'enabled' attribute.", ) + return + } + } + + if !policyContainsUserSetting { + if (!policy.AccessControlFilters.IsNull() && !policy.AccessControlFilters.IsUnknown()) || + (!policy.BranchRepeaterFilter.IsNull() && !policy.BranchRepeaterFilter.IsUnknown()) || + (!policy.ClientIPFilters.IsNull() && !policy.ClientIPFilters.IsUnknown()) || + (!policy.ClientNameFilters.IsNull() && !policy.ClientNameFilters.IsUnknown()) || + (!policy.UserFilters.IsNull() && !policy.UserFilters.IsUnknown()) { + resp.Diagnostics.AddError( + fmt.Sprintf("Error configuring Policy %s in Policy Set %s", policy.Name.ValueString(), plan.Name.ValueString()), + "None of `access_control_filters`, `branch_repeater_filter`, `client_ip_filters`, `client_name_filters`, and `user_filters` can be specified when policy does not contain any user setting.", + ) + return + } + } + + if !policy.DeliveryGroupFilters.IsNull() { + deliveryGroupFilters := util.ObjectSetToTypedArray[DeliveryGroupFilterModel](ctx, &resp.Diagnostics, policy.DeliveryGroupFilters) + for _, deliveryGroupFilter := range deliveryGroupFilters { + deliveryGroupId := deliveryGroupFilter.DeliveryGroupId.ValueString() + getDeliveryGroupRequest := r.client.ApiClient.DeliveryGroupsAPIsDAAS.DeliveryGroupsGetDeliveryGroup(ctx, deliveryGroupId) + _, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.DeliveryGroupDetailResponseModel](getDeliveryGroupRequest, r.client) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Delivery group %s specified in the delivery group filter does not exist.", deliveryGroupId), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + } + } + + if !policy.TagFilters.IsNull() { + tagFilters := util.ObjectSetToTypedArray[TagFilterModel](ctx, &resp.Diagnostics, policy.TagFilters) + for _, tagFilter := range tagFilters { + tagId := tagFilter.Tag.ValueString() + getTagRequest := r.client.ApiClient.TagsAPIsDAAS.TagsGetTag(ctx, tagId) + _, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.TagDetailResponseModel](getTagRequest, r.client) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Tag %s specified in the tag filter does not exist.", tagId), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } } } } @@ -193,6 +264,7 @@ func (r *policySetResource) Create(ctx context.Context, req resource.CreateReque "TransactionId: "+txId+ "\nError message: "+util.ReadClientError(err), ) + return } if successfulJobs < len(plan.Policies.Elements()) { @@ -202,6 +274,7 @@ func (r *policySetResource) Create(ctx context.Context, req resource.CreateReque "TransactionId: "+txId+ "\n"+errMsg, ) + return } // Try getting the new policy set with policy set GUID @@ -222,6 +295,7 @@ func (r *policySetResource) Create(ctx context.Context, req resource.CreateReque "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ "\nError message: "+util.ReadClientError(err), ) + return } } @@ -301,6 +375,13 @@ func (r *policySetResource) Update(ctx context.Context, req resource.UpdateReque return } + var state PolicySetResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Get refreshed policy set properties from Orchestration policySetId := plan.Id.ValueString() policySetName := plan.Name.ValueString() @@ -320,36 +401,128 @@ func (r *policySetResource) Update(ctx context.Context, req resource.UpdateReque } } - stateAndPlanDiff, _ := req.State.Raw.Diff(req.Plan.Raw) - var policiesModified bool - for _, diff := range stateAndPlanDiff { - if diff.Path.Steps()[0].Equal(tftypes.AttributeName("policies")) { - policiesModified = true - break + // Construct the update model + var editPolicySetRequestBody = &citrixorchestration.PolicySetRequest{} + editPolicySetRequestBody.SetName(policySetName) + editPolicySetRequestBody.SetDescription(plan.Description.ValueString()) + editPolicySetRequestBody.SetScopes(util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Scopes)) + + editPolicySetRequest := r.client.ApiClient.GpoDAAS.GpoUpdateGpoPolicySet(ctx, policySetId) + editPolicySetRequest = editPolicySetRequest.PolicySetRequest(*editPolicySetRequestBody) + + // Update Policy Set + httpResp, err := citrixdaasclient.AddRequestData(editPolicySetRequest, r.client).Execute() + if err != nil { + resp.Diagnostics.AddError( + "Error Updating Policy Set", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + policiesInPlan := util.ObjectListToTypedArray[PolicyModel](ctx, &resp.Diagnostics, plan.Policies) + policyIdsInPlan := []string{} + policiesToCreate := []PolicyModel{} + policiesToUpdate := []PolicyModel{} + for _, policy := range policiesInPlan { + if policy.Id.ValueString() == "" { + policiesToCreate = append(policiesToCreate, policy) + } else { + policyIdsInPlan = append(policyIdsInPlan, policy.Id.ValueString()) + policiesToUpdate = append(policiesToUpdate, policy) + } + } + + policyIdsInState := []string{} + policyIdMapFromState := map[string]PolicyModel{} + for _, policy := range util.ObjectListToTypedArray[PolicyModel](ctx, &resp.Diagnostics, state.Policies) { + policyIdMapFromState[strings.ToLower(policy.Id.ValueString())] = policy + policyIdsInState = append(policyIdsInState, policy.Id.ValueString()) + } + + policyIdsToDelete := []string{} + // Check if any policies are to be deleted + for _, policyId := range policyIdsInState { + if !slices.ContainsFunc(policyIdsInPlan, func(policyIdInPlan string) bool { + return strings.EqualFold(policyId, policyIdInPlan) + }) { + policyIdsToDelete = append(policyIdsToDelete, policyId) } } - if policiesModified { - // Get Remote Policies - policies, err := getPolicies(ctx, r.client, &resp.Diagnostics, policySetId) + // Rename policies to update with their policy id to avoid naming collision + if len(policiesToUpdate) > 0 { + batchApiHeaders, httpResp, err := generateBatchApiHeaders(r.client) if err != nil { + diags.AddError( + "Error updating policies in policy set "+policySetName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nCould not update policies within the policy set to be updated, unexpected error: "+util.ReadClientError(err), + ) return } + batchRequestItems := []citrixorchestration.BatchRequestItemModel{} + var batchRequestModel citrixorchestration.BatchRequestModel + for policyIndex, policy := range policiesToUpdate { + var updatePolicyRequest = citrixorchestration.PolicyRequest{} + updatePolicyRequest.SetName(policy.Id.ValueString()) + updatePolicyRequestBodyString, err := util.ConvertToString(updatePolicyRequest) + if err != nil { + diags.AddError( + "Error updating Policy "+policy.Name.ValueString()+" to Policy Set "+policySetName, + "An unexpected error occurred: "+err.Error(), + ) + return + } + relativeUrl := fmt.Sprintf("/gpo/policies/%s", policy.Id.ValueString()) + + var batchRequestItem citrixorchestration.BatchRequestItemModel + batchRequestItem.SetReference(fmt.Sprintf("renamePolicy%d", policyIndex)) + batchRequestItem.SetMethod(http.MethodPatch) + batchRequestItem.SetRelativeUrl(r.client.GetBatchRequestItemRelativeUrl(relativeUrl)) + batchRequestItem.SetHeaders(batchApiHeaders) + batchRequestItem.SetBody(updatePolicyRequestBodyString) + batchRequestItems = append(batchRequestItems, batchRequestItem) + } + batchRequestModel.SetItems(batchRequestItems) + successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, r.client, batchRequestModel) + if err != nil { + resp.Diagnostics.AddError( + "Error updating Policies in Policy Set "+policySetName, + "TransactionId: "+txId+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + if successfulJobs < len(batchRequestItems) { + errMsg := fmt.Sprintf("An error occurred while updating policies in the Policy Set. %d of %d policies were updated to the Policy Set.", successfulJobs, len(batchRequestItems)) + resp.Diagnostics.AddError( + "Error updating Policies to Policy Set "+policySetName, + "TransactionId: "+txId+ + "\n"+errMsg, + ) + return + } + } + + if len(policyIdsToDelete) > 0 { // Setup batch requests deletePolicyBatchRequestItems := []citrixorchestration.BatchRequestItemModel{} batchApiHeaders, httpResp, err := generateBatchApiHeaders(r.client) if err != nil { resp.Diagnostics.AddError( - "Error updating policies in policy set "+policySetName, + "Error deleting policies from policy set "+policySetName, "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nCould not update policies within the policy set, unexpected error: "+util.ReadClientError(err), + "\nCould not delete policies from the policy set, unexpected error: "+util.ReadClientError(err), ) return } - // Clean up all the policies, settings, and filters in policy set - for index, policy := range policies.Items { - relativeUrl := fmt.Sprintf("/gpo/policies/%s", policy.GetPolicyGuid()) + // batch delete policies + for index, policyId := range policyIdsToDelete { + relativeUrl := fmt.Sprintf("/gpo/policies/%s", policyId) var batchRequestItem citrixorchestration.BatchRequestItemModel batchRequestItem.SetReference(fmt.Sprintf("deletePolicy%s", strconv.Itoa(index))) @@ -365,7 +538,7 @@ func (r *policySetResource) Update(ctx context.Context, req resource.UpdateReque successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, r.client, deletePolicyBatchRequestModel) if err != nil { resp.Diagnostics.AddError( - "Error cleanup Policies in Policy Set "+policySetName, + "Error deleting Policies from Policy Set "+policySetName, "TransactionId: "+txId+ "\nError message: "+util.ReadClientError(err), ) @@ -373,24 +546,25 @@ func (r *policySetResource) Update(ctx context.Context, req resource.UpdateReque } if successfulJobs < len(deletePolicyBatchRequestItems) { - errMsg := fmt.Sprintf("An error occurred while deleting policies in the Policy Set. %d of %d policies were deleted from the Policy Set.", successfulJobs, len(deletePolicyBatchRequestItems)) + errMsg := fmt.Sprintf("An error occurred while deleting policies from the Policy Set. %d of %d policies were deleted from the Policy Set.", successfulJobs, len(deletePolicyBatchRequestItems)) resp.Diagnostics.AddError( - "Error deleting Policies to Policy Set "+policySetName, + "Error deleting Policies from Policy Set "+policySetName, "TransactionId: "+txId+ "\n"+errMsg, ) return } + } - // Create all the policies, settings, and filters in the plan - plannedPolicies := util.ObjectListToTypedArray[PolicyModel](ctx, &resp.Diagnostics, plan.Policies) - createPoliciesBatchRequestModel, err := constructCreatePolicyBatchRequestModel(ctx, &resp.Diagnostics, r.client, plannedPolicies, plan.Id.ValueString(), plan.Name.ValueString()) + if len(policiesToCreate) > 0 { + // Create new policies + batchRequestModel, err := constructCreatePolicyBatchRequestModel(ctx, &resp.Diagnostics, r.client, policiesToCreate, policySetId, policySetName) if err != nil { return } - successfulJobs, txId, err = citrixdaasclient.PerformBatchOperation(ctx, r.client, createPoliciesBatchRequestModel) + successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, r.client, batchRequestModel) if err != nil { resp.Diagnostics.AddError( "Error adding Policies to Policy Set "+policySetName, @@ -400,65 +574,201 @@ func (r *policySetResource) Update(ctx context.Context, req resource.UpdateReque return } - if successfulJobs < len(createPoliciesBatchRequestModel.Items) { - errMsg := fmt.Sprintf("An error occurred while adding policies to the Policy Set. %d of %d policies were added to the Policy Set.", successfulJobs, len(createPoliciesBatchRequestModel.Items)) + if successfulJobs < len(batchRequestModel.GetItems()) { + errMsg := fmt.Sprintf("An error occurred while adding policies to the Policy Set. %d of %d policies were added to the Policy Set.", successfulJobs, len(batchRequestModel.GetItems())) resp.Diagnostics.AddError( "Error adding Policies to Policy Set "+policySetName, "TransactionId: "+txId+ "\n"+errMsg, ) - return } + } - // Update policy priority - policySet, err := getPolicySet(ctx, r.client, &resp.Diagnostics, policySetId) - if err != nil { - return - } + if len(policiesToUpdate) > 0 { + // Update policies in policy set + for _, policy := range policiesToUpdate { + var editPolicyRequestModel = &citrixorchestration.PolicyBodyRequest{} + editPolicyRequestModel.SetName(policy.Name.ValueString()) + editPolicyRequestModel.SetDescription(policy.Description.ValueString()) + editPolicyRequestModel.SetIsEnabled(policy.Enabled.ValueBool()) - if len(policySet.Policies) > 0 { - plannedPolicies = util.ObjectListToTypedArray[PolicyModel](ctx, &resp.Diagnostics, plan.Policies) - policyPriorityRequest := constructPolicyPriorityRequest(ctx, r.client, policySet, plannedPolicies) - // Update policy priorities in the Policy Set - policyPriorityResponse, httpResp, err := citrixdaasclient.AddRequestData(policyPriorityRequest, r.client).Execute() - if err != nil || !policyPriorityResponse { + editPolicyRequest := r.client.ApiClient.GpoDAAS.GpoUpdateGpoPolicy(ctx, policy.Id.ValueString()) + editPolicyRequest = editPolicyRequest.PolicyBodyRequest(*editPolicyRequestModel) + + // Update policy + httpResp, err := citrixdaasclient.AddRequestData(editPolicyRequest, r.client).Execute() + if err != nil { resp.Diagnostics.AddError( - "Error updating Policy Priorities in Policy Set "+policySet.GetPolicySetGuid(), + "Error Updating Policy "+policy.Name.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ "\nError message: "+util.ReadClientError(err), ) return } - } - } - // Construct the update model - var editPolicySetRequestBody = &citrixorchestration.PolicySetRequest{} - editPolicySetRequestBody.SetName(policySetName) - editPolicySetRequestBody.SetDescription(plan.Description.ValueString()) - editPolicySetRequestBody.SetScopes(util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Scopes)) + policyInState := policyIdMapFromState[strings.ToLower(policy.Id.ValueString())] + // Perform policy setting updates + policySettingsInPlan := util.ObjectSetToTypedArray[PolicySettingModel](ctx, &resp.Diagnostics, policy.PolicySettings) + policySettingsInState := util.ObjectSetToTypedArray[PolicySettingModel](ctx, &resp.Diagnostics, policyInState.PolicySettings) + err = updatePolicySettings(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), policySettingsInPlan, policySettingsInState) + if err != nil { + return + } - editPolicySetRequest := r.client.ApiClient.GpoDAAS.GpoUpdateGpoPolicySet(ctx, policySetId) - editPolicySetRequest = editPolicySetRequest.PolicySetRequest(*editPolicySetRequestBody) + // Perform policy filter updates + // Update Access Control Filters + accessControlFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[AccessControlFilterModel](ctx, &resp.Diagnostics, policy.AccessControlFilters) { + accessControlFilterInterfaceInPlan = append(accessControlFilterInterfaceInPlan, filter) + } + accessControlFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[AccessControlFilterModel](ctx, &resp.Diagnostics, policyInState.AccessControlFilters) { + accessControlFilterInterfaceInState = append(accessControlFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), accessControlFilterInterfaceInPlan, accessControlFilterInterfaceInState) + if err != nil { + return + } - // Update Policy Set - httpResp, err := citrixdaasclient.AddRequestData(editPolicySetRequest, r.client).Execute() - if err != nil { - resp.Diagnostics.AddError( - "Error Updating Policy Set", - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), - ) - return + // Update Branch Repeater Filter + branchRepeaterFilterInterfaceInPlan := []PolicyFilterInterface{} + if !policy.BranchRepeaterFilter.IsNull() { + filter := util.ObjectValueToTypedObject[BranchRepeaterFilterModel](ctx, &resp.Diagnostics, policy.BranchRepeaterFilter) + branchRepeaterFilterInterfaceInPlan = append(branchRepeaterFilterInterfaceInPlan, filter) + } + branchRepeaterFilterInterfaceInState := []PolicyFilterInterface{} + if !policyInState.BranchRepeaterFilter.IsNull() { + filter := util.ObjectValueToTypedObject[BranchRepeaterFilterModel](ctx, &resp.Diagnostics, policyInState.BranchRepeaterFilter) + branchRepeaterFilterInterfaceInState = append(branchRepeaterFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), branchRepeaterFilterInterfaceInPlan, branchRepeaterFilterInterfaceInState) + if err != nil { + return + } + + // Update Client IP Filters + clientIpFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[ClientIPFilterModel](ctx, &resp.Diagnostics, policy.ClientIPFilters) { + clientIpFilterInterfaceInPlan = append(clientIpFilterInterfaceInPlan, filter) + } + clientIpFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[ClientIPFilterModel](ctx, &resp.Diagnostics, policyInState.ClientIPFilters) { + clientIpFilterInterfaceInState = append(clientIpFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), clientIpFilterInterfaceInPlan, clientIpFilterInterfaceInState) + if err != nil { + return + } + + // Update Client Name Filters + clientNameFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[ClientNameFilterModel](ctx, &resp.Diagnostics, policy.ClientNameFilters) { + clientNameFilterInterfaceInPlan = append(clientNameFilterInterfaceInPlan, filter) + } + clientNameFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[ClientNameFilterModel](ctx, &resp.Diagnostics, policyInState.ClientNameFilters) { + clientNameFilterInterfaceInState = append(clientNameFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), clientNameFilterInterfaceInPlan, clientNameFilterInterfaceInState) + if err != nil { + return + } + + // Update Delivery Group Filters + deliveryGroupFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[DeliveryGroupFilterModel](ctx, &resp.Diagnostics, policy.DeliveryGroupFilters) { + deliveryGroupFilterInterfaceInPlan = append(deliveryGroupFilterInterfaceInPlan, filter) + } + deliveryGroupFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[DeliveryGroupFilterModel](ctx, &resp.Diagnostics, policyInState.DeliveryGroupFilters) { + deliveryGroupFilterInterfaceInState = append(deliveryGroupFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), deliveryGroupFilterInterfaceInPlan, deliveryGroupFilterInterfaceInState) + if err != nil { + return + } + + // Update Delivery Group Type Filters + deliveryGroupTypeFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[DeliveryGroupTypeFilterModel](ctx, &resp.Diagnostics, policy.DeliveryGroupTypeFilters) { + deliveryGroupTypeFilterInterfaceInPlan = append(deliveryGroupTypeFilterInterfaceInPlan, filter) + } + deliveryGroupTypeFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[DeliveryGroupTypeFilterModel](ctx, &resp.Diagnostics, policyInState.DeliveryGroupTypeFilters) { + deliveryGroupTypeFilterInterfaceInState = append(deliveryGroupTypeFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), deliveryGroupTypeFilterInterfaceInPlan, deliveryGroupTypeFilterInterfaceInState) + if err != nil { + return + } + + // Update Tag Filters + tagFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[TagFilterModel](ctx, &resp.Diagnostics, policy.TagFilters) { + tagFilterInterfaceInPlan = append(tagFilterInterfaceInPlan, filter) + } + tagFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[TagFilterModel](ctx, &resp.Diagnostics, policyInState.TagFilters) { + tagFilterInterfaceInState = append(tagFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), tagFilterInterfaceInPlan, tagFilterInterfaceInState) + if err != nil { + return + } + + // Update Ou Filters + ouFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[OuFilterModel](ctx, &resp.Diagnostics, policy.OuFilters) { + tagFilterInterfaceInPlan = append(tagFilterInterfaceInPlan, filter) + } + ouFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[OuFilterModel](ctx, &resp.Diagnostics, policyInState.OuFilters) { + ouFilterInterfaceInState = append(ouFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), ouFilterInterfaceInPlan, ouFilterInterfaceInState) + if err != nil { + return + } + + // Update User Filters + userFilterInterfaceInPlan := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[UserFilterModel](ctx, &resp.Diagnostics, policy.UserFilters) { + userFilterInterfaceInPlan = append(userFilterInterfaceInPlan, filter) + } + userFilterInterfaceInState := []PolicyFilterInterface{} + for _, filter := range util.ObjectSetToTypedArray[UserFilterModel](ctx, &resp.Diagnostics, policyInState.UserFilters) { + userFilterInterfaceInState = append(userFilterInterfaceInState, filter) + } + err = updatePolicyFilters(ctx, r.client, &resp.Diagnostics, policy.Id.ValueString(), policy.Name.ValueString(), userFilterInterfaceInPlan, userFilterInterfaceInState) + if err != nil { + return + } + } } + // Update policy priority // Try getting the new policy set with policy set GUID policySet, err := getPolicySet(ctx, r.client, &resp.Diagnostics, policySetId) if err != nil { return } + if len(policySet.Policies) > 0 { + // Update Policy Priority + policyPriorityRequest := constructPolicyPriorityRequest(ctx, r.client, policySet, policiesInPlan) + // Update policy priorities in the Policy Set + policyPriorityResponse, httpResp, err := citrixdaasclient.AddRequestData(policyPriorityRequest, r.client).Execute() + if err != nil || !policyPriorityResponse { + resp.Diagnostics.AddError( + "Error Changing Policy Priorities in Policy Set "+policySet.GetPolicySetGuid(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + } + policies, err := getPolicies(ctx, r.client, &resp.Diagnostics, policySetId) if err != nil { return @@ -592,351 +902,6 @@ func (r *policySetResource) ImportState(ctx context.Context, req resource.Import resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -// Gets the policy set and logs any errors -func getPolicySets(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics) ([]citrixorchestration.PolicySetResponse, error) { - getPolicySetsRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicySets(ctx) - policySets, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.CollectionEnvelopeOfPolicySetResponse](getPolicySetsRequest, client) - if err != nil { - diagnostics.AddError( - "Error Reading Policy Sets", - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), - ) - return nil, err - } - - return policySets.Items, err -} - -func getPolicySet(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policySetId string) (*citrixorchestration.PolicySetResponse, error) { - getPolicySetRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicySet(ctx, policySetId) - getPolicySetRequest = getPolicySetRequest.WithPolicies(true) - policySet, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.PolicySetResponse](getPolicySetRequest, client) - if err != nil { - diagnostics.AddError( - "Error Reading Policy Set "+policySetId, - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), - ) - } - - return policySet, err -} - -func readPolicySet(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, policySetId string) (*citrixorchestration.PolicySetResponse, error) { - getPolicySetRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicySet(ctx, policySetId) - getPolicySetRequest = getPolicySetRequest.WithPolicies(true) - policySet, _, err := util.ReadResource[*citrixorchestration.PolicySetResponse](getPolicySetRequest, ctx, client, resp, "PolicySet", policySetId) - return policySet, err -} - -// Gets the policy set and logs any errors -func getPolicies(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policySetId string) (*citrixorchestration.CollectionEnvelopeOfPolicyResponse, error) { - getPoliciesRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicies(ctx) - getPoliciesRequest = getPoliciesRequest.PolicySetGuid(policySetId) - getPoliciesRequest = getPoliciesRequest.WithFilters(true) - getPoliciesRequest = getPoliciesRequest.WithSettings(true) - policies, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.CollectionEnvelopeOfPolicyResponse](getPoliciesRequest, client) - if err != nil { - diagnostics.AddError( - "Error Reading Policies in Policy Set "+policySetId, - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), - ) - } - - return policies, err -} - -func readPolicies(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, policySetId string) (*citrixorchestration.CollectionEnvelopeOfPolicyResponse, error) { - getPoliciesRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicies(ctx) - getPoliciesRequest = getPoliciesRequest.PolicySetGuid(policySetId) - getPoliciesRequest = getPoliciesRequest.WithFilters(true) - getPoliciesRequest = getPoliciesRequest.WithSettings(true) - policies, _, err := util.ReadResource[*citrixorchestration.CollectionEnvelopeOfPolicyResponse](getPoliciesRequest, ctx, client, resp, "Policies", policySetId) - return policies, err -} - -func generateBatchApiHeaders(client *citrixdaasclient.CitrixDaasClient) ([]citrixorchestration.NameValueStringPairModel, *http.Response, error) { - headers := []citrixorchestration.NameValueStringPairModel{} - - cwsAuthToken, httpResp, err := client.SignIn() - var token string - if err != nil { - return headers, httpResp, err - } - - if cwsAuthToken != "" { - token = strings.Split(cwsAuthToken, "=")[1] - var header citrixorchestration.NameValueStringPairModel - header.SetName("Authorization") - header.SetValue("Bearer " + token) - headers = append(headers, header) - } - - return headers, httpResp, err -} - -func constructCreatePolicyBatchRequestModel(ctx context.Context, diags *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, policiesToCreate []PolicyModel, policySetGuid string, policySetName string) (citrixorchestration.BatchRequestModel, error) { - batchRequestItems := []citrixorchestration.BatchRequestItemModel{} - var batchRequestModel citrixorchestration.BatchRequestModel - - for policyIndex, policyToCreate := range policiesToCreate { - var createPolicyRequest = citrixorchestration.PolicyRequest{} - createPolicyRequest.SetName(policyToCreate.Name.ValueString()) - createPolicyRequest.SetDescription(policyToCreate.Description.ValueString()) - createPolicyRequest.SetIsEnabled(policyToCreate.Enabled.ValueBool()) - // Add Policy Settings - policySettings := []citrixorchestration.SettingRequest{} - policySettingsToCreate := util.ObjectSetToTypedArray[PolicySettingModel](ctx, diags, policyToCreate.PolicySettings) - for _, policySetting := range policySettingsToCreate { - settingRequest := citrixorchestration.SettingRequest{} - settingRequest.SetSettingName(policySetting.Name.ValueString()) - settingRequest.SetUseDefault(policySetting.UseDefault.ValueBool()) - if policySetting.Value.ValueString() != "" { - settingRequest.SetSettingValue(policySetting.Value.ValueString()) - } else { - if policySetting.Enabled.ValueBool() { - settingRequest.SetSettingValue("1") - } else { - settingRequest.SetSettingValue("0") - } - } - policySettings = append(policySettings, settingRequest) - } - createPolicyRequest.SetSettings(policySettings) - - // Add Policy Filters - policyFilters, err := constructPolicyFilterRequests(ctx, diags, client, policyToCreate) - if err != nil { - return batchRequestModel, err - } - createPolicyRequest.SetFilters(policyFilters) - - createPolicyRequestBodyString, err := util.ConvertToString(createPolicyRequest) - if err != nil { - diags.AddError( - "Error adding Policy "+policyToCreate.Name.ValueString()+" to Policy Set "+policySetName, - "An unexpected error occurred: "+err.Error(), - ) - return batchRequestModel, err - } - - batchApiHeaders, httpResp, err := generateBatchApiHeaders(client) - if err != nil { - diags.AddError( - "Error deleting policy from policy set "+policySetName, - "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nCould not delete policies within the policy set to be updated, unexpected error: "+util.ReadClientError(err), - ) - return batchRequestModel, err - } - - relativeUrl := fmt.Sprintf("/gpo/policies?policySetGuid=%s", policySetGuid) - - var batchRequestItem citrixorchestration.BatchRequestItemModel - batchRequestItem.SetReference(fmt.Sprintf("createPolicy%d", policyIndex)) - batchRequestItem.SetMethod(http.MethodPost) - batchRequestItem.SetRelativeUrl(client.GetBatchRequestItemRelativeUrl(relativeUrl)) - batchRequestItem.SetHeaders(batchApiHeaders) - batchRequestItem.SetBody(createPolicyRequestBodyString) - batchRequestItems = append(batchRequestItems, batchRequestItem) - } - - batchRequestModel.SetItems(batchRequestItems) - return batchRequestModel, nil -} - -func constructPolicyFilterRequests(ctx context.Context, diags *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, policy PolicyModel) ([]citrixorchestration.FilterRequest, error) { - filterRequests := []citrixorchestration.FilterRequest{} - - serverValue := "" - if client.AuthConfig.OnPremises || !client.AuthConfig.ApiGateway { - serverValue = client.ApiClient.GetConfig().Host - } else { - serverValue = fmt.Sprintf("%s.xendesktop.net", client.ClientConfig.CustomerId) - } - - if !policy.AccessControlFilters.IsNull() && len(policy.AccessControlFilters.Elements()) > 0 { - accessControlFilters := util.ObjectSetToTypedArray[AccessControlFilterModel](ctx, diags, policy.AccessControlFilters) - for _, accessControlFilter := range accessControlFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("AccessControl") - - policyFilterDataClientModel := PolicyFilterGatewayDataClientModel{ - Connection: accessControlFilter.Connection, - Condition: accessControlFilter.Condition, - Gateway: accessControlFilter.Gateway, - } - - policyFilterDataJson, err := json.Marshal(policyFilterDataClientModel) - if err != nil { - diags.AddError( - "Error adding Access Control Policy Filter to Policy Set. ", - "An unexpected error occurred: "+err.Error(), - ) - return filterRequests, err - } - filterRequest.SetFilterData(string(policyFilterDataJson)) - filterRequest.SetIsAllowed(accessControlFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(accessControlFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.BranchRepeaterFilter.IsNull() { - branchRepeaterFilter := util.ObjectValueToTypedObject[BranchRepeaterFilterModel](ctx, diags, policy.BranchRepeaterFilter) - branchnRepeaterFilterRequest := citrixorchestration.FilterRequest{} - branchnRepeaterFilterRequest.SetFilterType("BranchRepeater") - branchnRepeaterFilterRequest.SetIsAllowed(branchRepeaterFilter.Allowed.ValueBool()) - branchnRepeaterFilterRequest.SetIsEnabled(branchRepeaterFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, branchnRepeaterFilterRequest) - } - - if !policy.ClientIPFilters.IsNull() && len(policy.ClientIPFilters.Elements()) > 0 { - clientIpFilters := util.ObjectSetToTypedArray[ClientIPFilterModel](ctx, diags, policy.ClientIPFilters) - for _, clientIpFilter := range clientIpFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("ClientIP") - - filterRequest.SetFilterData(clientIpFilter.IpAddress.ValueString()) - filterRequest.SetIsAllowed(clientIpFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(clientIpFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.ClientNameFilters.IsNull() && len(policy.ClientNameFilters.Elements()) > 0 { - clientNameFilters := util.ObjectSetToTypedArray[ClientNameFilterModel](ctx, diags, policy.ClientNameFilters) - for _, clientName := range clientNameFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("ClientName") - - filterRequest.SetFilterData(clientName.ClientName.ValueString()) - filterRequest.SetIsAllowed(clientName.Allowed.ValueBool()) - filterRequest.SetIsEnabled(clientName.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.DeliveryGroupFilters.IsNull() && len(policy.DeliveryGroupFilters.Elements()) > 0 { - deliveryGroupFilters := util.ObjectSetToTypedArray[DeliveryGroupFilterModel](ctx, diags, policy.DeliveryGroupFilters) - for _, deliveryGroupFilter := range deliveryGroupFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("DesktopGroup") - - policyFilterDataClientModel := PolicyFilterUuidDataClientModel{ - Uuid: deliveryGroupFilter.DeliveryGroupId.ValueString(), - Server: serverValue, - } - - policyFilterDataJson, err := json.Marshal(policyFilterDataClientModel) - if err != nil { - diags.AddError( - "Error adding Access Control Policy Filter to Policy Set. ", - "An unexpected error occurred: "+err.Error(), - ) - return filterRequests, err - } - - filterRequest.SetFilterData(string(policyFilterDataJson)) - filterRequest.SetIsAllowed(deliveryGroupFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(deliveryGroupFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.DeliveryGroupTypeFilters.IsNull() && len(policy.DeliveryGroupTypeFilters.Elements()) > 0 { - deliveryGroupTypeFilters := util.ObjectSetToTypedArray[DeliveryGroupTypeFilterModel](ctx, diags, policy.DeliveryGroupTypeFilters) - for _, deliveryGroupTypeFilter := range deliveryGroupTypeFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("DesktopKind") - - filterRequest.SetFilterData(deliveryGroupTypeFilter.DeliveryGroupType.ValueString()) - filterRequest.SetIsAllowed(deliveryGroupTypeFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(deliveryGroupTypeFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.TagFilters.IsNull() && len(policy.TagFilters.Elements()) > 0 { - tagFilters := util.ObjectSetToTypedArray[TagFilterModel](ctx, diags, policy.TagFilters) - for _, tagFilter := range tagFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("DesktopTag") - - policyFilterDataClientModel := PolicyFilterUuidDataClientModel{ - Uuid: tagFilter.Tag.ValueString(), - Server: serverValue, - } - - policyFilterDataJson, err := json.Marshal(policyFilterDataClientModel) - if err != nil { - diags.AddError( - "Error adding Access Control Policy Filter to Policy Set. ", - "An unexpected error occurred: "+err.Error(), - ) - return filterRequests, err - } - - filterRequest.SetFilterData(string(policyFilterDataJson)) - filterRequest.SetIsAllowed(tagFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(tagFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.OuFilters.IsNull() && len(policy.OuFilters.Elements()) > 0 { - ouFilters := util.ObjectSetToTypedArray[OuFilterModel](ctx, diags, policy.OuFilters) - for _, ouFilter := range ouFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("OU") - - filterRequest.SetFilterData(ouFilter.Ou.ValueString()) - filterRequest.SetIsAllowed(ouFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(ouFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - if !policy.UserFilters.IsNull() && len(policy.UserFilters.Elements()) > 0 { - userFilters := util.ObjectSetToTypedArray[UserFilterModel](ctx, diags, policy.UserFilters) - for _, userFilter := range userFilters { - filterRequest := citrixorchestration.FilterRequest{} - filterRequest.SetFilterType("User") - - filterRequest.SetFilterData(userFilter.UserSid.ValueString()) - filterRequest.SetIsAllowed(userFilter.Allowed.ValueBool()) - filterRequest.SetIsEnabled(userFilter.Enabled.ValueBool()) - filterRequests = append(filterRequests, filterRequest) - } - } - - return filterRequests, nil -} - -func constructPolicyPriorityRequest(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, policySet *citrixorchestration.PolicySetResponse, planedPolicies []PolicyModel) citrixorchestration.ApiGpoRankGpoPoliciesRequest { - // 1. Construct map of policy name: policy id - // 2. Construct array of policy id based on the policy name order - // 3. post policy priority - policyNameIdMap := map[types.String]types.String{} - if policySet.GetPolicies() != nil { - for _, policy := range policySet.GetPolicies() { - policyNameIdMap[types.StringValue(policy.GetPolicyName())] = types.StringValue(policy.GetPolicyGuid()) - } - } - policyPriority := []types.String{} - for _, policyToCreate := range planedPolicies { - policyPriority = append(policyPriority, policyNameIdMap[policyToCreate.Name]) - } - - policySetId := policySet.GetPolicySetGuid() - createPolicyPriorityRequest := client.ApiClient.GpoDAAS.GpoRankGpoPolicies(ctx) - createPolicyPriorityRequest = createPolicyPriorityRequest.PolicySetGuid(policySetId) - createPolicyPriorityRequest = createPolicyPriorityRequest.RequestBody(util.ConvertBaseStringArrayToPrimitiveStringArray(policyPriority)) - return createPolicyPriorityRequest -} - func (r *policySetResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { defer util.PanicHandler(&resp.Diagnostics) diff --git a/internal/daas/policies/policy_set_resource.md b/internal/daas/policies/policy_set_resource.md index 8a95a46..cb20126 100644 --- a/internal/daas/policies/policy_set_resource.md +++ b/internal/daas/policies/policy_set_resource.md @@ -85,11 +85,16 @@ When `allowed` is set to `true`, this means policy is applied to `Connections wi Example: ``` branch_repeater_filter = { - enabled = true allowed = true } ``` +``` +branch_repeater_filter = { + allowed = false +} +``` + ### Client IP Address Filter Type: `ClientIP` @@ -8038,6 +8043,13 @@ jsonencode([ ]) ``` +Setting Value for disable: +``` +jsonencode([ + "=disabled=" +]) +``` + ### Virtual channel allow list log throttling Description: ``` diff --git a/internal/daas/policies/policy_set_resource_model.go b/internal/daas/policies/policy_set_resource_model.go index ee8b41d..01d5b04 100644 --- a/internal/daas/policies/policy_set_resource_model.go +++ b/internal/daas/policies/policy_set_resource_model.go @@ -11,7 +11,7 @@ import ( citrixorchestration "github.com/citrix/citrix-daas-rest-go/citrixorchestration" "github.com/citrix/terraform-provider-citrix/internal/util" - "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/citrix/terraform-provider-citrix/internal/validators" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -37,31 +37,24 @@ func (PolicySettingModel) GetSchema() schema.NestedAttributeObject { return schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ - Description: "Name of the policy setting name.", + Description: "Name of the policy setting.", Required: true, }, "use_default": schema.BoolAttribute{ Description: "Indicate whether using default value for the policy setting.", Required: true, + Validators: []validator.Bool{ + validators.AlsoRequiresOneOfOnBoolValues([]bool{false}, path.MatchRelative().AtParent().AtName("value"), path.MatchRelative().AtParent().AtName("enabled")), + validators.ConflictsWithOnBoolValues([]bool{true}, path.MatchRelative().AtParent().AtName("value"), path.MatchRelative().AtParent().AtName("enabled")), + }, }, "value": schema.StringAttribute{ Description: "Value of the policy setting.", Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.ExactlyOneOf( - path.MatchRelative().AtParent().AtName("enabled"), - ), - }, }, "enabled": schema.BoolAttribute{ Description: "Whether of the policy setting has enabled or allowed value.", Optional: true, - Computed: true, - Validators: []validator.Bool{ - boolvalidator.ExactlyOneOf( - path.MatchRelative().AtParent().AtName("value")), - }, }, }, } @@ -82,293 +75,8 @@ type PolicyFilterGatewayDataClientModel struct { Gateway string `json:"gateway,omitempty"` } -type AccessControlFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - Connection string `json:"Connection"` - Condition string `json:"Condition"` - Gateway string `json:"Gateway"` -} - -func (AccessControlFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "connection": schema.StringAttribute{ - Description: "Gateway connection for the policy filter.", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf([]string{ - "WithAccessGateway", - "WithoutAccessGateway"}...), - }, - }, - "condition": schema.StringAttribute{ - Description: "Gateway condition for the policy filter.", - Required: true, - }, - "gateway": schema.StringAttribute{ - Description: "Gateway for the policy filter.", - Required: true, - }, - }, - } -} - -func (AccessControlFilterModel) GetAttributes() map[string]schema.Attribute { - return AccessControlFilterModel{}.GetSchema().Attributes -} - -type BranchRepeaterFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` -} - -func (BranchRepeaterFilterModel) GetSchema() schema.SingleNestedAttribute { - return schema.SingleNestedAttribute{ - Description: "Set of policy filters.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - }, - } -} - -func (BranchRepeaterFilterModel) GetAttributes() map[string]schema.Attribute { - return BranchRepeaterFilterModel{}.GetSchema().Attributes -} - -type ClientIPFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - IpAddress types.String `tfsdk:"ip_address"` -} - -func (ClientIPFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "ip_address": schema.StringAttribute{ - Description: "IP Address of the client to be filtered.", - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches(regexp.MustCompile(util.IPv4Regex), "must be a valid IPv4 address without protocol (http:// or https://) and port number"), - }, - }, - }, - } -} - -func (ClientIPFilterModel) GetAttributes() map[string]schema.Attribute { - return ClientIPFilterModel{}.GetSchema().Attributes -} - -type ClientNameFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - ClientName types.String `tfsdk:"client_name"` -} - -func (ClientNameFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "client_name": schema.StringAttribute{ - Description: "Name of the client to be filtered.", - Required: true, - }, - }, - } -} - -func (ClientNameFilterModel) GetAttributes() map[string]schema.Attribute { - return ClientNameFilterModel{}.GetSchema().Attributes -} - -type DeliveryGroupFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - DeliveryGroupId types.String `tfsdk:"delivery_group_id"` -} - -func (DeliveryGroupFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "delivery_group_id": schema.StringAttribute{ - Description: "Id of the delivery group to be filtered.", - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), - }, - }, - }, - } -} - -func (DeliveryGroupFilterModel) GetAttributes() map[string]schema.Attribute { - return DeliveryGroupFilterModel{}.GetSchema().Attributes -} - -type DeliveryGroupTypeFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - DeliveryGroupType types.String `tfsdk:"delivery_group_type"` -} - -func (DeliveryGroupTypeFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "delivery_group_type": schema.StringAttribute{ - Description: "Type of the delivery groups to be filtered.", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf([]string{ - "Private", - "PrivateApp", - "Shared", - "SharedApp"}...), - }, - }, - }, - } -} - -func (DeliveryGroupTypeFilterModel) GetAttributes() map[string]schema.Attribute { - return DeliveryGroupTypeFilterModel{}.GetSchema().Attributes -} - -type OuFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - Ou types.String `tfsdk:"ou"` -} - -func (OuFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "ou": schema.StringAttribute{ - Description: "Organizational Unit to be filtered.", - Required: true, - }, - }, - } -} - -func (OuFilterModel) GetAttributes() map[string]schema.Attribute { - return OuFilterModel{}.GetSchema().Attributes -} - -type UserFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - UserSid types.String `tfsdk:"sid"` -} - -func (UserFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "sid": schema.StringAttribute{ - Description: "SID of the user or user group to be filtered.", - Required: true, - }, - }, - } -} - -func (UserFilterModel) GetAttributes() map[string]schema.Attribute { - return UserFilterModel{}.GetSchema().Attributes -} - -type TagFilterModel struct { - Allowed types.Bool `tfsdk:"allowed"` - Enabled types.Bool `tfsdk:"enabled"` - Tag types.String `tfsdk:"tag"` -} - -func (TagFilterModel) GetSchema() schema.NestedAttributeObject { - return schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Indicate whether the filter is being enabled.", - Required: true, - }, - "allowed": schema.BoolAttribute{ - Description: "Indicate the filtered policy is allowed or denied if the filter condition is met.", - Required: true, - }, - "tag": schema.StringAttribute{ - Description: "Tag to be filtered.", - Required: true, - }, - }, - } -} - -func (TagFilterModel) GetAttributes() map[string]schema.Attribute { - return TagFilterModel{}.GetSchema().Attributes -} - type PolicyModel struct { + Id types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` Enabled types.Bool `tfsdk:"enabled"` @@ -387,6 +95,13 @@ type PolicyModel struct { func (PolicyModel) GetSchema() schema.NestedAttributeObject { return schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the policy.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "name": schema.StringAttribute{ Description: "Name of the policy.", Required: true, @@ -572,11 +287,7 @@ func (r PolicySetResourceModel) RefreshPropertyValues(ctx context.Context, diags r.Type = types.StringValue(string(policySet.GetPolicySetType())) // Set optional values - if policySet.GetDescription() != "" { - r.Description = types.StringValue(policySet.GetDescription()) - } else { - r.Description = types.StringNull() - } + r.Description = types.StringValue(policySet.GetDescription()) updatedPolicySetScopes := []string{} for _, scopeId := range policySetScopes { @@ -595,6 +306,7 @@ func (r PolicySetResourceModel) RefreshPropertyValues(ctx context.Context, diags refreshedPolicies := []PolicyModel{} for _, policy := range policyItems { policyModel := PolicyModel{ + Id: types.StringValue(policy.GetPolicyGuid()), Name: types.StringValue(policy.GetPolicyName()), Description: types.StringValue(policy.GetDescription()), Enabled: types.BoolValue(policy.GetIsEnabled()), @@ -607,26 +319,26 @@ func (r PolicySetResourceModel) RefreshPropertyValues(ctx context.Context, diags Name: types.StringValue(setting.GetSettingName()), UseDefault: types.BoolValue(setting.GetUseDefault()), } - - settingValue := types.StringValue(setting.GetSettingValue()) - if strings.EqualFold(setting.GetSettingValue(), "true") || - setting.GetSettingValue() == "1" { - policySetting.Enabled = types.BoolValue(true) - } else if strings.EqualFold(setting.GetSettingValue(), "false") || - setting.GetSettingValue() == "0" { - policySetting.Enabled = types.BoolValue(false) - } else { - policySetting.Value = settingValue + if !setting.GetUseDefault() { + settingValue := types.StringValue(setting.GetSettingValue()) + if strings.EqualFold(setting.GetSettingValue(), "true") || + setting.GetSettingValue() == "1" { + policySetting.Enabled = types.BoolValue(true) + policySetting.Value = types.StringNull() + } else if strings.EqualFold(setting.GetSettingValue(), "false") || + setting.GetSettingValue() == "0" { + policySetting.Enabled = types.BoolValue(false) + policySetting.Value = types.StringNull() + } else { + policySetting.Enabled = types.BoolNull() + policySetting.Value = settingValue + } } refreshedPolicySettings = append(refreshedPolicySettings, policySetting) } } - sort.Slice(refreshedPolicySettings, func(i, j int) bool { - return refreshedPolicySettings[i].Name.ValueString() < refreshedPolicySettings[j].Name.ValueString() - }) - policyModel.PolicySettings = util.TypedArrayToObjectSet(ctx, diags, refreshedPolicySettings) var accessControlFilters []AccessControlFilterModel @@ -654,55 +366,63 @@ func (r PolicySetResourceModel) RefreshPropertyValues(ctx context.Context, diags switch filterType { case "AccessControl": accessControlFilters = append(accessControlFilters, AccessControlFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), - Connection: gatewayFilterData.Connection, - Condition: gatewayFilterData.Condition, - Gateway: gatewayFilterData.Gateway, + Connection: types.StringValue(gatewayFilterData.Connection), + Condition: types.StringValue(gatewayFilterData.Condition), + Gateway: types.StringValue(gatewayFilterData.Gateway), }) case "BranchRepeater": policyModel.BranchRepeaterFilter = util.TypedObjectToObjectValue(ctx, diags, BranchRepeaterFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), - Enabled: types.BoolValue(filter.GetIsEnabled()), }) case "ClientIP": clientIpFilters = append(clientIpFilters, ClientIPFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), IpAddress: types.StringValue(filter.GetFilterData()), }) case "ClientName": clientNameFilters = append(clientNameFilters, ClientNameFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), ClientName: types.StringValue(filter.GetFilterData()), }) case "DesktopGroup": desktopGroupFilters = append(desktopGroupFilters, DeliveryGroupFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), DeliveryGroupId: types.StringValue(uuidFilterData.Uuid), }) case "DesktopKind": desktopKindFilters = append(desktopKindFilters, DeliveryGroupTypeFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), DeliveryGroupType: types.StringValue(filter.GetFilterData()), }) case "DesktopTag": desktopTagFilters = append(desktopTagFilters, TagFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), Tag: types.StringValue(uuidFilterData.Uuid), }) case "OU": ouFilters = append(ouFilters, OuFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), Ou: types.StringValue(filter.GetFilterData()), }) case "User": userFilters = append(userFilters, UserFilterModel{ + Id: types.StringValue(filter.GetFilterGuid()), Allowed: types.BoolValue(filter.GetIsAllowed()), Enabled: types.BoolValue(filter.GetIsEnabled()), UserSid: types.StringValue(filter.GetFilterData()), diff --git a/internal/daas/policies/policy_set_utils.go b/internal/daas/policies/policy_set_utils.go new file mode 100644 index 0000000..052e38d --- /dev/null +++ b/internal/daas/policies/policy_set_utils.go @@ -0,0 +1,634 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package policies + +import ( + "context" + "fmt" + "net/http" + "slices" + "strconv" + "strings" + + 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/util" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Gets the policy set and logs any errors +func getPolicySets(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics) ([]citrixorchestration.PolicySetResponse, error) { + getPolicySetsRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicySets(ctx) + policySets, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.CollectionEnvelopeOfPolicySetResponse](getPolicySetsRequest, client) + if err != nil { + diagnostics.AddError( + "Error Reading Policy Sets", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return nil, err + } + + return policySets.Items, err +} + +func getPolicySet(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policySetId string) (*citrixorchestration.PolicySetResponse, error) { + getPolicySetRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicySet(ctx, policySetId) + getPolicySetRequest = getPolicySetRequest.WithPolicies(true) + policySet, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.PolicySetResponse](getPolicySetRequest, client) + if err != nil { + diagnostics.AddError( + "Error Reading Policy Set "+policySetId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + } + + return policySet, err +} + +func readPolicySet(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, policySetId string) (*citrixorchestration.PolicySetResponse, error) { + getPolicySetRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicySet(ctx, policySetId) + getPolicySetRequest = getPolicySetRequest.WithPolicies(true) + policySet, _, err := util.ReadResource[*citrixorchestration.PolicySetResponse](getPolicySetRequest, ctx, client, resp, "PolicySet", policySetId) + return policySet, err +} + +// Gets the policy set and logs any errors +func getPolicies(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policySetId string) (*citrixorchestration.CollectionEnvelopeOfPolicyResponse, error) { + getPoliciesRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicies(ctx) + getPoliciesRequest = getPoliciesRequest.PolicySetGuid(policySetId) + getPoliciesRequest = getPoliciesRequest.WithFilters(true) + getPoliciesRequest = getPoliciesRequest.WithSettings(true) + policies, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.CollectionEnvelopeOfPolicyResponse](getPoliciesRequest, client) + if err != nil { + diagnostics.AddError( + "Error Reading Policies in Policy Set "+policySetId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + } + + return policies, err +} + +func readPolicies(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, policySetId string) (*citrixorchestration.CollectionEnvelopeOfPolicyResponse, error) { + getPoliciesRequest := client.ApiClient.GpoDAAS.GpoReadGpoPolicies(ctx) + getPoliciesRequest = getPoliciesRequest.PolicySetGuid(policySetId) + getPoliciesRequest = getPoliciesRequest.WithFilters(true) + getPoliciesRequest = getPoliciesRequest.WithSettings(true) + policies, _, err := util.ReadResource[*citrixorchestration.CollectionEnvelopeOfPolicyResponse](getPoliciesRequest, ctx, client, resp, "Policies", policySetId) + return policies, err +} + +func getPolicySettings(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyId string) (*citrixorchestration.CollectionEnvelopeOfSettingResponse, error) { + getPolicySettingsRequest := client.ApiClient.GpoDAAS.GpoReadGpoSettings(ctx) + getPolicySettingsRequest = getPolicySettingsRequest.PolicyGuid(policyId) + settings, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.CollectionEnvelopeOfSettingResponse](getPolicySettingsRequest, client) + if err != nil { + diagnostics.AddError( + "Error Reading Policy Settings in Policy "+policyId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + } + + return settings, err +} + +func generateBatchApiHeaders(client *citrixdaasclient.CitrixDaasClient) ([]citrixorchestration.NameValueStringPairModel, *http.Response, error) { + headers := []citrixorchestration.NameValueStringPairModel{} + + cwsAuthToken, httpResp, err := client.SignIn() + var token string + if err != nil { + return headers, httpResp, err + } + + if cwsAuthToken != "" { + token = strings.Split(cwsAuthToken, "=")[1] + var header citrixorchestration.NameValueStringPairModel + header.SetName("Authorization") + header.SetValue("Bearer " + token) + headers = append(headers, header) + } + + return headers, httpResp, err +} + +func constructCreatePolicyBatchRequestModel(ctx context.Context, diags *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, policiesToCreate []PolicyModel, policySetGuid string, policySetName string) (citrixorchestration.BatchRequestModel, error) { + batchRequestItems := []citrixorchestration.BatchRequestItemModel{} + var batchRequestModel citrixorchestration.BatchRequestModel + + batchApiHeaders, httpResp, err := generateBatchApiHeaders(client) + if err != nil { + diags.AddError( + "Error creating policy in policy set "+policySetName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nCould not create policies within the policy set, unexpected error: "+util.ReadClientError(err), + ) + return batchRequestModel, err + } + + for policyIndex, policyToCreate := range policiesToCreate { + var createPolicyRequest = citrixorchestration.PolicyRequest{} + createPolicyRequest.SetName(policyToCreate.Name.ValueString()) + createPolicyRequest.SetDescription(policyToCreate.Description.ValueString()) + createPolicyRequest.SetIsEnabled(policyToCreate.Enabled.ValueBool()) + // Add Policy Settings + policySettings := []citrixorchestration.SettingRequest{} + policySettingsToCreate := util.ObjectSetToTypedArray[PolicySettingModel](ctx, diags, policyToCreate.PolicySettings) + for _, policySetting := range policySettingsToCreate { + settingRequest := constructSettingRequest(policySetting) + policySettings = append(policySettings, settingRequest) + } + createPolicyRequest.SetSettings(policySettings) + + // Add Policy Filters + policyFilters, err := constructPolicyFilterRequests(ctx, diags, client, policyToCreate) + if err != nil { + return batchRequestModel, err + } + createPolicyRequest.SetFilters(policyFilters) + + createPolicyRequestBodyString, err := util.ConvertToString(createPolicyRequest) + if err != nil { + diags.AddError( + "Error adding Policy "+policyToCreate.Name.ValueString()+" to Policy Set "+policySetName, + "An unexpected error occurred: "+err.Error(), + ) + return batchRequestModel, err + } + + relativeUrl := fmt.Sprintf("/gpo/policies?policySetGuid=%s", policySetGuid) + + var batchRequestItem citrixorchestration.BatchRequestItemModel + batchRequestItem.SetReference(fmt.Sprintf("createPolicy%d", policyIndex)) + batchRequestItem.SetMethod(http.MethodPost) + batchRequestItem.SetRelativeUrl(client.GetBatchRequestItemRelativeUrl(relativeUrl)) + batchRequestItem.SetHeaders(batchApiHeaders) + batchRequestItem.SetBody(createPolicyRequestBodyString) + batchRequestItems = append(batchRequestItems, batchRequestItem) + } + + batchRequestModel.SetItems(batchRequestItems) + return batchRequestModel, nil +} + +func constructPolicyFilterRequests(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, policy PolicyModel) ([]citrixorchestration.FilterRequest, error) { + filterRequests := []citrixorchestration.FilterRequest{} + + serverValue := "" + if client.AuthConfig.OnPremises || !client.AuthConfig.ApiGateway { + serverValue = client.ApiClient.GetConfig().Host + } else { + serverValue = fmt.Sprintf("%s.xendesktop.net", client.ClientConfig.CustomerId) + } + + if !policy.AccessControlFilters.IsNull() && len(policy.AccessControlFilters.Elements()) > 0 { + accessControlFilters := util.ObjectSetToTypedArray[AccessControlFilterModel](ctx, diagnostics, policy.AccessControlFilters) + for _, accessControlFilter := range accessControlFilters { + filterRequest, err := accessControlFilter.GetFilterRequest(diagnostics, serverValue) + if err != nil { + return filterRequests, err + } + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.BranchRepeaterFilter.IsNull() { + branchRepeaterFilter := util.ObjectValueToTypedObject[BranchRepeaterFilterModel](ctx, diagnostics, policy.BranchRepeaterFilter) + branchRepeaterFilterRequest, _ := branchRepeaterFilter.GetFilterRequest(diagnostics, serverValue) + filterRequests = append(filterRequests, branchRepeaterFilterRequest) + } + + if !policy.ClientIPFilters.IsNull() && len(policy.ClientIPFilters.Elements()) > 0 { + clientIpFilters := util.ObjectSetToTypedArray[ClientIPFilterModel](ctx, diagnostics, policy.ClientIPFilters) + for _, clientIpFilter := range clientIpFilters { + filterRequest, _ := clientIpFilter.GetFilterRequest(diagnostics, serverValue) + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.ClientNameFilters.IsNull() && len(policy.ClientNameFilters.Elements()) > 0 { + clientNameFilters := util.ObjectSetToTypedArray[ClientNameFilterModel](ctx, diagnostics, policy.ClientNameFilters) + for _, clientNameFilter := range clientNameFilters { + filterRequest, _ := clientNameFilter.GetFilterRequest(diagnostics, serverValue) + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.DeliveryGroupFilters.IsNull() && len(policy.DeliveryGroupFilters.Elements()) > 0 { + deliveryGroupFilters := util.ObjectSetToTypedArray[DeliveryGroupFilterModel](ctx, diagnostics, policy.DeliveryGroupFilters) + for _, deliveryGroupFilter := range deliveryGroupFilters { + filterRequest, err := deliveryGroupFilter.GetFilterRequest(diagnostics, serverValue) + if err != nil { + return filterRequests, err + } + + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.DeliveryGroupTypeFilters.IsNull() && len(policy.DeliveryGroupTypeFilters.Elements()) > 0 { + deliveryGroupTypeFilters := util.ObjectSetToTypedArray[DeliveryGroupTypeFilterModel](ctx, diagnostics, policy.DeliveryGroupTypeFilters) + for _, deliveryGroupTypeFilter := range deliveryGroupTypeFilters { + filterRequest, _ := deliveryGroupTypeFilter.GetFilterRequest(diagnostics, serverValue) + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.TagFilters.IsNull() && len(policy.TagFilters.Elements()) > 0 { + tagFilters := util.ObjectSetToTypedArray[TagFilterModel](ctx, diagnostics, policy.TagFilters) + for _, tagFilter := range tagFilters { + filterRequest, err := tagFilter.GetFilterRequest(diagnostics, serverValue) + if err != nil { + return filterRequests, err + } + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.OuFilters.IsNull() && len(policy.OuFilters.Elements()) > 0 { + ouFilters := util.ObjectSetToTypedArray[OuFilterModel](ctx, diagnostics, policy.OuFilters) + for _, ouFilter := range ouFilters { + filterRequest, _ := ouFilter.GetFilterRequest(diagnostics, serverValue) + filterRequests = append(filterRequests, filterRequest) + } + } + + if !policy.UserFilters.IsNull() && len(policy.UserFilters.Elements()) > 0 { + userFilters := util.ObjectSetToTypedArray[UserFilterModel](ctx, diagnostics, policy.UserFilters) + for _, userFilter := range userFilters { + filterRequest, _ := userFilter.GetFilterRequest(diagnostics, serverValue) + filterRequests = append(filterRequests, filterRequest) + } + } + + return filterRequests, nil +} + +func constructPolicyPriorityRequest(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, policySet *citrixorchestration.PolicySetResponse, planedPolicies []PolicyModel) citrixorchestration.ApiGpoRankGpoPoliciesRequest { + // 1. Construct map of policy name: policy id + // 2. Construct array of policy id based on the policy name order + // 3. post policy priority + policyNameIdMap := map[types.String]types.String{} + if policySet.GetPolicies() != nil { + for _, policy := range policySet.GetPolicies() { + policyNameIdMap[types.StringValue(policy.GetPolicyName())] = types.StringValue(policy.GetPolicyGuid()) + } + } + policyPriority := []types.String{} + for _, policyToCreate := range planedPolicies { + policyPriority = append(policyPriority, policyNameIdMap[policyToCreate.Name]) + } + + policySetId := policySet.GetPolicySetGuid() + createPolicyPriorityRequest := client.ApiClient.GpoDAAS.GpoRankGpoPolicies(ctx) + createPolicyPriorityRequest = createPolicyPriorityRequest.PolicySetGuid(policySetId) + createPolicyPriorityRequest = createPolicyPriorityRequest.RequestBody(util.ConvertBaseStringArrayToPrimitiveStringArray(policyPriority)) + return createPolicyPriorityRequest +} + +func constructSettingRequest(policySetting PolicySettingModel) citrixorchestration.SettingRequest { + settingRequest := citrixorchestration.SettingRequest{} + settingRequest.SetSettingName(policySetting.Name.ValueString()) + settingRequest.SetUseDefault(policySetting.UseDefault.ValueBool()) + if policySetting.UseDefault.ValueBool() { + return settingRequest + } else if policySetting.Value.ValueString() != "" { + settingRequest.SetSettingValue(policySetting.Value.ValueString()) + } else { + if policySetting.Enabled.ValueBool() { + settingRequest.SetSettingValue("1") + } else { + settingRequest.SetSettingValue("0") + } + } + return settingRequest +} + +func updatePolicySettings(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyId string, policyName string, policySettingsInPlan []PolicySettingModel, policySettingsInState []PolicySettingModel) error { + // Delete policy settings + if len(policySettingsInState) > 0 { + err := deletePolicySettings(ctx, client, diagnostics, policyId) + if err != nil { + return err + } + } + + // Create policy settings + if len(policySettingsInPlan) > 0 { + err := createPolicySettings(ctx, client, diagnostics, policyId, policyName, policySettingsInPlan) + if err != nil { + return err + } + } + + return nil +} + +func createPolicySettings(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyId string, policyName string, policySettingsToCreate []PolicySettingModel) error { + // Batch create new policy settings + addPolicySettingBatchRequestItems := []citrixorchestration.BatchRequestItemModel{} + batchApiHeaders, httpResp, err := generateBatchApiHeaders(client) + if err != nil { + diagnostics.AddError( + "Error creating policy settings in policy "+policyName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nCould not create policy settings in the policy, unexpected error: "+util.ReadClientError(err), + ) + return err + } + for index, policySetting := range policySettingsToCreate { + relativeUrl := fmt.Sprintf("/gpo/settings?policyGuid=%s", policyId) + + settingRequest := constructSettingRequest(policySetting) + settingRequestStringBody, err := util.ConvertToString(settingRequest) + if err != nil { + diagnostics.AddError( + "Error adding policy setting to policy "+policyName, + "An unexpected error occurred: "+err.Error(), + ) + return err + } + + var batchRequestItem citrixorchestration.BatchRequestItemModel + batchRequestItem.SetReference(fmt.Sprintf("addPolicySetting%s", strconv.Itoa(index))) + batchRequestItem.SetMethod(http.MethodPost) + batchRequestItem.SetRelativeUrl(client.GetBatchRequestItemRelativeUrl(relativeUrl)) + batchRequestItem.SetHeaders(batchApiHeaders) + batchRequestItem.SetBody(settingRequestStringBody) + addPolicySettingBatchRequestItems = append(addPolicySettingBatchRequestItems, batchRequestItem) + } + + var batchRequestModel citrixorchestration.BatchRequestModel + batchRequestModel.SetItems(addPolicySettingBatchRequestItems) + successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, client, batchRequestModel) + if err != nil { + diagnostics.AddError( + "Error adding Policy Settings to Policy "+policyName, + "TransactionId: "+txId+ + "\nError message: "+util.ReadClientError(err), + ) + return err + } + + if successfulJobs < len(addPolicySettingBatchRequestItems) { + errMsg := fmt.Sprintf("An error occurred while adding policy settings to the Policy. %d of %d policy settings were added to the Policy.", successfulJobs, len(addPolicySettingBatchRequestItems)) + diagnostics.AddError( + "Error adding policy settings to Policy "+policyName, + "TransactionId: "+txId+ + "\n"+errMsg, + ) + return err + } + return nil +} + +func deletePolicySettings(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyId string) error { + // Setup batch requests + deletePolicySettingBatchRequestItems := []citrixorchestration.BatchRequestItemModel{} + batchApiHeaders, httpResp, err := generateBatchApiHeaders(client) + if err != nil { + diagnostics.AddError( + "Error deleting policy settings from policy "+policyId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nCould not delete policy settings from the policy, unexpected error: "+util.ReadClientError(err), + ) + return err + } + + policySettings, err := getPolicySettings(ctx, client, diagnostics, policyId) + if err != nil { + return err + } + // batch delete policy settings + for index, policySetting := range policySettings.GetItems() { + relativeUrl := fmt.Sprintf("/gpo/settings/%s", policySetting.GetSettingGuid()) + + var batchRequestItem citrixorchestration.BatchRequestItemModel + batchRequestItem.SetReference(fmt.Sprintf("removeSetting%s", strconv.Itoa(index))) + batchRequestItem.SetMethod(http.MethodDelete) + batchRequestItem.SetRelativeUrl(client.GetBatchRequestItemRelativeUrl(relativeUrl)) + batchRequestItem.SetHeaders(batchApiHeaders) + deletePolicySettingBatchRequestItems = append(deletePolicySettingBatchRequestItems, batchRequestItem) + } + + var deletePolicySettingBatchRequestModel citrixorchestration.BatchRequestModel + deletePolicySettingBatchRequestModel.SetItems(deletePolicySettingBatchRequestItems) + + successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, client, deletePolicySettingBatchRequestModel) + if err != nil { + diagnostics.AddError( + "Error deleting policy settings from Policy "+policyId, + "TransactionId: "+txId+ + "\nError message: "+util.ReadClientError(err), + ) + return err + } + + if successfulJobs < len(deletePolicySettingBatchRequestItems) { + errMsg := fmt.Sprintf("An error occurred while deleting policy settings from the Policy. %d of %d policy settings were deleted from the Policy.", successfulJobs, len(deletePolicySettingBatchRequestItems)) + diagnostics.AddError( + "Error deleting policy settings from Policy "+policyId, + "TransactionId: "+txId+ + "\n"+errMsg, + ) + + return err + } + return nil +} + +func updatePolicyFilters(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyId string, policyName string, policyFiltersInPlan []PolicyFilterInterface, policyFiltersInState []PolicyFilterInterface) error { + policyFilterIdsInPlan := []string{} + policyFiltersToCreate := []PolicyFilterInterface{} + policyFiltersToUpdate := []PolicyFilterInterface{} + for _, policyFilter := range policyFiltersInPlan { + if policyFilter.GetId() == "" { + policyFiltersToCreate = append(policyFiltersToCreate, policyFilter) + } else { + policyFilterIdsInPlan = append(policyFilterIdsInPlan, policyFilter.GetId()) + policyFiltersToUpdate = append(policyFiltersToUpdate, policyFilter) + } + } + + policyFilterIdsInState := []string{} + policyFilterIdMapFromState := map[string]PolicyFilterInterface{} + for _, policyFilter := range policyFiltersInState { + policyFilterIdMapFromState[strings.ToLower(policyFilter.GetId())] = policyFilter + policyFilterIdsInState = append(policyFilterIdsInState, policyFilter.GetId()) + } + + policyFilterIdsToDelete := []string{} + // Check if any policy settings are to be deleted + for _, policyFilterId := range policyFilterIdsInState { + if !slices.ContainsFunc(policyFilterIdsInPlan, func(policyFilterIdInPlan string) bool { + return strings.EqualFold(policyFilterId, policyFilterIdInPlan) + }) { + policyFilterIdsToDelete = append(policyFilterIdsToDelete, policyFilterId) + } + } + + // Delete policy filters + if len(policyFilterIdsToDelete) > 0 { + err := deletePolicyFilters(ctx, client, diagnostics, policyName, policyFilterIdsToDelete) + if err != nil { + return err + } + } + + serverValue := "" + if client.AuthConfig.OnPremises || !client.AuthConfig.ApiGateway { + serverValue = client.ApiClient.GetConfig().Host + } else { + serverValue = fmt.Sprintf("%s.xendesktop.net", client.ClientConfig.CustomerId) + } + + // Create policy filters + if len(policyFiltersToCreate) > 0 { + err := createPolicyFilters(ctx, client, diagnostics, policyId, policyName, serverValue, policyFiltersToCreate) + if err != nil { + return err + } + } + + // Update each policy filter + if len(policyFiltersToUpdate) > 0 { + for _, policyFilter := range policyFiltersToUpdate { + filterRequest, err := policyFilter.GetFilterRequest(diagnostics, serverValue) + if err != nil { + return err + } + editPolicyFilterRequest := client.ApiClient.GpoDAAS.GpoUpdateGpoFilter(ctx, policyFilter.GetId()) + editPolicyFilterRequest = editPolicyFilterRequest.FilterRequest(filterRequest) + + // Update policy setting + httpResp, err := citrixdaasclient.AddRequestData(editPolicyFilterRequest, client).Execute() + if err != nil { + diagnostics.AddError( + "Error Updating Policy Filter "+policyFilter.GetId(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return err + } + } + } + + return nil +} + +func createPolicyFilters(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyId string, policyName string, serverValue string, policyFiltersToCreate []PolicyFilterInterface) error { + // Batch create new policy filters + addPolicyFiltersBatchRequestItems := []citrixorchestration.BatchRequestItemModel{} + batchApiHeaders, httpResp, err := generateBatchApiHeaders(client) + if err != nil { + diagnostics.AddError( + "Error creating policy filters in policy "+policyName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nCould not create policy filters in the policy, unexpected error: "+util.ReadClientError(err), + ) + return err + } + for index, policyFilter := range policyFiltersToCreate { + relativeUrl := fmt.Sprintf("/gpo/filters?policyGuid=%s", policyId) + + filterRequest, err := policyFilter.GetFilterRequest(diagnostics, serverValue) + if err != nil { + return err + } + filterRequestStringBody, err := util.ConvertToString(filterRequest) + if err != nil { + diagnostics.AddError( + "Error adding policy filter to policy "+policyName, + "An unexpected error occurred: "+err.Error(), + ) + return err + } + + var batchRequestItem citrixorchestration.BatchRequestItemModel + batchRequestItem.SetReference(fmt.Sprintf("addPolicyFilter%s", strconv.Itoa(index))) + batchRequestItem.SetMethod(http.MethodPost) + batchRequestItem.SetRelativeUrl(client.GetBatchRequestItemRelativeUrl(relativeUrl)) + batchRequestItem.SetHeaders(batchApiHeaders) + batchRequestItem.SetBody(filterRequestStringBody) + addPolicyFiltersBatchRequestItems = append(addPolicyFiltersBatchRequestItems, batchRequestItem) + } + + var batchRequestModel citrixorchestration.BatchRequestModel + batchRequestModel.SetItems(addPolicyFiltersBatchRequestItems) + successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, client, batchRequestModel) + if err != nil { + diagnostics.AddError( + "Error adding Policy Filters to Policy "+policyName, + "TransactionId: "+txId+ + "\nError message: "+util.ReadClientError(err), + ) + return err + } + + if successfulJobs < len(addPolicyFiltersBatchRequestItems) { + errMsg := fmt.Sprintf("An error occurred while adding policy filters to the Policy. %d of %d policy filters were added to the Policy.", successfulJobs, len(addPolicyFiltersBatchRequestItems)) + diagnostics.AddError( + "Error adding policy filters to Policy "+policyName, + "TransactionId: "+txId+ + "\n"+errMsg, + ) + return err + } + return nil +} + +func deletePolicyFilters(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, policyName string, policyFilterIdsToDelete []string) error { + // Setup batch requests + deletePolicyFilterBatchRequestItems := []citrixorchestration.BatchRequestItemModel{} + batchApiHeaders, httpResp, err := generateBatchApiHeaders(client) + if err != nil { + diagnostics.AddError( + "Error deleting policy filters from policy "+policyName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nCould not delete policy filters from the policy, unexpected error: "+util.ReadClientError(err), + ) + return err + } + // batch delete policy filters + for index, policyFilterId := range policyFilterIdsToDelete { + relativeUrl := fmt.Sprintf("/gpo/filters/%s", policyFilterId) + + var batchRequestItem citrixorchestration.BatchRequestItemModel + batchRequestItem.SetReference(fmt.Sprintf("removeFilter%s", strconv.Itoa(index))) + batchRequestItem.SetMethod(http.MethodDelete) + batchRequestItem.SetRelativeUrl(client.GetBatchRequestItemRelativeUrl(relativeUrl)) + batchRequestItem.SetHeaders(batchApiHeaders) + deletePolicyFilterBatchRequestItems = append(deletePolicyFilterBatchRequestItems, batchRequestItem) + } + + var deletePolicyFilterBatchRequestModel citrixorchestration.BatchRequestModel + deletePolicyFilterBatchRequestModel.SetItems(deletePolicyFilterBatchRequestItems) + + successfulJobs, txId, err := citrixdaasclient.PerformBatchOperation(ctx, client, deletePolicyFilterBatchRequestModel) + if err != nil { + diagnostics.AddError( + "Error deleting policy filters from Policy "+policyName, + "TransactionId: "+txId+ + "\nError message: "+util.ReadClientError(err), + ) + return err + } + + if successfulJobs < len(deletePolicyFilterBatchRequestItems) { + errMsg := fmt.Sprintf("An error occurred while deleting policy filters from the Policy. %d of %d policy filters were deleted from the Policy.", successfulJobs, len(deletePolicyFilterBatchRequestItems)) + diagnostics.AddError( + "Error deleting policy filters from Policy "+policyName, + "TransactionId: "+txId+ + "\n"+errMsg, + ) + + return err + } + return nil +} diff --git a/internal/daas/tags/tag_resource.go b/internal/daas/tags/tag_resource.go index e2630e6..8530934 100644 --- a/internal/daas/tags/tag_resource.go +++ b/internal/daas/tags/tag_resource.go @@ -243,6 +243,11 @@ func (r *TagResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanReq return } + // Skip modify plan when doing destroy action + if req.Plan.Raw.IsNull() { + return + } + create := req.State.Raw.IsNull() var plan TagResourceModel diff --git a/internal/examples/data-sources/citrix_wem_configuration_set/data-source.tf b/internal/examples/data-sources/citrix_wem_configuration_set/data-source.tf new file mode 100644 index 0000000..a81f6c0 --- /dev/null +++ b/internal/examples/data-sources/citrix_wem_configuration_set/data-source.tf @@ -0,0 +1,9 @@ +# Get WEM configuration set by id +data "citrix_wem_configuration_set" "test_wem_configuration_set_by_id" { + id = "1" +} + +# Get WEM configuration set by name +data "citrix_wem_configuration_set" "test_wem_configuration_set_by_name" { + name = "Default Site" +} \ No newline at end of file diff --git a/internal/examples/resources/citrix_desktop_icon/import.sh b/internal/examples/resources/citrix_desktop_icon/import.sh new file mode 100644 index 0000000..6467654 --- /dev/null +++ b/internal/examples/resources/citrix_desktop_icon/import.sh @@ -0,0 +1,2 @@ +# Desktop icon can be imported by specifying the GUID +terraform import citrix_desktop_icon.example-desktop-icon 4cec0568-1c91-407f-a32e-cc487822d0a0 \ No newline at end of file diff --git a/internal/examples/resources/citrix_desktop_icon/resource.tf b/internal/examples/resources/citrix_desktop_icon/resource.tf new file mode 100644 index 0000000..67f1345 --- /dev/null +++ b/internal/examples/resources/citrix_desktop_icon/resource.tf @@ -0,0 +1,4 @@ +resource "citrix_desktop_icon" "example-desktop-icon" { + raw_data = filebase64("path/to/desktopicon.ico") +} +# Use filebase64 to encode a file's content in base64 format. \ No newline at end of file diff --git a/internal/examples/resources/citrix_policy_set/resource.tf b/internal/examples/resources/citrix_policy_set/resource.tf index 9e36e32..e8488f0 100644 --- a/internal/examples/resources/citrix_policy_set/resource.tf +++ b/internal/examples/resources/citrix_policy_set/resource.tf @@ -25,7 +25,6 @@ resource "citrix_policy_set" "example-policy-set" { }, ] branch_repeater_filter = { - enabled = true allowed = true }, client_ip_filters = [ diff --git a/internal/examples/resources/citrix_wem_configuration_set/import.sh b/internal/examples/resources/citrix_wem_configuration_set/import.sh new file mode 100644 index 0000000..ed58e65 --- /dev/null +++ b/internal/examples/resources/citrix_wem_configuration_set/import.sh @@ -0,0 +1,2 @@ +# WEM Configuration Set can be imported by specifying the ID +terraform import citrix_wem_configuration_set.example-config-set 1234 diff --git a/internal/examples/resources/citrix_wem_configuration_set/resource.tf b/internal/examples/resources/citrix_wem_configuration_set/resource.tf new file mode 100644 index 0000000..c5da5d8 --- /dev/null +++ b/internal/examples/resources/citrix_wem_configuration_set/resource.tf @@ -0,0 +1,4 @@ +resource "citrix_wem_configuration_set" "example-config-set"{ + name = "example config set" + description = "example WEM configuration set" +} diff --git a/internal/examples/resources/citrix_wem_directory_object/import.sh b/internal/examples/resources/citrix_wem_directory_object/import.sh new file mode 100644 index 0000000..5d6a0b1 --- /dev/null +++ b/internal/examples/resources/citrix_wem_directory_object/import.sh @@ -0,0 +1,2 @@ +# WEM Directory Object can be imported by specifying the ID +terraform import citrix_wem_directory_object.example-directory-object 1234 diff --git a/internal/examples/resources/citrix_wem_directory_object/resource.tf b/internal/examples/resources/citrix_wem_directory_object/resource.tf new file mode 100644 index 0000000..9a0c88e --- /dev/null +++ b/internal/examples/resources/citrix_wem_directory_object/resource.tf @@ -0,0 +1,5 @@ +resource "citrix_wem_directory_object" "example-directory-object" { + configuration_set_id = citrix_wem_configuration_set.example-config-set.id + machine_catalog_id = citrix_machine_catalog.example-machine-catalog.id + enabled = true +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e2e8c6e..db81484 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -29,6 +29,7 @@ import ( "github.com/citrix/terraform-provider-citrix/internal/daas/application" "github.com/citrix/terraform-provider-citrix/internal/daas/bearer_token" "github.com/citrix/terraform-provider-citrix/internal/daas/cvad_site" + "github.com/citrix/terraform-provider-citrix/internal/daas/desktop_icon" "github.com/citrix/terraform-provider-citrix/internal/daas/storefront_server" "github.com/citrix/terraform-provider-citrix/internal/daas/tags" "github.com/citrix/terraform-provider-citrix/internal/daas/vda" @@ -44,6 +45,9 @@ import ( "github.com/citrix/terraform-provider-citrix/internal/storefront/stf_store" "github.com/citrix/terraform-provider-citrix/internal/storefront/stf_webreceiver" + "github.com/citrix/terraform-provider-citrix/internal/wem/wem_machine_ad_object" + "github.com/citrix/terraform-provider-citrix/internal/wem/wem_site" + "github.com/citrix/terraform-provider-citrix/internal/daas/admin_folder" "github.com/citrix/terraform-provider-citrix/internal/daas/admin_scope" "github.com/citrix/terraform-provider-citrix/internal/daas/delivery_group" @@ -609,6 +613,21 @@ func validateAndInitializeDaaSClient(ctx context.Context, resp *provider.Configu cwsHostName = "cws.ctxwsstgapi.us" } + wemHostName := "" + if environment == "Production" { + wemHostName = "api.wem.cloud.com" + } else if environment == "Staging" { + wemHostName = "api.wem.cloudburrito.com" + } else if environment == "Japan" { + wemHostName = "api.wem.citrixcloud.jp" + } else if environment == "JapanStaging" { + wemHostName = "api.wem.citrixcloudstaging.jp" + } else if environment == "Gov" { + wemHostName = "api.wem.citrixworkspacesapi.us" + } else if environment == "GovStaging" { + wemHostName = "api.wem.ctxwsstgapi.us" + } + ctx = tflog.SetField(ctx, "citrix_hostname", hostname) if !onPremises { ctx = tflog.SetField(ctx, "citrix_customer_id", customerId) @@ -704,6 +723,10 @@ func validateAndInitializeDaaSClient(ctx context.Context, resp *provider.Configu if cwsHostName != "" { client.InitializeCwsClient(ctx, cwsHostName, middleware.MiddlewareAuthFunc) } + // Set WEM Client + if wemHostName != "" { + client.InitializeWemClient(ctx, wemHostName, middleware.MiddlewareAuthFunc) + } } func handleNetworkError(err error, resp *provider.ConfigureResponse) { @@ -779,6 +802,8 @@ func (p *citrixProvider) DataSources(_ context.Context) []func() datasource.Data cc_identity_providers.NewSamlIdentityProviderDataSource, // CC Resource Locations resource_locations.NewResourceLocationsDataSource, + // WEM + wem_site.NewWemSiteDataSource, } } @@ -806,6 +831,7 @@ func (p *citrixProvider) Resources(_ context.Context) []func() resource.Resource application.NewApplicationResource, application.NewApplicationGroupResource, application.NewApplicationIconResource, + desktop_icon.NewDesktopIconResource, admin_folder.NewAdminFolderResource, admin_role.NewAdminRoleResource, admin_scope.NewAdminScopeResource, @@ -831,6 +857,9 @@ func (p *citrixProvider) Resources(_ context.Context) []func() resource.Resource cc_identity_providers.NewGoogleIdentityProviderResource, cc_identity_providers.NewOktaIdentityProviderResource, cc_identity_providers.NewSamlIdentityProviderResource, + // Wem Resources + wem_site.NewWemSiteServiceResource, + wem_machine_ad_object.NewWemDirectoryResource, // Add resource here } } diff --git a/internal/test/azure_mcs_suite_test.go b/internal/test/azure_mcs_suite_test.go index ab6e1d6..ee34543 100644 --- a/internal/test/azure_mcs_suite_test.go +++ b/internal/test/azure_mcs_suite_test.go @@ -92,6 +92,7 @@ func TestAzureMcs(t *testing.T) { TestDeliveryGroupPreCheck(t) TestAdminFolderPreCheck(t) TestApplicationResourcePreCheck(t) + TestPolicySetResourcePreCheck(t) }, Steps: []resource.TestStep{ /****************** Zone Test ******************/ @@ -553,7 +554,7 @@ func TestAzureMcs(t *testing.T) { Config: composeTestResourceTf( BuildAdminRoleResource(t, adminRoleTestResource), BuildAdminScopeResource(t, adminScopeTestResource), - // BuildPolicySetResource(t, policy_set_testResource), + BuildPolicySetResource(t, policy_set_testResource), BuildApplicationResource(t, testApplicationResource), BuildAdminFolderResourceWithTwoTypes(t, testAdminFolderResource_twoTypes, "ContainsMachineCatalogs", "ContainsApplications"), BuildDeliveryGroupResource(t, testDeliveryGroupResources), @@ -574,26 +575,37 @@ func TestAzureMcs(t *testing.T) { resource.TestCheckResourceAttr("citrix_application.testApplication", "installed_app_properties.command_line_executable", "test.exe"), // /*** Verify Policy Set ***/ - // // Verify name of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "name", os.Getenv("TEST_POLICY_SET_NAME")+"-1"), - // // Verify description of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "description", "Test policy set description"), - // // Verify type of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "type", "DeliveryGroupPolicies"), - // // Verify the number of scopes of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "scopes.#", "0"), - // // Verify the number of policies in the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.#", "2"), - // // Verify name of the first policy in the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.name", "first-test-policy"), - // // Verify policy settings of the first policy in the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.#", "2"), - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.0.name", "AdvanceWarningPeriod"), - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.0.value", "13:00:00"), - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.1.name", "AllowFileDownload"), - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.1.enabled", "true"), - // // Verify name of the second policy in the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.1.name", "second-test-policy"), + // Verify name of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "name", os.Getenv("TEST_POLICY_SET_NAME")+"-1"), + // Verify description of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "description", "Test policy set description"), + // Verify type of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "type", "DeliveryGroupPolicies"), + // Verify the number of scopes of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "scopes.#", "0"), + // Verify the number of policies in the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.#", "2"), + // Verify name of the first policy in the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.name", "first-test-policy"), + // Verify policy settings of the first policy in the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs("citrix_policy_set.testPolicySet", "policies.0.policy_settings.*", map[string]string{ + "name": "AdvanceWarningPeriod", + "use_default": "false", + "value": "13:00:00", + }), + resource.TestCheckTypeSetElemNestedAttrs("citrix_policy_set.testPolicySet", "policies.0.policy_settings.*", map[string]string{ + "name": "AllowFileDownload", + "enabled": "true", + "use_default": "false", + }), + // Verify name of the second policy in the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.1.name", "second-test-policy"), + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.1.policy_settings.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs("citrix_policy_set.testPolicySet", "policies.1.policy_settings.*", map[string]string{ + "name": "AdvanceWarningPeriod", + "use_default": "true", + }), /*** Verify Admin Scope ***/ // Verify the name of the admin scope @@ -626,15 +638,15 @@ func TestAzureMcs(t *testing.T) { // API, therefore there is no value for it during import. ImportStateVerifyIgnore: []string{"delivery_groups", "installed_app_properties"}, }, - // // ImportState testing - Policy Set - // { - // ResourceName: "citrix_policy_set.testPolicySet", - // ImportState: true, - // ImportStateVerify: true, - // // The last_updated attribute does not exist in the Orchestration - // // API, therefore there is no value for it during import. - // ImportStateVerifyIgnore: []string{"last_updated"}, - // }, + // ImportState testing + { + ResourceName: "citrix_policy_set.testPolicySet", + ImportState: true, + ImportStateVerify: true, + // The last_updated attribute does not exist in the Orchestration + // API, therefore there is no value for it during import. + ImportStateVerifyIgnore: []string{"last_updated"}, + }, // ImportState testing - Admin Scope { ResourceName: "citrix_admin_scope.test_scope", @@ -659,7 +671,7 @@ func TestAzureMcs(t *testing.T) { Config: composeTestResourceTf( BuildAdminRoleResource(t, adminRoleTestResource_updated), BuildAdminScopeResource(t, adminScopeTestResource_updated), - // BuildPolicySetResource(t, policy_set_updated_testResource), + BuildPolicySetResource(t, policy_set_updated_testResource), BuildApplicationResource(t, testApplicationResource_updated), BuildAdminFolderResourceWithTwoTypes(t, testAdminFolderResource_twoTypes, "ContainsMachineCatalogs", "ContainsApplications"), BuildDeliveryGroupResource(t, testDeliveryGroupResources), @@ -681,19 +693,18 @@ func TestAzureMcs(t *testing.T) { // Verify the application folder path resource.TestCheckResourceAttr("citrix_application.testApplication", "application_folder_path", folder_name_2), - // /*** Verify Policy Set ***/ - // // Verify name of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "name", os.Getenv("TEST_POLICY_SET_NAME")+"-3"), - // // Verify description of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "description", "Test policy set description updated"), - // // Verify type of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "type", "DeliveryGroupPolicies"), - // // Verify the number of scopes of the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "scopes.#", "0"), - // // Verify the number of policies in the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.#", "1"), - // // Verify name of the second policy in the policy set - // resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.name", "first-test-policy"), + // Verify name of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "name", os.Getenv("TEST_POLICY_SET_NAME")+"-3"), + // Verify description of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "description", "Test policy set description updated"), + // Verify type of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "type", "DeliveryGroupPolicies"), + // Verify the number of scopes of the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "scopes.#", "0"), + // Verify the number of policies in the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.#", "1"), + // Verify name of the second policy in the policy set + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.name", "first-test-policy"), /*** Verify Admin Scope ***/ // Verify the name of the admin scope diff --git a/internal/test/delivery_group_test.go b/internal/test/delivery_group_test.go index af4b442..641915d 100644 --- a/internal/test/delivery_group_test.go +++ b/internal/test/delivery_group_test.go @@ -112,6 +112,8 @@ func TestDeliveryGroupResourceAzureRM(t *testing.T) { resource.TestCheckResourceAttr("citrix_delivery_group.testDeliveryGroup", "desktops.#", "1"), // Verify number of reboot schedules resource.TestCheckResourceAttr("citrix_delivery_group.testDeliveryGroup", "reboot_schedules.#", "1"), + // Verify number of reboot schedules + resource.TestCheckResourceAttr("citrix_delivery_group.testDeliveryGroup", "reboot_schedules.ignore_maintenance_mode", "false"), // Verify total number of machines in delivery group resource.TestCheckResourceAttr("citrix_delivery_group.testDeliveryGroup", "total_machines", "2"), // Verify the policy set id assigned to the delivery group @@ -308,7 +310,7 @@ resource "citrix_delivery_group" "testDeliveryGroup" { start_time = "12:12" start_date = "2024-05-25" reboot_duration_minutes = 0 - ignore_maintenance_mode = true + ignore_maintenance_mode = false natural_reboot_schedule = false } ] diff --git a/internal/test/policy_set_resource_test.go b/internal/test/policy_set_resource_test.go index 1963c67..c6ddf3a 100644 --- a/internal/test/policy_set_resource_test.go +++ b/internal/test/policy_set_resource_test.go @@ -22,6 +22,13 @@ resource "citrix_policy_set" "testPolicySet" { description = "First test policy with priority 0" enabled = true policy_settings = [ + { + name = "VirtualChannelWhiteList" + value = jsonencode([ + "=disabled=" + ]) + use_default = false + }, { name = "AdvanceWarningPeriod" value = "13:00:00" @@ -48,8 +55,7 @@ resource "citrix_policy_set" "testPolicySet" { policy_settings = [ { name = "AdvanceWarningPeriod" - value = "17:00:00" - use_default = false + use_default = true }, ] } @@ -70,8 +76,7 @@ resource "citrix_policy_set" "testPolicySet" { policy_settings = [ { name = "AdvanceWarningPeriod" - value = "17:00:00" - use_default = false + use_default = true }, ] }, @@ -179,21 +184,23 @@ func TestPolicySetResource(t *testing.T) { // Verify name of the first policy in the policy set resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.name", "first-test-policy"), // Verify policy settings of the first policy in the policy set - resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.#", "2"), + resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.0.policy_settings.#", "3"), resource.TestCheckTypeSetElemNestedAttrs("citrix_policy_set.testPolicySet", "policies.0.policy_settings.*", map[string]string{ - "name": "AdvanceWarningPeriod", - "value": "13:00:00", + "name": "AdvanceWarningPeriod", + "use_default": "false", + "value": "13:00:00", }), resource.TestCheckTypeSetElemNestedAttrs("citrix_policy_set.testPolicySet", "policies.0.policy_settings.*", map[string]string{ - "name": "AllowFileDownload", - "enabled": "true", + "name": "AllowFileDownload", + "enabled": "true", + "use_default": "false", }), // Verify name of the second policy in the policy set resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.1.name", "second-test-policy"), resource.TestCheckResourceAttr("citrix_policy_set.testPolicySet", "policies.1.policy_settings.#", "1"), resource.TestCheckTypeSetElemNestedAttrs("citrix_policy_set.testPolicySet", "policies.1.policy_settings.*", map[string]string{ - "name": "AdvanceWarningPeriod", - "value": "17:00:00", + "name": "AdvanceWarningPeriod", + "use_default": "true", }), ), }, diff --git a/internal/test/wem_directory_object_resource_test.go b/internal/test/wem_directory_object_resource_test.go new file mode 100644 index 0000000..410e645 --- /dev/null +++ b/internal/test/wem_directory_object_resource_test.go @@ -0,0 +1,89 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestDirectoryObject(t *testing.T) { + zoneInput := os.Getenv("TEST_ZONE_INPUT_AZURE") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestHypervisorPreCheck_Azure(t) + TestHypervisorResourcePoolPreCheck_Azure(t) + TestMachineCatalogPreCheck_Azure(t) + TestWemSiteResourcePreCheck(t) + }, + Steps: []resource.TestStep{ + // Create and read test + { + Config: composeTestResourceTf( + BuildDirectoryObjectResource(t, wem_directory_object_test_resource), + BuildWemSiteResource(t), + BuildMachineCatalogResourceWorkgroup(t, machinecatalog_testResources_workgroup), + BuildHypervisorResourcePoolResourceAzure(t, hypervisor_resource_pool_testResource_azure), + BuildHypervisorResourceAzure(t, hypervisor_testResources), + BuildZoneResource(t, zoneInput, false), + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify if the directory object is created and enabled + resource.TestCheckResourceAttr("citrix_wem_directory_object.test_wem_directory", "enabled", "true"), + ), + }, + // Import test + { + ResourceName: "citrix_wem_directory_object.test_wem_directory", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{}, + }, + // Update and Read test + { + Config: composeTestResourceTf( + BuildDirectoryObjectResource(t, wem_directory_object_test_resource_updated), + BuildWemSiteResource(t), + BuildMachineCatalogResourceWorkgroup(t, machinecatalog_testResources_workgroup), + BuildHypervisorResourcePoolResourceAzure(t, hypervisor_resource_pool_testResource_azure), + BuildHypervisorResourceAzure(t, hypervisor_testResources), + BuildZoneResource(t, zoneInput, false), + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify if the directory object is disabled + resource.TestCheckResourceAttr("citrix_wem_directory_object.test_wem_directory", "enabled", "false"), + ), + }, + }, + }) +} + +func BuildDirectoryObjectResource(t *testing.T, directoryResource string) string { + return fmt.Sprintf(directoryResource) +} + +var ( + wem_directory_object_test_resource = ` + resource "citrix_wem_directory_object" "test_wem_directory" { + configuration_set_id = citrix_wem_configuration_set.test_wem_site.id + machine_catalog_id = citrix_machine_catalog.testMachineCatalog-WG.id + enabled = true + } + ` +) + +var ( + wem_directory_object_test_resource_updated = ` + resource "citrix_wem_directory_object" "test_wem_directory" { + configuration_set_id = citrix_wem_configuration_set.test_wem_site.id + machine_catalog_id = citrix_machine_catalog.testMachineCatalog-WG.id + enabled = false + } + ` +) diff --git a/internal/test/wem_site_service_data_source_test.go b/internal/test/wem_site_service_data_source_test.go new file mode 100644 index 0000000..5698d41 --- /dev/null +++ b/internal/test/wem_site_service_data_source_test.go @@ -0,0 +1,82 @@ +// Copyright © 2024. Citrix Systems, Inc. +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestWemSiteDataSourcePreCheck(t *testing.T) { + + if v := os.Getenv("TEST_WEM_SITE_DATA_SOURCE_ID"); v == "" { + t.Fatal("TEST_WEM_SITE_DATA_SOURCE_ID must be set for acceptance tests") + } + if v := os.Getenv("TEST_WEM_SITE_DATA_SOURCE_NAME"); v == "" { + t.Fatal("TEST_WEM_SITE_DATA_SOURCE_NAME must be set for acceptance tests") + } + if v := os.Getenv("TEST_WEM_SITE_DATA_SOURCE_DESCRIPTION"); v == "" { + t.Fatal("TEST_WEM_SITE_DATA_SOURCE_DESCRIPTION must be set for acceptance tests") + } +} + +func TestWemSiteDataSource(t *testing.T) { + customerId := os.Getenv("CITRIX_CUSTOMER_ID") + isOnPremises := true + if customerId != "" && customerId != "CitrixOnPremises" { + // Tests being run in cloud env + isOnPremises = false + } + + id := os.Getenv("TEST_WEM_SITE_DATA_SOURCE_ID") + name := os.Getenv("TEST_WEM_SITE_DATA_SOURCE_NAME") + description := os.Getenv("TEST_WEM_SITE_DATA_SOURCE_DESCRIPTION") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestWemSiteDataSourcePreCheck(t) + }, + Steps: []resource.TestStep{ + { + Config: BuildWemSiteDataSource(t, wem_site_data_source_using_id, id), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.citrix_wem_configuration_set.test_wem_site", "id", id), + resource.TestCheckResourceAttr("data.citrix_wem_configuration_set.test_wem_site", "name", name), + resource.TestCheckResourceAttr("data.citrix_wem_configuration_set.test_wem_site", "description", description), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + { + Config: BuildWemSiteDataSource(t, wem_site_data_source_using_name, name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.citrix_wem_configuration_set.test_wem_site", "id", id), + resource.TestCheckResourceAttr("data.citrix_wem_configuration_set.test_wem_site", "name", name), + resource.TestCheckResourceAttr("data.citrix_wem_configuration_set.test_wem_site", "description", description), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + }, + }) +} + +func BuildWemSiteDataSource(t *testing.T, wemSiteDataSource string, idOrName string) string { + return fmt.Sprintf(wemSiteDataSource, idOrName) +} + +var ( + wem_site_data_source_using_id = ` + data "citrix_wem_configuration_set" "test_wem_site" { + id = "%s" + } + ` + + wem_site_data_source_using_name = ` + data "citrix_wem_configuration_set" "test_wem_site" { + name = "%s" + } + ` +) diff --git a/internal/test/wem_site_service_resource_test.go b/internal/test/wem_site_service_resource_test.go new file mode 100644 index 0000000..bd90553 --- /dev/null +++ b/internal/test/wem_site_service_resource_test.go @@ -0,0 +1,92 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// TestWemSiteResourcePreCheck validates the necessary env variable exist in the testing environment +func TestWemSiteResourcePreCheck(t *testing.T) { + if v := os.Getenv("TEST_WEM_SITE_RESOURCE_NAME"); v == "" { + t.Fatal("TEST_WEM_SITE_RESOURCE_NAME must be set for acceptance tests") + } + + if v := os.Getenv("TEST_WEM_SITE_RESOURCE_DESCRIPTION"); v == "" { + t.Fatal("TEST_WEM_SITE_RESOURCE_DESCRIPTION must be set for acceptance tests") + } +} + +func TestWemSiteResource(t *testing.T) { + wemSiteName := os.Getenv("TEST_WEM_SITE_RESOURCE_NAME") + wemSiteDescription := os.Getenv("TEST_WEM_SITE_RESOURCE_DESCRIPTION") + + wemSiteName_Updated := os.Getenv("TEST_WEM_SITE_RESOURCE_NAME") + "-updated" + wemSiteDescription_Updated := os.Getenv("TEST_WEM_SITE_RESOURCE_DESCRIPTION") + " description updated" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestWemSiteResourcePreCheck(t) + }, + Steps: []resource.TestStep{ + // Create and read test + { + Config: BuildWemSiteResource(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the name of the wem site + resource.TestCheckResourceAttr("citrix_wem_configuration_set.test_wem_site", "name", wemSiteName), + // Verify the description of wem site + resource.TestCheckResourceAttr("citrix_wem_configuration_set.test_wem_site", "description", wemSiteDescription), + ), + }, + // Import test + { + ResourceName: "citrix_wem_configuration_set.test_wem_site", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{}, + }, + // Update and Read test + { + Config: BuildWemSiteResource_Updated(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the wem site + resource.TestCheckResourceAttrSet("citrix_wem_configuration_set.test_wem_site", "id"), + // Verify the name of the wem site + resource.TestCheckResourceAttr("citrix_wem_configuration_set.test_wem_site", "name", wemSiteName_Updated), + // Verify the description of wem site + resource.TestCheckResourceAttr("citrix_wem_configuration_set.test_wem_site", "description", wemSiteDescription_Updated), + ), + }, + }, + }) +} + +func BuildWemSiteResource(t *testing.T) string { + wemSiteName := os.Getenv("TEST_WEM_SITE_RESOURCE_NAME") + wemSiteDescription := os.Getenv("TEST_WEM_SITE_RESOURCE_DESCRIPTION") + + return fmt.Sprintf(wem_site_test_resource, wemSiteName, wemSiteDescription) +} + +func BuildWemSiteResource_Updated(t *testing.T) string { + wemSiteName := os.Getenv("TEST_WEM_SITE_RESOURCE_NAME") + "-updated" + wemSiteDescription := os.Getenv("TEST_WEM_SITE_RESOURCE_DESCRIPTION") + " description updated" + + return fmt.Sprintf(wem_site_test_resource, wemSiteName, wemSiteDescription) +} + +var ( + wem_site_test_resource = ` + resource "citrix_wem_configuration_set" "test_wem_site" { + name = "%s" + description = "%s" + } + ` +) diff --git a/internal/util/common.go b/internal/util/common.go index d96e599..372cbe8 100644 --- a/internal/util/common.go +++ b/internal/util/common.go @@ -124,7 +124,7 @@ const EmailRegex string = `^[\w-\.]+@([\w-]+\.)+[\w-]+$` const OktaDomainRegex string = `\.okta\.com$|\.okta-eu\.com$|\.oktapreview\.com$` // Application Category Path -const AppCategoryPathRegex string = `^([a-zA-Z0-9 ]+\\)*[a-zA-Z0-9 ]+\\?$` +const AppCategoryPathRegex string = `^([a-zA-Z0-9 ]+\\)*[a-zA-Z0-9 ]+\\?$|^$` // SAML 2.0 Identity Provider Certificate REGEX const SamlIdpCertRegex string = `\.[Pp][Ee][Mm]$|\.[Cc][Rr][Tt]$|\.[Cc][Ee][Rr]$` @@ -133,6 +133,9 @@ const SamlIdpCertRegex string = `\.[Pp][Ee][Mm]$|\.[Cc][Rr][Tt]$|\.[Cc][Ee][Rr]$ const AdminFolderPathWithBackslashRegex string = `^[^\\].*[^\\]$` const AdminFolderPathSpecialCharactersRegex string = `^[^/;:#.*?=<>|[\](){}"'\` + "`~]+$" +// String REGEX without trailing and leading whitespace +const StringWithoutTrailingLeadingWhitespaceRegex string = `^\S(.*\S)?$` + // Check if it does not contain path separator const NoPathRegex = `^[^\\/]*$` @@ -769,7 +772,7 @@ func RefreshList(state []string, remote []string) []string { // // Global panic handler to catch all unexpected errors to prevent provider from crashing. -// Writes crash stack into local txt file for troubleshooting, and displays error message in Terrafor Diagnostics. +// Writes crash stack into local txt file for troubleshooting, and displays error message in Terraform Diagnostics. // // Terraform Diagnostics from context func PanicHandler(diagnostics *diag.Diagnostics) { diff --git a/internal/validators/also_requires_on_values_validator.go b/internal/validators/also_requires_on_values_validator.go index 02e0a2c..1831bc9 100644 --- a/internal/validators/also_requires_on_values_validator.go +++ b/internal/validators/also_requires_on_values_validator.go @@ -34,13 +34,13 @@ func (v AlsoRequiresOnValuesValidator) Description(ctx context.Context) string { // MarkdownDescription implements validator.String. func (v AlsoRequiresOnValuesValidator) MarkdownDescription(context.Context) string { if len(v.OnStringValues) > 0 { - return fmt.Sprintf("If the current attribute is set to one of [%s], the following also need to be set: %q", strings.Join(v.OnStringValues, ","), v.PathExpressions) + return fmt.Sprintf("If the current attribute is set to one of [%s], all of the following also need to be set: %q", strings.Join(v.OnStringValues, ","), v.PathExpressions) } else if len(v.OnBoolValues) > 0 { boolValueArray := []string{} for _, boolValue := range v.OnBoolValues { boolValueArray = append(boolValueArray, strconv.FormatBool(boolValue)) } - return fmt.Sprintf("If the current attribute is set to one of [%v], the following also need to be set: %q", strings.Join(boolValueArray, ","), v.PathExpressions) + return fmt.Sprintf("If the current attribute is set to one of [%v], all of the following also need to be set: %q", strings.Join(boolValueArray, ","), v.PathExpressions) } return "" } diff --git a/internal/validators/also_requires_one_of_on_values_validator.go b/internal/validators/also_requires_one_of_on_values_validator.go new file mode 100644 index 0000000..f6639c6 --- /dev/null +++ b/internal/validators/also_requires_one_of_on_values_validator.go @@ -0,0 +1,180 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package validators + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +var ( + _ validator.String = AlsoRequiresOneOfOnValuesValidator{} + _ validator.Bool = AlsoRequiresOneOfOnValuesValidator{} +) + +// AlsoRequiresOneOfOnValuesValidator is the underlying struct implementing AlsoRequiresOnValue. +type AlsoRequiresOneOfOnValuesValidator struct { + OnStringValues []string + OnBoolValues []bool + PathExpressions path.Expressions +} + +type AlsoRequiresOneOfOnValuesValidatorRequest struct { + Config tfsdk.Config + ConfigValue attr.Value + Path path.Path + PathExpression path.Expression +} + +type AlsoRequiresOneOfOnValuesValidatorResponse struct { + Diagnostics diag.Diagnostics +} + +// Description implements validator.String. +func (v AlsoRequiresOneOfOnValuesValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +// MarkdownDescription implements validator.String. +func (v AlsoRequiresOneOfOnValuesValidator) MarkdownDescription(context.Context) string { + if len(v.OnStringValues) > 0 { + return fmt.Sprintf("If the current attribute is set to one of [%s], exactly one of the following also need to be set: %q", strings.Join(v.OnStringValues, ","), v.PathExpressions) + } else if len(v.OnBoolValues) > 0 { + boolValueArray := []string{} + for _, boolValue := range v.OnBoolValues { + boolValueArray = append(boolValueArray, strconv.FormatBool(boolValue)) + } + return fmt.Sprintf("If the current attribute is set to one of [%v], exactly one of the following also need to be set: %q", strings.Join(boolValueArray, ","), v.PathExpressions) + } + return "" +} + +func (v AlsoRequiresOneOfOnValuesValidator) Validate(ctx context.Context, req AlsoRequiresOneOfOnValuesValidatorRequest, res *AlsoRequiresOneOfOnValuesValidatorResponse) { + // If attribute configuration is null, there is nothing else to validate + if req.ConfigValue.IsNull() { + return + } + + expressions := req.PathExpression.MergeExpressions(v.PathExpressions...) + + matchedCount := 0 + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.Path) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } + + if mpVal.IsNull() { + continue + } + + matchedCount++ + } + } + if matchedCount == 0 { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.Path, + fmt.Sprintf("One of attributes %q must be specified", expressions), + )) + } else if matchedCount > 1 { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.Path, + fmt.Sprintf("Only one of attributes %q can be specified", expressions), + )) + } +} + +// ValidateString implements validator.String. +func (v AlsoRequiresOneOfOnValuesValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + // If attribute configuration is null, there is nothing else to validate + validateReq := AlsoRequiresOneOfOnValuesValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &AlsoRequiresOneOfOnValuesValidatorResponse{} + + for _, value := range v.OnStringValues { + if value == req.ConfigValue.ValueString() { + v.Validate(ctx, validateReq, validateResp) + return + } + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// ValidateBool implements validator.Bool. +func (v AlsoRequiresOneOfOnValuesValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + // If attribute configuration is null, there is nothing else to validate + validateReq := AlsoRequiresOneOfOnValuesValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &AlsoRequiresOneOfOnValuesValidatorResponse{} + + for _, value := range v.OnBoolValues { + if value == req.ConfigValue.ValueBool() { + v.Validate(ctx, validateReq, validateResp) + return + } + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// AlsoRequiresOneOfOnValues checks that a set of path.Expression has a non-null value, +// if the current attribute or block is set to one of the values defined in onValues array. +// +// Relative path.Expression will be resolved using the attribute or block +// being validated. +func AlsoRequiresOneOfOnStringValues(onValues []string, expressions ...path.Expression) validator.String { + return AlsoRequiresOneOfOnValuesValidator{ + OnStringValues: onValues, + PathExpressions: expressions, + } +} + +func AlsoRequiresOneOfOnBoolValues(onValues []bool, expressions ...path.Expression) validator.Bool { + return AlsoRequiresOneOfOnValuesValidator{ + OnBoolValues: onValues, + PathExpressions: expressions, + } +} diff --git a/internal/validators/also_requires_one_of_on_values_validator_example_test.go b/internal/validators/also_requires_one_of_on_values_validator_example_test.go new file mode 100644 index 0000000..37af24f --- /dev/null +++ b/internal/validators/also_requires_one_of_on_values_validator_example_test.go @@ -0,0 +1,66 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package validators + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleAlsoRequiresOneOfOnStringValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_string_attr": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + // Validate exactly one of other_attr1 and other_attr2 must be configured if this attribute is set to one of ["value1", "value2"]. + AlsoRequiresOneOfOnStringValues( + []string{ + "value1", + "value2", + }, + path.Expressions{ + path.MatchRoot("other_attr1"), + path.MatchRoot("other_attr2"), + }...), + }, + }, + "other_attr1": schema.StringAttribute{ + Optional: true, + }, + "other_attr2": schema.StringAttribute{ + Optional: true, + }, + }, + } +} + +func ExampleAlsoRequiresOneOfOnBoolValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_bool_attr": schema.BoolAttribute{ + Optional: true, + Validators: []validator.Bool{ + // Validate exactly one of other_attr1 and other_attr2 must be configured if this attribute is set to one of [true]. + AlsoRequiresOneOfOnBoolValues( + []bool{ + true, + }, + path.Expressions{ + path.MatchRoot("other_attr1"), + path.MatchRoot("other_attr2"), + }...), + }, + }, + "other_attr1": schema.StringAttribute{ + Optional: true, + }, + "other_attr2": schema.StringAttribute{ + Optional: true, + }, + }, + } +} diff --git a/internal/validators/conflicts_with_on_values_validator.go b/internal/validators/conflicts_with_on_values_validator.go new file mode 100644 index 0000000..1583f74 --- /dev/null +++ b/internal/validators/conflicts_with_on_values_validator.go @@ -0,0 +1,95 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package validators + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var ( + _ validator.String = ConflictsWithOnValuesValidator{} + _ validator.Bool = ConflictsWithOnValuesValidator{} +) + +// ConflictsWithOnValuesValidator is the underlying struct implementing AlsoRequiresOnValue. +type ConflictsWithOnValuesValidator struct { + OnStringValues []string + OnBoolValues []bool + PathExpressions path.Expressions +} + +// Description implements validator.String. +func (v ConflictsWithOnValuesValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +// MarkdownDescription implements validator.String. +func (v ConflictsWithOnValuesValidator) MarkdownDescription(context.Context) string { + if len(v.OnStringValues) > 0 { + return fmt.Sprintf("If the current attribute is set to one of [%s], none of the following can be set: %q", strings.Join(v.OnStringValues, ","), v.PathExpressions) + } else if len(v.OnBoolValues) > 0 { + boolValueArray := []string{} + for _, boolValue := range v.OnBoolValues { + boolValueArray = append(boolValueArray, strconv.FormatBool(boolValue)) + } + return fmt.Sprintf("If the current attribute is set to one of [%v], none of the following can be set: %q", strings.Join(boolValueArray, ","), v.PathExpressions) + } + return "" +} + +// ValidateString implements validator.String. +func (v ConflictsWithOnValuesValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + // If attribute configuration is null, there is nothing else to validate + if req.ConfigValue.IsNull() { + return + } + + for _, value := range v.OnStringValues { + if value == req.ConfigValue.ValueString() { + stringvalidator.ConflictsWith(v.PathExpressions...).ValidateString(ctx, req, resp) + return + } + } +} + +// ValidateBool implements validator.Bool. +func (v ConflictsWithOnValuesValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + // If attribute configuration is null, there is nothing else to validate + if req.ConfigValue.IsNull() { + return + } + + for _, value := range v.OnBoolValues { + if value == req.ConfigValue.ValueBool() { + boolvalidator.ConflictsWith(v.PathExpressions...).ValidateBool(ctx, req, resp) + return + } + } +} + +// ConflictsWithOnValues checks that a set of path.Expression has a non-null value, +// if the current attribute or block is set to one of the values defined in onValues array. +// +// Relative path.Expression will be resolved using the attribute or block +// being validated. +func ConflictsWithOnStringValues(onValues []string, expressions ...path.Expression) validator.String { + return ConflictsWithOnValuesValidator{ + OnStringValues: onValues, + PathExpressions: expressions, + } +} + +func ConflictsWithOnBoolValues(onValues []bool, expressions ...path.Expression) validator.Bool { + return ConflictsWithOnValuesValidator{ + OnBoolValues: onValues, + PathExpressions: expressions, + } +} diff --git a/internal/validators/conflicts_with_on_values_validator_example_test.go b/internal/validators/conflicts_with_on_values_validator_example_test.go new file mode 100644 index 0000000..11f3653 --- /dev/null +++ b/internal/validators/conflicts_with_on_values_validator_example_test.go @@ -0,0 +1,58 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package validators + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleConflictsWithOnStringValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_string_attr": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + // Validate other_attr cannot be configured if this attribute is set to one of ["value1", "value2"]. + ConflictsWithOnStringValues( + []string{ + "value1", + "value2", + }, + path.Expressions{ + path.MatchRoot("other_attr"), + }...), + }, + }, + "other_attr": schema.StringAttribute{ + Optional: true, + }, + }, + } +} + +func ExampleConflictsWithOnBoolValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_bool_attr": schema.BoolAttribute{ + Optional: true, + Validators: []validator.Bool{ + // Validate other_attr cannot be configured if this attribute is set to one of [true]. + ConflictsWithOnBoolValues( + []bool{ + true, + }, + path.Expressions{ + path.MatchRoot("other_attr"), + }...), + }, + }, + "other_attr": schema.StringAttribute{ + Optional: true, + }, + }, + } +} diff --git a/internal/wem/wem_machine_ad_object/wem_directory_object_resource.go b/internal/wem/wem_machine_ad_object/wem_directory_object_resource.go new file mode 100644 index 0000000..0ef6532 --- /dev/null +++ b/internal/wem/wem_machine_ad_object/wem_directory_object_resource.go @@ -0,0 +1,268 @@ +package wem_machine_ad_object + +import ( + "context" + "net/http" + "strconv" + + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &wemDirectoryResource{} + _ resource.ResourceWithConfigure = &wemDirectoryResource{} + _ resource.ResourceWithImportState = &wemDirectoryResource{} + _ resource.ResourceWithModifyPlan = &wemDirectoryResource{} +) + +// Define the wemMachineResource struct +type wemDirectoryResource struct { + client *citrixdaasclient.CitrixDaasClient +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +func (w *wemDirectoryResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + if w.client != nil && (w.client.ApiClient == nil || w.client.WemClient == nil) { + resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) + return + } + + if w.client.AuthConfig.OnPremises { + resp.Diagnostics.AddError("Error managing WEM Directory Object", "Directory Objects are only supported for Cloud customers.") + } +} + +// ImportState implements resource.ResourceWithImportState. +func (w *wemDirectoryResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Configure implements resource.ResourceWithConfigure. +func (w *wemDirectoryResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + w.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) +} + +// Schema implements resource.Resource. +func (w *wemDirectoryResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = WemDirectoryResourceModel{}.GetSchema() +} + +// Metadata implements resource.Resource. +func (w *wemDirectoryResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_wem_directory_object" +} + +// NewWemDirectoryResource creates a new instance of the WemDirectoryResource. +func NewWemDirectoryResource() resource.Resource { + return &wemDirectoryResource{} +} + +// Create implements resource.Resource. +func (w *wemDirectoryResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan WemDirectoryResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Supporting only catalog as machine-level AD objects in WEM + machineCatalogId := plan.CatalogId.ValueString() + catalog, err := util.GetMachineCatalog(ctx, w.client, &diags, machineCatalogId, true) + if err != nil { + resp.Diagnostics.AddError( + "Error reading machine catalog", + "Could not read machine catalog with ID "+machineCatalogId+"\nError message: "+util.ReadClientError(err), + ) + return + } + + machineCatalogName := catalog.GetName() + + // Generate API request body from plan + var body citrixwemservice.MachineModel + body.SetSiteId(plan.SiteId.ValueInt64()) + body.SetSid(machineCatalogId) + body.SetName(machineCatalogName) + body.SetType("Catalog") + body.SetEnabled(plan.Enabled.ValueBool()) + body.SetPriority(1000) // reserved priority, currently not used in WEM API + + // Generate Create MAchine AD Object API request + machineADObjectCreateRequest := w.client.WemClient.MachineADObjectDAAS.AdObjectCreate(ctx) + machineADObjectCreateRequest = machineADObjectCreateRequest.Body(body) + httpResp, err := citrixdaasclient.AddRequestData(machineADObjectCreateRequest, w.client).Execute() + + // In case of 400 Bad Request, add it to diagnostics and return + if httpResp.StatusCode == http.StatusBadRequest && err.Error() == "400 Bad Request (Duplicate property)" { + resp.Diagnostics.AddError( + "Failed to create directory object for catalog '"+machineCatalogName+"'", + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: A Directory Object with the same Machine Catalog ID already exists.", + ) + return + } + + // In case of any other error, add it to diagnostics and return + if err != nil { + resp.Diagnostics.AddError( + "Error binding "+machineCatalogName+" to WEM configuration set ID "+strconv.FormatInt(plan.SiteId.ValueInt64(), 10), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: "+util.ReadClientError(err), + ) + return + } + + // Get newly created machine AD object from remote + machineADObject, err := getMachineADObjectBySid(ctx, w.client, machineCatalogId) + if err != nil { + return + } + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, &machineADObject) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete implements resource.Resource. +func (w *wemDirectoryResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get current state + var state WemDirectoryResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate Delete API request + machineADObjectDeleteRequest := w.client.WemClient.MachineADObjectDAAS.AdObjectDelete(ctx, state.Id.ValueString()) + httpResp, err := citrixdaasclient.AddRequestData(machineADObjectDeleteRequest, w.client).Execute() + + // In case of error, add it to diagnostics and return + if err != nil { + resp.Diagnostics.AddError( + "Error Deleting WEM Directory Object "+state.Id.String(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: "+util.ReadClientError(err), + ) + return + } +} + +// Read implements resource.Resource. +func (w *wemDirectoryResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get current state + var state WemDirectoryResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get WEM Directory object by ID + machineADObject, err := readDirectoryObject(ctx, w.client, resp, state.Id.ValueString()) + if err != nil { + return + } + + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, machineADObject) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update implements resource.Resource. +func (w *wemDirectoryResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan WemDirectoryResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Supporting only catalog as machine-level AD objects in WEM + machineCatalogId := plan.CatalogId.ValueString() + catalog, err := util.GetMachineCatalog(ctx, w.client, &diags, machineCatalogId, true) + if err != nil { + resp.Diagnostics.AddError( + "Error reading machine catalog", + "Could not read machine catalog with ID "+machineCatalogId+"\nError message: "+util.ReadClientError(err), + ) + return + } + + machineCatalogName := catalog.GetName() + + // Generate API request body from plan + var body citrixwemservice.MachineModel + idInt64, err := strconv.ParseInt(plan.Id.ValueString(), 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "Error converting ID to int64", + "Could not convert ID "+plan.Id.ValueString()+" to int64: "+err.Error(), + ) + return + } + body.SetId(idInt64) + body.SetSiteId(plan.SiteId.ValueInt64()) + body.SetSid(machineCatalogId) + body.SetName(machineCatalogName) + body.SetType("Catalog") + body.SetEnabled(plan.Enabled.ValueBool()) + body.SetPriority(1000) // reserved priority, currently not used in WEM API + + // Generate Update API request + machineADObjectUpdateRequest := w.client.WemClient.MachineADObjectDAAS.AdObjectUpdate(ctx) + machineADObjectUpdateRequest = machineADObjectUpdateRequest.Body(body) + httpResp, err := citrixdaasclient.AddRequestData(machineADObjectUpdateRequest, w.client).Execute() + + // In case of error, add it to diagnostics and return + if err != nil { + resp.Diagnostics.AddError( + "Error Updating WEM Directory Object with ID "+plan.Id.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: "+util.ReadClientError(err), + ) + return + } + + // Get Newly Updated Machine AD Object from remote by Id + machineADObject, err := getMachineADObjectById(ctx, w.client, plan.Id.ValueString()) + if err != nil { + return + } + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, machineADObject) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/wem/wem_machine_ad_object/wem_directory_object_resource_model.go b/internal/wem/wem_machine_ad_object/wem_directory_object_resource_model.go new file mode 100644 index 0000000..62cee5a --- /dev/null +++ b/internal/wem/wem_machine_ad_object/wem_directory_object_resource_model.go @@ -0,0 +1,73 @@ +package wem_machine_ad_object + +import ( + "context" + "regexp" + "strconv" + + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type WemDirectoryResourceModel struct { + Id types.String `tfsdk:"id"` + CatalogId types.String `tfsdk:"machine_catalog_id"` + SiteId types.Int64 `tfsdk:"configuration_set_id"` + Enabled types.Bool `tfsdk:"enabled"` +} + +func (WemDirectoryResourceModel) GetSchema() schema.Schema { + return schema.Schema{ + Description: "WEM --- Manages machine-level AD objects within a WEM deployment." + + "\n\n~> **Disclaimer** This feature is supported for Citrix Cloud customers, and will be made available for On-Premises soon." + + "\n\n~> **Warning** Having more than one Directory Object with the same Catalog ID is not allowed.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Identifier of the directory object.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "machine_catalog_id": schema.StringAttribute{ + Description: "GUID identifier of the machine catalog.", + Required: true, + Validators: []validator.String{ + validator.String( + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + ), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "configuration_set_id": schema.Int64Attribute{ + Description: "Identifier of the site to which the machine-level AD object belongs.", + Required: true, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicates whether the machine-level AD object is enabled.", + Required: true, + }, + }, + } +} + +func (WemDirectoryResourceModel) GetAttributes() map[string]schema.Attribute { + return WemDirectoryResourceModel{}.GetSchema().Attributes +} + +func (r WemDirectoryResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, wemSite *citrixwemservice.MachineModel) WemDirectoryResourceModel { + r.Id = types.StringValue(strconv.FormatInt(wemSite.GetId(), 10)) + r.CatalogId = types.StringValue(wemSite.GetSid()) + r.SiteId = types.Int64Value(wemSite.GetSiteId()) + r.Enabled = types.BoolValue(wemSite.GetEnabled()) + return r +} diff --git a/internal/wem/wem_machine_ad_object/wem_directory_object_utils.go b/internal/wem/wem_machine_ad_object/wem_directory_object_utils.go new file mode 100644 index 0000000..469d6dc --- /dev/null +++ b/internal/wem/wem_machine_ad_object/wem_directory_object_utils.go @@ -0,0 +1,59 @@ +package wem_machine_ad_object + +import ( + "context" + "fmt" + "strconv" + + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func readDirectoryObject(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, machineADObjectId string) (*citrixwemservice.MachineModel, error) { + idInt64, err := strconv.ParseInt(machineADObjectId, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid WEM Directory object ID: %v", err) + } + machineADObjectQueryRequest := client.WemClient.MachineADObjectDAAS.AdObjectQueryById(ctx, idInt64) + machineADObjectQueryResponse, _, err := util.ReadResource[*citrixwemservice.MachineModel](machineADObjectQueryRequest, ctx, client, resp, "Catalog Directory Object", machineADObjectId) + return machineADObjectQueryResponse, err +} + +func getMachineADObjectBySid(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, machineCatalogId string) (citrixwemservice.MachineModel, error) { + machineADObjectQueryRequest := client.WemClient.MachineADObjectDAAS.AdObjectQuery(ctx) + machineADObjectQueryRequest = machineADObjectQueryRequest.Sid(machineCatalogId) + machineADObjectQueryResponse, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixwemservice.AdObjectQuery200Response](machineADObjectQueryRequest, client) + + machineADObjectList := machineADObjectQueryResponse.GetItems() + + if err != nil { + err = fmt.Errorf("TransactionId: " + citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp) + "\nError message: " + util.ReadClientError(err)) + return citrixwemservice.MachineModel{}, err + } + + if len(machineADObjectList) != 0 { + return machineADObjectList[0], nil + } + return citrixwemservice.MachineModel{}, fmt.Errorf("WEM Directory object with SID " + machineCatalogId + " not found") +} + +func getMachineADObjectById(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, machineADObjectId string) (*citrixwemservice.MachineModel, error) { + idInt64, err := strconv.ParseInt(machineADObjectId, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid WEM Directory object ID: %v", err) + } + machineADObjectQueryRequest := client.WemClient.MachineADObjectDAAS.AdObjectQueryById(ctx, idInt64) + machineADObjectQueryResponse, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixwemservice.MachineModel](machineADObjectQueryRequest, client) + + if err != nil { + err = fmt.Errorf("TransactionId: " + citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp) + "\nError message: " + util.ReadClientError(err)) + return machineADObjectQueryResponse, err + } + + if machineADObjectQueryResponse == nil { + return machineADObjectQueryResponse, fmt.Errorf("wem directory object with ID " + machineADObjectId + " not found") + } + return machineADObjectQueryResponse, nil +} diff --git a/internal/wem/wem_site/wem_site_service_data_source.go b/internal/wem/wem_site/wem_site_service_data_source.go new file mode 100644 index 0000000..b4834db --- /dev/null +++ b/internal/wem/wem_site/wem_site_service_data_source.go @@ -0,0 +1,120 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package wem_site + +import ( + "context" + "strconv" + + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/util" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +var ( + _ datasource.DataSource = &WemSiteDataSource{} + _ datasource.DataSourceWithConfigValidators = &WemSiteDataSource{} +) + +func NewWemSiteDataSource() datasource.DataSource { + return &WemSiteDataSource{} +} + +type WemSiteDataSource struct { + client *citrixdaasclient.CitrixDaasClient +} + +// WEM Configuration Set and WEM Site refer to the same object. These terms have been used interchangeably below. +func (d *WemSiteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_wem_configuration_set" +} + +func (d *WemSiteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = GetWemSiteDataSourceSchema() +} + +func (d *WemSiteDataSource) 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 *WemSiteDataSource) 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 WemSiteDataSourceModel + + // 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 + var wemSiteNameOrId string + + getWemSiteRequest := d.client.WemClient.SiteDAAS.SiteQuery(ctx) + if !data.Id.IsNull() { + wemSiteNameOrId = data.Id.ValueString() + idInt64, err := strconv.ParseInt(data.Id.ValueString(), 10, 64) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Error fetching WEM configuration set", + "The WEM configuration set id format is incorrect. Please provide an existing WEM configuration set id and try again.", + ) + return + } + getWemSiteRequest = getWemSiteRequest.Id(idInt64) + } + if !data.Name.IsNull() { + wemSiteNameOrId = data.Name.ValueString() + getWemSiteRequest = getWemSiteRequest.Name(data.Name.ValueString()) + } + getWemSiteResponse, httpResp, err := citrixdaasclient.AddRequestData(getWemSiteRequest, d.client).Execute() + + if err != nil { + resp.Diagnostics.AddError( + "Error fetching WEM configuration set: "+wemSiteNameOrId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + wemSites := getWemSiteResponse.GetItems() + + if len(wemSites) == 0 { + resp.Diagnostics.AddError( + "Error fetching WEM configuration set", + "Could not find WEM configuration set: "+wemSiteNameOrId+". Please provide the id or name of an existing WEM configuration set and try again.", + ) + return + } + + data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, &wemSites[0]) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (w *WemSiteDataSource) ConfigValidators(ctx context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("name"), + ), + } +} diff --git a/internal/wem/wem_site/wem_site_service_data_source_model.go b/internal/wem/wem_site/wem_site_service_data_source_model.go new file mode 100644 index 0000000..d7b34d9 --- /dev/null +++ b/internal/wem/wem_site/wem_site_service_data_source_model.go @@ -0,0 +1,55 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package wem_site + +import ( + "context" + "strconv" + + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type WemSiteDataSourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` +} + +func (r WemSiteDataSourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, wemSite *citrixwemservice.SiteModel) WemSiteDataSourceModel { + + r.Id = types.StringValue(strconv.FormatInt(wemSite.GetId(), 10)) + r.Name = types.StringValue(wemSite.GetName()) + r.Description = types.StringValue(wemSite.GetDescription()) + + return r +} + +func GetWemSiteDataSourceSchema() schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + Description: "WEM --- Data source to get details regarding a specific configuration set.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the WEM configuration set.", + Optional: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the configuration set.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 64), + }, + }, + "description": schema.StringAttribute{ + Description: "Description of the configuration set.", + Computed: true, + }, + }, + } +} diff --git a/internal/wem/wem_site/wem_site_service_resource.go b/internal/wem/wem_site/wem_site_service_resource.go new file mode 100644 index 0000000..21486e0 --- /dev/null +++ b/internal/wem/wem_site/wem_site_service_resource.go @@ -0,0 +1,240 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package wem_site + +import ( + "context" + "strconv" + + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &wemSiteServiceResource{} + _ resource.ResourceWithConfigure = &wemSiteServiceResource{} + _ resource.ResourceWithImportState = &wemSiteServiceResource{} + _ resource.ResourceWithModifyPlan = &wemSiteServiceResource{} +) + +// wemSiteServiceResource is the resource implementation. +type wemSiteServiceResource struct { + client *citrixdaasclient.CitrixDaasClient +} + +// ImportState implements resource.ResourceWithImportState. +func (w *wemSiteServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Metadata implements resource.Resource. +func (w *wemSiteServiceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_wem_configuration_set" +} + +// Configure implements resource.ResourceWithConfigure. +func (w *wemSiteServiceResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + w.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) +} + +// NewWemSiteServiceResource is a helper function to simplify the provider implementation. +func NewWemSiteServiceResource() resource.Resource { + return &wemSiteServiceResource{} +} + +// Schema implements resource.Resource. +func (w *wemSiteServiceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = WemSiteResourceModel{}.GetSchema() +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +func (w *wemSiteServiceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + if w.client != nil && (w.client.ApiClient == nil || w.client.WemClient == nil) { + resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) + return + } + + if w.client.AuthConfig.OnPremises { + resp.Diagnostics.AddError("Error managing WEM Configuration Sets", "Configuration Sets are only supported for Cloud customers.") + } +} + +// Create implements resource.Resource. +func (w *wemSiteServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan WemSiteResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + var body citrixwemservice.SiteModel + body.SetName(plan.Name.ValueString()) + body.SetDescription(plan.Description.ValueString()) + + // Generate Create API request + siteCreateRequest := w.client.WemClient.SiteDAAS.SiteCreate(ctx) + siteCreateRequest = siteCreateRequest.Body(body) + httpResp, err := citrixdaasclient.AddRequestData(siteCreateRequest, w.client).Execute() + + // In case of error, add it to diagnostics and return + if err != nil { + resp.Diagnostics.AddError( + "Error creating WEM site "+plan.Name.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: "+util.ReadClientError(err), + ) + return + } + + // Get Newly created site by name from remote (ID is not available yet) + siteConfig, err := getSiteByName(ctx, w.client, plan) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching WEM site", + util.ReadClientError(err), + ) + return + } + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, &siteConfig) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete implements resource.Resource. +func (w *wemSiteServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get current state + var state WemSiteResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Convert site Id to int64 + siteId, err := strconv.ParseInt(state.Id.ValueString(), 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "Error converting site Id to int64", + err.Error(), + ) + return + } + + // Generate Delete API request + siteDeleteRequest := w.client.WemClient.SiteDAAS.SiteDelete(ctx, siteId) + httpResp, err := citrixdaasclient.AddRequestData(siteDeleteRequest, w.client).Execute() + + // In case of error, add it to diagnostics and return + if err != nil { + resp.Diagnostics.AddError( + "Error Deleting WEM site "+state.Name.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: "+util.ReadClientError(err), + ) + return + } +} + +// Read implements resource.Resource. +func (w *wemSiteServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get current state + var state WemSiteResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get site from remote using site Id + siteConfig, err := readConfigurationSet(ctx, w.client, resp, state) + if err != nil { + return + } + + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, siteConfig) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update implements resource.Resource. +func (w *wemSiteServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get plan values + var plan WemSiteResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + var body citrixwemservice.SiteModel + siteId, err := strconv.ParseInt(plan.Id.ValueString(), 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "Error converting site Id to int64", + err.Error(), + ) + return + } + body.SetId(siteId) + body.SetName(plan.Name.ValueString()) + body.SetDescription(plan.Description.ValueString()) + + // Generate Update API request + siteUpdateRequest := w.client.WemClient.SiteDAAS.SiteUpdate(ctx) + siteUpdateRequest = siteUpdateRequest.Body(body) + httpResp, err := citrixdaasclient.AddRequestData(siteUpdateRequest, w.client).Execute() + + // In case of error, add it to diagnostics and return + if err != nil { + resp.Diagnostics.AddError( + "Error Updating WEM site "+plan.Name.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+"\nError message: "+util.ReadClientError(err), + ) + return + } + + // Get Newly Updated site from remote by Id + siteConfig, err := getSiteById(ctx, w.client, plan) + if err != nil { + return + } + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, siteConfig) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/wem/wem_site/wem_site_service_resource_model.go b/internal/wem/wem_site/wem_site_service_resource_model.go new file mode 100644 index 0000000..5855101 --- /dev/null +++ b/internal/wem/wem_site/wem_site_service_resource_model.go @@ -0,0 +1,66 @@ +// Copyright © 2024. Citrix Systems, Inc. +package wem_site + +import ( + "context" + "regexp" + "strconv" + + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type WemSiteResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` +} + +func (WemSiteResourceModel) GetSchema() schema.Schema { + return schema.Schema{ + Description: "WEM --- Manages configuration sets within a WEM deployment." + + "\n\n~> **Disclaimer** This feature is supported for Citrix Cloud customers, and will be made available for On-Premises soon.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Identifier of the configuration set.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "Name of the configuration set. WEM Site Name should be unique.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 64), + stringvalidator.RegexMatches(regexp.MustCompile(util.StringWithoutTrailingLeadingWhitespaceRegex), "must not have any leading or trailing whitespace"), + }, + }, + "description": schema.StringAttribute{ + Description: "Description of the configuration set. Default value is empty string.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), // Default value is empty string + }, + }, + } +} + +func (WemSiteResourceModel) GetAttributes() map[string]schema.Attribute { + return WemSiteResourceModel{}.GetSchema().Attributes +} + +func (r WemSiteResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, wemSite *citrixwemservice.SiteModel) WemSiteResourceModel { + r.Id = types.StringValue(strconv.FormatInt(wemSite.GetId(), 10)) + r.Name = types.StringValue(wemSite.GetName()) + r.Description = types.StringValue(wemSite.GetDescription()) + return r +} diff --git a/internal/wem/wem_site/wem_site_service_utils.go b/internal/wem/wem_site/wem_site_service_utils.go new file mode 100644 index 0000000..4bb1600 --- /dev/null +++ b/internal/wem/wem_site/wem_site_service_utils.go @@ -0,0 +1,66 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package wem_site + +import ( + "context" + "fmt" + "strconv" + + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + citrixwemservice "github.com/citrix/citrix-daas-rest-go/devicemanagement" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func readConfigurationSet(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, wemResource WemSiteResourceModel) (*citrixwemservice.SiteModel, error) { + idInt64, err := strconv.ParseInt(wemResource.Id.ValueString(), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid id: %v", err) + } + siteGetRequest := client.WemClient.SiteDAAS.SiteQueryById(ctx, idInt64) + siteGetResponse, _, err := util.ReadResource[*citrixwemservice.SiteModel](siteGetRequest, ctx, client, resp, "Configuration Set", wemResource.Name.ValueString()) + return siteGetResponse, err +} + +func getSiteByName(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, wemResource WemSiteResourceModel) (citrixwemservice.SiteModel, error) { + siteName := wemResource.Name.ValueString() + siteGetRequest := client.WemClient.SiteDAAS.SiteQuery(ctx) + siteGetRequest = siteGetRequest.Name(siteName) + siteGetResponse, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixwemservice.SiteQuery200Response](siteGetRequest, client) + + siteConfigList := siteGetResponse.GetItems() + var siteConfig citrixwemservice.SiteModel + + if err != nil { + err = fmt.Errorf("TransactionId: " + citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp) + "\nError message: " + util.ReadClientError(err)) + return siteConfig, err + } + + if len(siteConfigList) != 0 { + siteConfig = siteConfigList[0] + } + if siteConfig.Id == nil { + return siteConfig, fmt.Errorf("site with name %s not found", wemResource.Name.ValueString()) + } + return siteConfig, nil +} + +func getSiteById(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, wemResource WemSiteResourceModel) (*citrixwemservice.SiteModel, error) { + idInt64, err := strconv.ParseInt(wemResource.Id.ValueString(), 10, 64) + if err != nil { + return &citrixwemservice.SiteModel{}, fmt.Errorf("invalid id: %v", err) + } + siteGetRequest := client.WemClient.SiteDAAS.SiteQueryById(ctx, idInt64) + siteGetResponse, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixwemservice.SiteModel](siteGetRequest, client) + + if err != nil { + err = fmt.Errorf("TransactionId: " + citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp) + "\nError message: " + util.ReadClientError(err)) + return siteGetResponse, err + } + + if siteGetResponse == nil { + return nil, fmt.Errorf("site with name %s not found", wemResource.Name.ValueString()) + } + return siteGetResponse, nil +} diff --git a/scripts/onboarding-helper/README.md b/scripts/onboarding-helper/README.md index f7d3077..d6f337f 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.1` or higher +- Citrix Provider version `1.0.5` - 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 f0b09de..cd4c43f 100644 --- a/scripts/onboarding-helper/terraform-onboarding.ps1 +++ b/scripts/onboarding-helper/terraform-onboarding.ps1 @@ -206,6 +206,7 @@ function Start-GetRequest { function New-RequiredFiles { + Write-Verbose "Creating required files for terraform." # Create temporary import.tf for terraform import if (!(Test-Path ".\citrix.tf")) { New-Item -path ".\" -name "citrix.tf" -type "file" -Force @@ -259,6 +260,8 @@ provider "citrix" { Write-Verbose "Cleared content in terraform resource file." } + Write-Verbose "Required files created successfully." + } # Function to get list of resources for a given resource provider @@ -357,6 +360,7 @@ function Get-ImportMap { # List all CVAD objects from existing site function Get-ExistingCVADResources { + Write-Verbose "Get list of all existing CVAD resources from the site." $resources = @{ "zone" = @{ "resourceApi" = "zones" @@ -410,7 +414,7 @@ function Get-ExistingCVADResources { "resourceApi" = "/gpo/policySets" "resourceProviderName" = "policy_set" } - "application" = @{ + "application" = @{ "resourceApi" = "Applications" "resourceProviderName" = "application" } @@ -422,7 +426,7 @@ function Get-ExistingCVADResources { "resourceApi" = "ApplicationGroups" "resourceProviderName" = "application_group" } - "application_icon" = @{ + "application_icon" = @{ "resourceApi" = "Icons" "resourceProviderName" = "application_icon" } @@ -445,10 +449,12 @@ function Get-ExistingCVADResources { } } } + Write-Verbose "Successfully retrieved all CVAD resources from the site." } # Function to import terraform resources into state function Import-ResourcesToState { + Write-Verbose "Importing terraform resources into state." foreach ($resource in $script:cvadResourcesMap.Keys) { foreach ($id in $script:cvadResourcesMap[$resource].Keys) { terraform import "citrix_$($resource).$($script:cvadResourcesMap[$resource][$id])" "$id" @@ -487,6 +493,19 @@ function InjectSecretValues { return $content } +function PostProcessProviderConfig { + + Write-Verbose "Post-processing provider config." + # Post-process the provider config output in citrix.tf + $content = Get-Content -Path ".\citrix.tf" -Raw + + # Uncomment field for client secret in provider config + $content = $content -replace "# ", "" + + # Overwrite provider config with processed value + Set-Content -Path ".\citrix.tf" -Value $content +} + function RemoveComputedPropertiesForZone { param( [parameter(Mandatory = $true)] @@ -494,11 +513,13 @@ function RemoveComputedPropertiesForZone { ) if ($script:onPremise) { + Write-Verbose "Removing computed properties for zone resource in on-premises." # Remove resource_location_id property from each zone resource for on-premises $resourceLocationIdRegex = "(\s+)resource_location_id(\s+)= (\S+)" $content = $content -replace $resourceLocationIdRegex, "" } else { + Write-Verbose "Removing computed properties for zone resource in cloud." # Remove name property from each zone resource in cloud $filteredOutput = @() $lines = $content -split "`r?`n" @@ -527,29 +548,42 @@ function RemoveComputedProperties { [string] $content ) - # Remove Id property from each resource since they are computed - $idRegex = "(\s+)id(\s+)= (\S+)" - $content = $content -replace $idRegex, "" + Write-Verbose "Removing computed properties from terraform output." + # Define an array of regex patterns to remove computed properties + $regexPatterns = @( + "(\s+)id(\s+)= (\S+)", + "(\s+)total_machines(\s+)= (\S+)", + '(\s+)path\s*=\s*"(.*?)"', + "(\s+)assigned(\s+)= (\S+)", + "(\s+)is_built_in(\s+)= (\S+)", + "(\s+)built_in_scopes\s*=\s*\[[\s\S]*?\]", + "(\s+)inherited_scopes\s*=\s*\[[\s\S]*?\]" + ) - # Remove total_machines property from machine_catalog since it is computed - $totalMachineRegex = "(\s+)total_machines(\s+)= (\S+)" - $content = $content -replace $totalMachineRegex, "" + # Identify the delivery_groups_priority block + $deliveryGroupsPriorityPattern = "(\s*)delivery_groups_priority\s*=\s*\[[\s\S]*?\]" + $deliveryGroupsPriorityMatches = [regex]::Matches($content, $deliveryGroupsPriorityPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - # Remove path property from application_folder since it is computed - $pathRegex = '(\s+)path\s*=\s*".*\\\\.*"' - $content = $content -replace $pathRegex, "" + # Extract the delivery_groups_priority block and replace it with a placeholder + foreach ($match in $deliveryGroupsPriorityMatches) { + $deliveryGroupsPriorityBlock = $match.Value + $content = $content -replace [regex]::Escape($deliveryGroupsPriorityBlock), "PLACEHOLDER_DELIVERY_GROUPS_PRIORITY" + } - # Remove assigned property from application since it is computed - $isAssignedRegex = "(\s+)assigned(\s+)= (\S+)" - $content = $content -replace $isAssignedRegex, "" + # Loop through each regex pattern and replace matches in the content + foreach ($pattern in $regexPatterns) { + $content = $content -replace $pattern, "" + } - # Remove is_built_in property from admin_role since it is computed - $isBuiltInRegex = "(\s+)is_built_in(\s+)= (\S+)" - $content = $content -replace $isBuiltInRegex, "" + # Restore the delivery_groups_priority block + foreach ($match in $deliveryGroupsPriorityMatches) { + $content = $content -replace "PLACEHOLDER_DELIVERY_GROUPS_PRIORITY", $match.Value + } - # Remove contents for zone respource + # Remove contents for zone resource $content = RemoveComputedPropertiesForZone -content $content + Write-Verbose "Computed properties removed successfully." return $content } @@ -563,6 +597,7 @@ function ReplaceDependencyRelationships { return $content } + Write-Verbose "Creating dependency relationships between resources." # Create dependency relationships between resources with id references foreach ($resource in $script:cvadResourcesMap.Keys) { foreach ($id in $script:cvadResourcesMap[$resource].Keys) { @@ -585,6 +620,7 @@ function InjectPlaceHolderSensitiveValues { [string] $content ) + Write-Verbose "Injecting placeholder for sensitive values in terraform output." ### hypervisor secrets ### ###### Azure ###### $content = InjectSecretValues -targetProperty "application_id" -newProperty "application_secret" -content $content @@ -601,6 +637,7 @@ function InjectPlaceHolderSensitiveValues { return $content } + function ExtractAndSaveApplicationIcons { param( [parameter(Mandatory = $true)] @@ -612,6 +649,8 @@ function ExtractAndSaveApplicationIcons { return } + Write-Verbose "Extracting and saving application icons into icons folder." + $filteredOutput = @() $lines = $content -split "`r?`n" $iconCounter = 0 @@ -647,9 +686,40 @@ function ExtractAndSaveApplicationIcons { } $content = $filteredOutput -join "`n" + Write-Verbose "Extracted and saved $iconCounter application icons." return $content } +function OrganizeTerraformResources { + param( + [parameter(Mandatory = $true)] + [string] $content + ) + + Write-Verbose "Organizing terraform resources into separate files." + # Post-process the terraform output + $content = Get-Content -Path ".\resource.tf" -Raw + + # Regular expression to match resource blocks starting with # and ending with an empty line + $resourcePattern = '(#\s*(\w+)\.\w+:\s*.*?)(\n\s*\n|\s*$)' + + # Find all resource blocks + $resources = [regex]::Matches($content, $resourcePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) + + # Create a new .tf file for each resource type in its respective folder + foreach ($resource in $resources) { + $resourceBlock = $resource.Groups[1].Value + $resourceType = $resource.Groups[2].Value + $filename = "$resourceType.tf" + + # Append the resource block to the file + Add-Content -Path $filename -Value $resourceBlock + Add-Content -Path $filename -Value "`n" # Add a newline for separation + } + + Write-Verbose "Resource files created successfully." +} + function PostProcessTerraformOutput { # Post-process the terraform output @@ -670,18 +740,8 @@ function PostProcessTerraformOutput { # Overwrite extracted terraform with processed value Set-Content -Path ".\resource.tf" -Value $content -} - -function PostProcessProviderConfig { - - # Post-process the provider config output in citrix.tf - $content = Get-Content -Path ".\citrix.tf" -Raw - - # Uncomment field for client secret in provider config - $content = $content -replace "# ", "" - - # Overwrite provider config with processed value - Set-Content -Path ".\citrix.tf" -Value $content + # Organize terraform resources into separate files + OrganizeTerraformResources -content $content } if ($DisableSSLValidation -and $PSVersionTable.PSVersion.Major -lt 7) { @@ -745,8 +805,9 @@ try { # Post-process terraform output PostProcessTerraformOutput - # Remove temporary TF file + # Remove temporary files Remove-Item ".\import.tf" + Remove-Item ".\resource.tf" # Format terraform files terraform fmt diff --git a/scripts/onboarding-helper/terraform.tf b/scripts/onboarding-helper/terraform.tf index c627e41..007b69e 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.1" + version = "=1.0.5" } } diff --git a/settings.cloud.example.json b/settings.cloud.example.json index 5396a62..05fc1ae 100644 --- a/settings.cloud.example.json +++ b/settings.cloud.example.json @@ -477,7 +477,16 @@ "TEST_TAG_DATA_SOURCE_DESCRIPTION": "{Description of the tag}", // Tag Resource Go Tests Env Variables "TEST_TAG_RESOURCE_NAME": "{Name of the tag resource}", - "TEST_TAG_RESOURCE_DESCRIPTION": "{Description of the tag resource}" + "TEST_TAG_RESOURCE_DESCRIPTION": "{Description of the tag resource}", + + // Wem Resource Go Tests Env Variables + "TEST_WEM_SITE_RESOURCE_NAME": "{Name of the WEM Site Resource}", + "TEST_WEM_SITE_RESOURCE_DESCRIPTION": "{Description of the WEM Site Resource}", + + // WEM Datasource go tests + "TEST_WEM_SITE_DATA_SOURCE_ID": "{Id of the WEM Configuration Set}", + "TEST_WEM_SITE_DATA_SOURCE_NAME": "{Name of the WEM Configuration Set}", + "TEST_WEM_SITE_DATA_SOURCE_DESCRIPTION": "{Description of the WEM Configuration Set}" }, "go.testTimeout": "120m" } \ No newline at end of file